]> git.openstreetmap.org Git - rails.git/commitdiff
resync from rails_port 11795:12304
authorShaun McDonald <shaun@shaunmcdonald.me.uk>
Fri, 12 Dec 2008 19:29:27 +0000 (19:29 +0000)
committerShaun McDonald <shaun@shaunmcdonald.me.uk>
Fri, 12 Dec 2008 19:29:27 +0000 (19:29 +0000)
346 files changed:
app/controllers/amf_controller.rb
app/controllers/api_controller.rb
app/controllers/application.rb
app/controllers/browse_controller.rb
app/controllers/changeset_controller.rb [new file with mode: 0644]
app/controllers/changeset_tag_controller.rb [new file with mode: 0644]
app/controllers/diary_entry_controller.rb
app/controllers/message_controller.rb
app/controllers/node_controller.rb
app/controllers/old_node_controller.rb
app/controllers/old_relation_controller.rb
app/controllers/old_way_controller.rb
app/controllers/relation_controller.rb
app/controllers/trace_controller.rb
app/controllers/user_controller.rb
app/controllers/user_preference_controller.rb
app/controllers/way_controller.rb
app/helpers/browse_helper.rb
app/models/changeset.rb [new file with mode: 0644]
app/models/changeset_tag.rb [new file with mode: 0644]
app/models/diary_entry.rb
app/models/message.rb
app/models/node.rb
app/models/node_tag.rb [new file with mode: 0644]
app/models/old_node.rb
app/models/old_node_tag.rb [new file with mode: 0644]
app/models/old_relation.rb
app/models/old_relation_member.rb
app/models/old_relation_tag.rb
app/models/old_way.rb
app/models/old_way_tag.rb
app/models/relation.rb
app/models/relation_member.rb
app/models/relation_tag.rb
app/models/trace.rb
app/models/tracetag.rb
app/models/user.rb
app/models/user_preference.rb
app/models/way.rb
app/models/way_tag.rb
app/views/browse/_changeset_details.rhtml [new file with mode: 0644]
app/views/browse/_common_details.rhtml
app/views/browse/_paging_nav.rhtml [new file with mode: 0644]
app/views/browse/changeset.rhtml [new file with mode: 0644]
app/views/browse/index.rhtml
app/views/browse/not_found.rhtml [new file with mode: 0644]
app/views/browse/start.rjs
app/views/diary_entry/edit.rhtml
app/views/diary_entry/list.rhtml
app/views/diary_entry/no_such_entry.rhtml [new file with mode: 0644]
app/views/layouts/site.rhtml
app/views/message/_message_summary.rhtml
app/views/message/_sent_message_summary.rhtml
app/views/message/new.rhtml
app/views/message/no_such_user.rhtml [new file with mode: 0644]
app/views/user/account.rhtml
app/views/user/confirm.rhtml
app/views/user/login.rhtml
app/views/user/new.rhtml
app/views/user/view.rhtml
config/application.yml
config/database.yml
config/environment.rb
config/environments/development.rb
config/initializers/composite_primary_keys.rb [deleted file]
config/initializers/libxml.rb
config/potlatch/autocomplete.txt
config/potlatch/colours.txt
config/potlatch/presets.txt
config/routes.rb
db/README
db/functions/Makefile
db/functions/maptile.c
db/migrate/001_create_osm_db.rb
db/migrate/002_cleanup_osm_db.rb
db/migrate/003_sql_session_store_setup.rb
db/migrate/004_user_enhancements.rb
db/migrate/005_tile_tracepoints.rb
db/migrate/006_tile_nodes.rb
db/migrate/007_add_relations.rb
db/migrate/010_diary_comments.rb
db/migrate/013_add_email_valid.rb
db/migrate/015_add_user_visible.rb
db/migrate/018_add_timestamp_indexes.rb [new file with mode: 0644]
db/migrate/019_populate_node_tags_and_remove.rb [new file with mode: 0644]
db/migrate/019_populate_node_tags_and_remove_helper.c [new file with mode: 0644]
db/migrate/020_move_to_innodb.rb [new file with mode: 0644]
db/migrate/021_key_constraints.rb [new file with mode: 0644]
db/migrate/022_add_changesets.rb [new file with mode: 0644]
db/migrate/023_order_relation_members.rb [new file with mode: 0644]
db/migrate/024_add_end_time_to_changesets.rb [new file with mode: 0644]
doc/README_FOR_APP
lib/consistency_validations.rb [new file with mode: 0644]
lib/diff_reader.rb [new file with mode: 0644]
lib/geo_record.rb
lib/map_boundary.rb
lib/migrate.rb
lib/osm.rb
lib/potlatch.rb
lib/tasks/populate_node_tags.rake [deleted file]
lib/validators.rb [new file with mode: 0644]
public/404.html
public/500.html
public/images/new.png [new file with mode: 0644]
public/javascripts/map.js
public/potlatch/potlatch.swf
public/stylesheets/site.css
test/fixtures/changeset_tags.yml [new file with mode: 0644]
test/fixtures/changesets.yml [new file with mode: 0644]
test/fixtures/current_node_tags.yml [new file with mode: 0644]
test/fixtures/current_nodes.yml
test/fixtures/current_relation_members.yml
test/fixtures/current_relation_tags.yml
test/fixtures/current_relations.yml
test/fixtures/current_way_nodes.yml
test/fixtures/current_way_tags.yml
test/fixtures/current_ways.yml
test/fixtures/diary_comments.yml [new file with mode: 0644]
test/fixtures/diary_entries.yml [new file with mode: 0644]
test/fixtures/friends.yml [new file with mode: 0644]
test/fixtures/gps_points.yml [new file with mode: 0644]
test/fixtures/gpx_file_tags.yml [new file with mode: 0644]
test/fixtures/gpx_files.yml [new file with mode: 0644]
test/fixtures/messages.yml
test/fixtures/node_tags.yml [new file with mode: 0644]
test/fixtures/nodes.yml
test/fixtures/relation_tags.yml
test/fixtures/relations.yml
test/fixtures/user_preferences.yml
test/fixtures/users.yml
test/fixtures/way_nodes.yml
test/fixtures/way_tags.yml
test/fixtures/ways.yml
test/functional/amf_controller_test.rb [new file with mode: 0644]
test/functional/api_controller_test.rb
test/functional/browse_controller_test.rb [new file with mode: 0644]
test/functional/changeset_controller_test.rb [new file with mode: 0644]
test/functional/changeset_tag_controller_test.rb [new file with mode: 0644]
test/functional/diary_entry_controller_test.rb [new file with mode: 0644]
test/functional/export_controller_test.rb [new file with mode: 0644]
test/functional/friend_controller_test.rb [new file with mode: 0644]
test/functional/geocoder_controller_test.rb
test/functional/message_controller_test.rb
test/functional/node_controller_test.rb
test/functional/old_node_controller_test.rb [new file with mode: 0644]
test/functional/old_relation_controller_test.rb
test/functional/old_way_controller_test.rb
test/functional/relation_controller_test.rb
test/functional/search_controller_test.rb [new file with mode: 0644]
test/functional/site_controller_test.rb [new file with mode: 0644]
test/functional/swf_controller_test.rb [new file with mode: 0644]
test/functional/trace_controller_test.rb [new file with mode: 0644]
test/functional/user_controller_test.rb [new file with mode: 0644]
test/functional/user_preference_controller_test.rb
test/functional/way_controller_test.rb
test/integration/user_diaries_test.rb [new file with mode: 0644]
test/test_helper.rb
test/unit/changeset_tag_test.rb [new file with mode: 0644]
test/unit/changeset_test.rb [new file with mode: 0644]
test/unit/diary_comment_test.rb [new file with mode: 0644]
test/unit/diary_entry_test.rb [new file with mode: 0644]
test/unit/friend_test.rb [new file with mode: 0644]
test/unit/message_test.rb
test/unit/node_tag_test.rb [new file with mode: 0644]
test/unit/node_test.rb
test/unit/old_node_tag_test.rb [new file with mode: 0644]
test/unit/old_node_test.rb [new file with mode: 0644]
test/unit/old_relation_tag_test.rb [new file with mode: 0644]
test/unit/old_way_tag_test.rb [new file with mode: 0644]
test/unit/relation_member_test.rb [new file with mode: 0644]
test/unit/relation_tag_test.rb [new file with mode: 0644]
test/unit/relation_test.rb [new file with mode: 0644]
test/unit/trace_test.rb [new file with mode: 0644]
test/unit/tracepoint_test.rb [new file with mode: 0644]
test/unit/tracetag_test.rb [new file with mode: 0644]
test/unit/user_preference_test.rb
test/unit/user_test.rb
test/unit/user_token_test.rb [new file with mode: 0644]
test/unit/way_node_test.rb [new file with mode: 0644]
test/unit/way_tag_test.rb [new file with mode: 0644]
test/unit/way_test.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/History.txt [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/Manifest.txt [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/README.txt [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/README_DB2.txt [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/Rakefile [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/init.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/install.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/lib/adapter_helper/base.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/lib/adapter_helper/mysql.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/lib/adapter_helper/oracle.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/lib/adapter_helper/postgresql.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/lib/adapter_helper/sqlite3.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/association_preload.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/associations.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/attribute_methods.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/base.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/calculations.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/composite_arrays.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/connection_adapters/ibm_db_adapter.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/connection_adapters/oracle_adapter.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/connection_adapters/postgresql_adapter.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/connection_adapters/sqlite3_adapter.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/fixtures.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/migration.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/reflection.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/version.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/loader.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/local/database_connections.rb.sample [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/local/paths.rb.sample [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/local/tasks.rb.sample [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/scripts/console.rb [new file with mode: 0755]
vendor/gems/composite_primary_keys-1.1.0/scripts/txt2html [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/scripts/txt2js [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/tasks/activerecord_selection.rake [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/tasks/databases.rake [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/tasks/databases/mysql.rake [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/tasks/databases/oracle.rake [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/tasks/databases/postgresql.rake [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/tasks/databases/sqlite3.rake [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/tasks/deployment.rake [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/tasks/local_setup.rake [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/tasks/website.rake [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/README_tests.txt [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/abstract_unit.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/connections/native_ibm_db/connection.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/connections/native_mysql/connection.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/connections/native_oracle/connection.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/connections/native_postgresql/connection.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/connections/native_sqlite/connection.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/article.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/articles.yml [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/comment.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/comments.yml [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/db_definitions/db2-create-tables.sql [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/db_definitions/db2-drop-tables.sql [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/db_definitions/mysql.sql [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/db_definitions/oracle.drop.sql [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/db_definitions/oracle.sql [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/db_definitions/postgresql.sql [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/db_definitions/sqlite.sql [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/department.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/departments.yml [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/employee.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/employees.yml [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/group.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/groups.yml [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/hack.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/hacks.yml [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/membership.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/membership_status.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/membership_statuses.yml [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/memberships.yml [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/product.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/product_tariff.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/product_tariffs.yml [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/products.yml [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/reading.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/readings.yml [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/reference_code.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/reference_codes.yml [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/reference_type.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/reference_types.yml [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/street.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/streets.yml [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/suburb.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/suburbs.yml [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/tariff.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/tariffs.yml [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/user.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/fixtures/users.yml [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/hash_tricks.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/plugins/pagination.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/plugins/pagination_helper.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/test_associations.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/test_attribute_methods.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/test_attributes.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/test_clone.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/test_composite_arrays.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/test_create.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/test_delete.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/test_dummy.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/test_find.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/test_ids.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/test_miscellaneous.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/test_pagination.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/test_polymorphic.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/test_santiago.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/test_tutorial_examle.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/test/test_update.rb [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/tmp/test.db [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/website/index.html [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/website/index.txt [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/website/javascripts/rounded_corners_lite.inc.js [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/website/stylesheets/screen.css [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/website/template.js [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/website/template.rhtml [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/website/version-raw.js [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/website/version-raw.txt [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/website/version.js [new file with mode: 0644]
vendor/gems/composite_primary_keys-1.1.0/website/version.txt [new file with mode: 0644]
vendor/plugins/classic_pagination/lib/pagination.rb
vendor/plugins/deadlock_retry/README [new file with mode: 0644]
vendor/plugins/deadlock_retry/Rakefile [new file with mode: 0644]
vendor/plugins/deadlock_retry/init.rb [new file with mode: 0644]
vendor/plugins/deadlock_retry/lib/deadlock_retry.rb [new file with mode: 0644]
vendor/plugins/deadlock_retry/test/deadlock_retry_test.rb [new file with mode: 0644]
vendor/plugins/file_column/CHANGELOG [new file with mode: 0644]
vendor/plugins/file_column/README [new file with mode: 0644]
vendor/plugins/file_column/Rakefile [new file with mode: 0644]
vendor/plugins/file_column/TODO [new file with mode: 0644]
vendor/plugins/file_column/init.rb [new file with mode: 0644]
vendor/plugins/file_column/lib/file_column.rb [new file with mode: 0644]
vendor/plugins/file_column/lib/file_column_helper.rb [new file with mode: 0644]
vendor/plugins/file_column/lib/file_compat.rb [new file with mode: 0644]
vendor/plugins/file_column/lib/magick_file_column.rb [new file with mode: 0644]
vendor/plugins/file_column/lib/rails_file_column.rb [new file with mode: 0644]
vendor/plugins/file_column/lib/test_case.rb [new file with mode: 0644]
vendor/plugins/file_column/lib/validations.rb [new file with mode: 0644]
vendor/plugins/file_column/test/abstract_unit.rb [new file with mode: 0644]
vendor/plugins/file_column/test/connection.rb [new file with mode: 0644]
vendor/plugins/file_column/test/file_column_helper_test.rb [new file with mode: 0644]
vendor/plugins/file_column/test/file_column_test.rb [new file with mode: 0755]
vendor/plugins/file_column/test/fixtures/entry.rb [new file with mode: 0644]
vendor/plugins/file_column/test/fixtures/invalid-image.jpg [new file with mode: 0644]
vendor/plugins/file_column/test/fixtures/kerb.jpg [new file with mode: 0644]
vendor/plugins/file_column/test/fixtures/mysql.sql [new file with mode: 0644]
vendor/plugins/file_column/test/fixtures/schema.rb [new file with mode: 0644]
vendor/plugins/file_column/test/fixtures/skanthak.png [new file with mode: 0644]
vendor/plugins/file_column/test/magick_test.rb [new file with mode: 0644]
vendor/plugins/file_column/test/magick_view_only_test.rb [new file with mode: 0644]
vendor/plugins/sql_session_store/LICENSE [new file with mode: 0644]
vendor/plugins/sql_session_store/README [new file with mode: 0755]
vendor/plugins/sql_session_store/Rakefile [new file with mode: 0755]
vendor/plugins/sql_session_store/generators/sql_session_store/USAGE [new file with mode: 0755]
vendor/plugins/sql_session_store/generators/sql_session_store/sql_session_store_generator.rb [new file with mode: 0755]
vendor/plugins/sql_session_store/generators/sql_session_store/templates/migration.rb [new file with mode: 0755]
vendor/plugins/sql_session_store/init.rb [new file with mode: 0755]
vendor/plugins/sql_session_store/install.rb [new file with mode: 0755]
vendor/plugins/sql_session_store/lib/mysql_session.rb [new file with mode: 0755]
vendor/plugins/sql_session_store/lib/oracle_session.rb [new file with mode: 0755]
vendor/plugins/sql_session_store/lib/postgresql_session.rb [new file with mode: 0755]
vendor/plugins/sql_session_store/lib/sql_session.rb [new file with mode: 0644]
vendor/plugins/sql_session_store/lib/sql_session_store.rb [new file with mode: 0755]
vendor/plugins/sql_session_store/lib/sqlite_session.rb [new file with mode: 0755]

index 935746ed4fa3aa3298cb2603d6b0ce9c6756756d..7061805574c427e0dff278fc20338c2b6b02549d 100644 (file)
@@ -3,7 +3,7 @@
 # OSM database takes place using this controller. Messages are 
 # encoded in the Actionscript Message Format (AMF).
 #
-# Helper functions are in /lib/potlatch.
+# Helper functions are in /lib/potlatch.rb
 #
 # Author::     editions Systeme D / Richard Fairhurst 2004-2008
 # Licence::    public domain.
 # from the AMF message), each method generally takes arguments in the order 
 # they were sent by the Potlatch SWF. Do not assume typing has been preserved. 
 # Methods all return an array to the SWF.
+#
+# == API 0.6
+#
+# Note that this requires a patched version of composite_primary_keys 1.1.0
+# (see http://groups.google.com/group/compositekeys/t/a00e7562b677e193) 
+# if you are to run with POTLATCH_USE_SQL=false .
 # 
 # == Debugging
 # 
@@ -28,6 +34,9 @@ class AmfController < ApplicationController
 
   include Potlatch
 
+  # Help methods for checking boundary sanity and area size
+  include MapBoundary
+
   session :off
   before_filter :check_write_availability
 
@@ -36,234 +45,353 @@ class AmfController < ApplicationController
   # ** FIXME: refactor to reduce duplication of code across read/write
   
   def amf_read
-       req=StringIO.new(request.raw_post+0.chr)# Get POST data as request
-                                                                                       # (cf http://www.ruby-forum.com/topic/122163)
-       req.read(2)                                                             # Skip version indicator and client ID
-       results={}                                                              # Results of each body
-
-       # Parse request
-
-       headers=AMF.getint(req)                                 # Read number of headers
-
-       headers.times do                                                # Read each header
-         name=AMF.getstring(req)                               #  |
-         req.getc                                                              #  | skip boolean
-         value=AMF.getvalue(req)                               #  |
-         header["name"]=value                                  #  |
-       end
-
-       bodies=AMF.getint(req)                                  # Read number of bodies
-       bodies.times do                                                 # Read each body
-         message=AMF.getstring(req)                    #  | get message name
-         index=AMF.getstring(req)                              #  | get index in response sequence
-         bytes=AMF.getlong(req)                                #  | get total size in bytes
-         args=AMF.getvalue(req)                                #  | get response (probably an array)
-
-         case message
-               when 'getpresets';                      results[index]=AMF.putdata(index,getpresets())
-               when 'whichways';                       results[index]=AMF.putdata(index,whichways(*args))
-               when 'whichways_deleted';       results[index]=AMF.putdata(index,whichways_deleted(*args))
-               when 'getway';                          results[index]=AMF.putdata(index,getway(args[0].to_i))
-               when 'getrelation';                     results[index]=AMF.putdata(index,getrelation(args[0].to_i))
-               when 'getway_old';                      results[index]=AMF.putdata(index,getway_old(args[0].to_i,args[1].to_i))
-               when 'getway_history';          results[index]=AMF.putdata(index,getway_history(args[0].to_i))
-               when 'getnode_history';         results[index]=AMF.putdata(index,getnode_history(args[0].to_i))
-               when 'findrelations';           results[index]=AMF.putdata(index,findrelations(*args))
-               when 'getpoi';                          results[index]=AMF.putdata(index,getpoi(*args))
-         end
-       end
+    req=StringIO.new(request.raw_post+0.chr)# Get POST data as request
+                              # (cf http://www.ruby-forum.com/topic/122163)
+    req.read(2)                                                                # Skip version indicator and client ID
+    results={}                                                         # Results of each body
+
+    # Parse request
+
+    headers=AMF.getint(req)                                    # Read number of headers
+
+    headers.times do                                           # Read each header
+      name=AMF.getstring(req)                          #  |
+      req.getc                                                         #  | skip boolean
+      value=AMF.getvalue(req)                          #  |
+      header["name"]=value                                     #  |
+    end
+
+    bodies=AMF.getint(req)                                     # Read number of bodies
+    bodies.times do                                                    # Read each body
+      message=AMF.getstring(req)                       #  | get message name
+      index=AMF.getstring(req)                         #  | get index in response sequence
+      bytes=AMF.getlong(req)                           #  | get total size in bytes
+      args=AMF.getvalue(req)                           #  | get response (probably an array)
+      logger.info "Executing AMF #{message}:#{index}"
+
+      case message
+        when 'getpresets';                     results[index]=AMF.putdata(index,getpresets())
+        when 'whichways';                      results[index]=AMF.putdata(index,whichways(*args))
+        when 'whichways_deleted';      results[index]=AMF.putdata(index,whichways_deleted(*args))
+        when 'getway';                         results[index]=AMF.putdata(index,getway(args[0].to_i))
+        when 'getrelation';                    results[index]=AMF.putdata(index,getrelation(args[0].to_i))
+        when 'getway_old';                     results[index]=AMF.putdata(index,getway_old(args[0].to_i,args[1].to_i))
+        when 'getway_history';         results[index]=AMF.putdata(index,getway_history(args[0].to_i))
+        when 'getnode_history';                results[index]=AMF.putdata(index,getnode_history(args[0].to_i))
+        when 'findgpx';                                results[index]=AMF.putdata(index,findgpx(*args))
+        when 'findrelations';          results[index]=AMF.putdata(index,findrelations(*args))
+        when 'getpoi';                         results[index]=AMF.putdata(index,getpoi(*args))
+      end
+    end
+    logger.info("encoding AMF results")
     sendresponse(results)
   end
 
   def amf_write
-       req=StringIO.new(request.raw_post+0.chr)
-       req.read(2)
-       results={}
-       renumberednodes={}                                              # Shared across repeated putways
-       renumberedways={}                                               # Shared across repeated putways
-
-       headers=AMF.getint(req)                                 # Read number of headers
-       headers.times do                                                # Read each header
-         name=AMF.getstring(req)                               #  |
-         req.getc                                                              #  | skip boolean
-         value=AMF.getvalue(req)                               #  |
-         header["name"]=value                                  #  |
-       end
-
-       bodies=AMF.getint(req)                                  # Read number of bodies
-       bodies.times do                                                 # Read each body
-         message=AMF.getstring(req)                    #  | get message name
-         index=AMF.getstring(req)                              #  | get index in response sequence
-         bytes=AMF.getlong(req)                                #  | get total size in bytes
-         args=AMF.getvalue(req)                                #  | get response (probably an array)
-
-         case message
-               when 'putway';                          r=putway(renumberednodes,*args)
+    req=StringIO.new(request.raw_post+0.chr)
+    req.read(2)
+    results={}
+    renumberednodes={}                                         # Shared across repeated putways
+    renumberedways={}                                          # Shared across repeated putways
+
+    headers=AMF.getint(req)                                    # Read number of headers
+    headers.times do                                           # Read each header
+      name=AMF.getstring(req)                          #  |
+      req.getc                                                         #  | skip boolean
+      value=AMF.getvalue(req)                          #  |
+      header["name"]=value                                     #  |
+    end
+
+    bodies=AMF.getint(req)                                     # Read number of bodies
+    bodies.times do                                                    # Read each body
+      message=AMF.getstring(req)                       #  | get message name
+      index=AMF.getstring(req)                         #  | get index in response sequence
+      bytes=AMF.getlong(req)                           #  | get total size in bytes
+      args=AMF.getvalue(req)                           #  | get response (probably an array)
+
+      case message
+        when 'putway';                         r=putway(renumberednodes,*args)
                                                                        renumberednodes=r[3]
-                                                                       if r[1] != r[2]
-                                                                         renumberedways[r[1]] = r[2]
-                                                                       end
+                                                                       if r[1] != r[2] then renumberedways[r[1]] = r[2] end
                                                                        results[index]=AMF.putdata(index,r)
-               when 'putrelation';                     results[index]=AMF.putdata(index,putrelation(renumberednodes, renumberedways, *args))
-               when 'deleteway';                       results[index]=AMF.putdata(index,deleteway(args[0],args[1].to_i))
-               when 'putpoi';                          results[index]=AMF.putdata(index,putpoi(*args))
-         end
-       end
+        when 'putrelation';                    results[index]=AMF.putdata(index,putrelation(renumberednodes, renumberedways, *args))
+        when 'deleteway';                      results[index]=AMF.putdata(index,deleteway(*args))
+        when 'putpoi';                         r=putpoi(*args)
+                                                                       if r[1] != r[2] then renumberednodes[r[1]] = r[2] end
+                                                               results[index]=AMF.putdata(index,r)
+        when 'startchangeset';         results[index]=AMF.putdata(index,startchangeset(*args))
+      end
+    end
     sendresponse(results)
   end
 
   private
 
+  # Start new changeset
+  
+  def startchangeset(usertoken, cstags, closeid, closecomment)
+    user = getuserid(usertoken)
+    if !user then return -1,"You are not logged in, so Potlatch can't write any changes to the database." end
+
+    # close previous changeset and add comment
+    if closeid
+      cs = Changeset.find(closeid)
+      cs.set_closed_time_now
+      if closecomment.empty?
+        cs.save!
+      else
+        cs.tags['comment']=closecomment
+        cs.save_with_tags!
+      end
+    end
+       
+    # open a new changeset
+    cs = Changeset.new
+    cs.tags = cstags
+    cs.user_id = uid
+    # Don't like the next two lines. These need to be abstracted to the model more/better
+    cs.created_at = Time.now
+    cs.closed_at = Time.new + Changeset::IDLE_TIMEOUT
+    cs.save_with_tags!
+    return [0,cs.id]
+  end
+
   # Return presets (default tags, localisation etc.):
   # uses POTLATCH_PRESETS global, set up in OSM::Potlatch.
 
   def getpresets() #:doc:
-       return POTLATCH_PRESETS
+    return POTLATCH_PRESETS
   end
 
+  ##
   # Find all the ways, POI nodes (i.e. not part of ways), and relations
   # in a given bounding box. Nodes are returned in full; ways and relations 
   # are IDs only. 
-
+  #
+  # return is of the form: 
+  # [error_code, 
+  #  [[way_id, way_version], ...],
+  #  [[node_id, lat, lon, [tags, ...], node_version], ...],
+  #  [[rel_id, rel_version], ...]]
+  # where the ways are any visible ways which refer to any visible
+  # nodes in the bbox, nodes are any visible nodes in the bbox but not
+  # used in any way, rel is any relation which refers to either a way
+  # or node that we're returning.
   def whichways(xmin, ymin, xmax, ymax) #:doc:
-       xmin -= 0.01; ymin -= 0.01
-       xmax += 0.01; ymax += 0.01
-
-       if POTLATCH_USE_SQL then
-         way_ids = sql_find_way_ids_in_area(xmin, ymin, xmax, ymax)
-         points = sql_find_pois_in_area(xmin, ymin, xmax, ymax)
-         relation_ids = sql_find_relations_in_area_and_ways(xmin, ymin, xmax, ymax, way_ids)
-       else
-         # find the way ids in an area
-         nodes_in_area = Node.find_by_area(ymin, xmin, ymax, xmax, :conditions => "current_nodes.visible = 1", :include => :ways)
-         way_ids = nodes_in_area.collect { |node| node.way_ids }.flatten.uniq
-
-         # find the node ids in an area that aren't part of ways
-         nodes_not_used_in_area = nodes_in_area.select { |node| node.ways.empty? }
-         points = nodes_not_used_in_area.collect { |n| [n.id, n.lon, n.lat, n.tags_as_hash] }
-
-         # find the relations used by those nodes and ways
-         relations = Relation.find_for_nodes(nodes_in_area.collect { |n| n.id }, :conditions => "visible = 1") +
-                  Relation.find_for_ways(way_ids, :conditions => "visible = 1")
-         relation_ids = relations.collect { |relation| relation.id }.uniq
-       end
-
-       [way_ids, points, relation_ids]
+    xmin -= 0.01; ymin -= 0.01
+    xmax += 0.01; ymax += 0.01
+    
+    # check boundary is sane and area within defined
+    # see /config/application.yml
+    check_boundaries(xmin, ymin, xmax, ymax)
+
+    if POTLATCH_USE_SQL then
+      ways = sql_find_ways_in_area(xmin, ymin, xmax, ymax)
+      points = sql_find_pois_in_area(xmin, ymin, xmax, ymax)
+      relations = sql_find_relations_in_area_and_ways(xmin, ymin, xmax, ymax, ways.collect {|x| x[0]})
+    else
+      # find the way ids in an area
+      nodes_in_area = Node.find_by_area(ymin, xmin, ymax, xmax, :conditions => ["current_nodes.visible = ?", true], :include => :ways)
+      ways = nodes_in_area.inject([]) { |sum, node| 
+        visible_ways = node.ways.select { |w| w.visible? }
+        sum + visible_ways.collect { |w| [w.id,w.version] }
+      }.uniq
+      ways.delete([])
+
+      # find the node ids in an area that aren't part of ways
+      nodes_not_used_in_area = nodes_in_area.select { |node| node.ways.empty? }
+      points = nodes_not_used_in_area.collect { |n| [n.id, n.lon, n.lat, n.tags, n.version] }
+
+      # find the relations used by those nodes and ways
+      relations = Relation.find_for_nodes(nodes_in_area.collect { |n| n.id }, :conditions => {:visible => true}) +
+                  Relation.find_for_ways(ways.collect { |w| w[0] }, :conditions => {:visible => true})
+      relations = relations.collect { |relation| [relation.id,relation.version] }.uniq
+    end
+
+    [0,ways, points, relations]
+
+  rescue Exception => err
+    [-2,"Sorry - I can't get the map for that area."]
   end
 
   # Find deleted ways in current bounding box (similar to whichways, but ways
   # with a deleted node only - not POIs or relations).
 
   def whichways_deleted(xmin, ymin, xmax, ymax) #:doc:
-       xmin -= 0.01; ymin -= 0.01
-       xmax += 0.01; ymax += 0.01
+    xmin -= 0.01; ymin -= 0.01
+    xmax += 0.01; ymax += 0.01
 
-       nodes_in_area = Node.find_by_area(ymin, xmin, ymax, xmax, :conditions => "current_nodes.visible = 0 AND current_ways.visible = 0", :include => :ways_via_history)
-       way_ids = nodes_in_area.collect { |node| node.ways_via_history_ids }.flatten.uniq
+    # check boundary is sane and area within defined
+    # see /config/application.yml
+    begin
+      check_boundaries(xmin, ymin, xmax, ymax)
+    rescue Exception => err
+      return [-2,"Sorry - I can't get the map for that area."]
+    end
 
-       [way_ids]
+    nodes_in_area = Node.find_by_area(ymin, xmin, ymax, xmax, :conditions => ["current_ways.visible = ?", false], :include => :ways_via_history)
+    way_ids = nodes_in_area.collect { |node| node.ways_via_history_ids }.flatten.uniq
+
+    [0,way_ids]
   end
 
   # Get a way including nodes and tags.
-  # Returns 0 (success), a Potlatch-style array of points, and a hash of tags.
+  # Returns the way id, a Potlatch-style array of points, a hash of tags, and the version number.
 
   def getway(wayid) #:doc:
-       if POTLATCH_USE_SQL then
-         points = sql_get_nodes_in_way(wayid)
-         tags = sql_get_tags_in_way(wayid)
-       else
-         # Ideally we would do ":include => :nodes" here but if we do that
-         # then rails only seems to return the first copy of a node when a
-         # way includes a node more than once
-         way = Way.find(wayid)
-         points = way.nodes.collect do |node|
-               nodetags=node.tags_as_hash
-               nodetags.delete('created_by')
-               [node.lon, node.lat, node.id, nodetags]
-         end
-         tags = way.tags
-       end
-
-       [wayid, points, tags]
+    if POTLATCH_USE_SQL then
+      points = sql_get_nodes_in_way(wayid)
+      tags = sql_get_tags_in_way(wayid)
+      version = sql_get_way_version(wayid)
+      else
+        # Ideally we would do ":include => :nodes" here but if we do that
+        # then rails only seems to return the first copy of a node when a
+        # way includes a node more than once
+        begin
+          way = Way.find(wayid)
+        rescue ActiveRecord::RecordNotFound
+          return [wayid,[],{}]
+        end
+
+        # check case where way has been deleted or doesn't exist
+        return [wayid,[],{}] if way.nil? or !way.visible
+
+        points = way.nodes.collect do |node|
+        nodetags=node.tags
+        nodetags.delete('created_by')
+        [node.lon, node.lat, node.id, nodetags]
+      end
+      tags = way.tags
+      version = way.version
+    end
+
+    [wayid, points, tags, version]
   end
 
   # Get an old version of a way, and all constituent nodes.
   #
-  # For undelete (version=0), always uses the most recent version of each node, 
-  # even if it's moved.  For revert (version=1+), uses the node in existence 
+  # For undelete (version<0), always uses the most recent version of each node, 
+  # even if it's moved.  For revert (version >= 0), uses the node in existence 
   # at the time, generating a new id if it's still visible and has been moved/
   # retagged.
+  #
+  # Returns:
+  # 0. success code, 
+  # 1. id, 
+  # 2. array of points, 
+  # 3. hash of tags, 
+  # 4. version, 
+  # 5. is this the current, visible version? (boolean)
 
   def getway_old(id, version) #:doc:
-       if version < 0
-         old_way = OldWay.find(:first, :conditions => ['visible = 1 AND id = ?', id], :order => 'version DESC')
-         points = old_way.get_nodes_undelete
-       else
-         old_way = OldWay.find(:first, :conditions => ['id = ? AND version = ?', id, version])
-         points = old_way.get_nodes_revert
-       end
-
-       old_way.tags['history'] = "Retrieved from v#{old_way.version}"
-
-       [0, id, points, old_way.tags, old_way.version]
+    if version < 0
+      old_way = OldWay.find(:first, :conditions => ['visible = ? AND id = ?', true, id], :order => 'version DESC')
+      points = old_way.get_nodes_undelete unless old_way.nil?
+    else
+      old_way = OldWay.find(:first, :conditions => ['id = ? AND version = ?', id, version])
+      points = old_way.get_nodes_revert unless old_way.nil?
+    end
+
+    if old_way.nil?
+      return [-1, id, [], {}, -1,0]
+    else
+      curway=Way.find(id)
+      old_way.tags['history'] = "Retrieved from v#{old_way.version}"
+      return [0, id, points, old_way.tags, old_way.version, (curway.version==old_way.version and curway.visible)]
+    end
   end
   
   # Find history of a way. Returns 'way', id, and 
   # an array of previous versions.
 
   def getway_history(wayid) #:doc:
-       history = Way.find(wayid).old_ways.reverse.collect do |old_way|
-         user = old_way.user.data_public? ? old_way.user.display_name : 'anonymous'
-         uid  = old_way.user.data_public? ? old_way.user.id : 0
-         [old_way.version, old_way.timestamp.strftime("%d %b %Y, %H:%M"), old_way.visible ? 1 : 0, user, uid]
-       end
-
-       ['way',wayid,history]
+    begin
+      history = Way.find(wayid).old_ways.reverse.collect do |old_way|
+        user_object = old_way.changeset.user
+        user = user_object.data_public? ? user_object.display_name : 'anonymous'
+        uid  = user_object.data_public? ? user_object.id : 0
+        [old_way.version, old_way.timestamp.strftime("%d %b %Y, %H:%M"), old_way.visible ? 1 : 0, user, uid]
+      end
+
+      return ['way',wayid,history]
+    rescue ActiveRecord::RecordNotFound
+      return ['way', wayid, []]
+    end
   end
 
   # Find history of a node. Returns 'node', id, and 
   # an array of previous versions.
 
   def getnode_history(nodeid) #:doc:
-       history = Node.find(nodeid).old_nodes.reverse.collect do |old_node|
-         user = old_node.user.data_public? ? old_node.user.display_name : 'anonymous'
-         uid  = old_node.user.data_public? ? old_node.user.id : 0
-         [old_node.timestamp.to_i, old_node.timestamp.strftime("%d %b %Y, %H:%M"), old_node.visible ? 1 : 0, user, uid]
-       end
+    history = Node.find(nodeid).old_nodes.reverse.collect do |old_node|
+      user_object = old_node.changeset.user
+      user = user_object.data_public? ? user_object.display_name : 'anonymous'
+      uid  = user_object.data_public? ? user_object.id : 0
+      [old_node.version, old_node.timestamp.strftime("%d %b %Y, %H:%M"), old_node.visible ? 1 : 0, user, uid]
+    end
+    
+    return ['node',nodeid,history]
+  rescue ActiveRecord::RecordNotFound
+    return ['node', nodeid, []]
+  end
 
-       ['node',nodeid,history]
+  # Find GPS traces with specified name/id.
+  # Returns array listing GPXs, each one comprising id, name and description.
+  
+  def findgpx(searchterm, usertoken)
+    uid = getuserid(usertoken)
+    if !uid then return -1,"You must be logged in to search for GPX traces." end
+
+    gpxs = []
+    if searchterm.to_i>0 then
+      gpx = Trace.find(searchterm.to_i, :conditions => ["visible=? AND (public=? OR user_id=?)",true,true,uid] )
+      if gpx then
+        gpxs.push([gpx.id, gpx.name, gpx.description])
+      end
+    else
+      Trace.find(:all, :limit => 21, :conditions => ["visible=? AND (public=? OR user_id=?) AND MATCH(name) AGAINST (?)",true,true,uid,searchterm] ).each do |gpx|
+      gpxs.push([gpx.id, gpx.name, gpx.description])
+         end
+       end
+    gpxs
   end
 
   # Get a relation with all tags and members.
   # Returns:
   # 0. relation id,
   # 1. hash of tags,
-  # 2. list of members.
+  # 2. list of members,
+  # 3. version.
   
   def getrelation(relid) #:doc:
-       rel = Relation.find(relid)
-
-       [relid, rel.tags, rel.members]
+    begin
+      rel = Relation.find(relid)
+    rescue ActiveRecord::RecordNotFound
+      return [relid, {}, []]
+    end
+
+    return [relid, {}, [], nil] if rel.nil? or !rel.visible
+    [relid, rel.tags, rel.members, rel.version]
   end
 
   # Find relations with specified name/id.
   # Returns array of relations, each in same form as getrelation.
   
   def findrelations(searchterm)
-       rels = []
-       if searchterm.to_i>0 then
-         rel = Relation.find(searchterm.to_i)
-         if rel and rel.visible then
-           rels.push([rel.id, rel.tags, rel.members])
-         end
-       else
-         RelationTag.find(:all, :limit => 11, :conditions => ["match(v) against (?)", searchterm] ).each do |t|
-               if t.relation.visible then
+    rels = []
+    if searchterm.to_i>0 then
+      rel = Relation.find(searchterm.to_i)
+      if rel and rel.visible then
+        rels.push([rel.id, rel.tags, rel.members])
+      end
+    else
+      RelationTag.find(:all, :limit => 11, :conditions => ["match(v) against (?)", searchterm] ).each do |t|
+      if t.relation.visible then
              rels.push([t.relation.id, t.relation.tags, t.relation.members])
            end
          end
        end
-       rels
+    rels
   end
 
   # Save a relation.
@@ -272,50 +400,69 @@ class AmfController < ApplicationController
   # 1. original relation id (unchanged),
   # 2. new relation id.
 
-  def putrelation(renumberednodes, renumberedways, usertoken, relid, tags, members, visible) #:doc:
-       uid = getuserid(usertoken)
-       if !uid then return -1,"You are not logged in, so the relation could not be saved." end
-
-       relid = relid.to_i
-       visible = visible.to_i
-
-       # create a new relation, or find the existing one
-       if relid <= 0
-         rel = Relation.new
-       else
-         rel = Relation.find(relid)
-       end
-
-       # check the members are all positive, and correctly type
-       typedmembers = []
-       members.each do |m|
-         mid = m[1].to_i
-         if mid < 0
-               mid = renumberednodes[mid] if m[0] == 'node'
-               mid = renumberedways[mid] if m[0] == 'way'
-         end
+  def putrelation(renumberednodes, renumberedways, usertoken, changeset, version, relid, tags, members, visible) #:doc:
+    user = getuserid(usertoken)
+    if !user then return -1,"You are not logged in, so the relation could not be saved." end
+
+    relid = relid.to_i
+    visible = (visible.to_i != 0)
+
+    # create a new relation, or find the existing one
+    if relid > 0
+      relation = Relation.find(relid)
+    end
+    # We always need a new node, based on the data that has been sent to us
+    new_relation = Relation.new
+
+    # check the members are all positive, and correctly type
+    typedmembers = []
+    members.each do |m|
+      mid = m[1].to_i
+      if mid < 0
+        mid = renumberednodes[mid] if m[0] == 'node'
+        mid = renumberedways[mid] if m[0] == 'way'
+      end
       if mid
-           typedmembers << [m[0], mid, m[2]]
-         end
-       end
-
-       # assign new contents
-       rel.members = typedmembers
-       rel.tags = tags
-       rel.visible = visible
-       rel.user_id = uid
-
-       # check it then save it
-       # BUG: the following is commented out because it always fails on my
-       #  install. I think it's a Rails bug.
-
-       #if !rel.preconditions_ok?
-       #  return -2, "Relation preconditions failed"
-       #else
-         rel.save_with_history!
-       #end
-
-       [0, relid, rel.id]
+        typedmembers << [m[0], mid, m[2]]
+      end
+    end
+
+    # assign new contents
+    new_relation.members = typedmembers
+    new_relation.tags = tags
+    new_relation.visible = visible
+    new_relation.changeset_id = changeset
+    new_relation.version = version
+
+
+    if id <= 0
+      # We're creating the node
+      new_relation.create_with_history(user)
+    elsif visible
+      # We're updating the node
+      relation.update_from(new_relation, user)
+    else
+      # We're deleting the node
+      relation.delete_with_history!(new_relation, user)
+    end
+      
+    if id <= 0
+      return [0, relid, new_relation.id, new_relation.version]
+    else
+      return [0, relid, relation.id, relation.version]
+    end
+  rescue OSM::APIChangesetAlreadyClosedError => ex
+    return [-1, "The changeset #{ex.changeset.id} was closed at #{ex.changeset.closed_at}"]
+  rescue OSM::APIVersionMismatchError => ex
+    # Really need to check to see whether this is a server load issue, and the 
+    # last version was in the same changeset, or belongs to the same user, then
+    # we can return something different
+    return [-3, "You have taken too long to edit, please reload the area"]
+  rescue OSM::APIAlreadyDeletedError => ex
+    return [-1, "The object has already been deleted"]
+  rescue OSM::APIError => ex
+    # Some error that we don't specifically catch
+    return [-2, "Something really bad happened :-()"]
   end
 
   # Save a way to the database, including all nodes. Any nodes in the previous
@@ -325,315 +472,434 @@ class AmfController < ApplicationController
   # 0. '0' (code for success),
   # 1. original way id (unchanged),
   # 2. new way id,
-  # 3. hash of renumbered nodes (old id=>new id)
+  # 3. hash of renumbered nodes (old id=>new id),
+  # 4. version
 
-  def putway(renumberednodes, usertoken, originalway, points, attributes) #:doc:
+  def putway(renumberednodes, usertoken, changeset, originalway, points, attributes) #:doc:
 
-       # -- Initialise and carry out checks
+    # -- Initialise and carry out checks
        
-       uid = getuserid(usertoken)
-       if !uid then return -1,"You are not logged in, so the way could not be saved." end
-
-       originalway = originalway.to_i
-
-       points.each do |a|
-         if a[2] == 0 or a[2].nil? then return -2,"Server error - node with id 0 found in way #{originalway}." end
-         if a[1] == 90 then return -2,"Server error - node with lat -90 found in way #{originalway}." end
-       end
-
-       if points.length < 2 then return -2,"Server error - way is only #{points.length} points long." end
-
-       # -- Get unique nodes
-
-       if originalway < 0
-         way = Way.new
-         uniques = []
-       else
-         way = Way.find(originalway)
-         uniques = way.unshared_node_ids
-       end
-
-       # -- Compare nodes and save changes to any that have changed
-
-       nodes = []
-
-       points.each do |n|
-         lon = n[0].to_f
-         lat = n[1].to_f
-         id = n[2].to_i
-         savenode = false
-
-         if renumberednodes[id]
-           id = renumberednodes[id]
-         elsif id < 0
-               # Create new node
-               node = Node.new
-               savenode = true
-         else
-               node = Node.find(id)
-               nodetags=node.tags_as_hash
-               nodetags.delete('created_by')
-               if !fpcomp(lat, node.lat) or !fpcomp(lon, node.lon) or
-                  n[4] != nodetags or !node.visible?
-                 savenode = true
-               end
-         end
-
-         if savenode
-               node.user_id = uid
-           node.lat = lat
-        node.lon = lon
-           node.tags = Tags.join(n[4])
-           node.visible = true
-           node.save_with_history!
-
-               if id != node.id
-                 renumberednodes[id] = node.id
-                 id = node.id
-           end
-         end
-
-         uniques = uniques - [id]
-         nodes.push(id)
-       end
-
-       # -- Delete any unique nodes
+    user = getuser(usertoken)
+    if !user then return -1,"You are not logged in, so the way could not be saved." end
+
+    originalway = originalway.to_i
+
+    points.each do |a|
+      if a[2] == 0 or a[2].nil? then return -2,"Server error - node with id 0 found in way #{originalway}." end
+      if a[1] == 90 then return -2,"Server error - node with lat -90 found in way #{originalway}." end
+    end
+
+    if points.length < 2 then return -2,"Server error - way is only #{points.length} points long." end
+
+    # -- Get unique nodes
+
+    if originalway <= 0
+      uniques = []
+    else
+      way = Way.find(originalway)
+      uniques = way.unshared_node_ids
+    end
+    new_way = Way.new
+
+    # -- Compare nodes and save changes to any that have changed
+
+    nodes = []
+
+    points.each do |n|
+      lon = n[0].to_f
+      lat = n[1].to_f
+      id = n[2].to_i
+      version = n[3].to_i # FIXME which index does the version come in on????
+      savenode = false
+      # We always need a new node if we are saving it
+      new_node = Node.new
+
+
+      if renumberednodes[id]
+        id = renumberednodes[id]
+      end
+      if id <= 0
+        # Create new node
+        savenode = true
+      else
+        # Don't modify this node, make any changes you want to the new_node above
+        node = Node.find(id)
+        nodetags=node.tags
+        nodetags.delete('created_by')
+        if !fpcomp(lat, node.lat) or !fpcomp(lon, node.lon) or
+           n[4] != nodetags or !node.visible?
+          savenode = true
+        end
+      end
+
+      if savenode
+        new_node.changeset_id = changeset
+        new_node.lat = lat
+        new_node.lon = lon
+        new_node.tags = n[4]
+        new_node.visible = true
+        new_node.version = version
+        if id <= 0
+          # We're creating the node
+          new_node.create_with_history(user)
+        else
+          # We're updating the node (no delete here)
+          node.update_from(new_node, user)
+        end
+
+        if id != node.id
+          renumberednodes[id] = node.id
+          id = node.id
+        end
+      end
+
+      uniques = uniques - [id]
+      nodes.push(id)
+    end
+
+    # -- Delete any unique nodes
        
-       uniques.each do |n|
-         deleteitemrelations(n, 'node')
-
-         node = Node.find(n)
-         node.user_id = uid
-         node.visible = false
-         node.save_with_history!
-       end
-
-       # -- Save revised way
-
-       way.tags = attributes
-       way.nds = nodes
-       way.user_id = uid
-       way.visible = true
-       way.save_with_history!
-
-       [0, originalway, way.id, renumberednodes]
+    uniques.each do |n|
+      deleteitemrelations(n, 'node')
+
+      node = Node.find(n)
+      new_node = Node.new
+      new_node.changeset_id = changeset
+      new_node.version = version
+      node.delete_with_history!(new_node, user)
+    end
+
+    # -- Save revised way
+
+    if way.tags!=attributes or way.nds!=nodes or !way.visible?
+      new_way = Way.new
+      new_way.tags = attributes
+      new_way.nds = nodes
+      new_way.changeset_id = changeset
+      new_way.version = version
+      way.update_from(new_way, user)
+    end
+
+    [0, originalway, way.id, renumberednodes, way.version]
+  rescue OSM::APIChangesetAlreadyClosedError => ex
+    return [-1, "The changeset #{ex.changeset.id} was closed at #{ex.changeset.closed_at}"]
+  rescue OSM::APIVersionMismatchError => ex
+    # Really need to check to see whether this is a server load issue, and the 
+    # last version was in the same changeset, or belongs to the same user, then
+    # we can return something different
+    return [-3, "You have taken too long to edit, please reload the area"]
+  rescue OSM::APITooManyWayNodesError => ex
+    return [-1, "You have tried to upload a way with #{ex.provided}, however only #{ex.max} are allowed."]
+  rescue OSM::APIAlreadyDeletedError => ex
+    return [-1, "The object has already been deleted"]
+  rescue OSM::APIError => ex
+    # Some error that we don't specifically catch
+    return [-2, "Something really bad happened :-()"]
   end
 
   # Save POI to the database.
   # Refuses save if the node has since become part of a way.
-  # Returns:
+  # Returns array with:
   # 0. 0 (success),
   # 1. original node id (unchanged),
-  # 2. new node id.
-
-  def putpoi(usertoken, id, lon, lat, tags, visible) #:doc:
-       uid = getuserid(usertoken)
-       if !uid then return -1,"You are not logged in, so the point could not be saved." end
-
-       id = id.to_i
-       visible = (visible.to_i == 1)
-
-       if id > 0 then
-         node = Node.find(id)
-
-         if !visible then
-           unless node.ways.empty? then return -1,"The point has since become part of a way, so you cannot save it as a POI." end
-           deleteitemrelations(id, 'node')
-         end
-       else
-         node = Node.new
-       end
-
-       node.user_id = uid
-       node.lat = lat
-       node.lon = lon
-       node.tags = Tags.join(tags)
-       node.visible = visible
-       node.save_with_history!
-
-       [0, id, node.id]
+  # 2. new node id,
+  # 3. version.
+
+  def putpoi(usertoken, changeset, version, id, lon, lat, tags, visible) #:doc:
+    user = getuser(usertoken)
+    if !user then return -1,"You are not logged in, so the point could not be saved." end
+
+    id = id.to_i
+    visible = (visible.to_i == 1)
+
+    if id > 0 then
+      node = Node.find(id)
+
+      if !visible then
+        unless node.ways.empty? then return -1,"The point has since become part of a way, so you cannot save it as a POI." end
+        deleteitemrelations(id, 'node')
+      end
+    end
+    # We always need a new node, based on the data that has been sent to us
+    new_node = Node.new
+
+    new_node.changeset_id = changeset
+    new_node.version = version
+    new_node.lat = lat
+    new_node.lon = lon
+    new_node.tags = tags
+    if id <= 0 
+      # We're creating the node
+      new_node.create_with_history(user)
+    elsif visible
+      # We're updating the node
+      node.update_from(new_node, user)
+    else
+      # We're deleting the node
+      node.delete_with_history!(new_node, user)
+    end
+
+    if id <= 0
+      return [0, id, new_node.id, new_node.version]
+    else
+      return [0, id, node.id, node.version]
+    end
+  rescue OSM::APIChangesetAlreadyClosedError => ex
+    return [-1, "The changeset #{ex.changeset.id} was closed at #{ex.changeset.closed_at}"]
+  rescue OSM::APIVersionMismatchError => ex
+    # Really need to check to see whether this is a server load issue, and the 
+    # last version was in the same changeset, or belongs to the same user, then
+    # we can return something different
+    return [-3, "You have taken too long to edit, please reload the area"]
+  rescue OSM::APIAlreadyDeletedError => ex
+    return [-1, "The object has already been deleted"]
+  rescue OSM::APIError => ex
+    # Some error that we don't specifically catch
+    return [-2, "Something really bad happened :-()"]
   end
 
   # Read POI from database
   # (only called on revert: POIs are usually read by whichways).
   #
-  # Returns array of id, long, lat, hash of tags.
-
-  def getpoi(id,timestamp) #:doc:
-       if timestamp>0 then
-         n = OldNode.find(id, :conditions=>['UNIX_TIMESTAMP(timestamp)=?',timestamp])
-       else
-         n = Node.find(id)
-       end
-
-       if n
-         return [n.id, n.lon, n.lat, n.tags_as_hash]
-       else
-         return [nil, nil, nil, '']
-       end
+  # Returns array of id, long, lat, hash of tags, version.
+
+  def getpoi(id,version) #:doc:
+    if version>0 then
+        n = OldNode.find(id, :conditions=>['version=?',version])
+    else
+      n = Node.find(id)
+    end
+
+    if n
+      return [n.id, n.lon, n.lat, n.tags, n.version]
+    else
+      return [nil, nil, nil, {}, nil]
+    end
   end
 
   # Delete way and all constituent nodes. Also removes from any relations.
+  # Params:
+  # * The user token
+  # * the changeset id
+  # * the id of the way to change
+  # * the version of the way that was downloaded
+  # * a hash of the id and versions of all the nodes that are in the way, if any 
+  # of the nodes have been changed by someone else then, there is a problem!
   # Returns 0 (success), unchanged way id.
 
-  def deleteway(usertoken, way_id) #:doc:
-       uid = getuserid(usertoken)
-       if !uid then return -1,"You are not logged in, so the way could not be deleted." end
-
-       # FIXME: would be good not to make two history entries when removing
-       #                two nodes from the same relation
-       user = User.find(uid)
-       way = Way.find(way_id)
-       way.unshared_node_ids.each do |n|
-         deleteitemrelations(n, 'node')
-       end
-       deleteitemrelations(way_id, 'way')
-
-       way.delete_with_relations_and_nodes_and_history(user)  
-
-       [0, way_id]
+  def deleteway(usertoken, changeset_id, way_id, way_version, node_id_version) #:doc:
+    user = getuser(usertoken)
+    unless user then return -1,"You are not logged in, so the way could not be deleted." end
+    # Need a transaction so that if one item fails to delete, the whole delete fails.
+    Way.transaction do
+      way_id = way_id.to_i
+
+      # FIXME: would be good not to make two history entries when removing
+      #                 two nodes from the same relation
+      old_way = Way.find(way_id)
+      #old_way.unshared_node_ids.each do |n|
+      #  deleteitemrelations(n, 'node')
+      #end
+      #deleteitemrelations(way_id, 'way')
+
+   
+      #way.delete_with_relations_and_nodes_and_history(changeset_id.to_i)
+      old_way.unshared_node_ids.each do |node_id|
+        # delete the node
+        node = Node.find(node_id)
+        delete_node = Node.new
+        delete_node.version = node_id_version[node_id]
+        node.delete_with_history!(delete_node, user)
+      end
+      # delete the way
+      delete_way = Way.new
+      delete_way.version = way_version
+      old_way.delete_with_history!(delete_way, user)
+    end
+    [0, way_id]
+  rescue OSM::APIChangesetAlreadyClosedError => ex
+    return [-1, "The changeset #{ex.changeset.id} was closed at #{ex.changeset.closed_at}"]
+  rescue OSM::APIVersionMismatchError => ex
+    # Really need to check to see whether this is a server load issue, and the 
+    # last version was in the same changeset, or belongs to the same user, then
+    # we can return something different
+    return [-3, "You have taken too long to edit, please reload the area"]
+  rescue OSM::APIAlreadyDeletedError => ex
+    return [-1, "The object has already been deleted"]
+  rescue OSM::APIError => ex
+    # Some error that we don't specifically catch
+    return [-2, "Something really bad happened :-()"]
   end
 
 
   # ====================================================================
   # Support functions
 
+  # delete a way and its nodes that aren't part of other ways
+  # this functionality used to be in the model, however it is specific to amf
+  # controller
+  #def delete_unshared_nodes(changeset_id, way_id)
+  
   # Remove a node or way from all relations
-
-  def deleteitemrelations(objid, type) #:doc:
-       relations = RelationMember.find(:all, 
+  # FIXME needs version, changeset, and user
+  def deleteitemrelations(objid, type, version) #:doc:
+    relations = RelationMember.find(:all, 
                                                                        :conditions => ['member_type = ? and member_id = ?', type, objid], 
                                                                        :include => :relation).collect { |rm| rm.relation }.uniq
 
-       relations.each do |rel|
-         rel.members.delete_if { |x| x[0] == type and x[1] == objid }
-         rel.save_with_history!
-       end
+    relations.each do |rel|
+      rel.members.delete_if { |x| x[0] == type and x[1] == objid }
+      # FIXME need to create the new relation
+      new_rel = Relation.new
+      new_rel.version = version
+      new_rel.members = members
+      new_rel.changeset = changeset
+      rel.delete_with_history(new_rel, user)
+    end
   end
 
   # Break out node tags into a hash
   # (should become obsolete as of API 0.6)
 
   def tagstring_to_hash(a) #:doc:
-       tags={}
-       Tags.split(a) do |k, v|
-         tags[k]=v
-       end
-       tags
+    tags={}
+    Tags.split(a) do |k, v|
+      tags[k]=v
+    end
+    tags
   end
 
   # Authenticate token
-  # (could be removed if no-one uses the username+password form)
-
-  def getuserid(token) #:doc:
-       if (token =~ /^(.+)\+(.+)$/) then
-         user = User.authenticate(:username => $1, :password => $2)
-       else
-         user = User.authenticate(:token => token)
-       end
-
-       return user ? user.id : nil;
+  # (can also be of form user:pass)
+  # When we are writing to the api, we need the actual user model, 
+  # not just the id, hence this abstraction
+
+  def getuser(token) #:doc:
+    if (token =~ /^(.+)\:(.+)$/) then
+      user = User.authenticate(:username => $1, :password => $2)
+    else
+      user = User.authenticate(:token => token)
+    end
+    return user
+  end
+  
+  def getuserid(token)
+    user = getuser(token)
+    return user ? user.id : nil;
   end
 
   # Compare two floating-point numbers to within 0.0000001
 
   def fpcomp(a,b) #:doc:
-       return ((a/0.0000001).round==(b/0.0000001).round)
+    return ((a/0.0000001).round==(b/0.0000001).round)
   end
 
   # Send AMF response
   
   def sendresponse(results)
-       a,b=results.length.divmod(256)
-       render :content_type => "application/x-amf", :text => proc { |response, output| 
-         output.write 0.chr+0.chr+0.chr+0.chr+a.chr+b.chr
-         results.each do |k,v|
-               output.write(v)
-         end
-       }
+    a,b=results.length.divmod(256)
+    render :content_type => "application/x-amf", :text => proc { |response, output| 
+      # ** move amf writing loop into here - 
+      # basically we read the messages in first (into an array of some sort),
+      # then iterate through that array within here, and do all the AMF writing
+      output.write 0.chr+0.chr+0.chr+0.chr+a.chr+b.chr
+      results.each do |k,v|
+        output.write(v)
+      end
+    }
   end
 
 
   # ====================================================================
   # Alternative SQL queries for getway/whichways
 
-  def sql_find_way_ids_in_area(xmin,ymin,xmax,ymax)
-       sql=<<-EOF
-  SELECT DISTINCT current_way_nodes.id AS wayid
-               FROM current_way_nodes
-  INNER JOIN current_nodes ON current_nodes.id=current_way_nodes.node_id
-  INNER JOIN current_ways  ON current_ways.id =current_way_nodes.id
-          WHERE current_nodes.visible=1 
-                AND current_ways.visible=1 
-                AND #{OSM.sql_for_area(ymin, xmin, ymax, xmax, "current_nodes.")}
-       EOF
-       return ActiveRecord::Base.connection.select_all(sql).collect { |a| a['wayid'].to_i }
+  def sql_find_ways_in_area(xmin,ymin,xmax,ymax)
+    sql=<<-EOF
+    SELECT DISTINCT current_ways.id AS wayid,current_ways.version AS version
+      FROM current_way_nodes
+    INNER JOIN current_nodes ON current_nodes.id=current_way_nodes.node_id
+    INNER JOIN current_ways  ON current_ways.id =current_way_nodes.id
+       WHERE current_nodes.visible=TRUE 
+       AND current_ways.visible=TRUE 
+       AND #{OSM.sql_for_area(ymin, xmin, ymax, xmax, "current_nodes.")}
+    EOF
+    return ActiveRecord::Base.connection.select_all(sql).collect { |a| [a['wayid'].to_i,a['version'].to_i] }
   end
        
   def sql_find_pois_in_area(xmin,ymin,xmax,ymax)
-       sql=<<-EOF
-                 SELECT current_nodes.id,current_nodes.latitude*0.0000001 AS lat,current_nodes.longitude*0.0000001 AS lon,current_nodes.tags 
+    pois=[]
+    sql=<<-EOF
+                 SELECT current_nodes.id,current_nodes.latitude*0.0000001 AS lat,current_nodes.longitude*0.0000001 AS lon,current_nodes.version 
                        FROM current_nodes 
- LEFT OUTER JOIN current_way_nodes cwn ON cwn.node_id=current_nodes.id 
-                  WHERE current_nodes.visible=1
      LEFT OUTER JOIN current_way_nodes cwn ON cwn.node_id=current_nodes.id 
+                  WHERE current_nodes.visible=TRUE
                         AND cwn.id IS NULL
                         AND #{OSM.sql_for_area(ymin, xmin, ymax, xmax, "current_nodes.")}
-       EOF
-       return ActiveRecord::Base.connection.select_all(sql).collect { |n| [n['id'].to_i,n['lon'].to_f,n['lat'].to_f,tagstring_to_hash(n['tags'])] }
+    EOF
+    ActiveRecord::Base.connection.select_all(sql).each do |row|
+      poitags={}
+      ActiveRecord::Base.connection.select_all("SELECT k,v FROM current_node_tags WHERE id=#{row['id']}").each do |n|
+        poitags[n['k']]=n['v']
+      end
+      pois << [row['id'].to_i, row['lon'].to_f, row['lat'].to_f, poitags, row['version'].to_i]
+    end
+    pois
   end
        
   def sql_find_relations_in_area_and_ways(xmin,ymin,xmax,ymax,way_ids)
-       # ** It would be more Potlatchy to get relations for nodes within ways
-       #    during 'getway', not here
-       sql=<<-EOF
-         SELECT DISTINCT cr.id AS relid 
-               FROM current_relations cr
-  INNER JOIN current_relation_members crm ON crm.id=cr.id 
-  INNER JOIN current_nodes cn ON crm.member_id=cn.id AND crm.member_type='node' 
-          WHERE #{OSM.sql_for_area(ymin, xmin, ymax, xmax, "cn.")}
-       EOF
-       unless way_ids.empty?
-         sql+=<<-EOF
-          UNION
-         SELECT DISTINCT cr.id AS relid
-               FROM current_relations cr
-  INNER JOIN current_relation_members crm ON crm.id=cr.id
-          WHERE crm.member_type='way' 
-                AND crm.member_id IN (#{way_ids.join(',')})
-         EOF
-       end
-       return ActiveRecord::Base.connection.select_all(sql).collect { |a| a['relid'].to_i }.uniq
+    # ** It would be more Potlatchy to get relations for nodes within ways
+    #    during 'getway', not here
+    sql=<<-EOF
+      SELECT DISTINCT cr.id AS relid,cr.version AS version 
+      FROM current_relations cr
+      INNER JOIN current_relation_members crm ON crm.id=cr.id 
+      INNER JOIN current_nodes cn ON crm.member_id=cn.id AND crm.member_type='node' 
+       WHERE #{OSM.sql_for_area(ymin, xmin, ymax, xmax, "cn.")}
+      EOF
+    unless way_ids.empty?
+      sql+=<<-EOF
+       UNION
+        SELECT DISTINCT cr.id AS relid,cr.version AS version
+        FROM current_relations cr
+        INNER JOIN current_relation_members crm ON crm.id=cr.id
+         WHERE crm.member_type='way' 
+         AND crm.member_id IN (#{way_ids.join(',')})
+        EOF
+    end
+    return ActiveRecord::Base.connection.select_all(sql).collect { |a| [a['relid'].to_i,a['version'].to_i] }
   end
        
   def sql_get_nodes_in_way(wayid)
-       points=[]
-       sql=<<-EOF
-               SELECT latitude*0.0000001 AS lat,longitude*0.0000001 AS lon,current_nodes.id,tags 
-                 FROM current_way_nodes,current_nodes 
-                WHERE current_way_nodes.id=#{wayid.to_i} 
+    points=[]
+    sql=<<-EOF
+      SELECT latitude*0.0000001 AS lat,longitude*0.0000001 AS lon,current_nodes.id 
+      FROM current_way_nodes,current_nodes 
+       WHERE current_way_nodes.id=#{wayid.to_i} 
                   AND current_way_nodes.node_id=current_nodes.id 
-                  AND current_nodes.visible=1
-         ORDER BY sequence_id
+                  AND current_nodes.visible=TRUE
+      ORDER BY sequence_id
          EOF
-       ActiveRecord::Base.connection.select_all(sql).each do |row|
-         nodetags=tagstring_to_hash(row['tags'])
-         nodetags.delete('created_by')
-         points << [row['lon'].to_f,row['lat'].to_f,row['id'].to_i,nodetags]
-       end
-       points
+    ActiveRecord::Base.connection.select_all(sql).each do |row|
+      nodetags={}
+      ActiveRecord::Base.connection.select_all("SELECT k,v FROM current_node_tags WHERE id=#{row['id']}").each do |n|
+        nodetags[n['k']]=n['v']
+      end
+      nodetags.delete('created_by')
+      points << [row['lon'].to_f,row['lat'].to_f,row['id'].to_i,nodetags]
+    end
+    points
   end
        
   def sql_get_tags_in_way(wayid)
-       tags={}
-       ActiveRecord::Base.connection.select_all("SELECT k,v FROM current_way_tags WHERE id=#{wayid.to_i}").each do |row|
-         tags[row['k']]=row['v']
-       end
-       tags
+    tags={}
+    ActiveRecord::Base.connection.select_all("SELECT k,v FROM current_way_tags WHERE id=#{wayid.to_i}").each do |row|
+      tags[row['k']]=row['v']
+    end
+    tags
   end
 
+  def sql_get_way_version(wayid)
+    ActiveRecord::Base.connection.select_one("SELECT version FROM current_ways WHERE id=#{wayid.to_i}")
+  end
 end
 
-# Local Variables:
-# indent-tabs-mode: t
-# tab-width: 4
-# End:
index 6b36b41ae947e25fdfe035f5377e2cc652e0d3a5..0724a3712651cfd23e05fcd81c0fc38f24182be3 100644 (file)
@@ -11,12 +11,13 @@ class ApiController < ApplicationController
   @@count = COUNT
 
   # The maximum area you're allowed to request, in square degrees
-  MAX_REQUEST_AREA = 0.25
+  MAX_REQUEST_AREA = APP_CONFIG['max_request_area']
 
   # Number of GPS trace/trackpoints returned per-page
-  TRACEPOINTS_PER_PAGE = 5000
+  TRACEPOINTS_PER_PAGE = APP_CONFIG['tracepoints_per_page']
 
-  
+  # Get an XML response containing a list of tracepoints that have been uploaded
+  # within the specified bounding box, and in the specified page.
   def trackpoints
     @@count+=1
     #retrieve the page number
@@ -84,6 +85,15 @@ class ApiController < ApplicationController
     render :text => doc.to_s, :content_type => "text/xml"
   end
 
+  # This is probably the most common call of all. It is used for getting the 
+  # OSM data for a specified bounding box, usually for editing. First the
+  # bounding box (bbox) is checked to make sure that it is sane. All nodes 
+  # are searched, then all the ways that reference those nodes are found.
+  # All Nodes that are referenced by those ways are fetched and added to the list
+  # of nodes.
+  # Then all the relations that reference the already found nodes and ways are
+  # fetched. All the nodes and ways that are referenced by those ways are then 
+  # fetched. Finally all the xml is returned.
   def map
     GC.start
     @@count+=1
@@ -109,18 +119,19 @@ class ApiController < ApplicationController
       return
     end
 
-    @nodes = Node.find_by_area(min_lat, min_lon, max_lat, max_lon, :conditions => "visible = 1", :limit => APP_CONFIG['max_number_of_nodes']+1)
+    # FIXME um why is this area using a different order for the lat/lon from above???
+    @nodes = Node.find_by_area(min_lat, min_lon, max_lat, max_lon, :conditions => {:visible => true}, :limit => APP_CONFIG['max_number_of_nodes']+1)
     # get all the nodes, by tag not yet working, waiting for change from NickB
     # need to be @nodes (instance var) so tests in /spec can be performed
     #@nodes = Node.search(bbox, params[:tag])
 
     node_ids = @nodes.collect(&:id)
     if node_ids.length > APP_CONFIG['max_number_of_nodes']
-      report_error("You requested too many nodes (limit is 50,000). Either request a smaller area, or use planet.osm")
+      report_error("You requested too many nodes (limit is #{APP_CONFIG['max_number_of_nodes']}). Either request a smaller area, or use planet.osm")
       return
     end
     if node_ids.length == 0
-      render :text => "<osm version='0.5'></osm>", :content_type => "text/xml"
+      render :text => "<osm version='#{API_VERSION}' generator='#{GENERATOR}'></osm>", :content_type => "text/xml"
       return
     end
 
@@ -176,15 +187,15 @@ class ApiController < ApplicationController
       end
     end 
 
-    relations = Relation.find_for_nodes(visible_nodes.keys, :conditions => "visible = 1") +
-                Relation.find_for_ways(way_ids, :conditions => "visible = 1")
+    relations = Relation.find_for_nodes(visible_nodes.keys, :conditions => {:visible => true}) +
+                Relation.find_for_ways(way_ids, :conditions => {:visible => true})
 
     # we do not normally return the "other" partners referenced by an relation, 
     # e.g. if we return a way A that is referenced by relation X, and there's 
     # another way B also referenced, that is not returned. But we do make 
     # an exception for cases where an relation references another *relation*; 
     # in that case we return that as well (but we don't go recursive here)
-    relations += Relation.find_for_relations(relations.collect { |r| r.id }, :conditions => "visible = 1")
+    relations += Relation.find_for_relations(relations.collect { |r| r.id }, :conditions => {:visible => true})
 
     # this "uniq" may be slightly inefficient; it may be better to first collect and output
     # all node-related relations, then find the *not yet covered* way-related ones etc.
@@ -204,6 +215,8 @@ class ApiController < ApplicationController
     end
   end
 
+  # Get a list of the tiles that have changed within a specified time
+  # period
   def changes
     zoom = (params[:zoom] || '12').to_i
 
@@ -217,7 +230,7 @@ class ApiController < ApplicationController
     end
 
     if zoom >= 1 and zoom <= 16 and
-       endtime >= starttime and endtime - starttime <= 24.hours
+       endtime > starttime and endtime - starttime <= 24.hours
       mask = (1 << zoom) - 1
 
       tiles = Node.count(:conditions => ["timestamp BETWEEN ? AND ?", starttime, endtime],
@@ -245,21 +258,32 @@ class ApiController < ApplicationController
 
       render :text => doc.to_s, :content_type => "text/xml"
     else
-      render :nothing => true, :status => :bad_request
+      render :text => "Requested zoom is invalid, or the supplied start is after the end time, or the start duration is more than 24 hours", :status => :bad_request
     end
   end
 
+  # External apps that use the api are able to query the api to find out some 
+  # parameters of the API. It currently returns: 
+  # * minimum and maximum API versions that can be used.
+  # * maximum area that can be requested in a bbox request in square degrees
+  # * number of tracepoints that are returned in each tracepoints page
   def capabilities
     doc = OSM::API.new.get_xml_doc
 
     api = XML::Node.new 'api'
     version = XML::Node.new 'version'
-    version['minimum'] = '0.5';
-    version['maximum'] = '0.5';
+    version['minimum'] = "#{API_VERSION}";
+    version['maximum'] = "#{API_VERSION}";
     api << version
     area = XML::Node.new 'area'
     area['maximum'] = MAX_REQUEST_AREA.to_s;
     api << area
+    tracepoints = XML::Node.new 'tracepoints'
+    tracepoints['per_page'] = APP_CONFIG['tracepoints_per_page'].to_s
+    api << tracepoints
+    waynodes = XML::Node.new 'waynodes'
+    waynodes['maximum'] = APP_CONFIG['max_number_of_way_nodes'].to_s
+    api << waynodes
     
     doc.root << api
 
index ce13a6aa3a6ae625407d3ac7fe2eaa11f7ab6ed9..f5ea0063db38b76c7d9cdb74b536b1c8f25cd182 100644 (file)
@@ -8,7 +8,7 @@ class ApplicationController < ActionController::Base
 
   def authorize_web
     if session[:user]
-      @user = User.find(session[:user], :conditions => "visible = 1")
+      @user = User.find(session[:user], :conditions => {:visible => true})
     elsif session[:token]
       @user = User.authenticate(:token => session[:token])
       session[:user] = @user.id
@@ -22,7 +22,11 @@ class ApplicationController < ActionController::Base
     redirect_to :controller => 'user', :action => 'login', :referer => request.request_uri unless @user
   end
 
-  def authorize(realm='Web Password', errormessage="Couldn't authenticate you") 
+  ##
+  # sets up the @user object for use by other methods. this is mostly called
+  # from the authorize method, but can be called elsewhere if authorisation
+  # is optional.
+  def setup_user_auth
     username, passwd = get_auth_data # parse from headers
     # authenticate per-scheme
     if username.nil?
@@ -32,6 +36,11 @@ class ApplicationController < ActionController::Base
     else
       @user = User.authenticate(:username => username, :password => passwd) # basic auth
     end
+  end
+
+  def authorize(realm='Web Password', errormessage="Couldn't authenticate you") 
+    # make the @user object from any auth sources we have
+    setup_user_auth
 
     # handle authenticate pass/fail
     unless @user
@@ -71,7 +80,7 @@ class ApplicationController < ActionController::Base
   #  phrase from that, we can also put the error message into the status
   #  message. For now, rails won't let us)
   def report_error(message)
-    render :nothing => true, :status => :bad_request
+    render :text => message, :status => :bad_request
     # Todo: some sort of escaping of problem characters in the message
     response.headers['Error'] = message
   end
@@ -82,6 +91,8 @@ private
   def get_auth_data 
     if request.env.has_key? 'X-HTTP_AUTHORIZATION'          # where mod_rewrite might have put it 
       authdata = request.env['X-HTTP_AUTHORIZATION'].to_s.split 
+    elsif request.env.has_key? 'REDIRECT_X_HTTP_AUTHORIZATION'          # mod_fcgi 
+      authdata = request.env['REDIRECT_X_HTTP_AUTHORIZATION'].to_s.split 
     elsif request.env.has_key? 'HTTP_AUTHORIZATION'         # regular location
       authdata = request.env['HTTP_AUTHORIZATION'].to_s.split
     end 
index f3a04519cbd362b5f8133a1c7c079d7ef9dda7ff..60f580963cda9719326173b4479c7b70734a46c2 100644 (file)
@@ -17,14 +17,15 @@ class BrowseController < ApplicationController
      
       @name = @relation.tags['name'].to_s 
       if @name.length == 0:
-       @name = "#" + @relation.id.to_s
+          @name = "#" + @relation.id.to_s
       end
        
       @title = 'Relation | ' + (@name)
       @next = Relation.find(:first, :order => "id ASC", :conditions => [ "visible = true AND id > :id", { :id => @relation.id }] ) 
       @prev = Relation.find(:first, :order => "id DESC", :conditions => [ "visible = true AND id < :id", { :id => @relation.id }] ) 
     rescue ActiveRecord::RecordNotFound
-      render :nothing => true, :status => :not_found
+      @type = "relation"
+      render :action => "not_found", :status => :not_found
     end
   end
   
@@ -34,12 +35,13 @@ class BrowseController < ApplicationController
      
       @name = @relation.tags['name'].to_s 
       if @name.length == 0:
-       @name = "#" + @relation.id.to_s
+          @name = "#" + @relation.id.to_s
       end
        
       @title = 'Relation History | ' + (@name)
     rescue ActiveRecord::RecordNotFound
-      render :nothing => true, :status => :not_found
+      @type = "relation"
+      render :action => "not_found", :status => :not_found
     end
   end
   
@@ -49,14 +51,15 @@ class BrowseController < ApplicationController
      
       @name = @way.tags['name'].to_s 
       if @name.length == 0:
-       @name = "#" + @way.id.to_s
+          @name = "#" + @way.id.to_s
       end
        
       @title = 'Way | ' + (@name)
       @next = Way.find(:first, :order => "id ASC", :conditions => [ "visible = true AND id > :id", { :id => @way.id }] ) 
       @prev = Way.find(:first, :order => "id DESC", :conditions => [ "visible = true AND id < :id", { :id => @way.id }] ) 
     rescue ActiveRecord::RecordNotFound
-      render :nothing => true, :status => :not_found
+      @type = "way"
+      render :action => "not_found", :status => :not_found
     end
   end
   
@@ -66,12 +69,13 @@ class BrowseController < ApplicationController
      
       @name = @way.tags['name'].to_s 
       if @name.length == 0:
-       @name = "#" + @way.id.to_s
+          @name = "#" + @way.id.to_s
       end
        
       @title = 'Way History | ' + (@name)
     rescue ActiveRecord::RecordNotFound
-      render :nothing => true, :status => :not_found
+      @type = "way"
+      render :action => "not_found", :status => :not_found
     end
   end
 
@@ -81,14 +85,15 @@ class BrowseController < ApplicationController
      
       @name = @node.tags_as_hash['name'].to_s 
       if @name.length == 0:
-       @name = "#" + @node.id.to_s
+          @name = "#" + @node.id.to_s
       end
        
       @title = 'Node | ' + (@name)
       @next = Node.find(:first, :order => "id ASC", :conditions => [ "visible = true AND id > :id", { :id => @node.id }] ) 
       @prev = Node.find(:first, :order => "id DESC", :conditions => [ "visible = true AND id < :id", { :id => @node.id }] ) 
     rescue ActiveRecord::RecordNotFound
-      render :nothing => true, :status => :not_found
+      @type = "node"
+      render :action => "not_found", :status => :not_found
     end
   end
   
@@ -98,12 +103,29 @@ class BrowseController < ApplicationController
      
       @name = @node.tags_as_hash['name'].to_s 
       if @name.length == 0:
-       @name = "#" + @node.id.to_s
+          @name = "#" + @node.id.to_s
       end
        
       @title = 'Node History | ' + (@name)
     rescue ActiveRecord::RecordNotFound
-      render :nothing => true, :status => :not_found
+      @type = "way"
+      render :action => "not_found", :status => :not_found
+    end
+  end
+  
+  def changeset
+    begin
+      @changeset = Changeset.find(params[:id])
+      @node_pages, @nodes = paginate(:old_nodes, :conditions => {:changeset_id => @changeset.id}, :per_page => 20, :parameter => 'node_page')
+      @way_pages, @ways = paginate(:old_ways, :conditions => {:changeset_id => @changeset.id}, :per_page => 20, :parameter => 'way_page')
+      @relation_pages, @relations = paginate(:old_relations, :conditions => {:changeset_id => @changeset.id}, :per_page => 20, :parameter => 'relation_page')
+      
+      @title = "Changeset | #{@changeset.id}"
+      @next = Changeset.find(:first, :order => "id ASC", :conditions => [ "id > :id", { :id => @changeset.id }] ) 
+      @prev = Changeset.find(:first, :order => "id DESC", :conditions => [ "id < :id", { :id => @changeset.id }] ) 
+    rescue ActiveRecord::RecordNotFound
+      @type = "changeset"
+      render :action => "not_found", :status => :not_found
     end
   end
 end
diff --git a/app/controllers/changeset_controller.rb b/app/controllers/changeset_controller.rb
new file mode 100644 (file)
index 0000000..f7f4dc9
--- /dev/null
@@ -0,0 +1,385 @@
+# The ChangesetController is the RESTful interface to Changeset objects
+
+class ChangesetController < ApplicationController
+  require 'xml/libxml'
+
+  session :off
+  before_filter :authorize, :only => [:create, :update, :delete, :upload, :include, :close]
+  before_filter :check_write_availability, :only => [:create, :update, :delete, :upload, :include]
+  before_filter :check_read_availability, :except => [:create, :update, :delete, :upload, :download, :query]
+  after_filter :compress_output
+
+  # Help methods for checking boundary sanity and area size
+  include MapBoundary
+
+  # Helper methods for checking consistency
+  include ConsistencyValidations
+
+  # Create a changeset from XML.
+  def create
+    if request.put?
+      cs = Changeset.from_xml(request.raw_post, true)
+
+      if cs
+        cs.user_id = @user.id
+        cs.save_with_tags!
+        render :text => cs.id.to_s, :content_type => "text/plain"
+      else
+        render :nothing => true, :status => :bad_request
+      end
+    else
+      render :nothing => true, :status => :method_not_allowed
+    end
+  end
+
+  ##
+  # Return XML giving the basic info about the changeset. Does not 
+  # return anything about the nodes, ways and relations in the changeset.
+  def read
+    begin
+      changeset = Changeset.find(params[:id])
+      render :text => changeset.to_xml.to_s, :content_type => "text/xml"
+    rescue ActiveRecord::RecordNotFound
+      render :nothing => true, :status => :not_found
+    end
+  end
+  
+  ##
+  # marks a changeset as closed. this may be called multiple times
+  # on the same changeset, so is idempotent.
+  def close 
+    unless request.put?
+      render :nothing => true, :status => :method_not_allowed
+      return
+    end
+    
+    changeset = Changeset.find(params[:id])    
+    check_changeset_consistency(changeset, @user)
+
+    # to close the changeset, we'll just set its closed_at time to
+    # now. this might not be enough if there are concurrency issues, 
+    # but we'll have to wait and see.
+    changeset.set_closed_time_now
+
+    changeset.save!
+    render :nothing => true
+  rescue ActiveRecord::RecordNotFound
+    render :nothing => true, :status => :not_found
+  rescue OSM::APIError => ex
+    render ex.render_opts
+  end
+
+  ##
+  # insert a (set of) points into a changeset bounding box. this can only
+  # increase the size of the bounding box. this is a hint that clients can
+  # set either before uploading a large number of changes, or changes that
+  # the client (but not the server) knows will affect areas further away.
+  def expand_bbox
+    # only allow POST requests, because although this method is
+    # idempotent, there is no "document" to PUT really...
+    if request.post?
+      cs = Changeset.find(params[:id])
+      check_changeset_consistency(cs, @user)
+
+      # keep an array of lons and lats
+      lon = Array.new
+      lat = Array.new
+
+      # the request is in pseudo-osm format... this is kind-of an
+      # abuse, maybe should change to some other format?
+      doc = XML::Parser.string(request.raw_post).parse
+      doc.find("//osm/node").each do |n|
+        lon << n['lon'].to_f * GeoRecord::SCALE
+        lat << n['lat'].to_f * GeoRecord::SCALE
+      end
+
+      # add the existing bounding box to the lon-lat array
+      lon << cs.min_lon unless cs.min_lon.nil?
+      lat << cs.min_lat unless cs.min_lat.nil?
+      lon << cs.max_lon unless cs.max_lon.nil?
+      lat << cs.max_lat unless cs.max_lat.nil?
+
+      # collapse the arrays to minimum and maximum
+      cs.min_lon, cs.min_lat, cs.max_lon, cs.max_lat = 
+        lon.min, lat.min, lon.max, lat.max
+
+      # save the larger bounding box and return the changeset, which
+      # will include the bigger bounding box.
+      cs.save!
+      render :text => cs.to_xml.to_s, :content_type => "text/xml"
+
+    else
+      render :nothing => true, :status => :method_not_allowed
+    end
+
+  rescue ActiveRecord::RecordNotFound
+    render :nothing => true, :status => :not_found
+  rescue OSM::APIError => ex
+    render ex.render_opts
+  end
+
+  ##
+  # Upload a diff in a single transaction.
+  #
+  # This means that each change within the diff must succeed, i.e: that
+  # each version number mentioned is still current. Otherwise the entire
+  # transaction *must* be rolled back.
+  #
+  # Furthermore, each element in the diff can only reference the current
+  # changeset.
+  #
+  # Returns: a diffResult document, as described in 
+  # http://wiki.openstreetmap.org/index.php/OSM_Protocol_Version_0.6
+  def upload
+    # only allow POST requests, as the upload method is most definitely
+    # not idempotent, as several uploads with placeholder IDs will have
+    # different side-effects.
+    # see http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.1.2
+    unless request.post?
+      render :nothing => true, :status => :method_not_allowed
+      return
+    end
+
+    changeset = Changeset.find(params[:id])
+    check_changeset_consistency(changeset, @user)
+    
+    diff_reader = DiffReader.new(request.raw_post, changeset)
+    Changeset.transaction do
+      result = diff_reader.commit
+      render :text => result.to_s, :content_type => "text/xml"
+    end
+    
+  rescue ActiveRecord::RecordNotFound
+    render :nothing => true, :status => :not_found
+  rescue OSM::APIError => ex
+    render ex.render_opts
+  end
+
+  ##
+  # download the changeset as an osmChange document.
+  #
+  # to make it easier to revert diffs it would be better if the osmChange
+  # format were reversible, i.e: contained both old and new versions of 
+  # modified elements. but it doesn't at the moment...
+  #
+  # this method cannot order the database changes fully (i.e: timestamp and
+  # version number may be too coarse) so the resulting diff may not apply
+  # to a different database. however since changesets are not atomic this 
+  # behaviour cannot be guaranteed anyway and is the result of a design
+  # choice.
+  def download
+    changeset = Changeset.find(params[:id])
+    
+    # get all the elements in the changeset and stick them in a big array.
+    elements = [changeset.old_nodes, 
+                changeset.old_ways, 
+                changeset.old_relations].flatten
+    
+    # sort the elements by timestamp and version number, as this is the 
+    # almost sensible ordering available. this would be much nicer if 
+    # global (SVN-style) versioning were used - then that would be 
+    # unambiguous.
+    elements.sort! do |a, b| 
+      if (a.timestamp == b.timestamp)
+        a.version <=> b.version
+      else
+        a.timestamp <=> b.timestamp 
+      end
+    end
+    
+    # create an osmChange document for the output
+    result = OSM::API.new.get_xml_doc
+    result.root.name = "osmChange"
+
+    # generate an output element for each operation. note: we avoid looking
+    # at the history because it is simpler - but it would be more correct to 
+    # check these assertions.
+    elements.each do |elt|
+      result.root <<
+        if (elt.version == 1) 
+          # first version, so it must be newly-created.
+          created = XML::Node.new "create"
+          created << elt.to_xml_node
+        else
+          # get the previous version from the element history
+          prev_elt = elt.class.find(:first, :conditions => 
+                                    ['id = ? and version = ?',
+                                     elt.id, elt.version])
+          unless elt.visible
+            # if the element isn't visible then it must have been deleted, so
+            # output the *previous* XML
+            deleted = XML::Node.new "delete"
+            deleted << prev_elt.to_xml_node
+          else
+            # must be a modify, for which we don't need the previous version
+            # yet...
+            modified = XML::Node.new "modify"
+            modified << elt.to_xml_node
+          end
+        end
+    end
+
+    render :text => result.to_s, :content_type => "text/xml"
+            
+  rescue ActiveRecord::RecordNotFound
+    render :nothing => true, :status => :not_found
+  rescue OSM::APIError => ex
+    render ex.render_opts
+  end
+
+  ##
+  # query changesets by bounding box, time, user or open/closed status.
+  def query
+    # create the conditions that the user asked for. some or all of
+    # these may be nil.
+    conditions = conditions_bbox(params['bbox'])
+    conditions = cond_merge conditions, conditions_user(params['user'])
+    conditions = cond_merge conditions, conditions_time(params['time'])
+    conditions = cond_merge conditions, conditions_open(params['open'])
+
+    # create the results document
+    results = OSM::API.new.get_xml_doc
+
+    # add all matching changesets to the XML results document
+    Changeset.find(:all, 
+                   :conditions => conditions, 
+                   :limit => 100,
+                   :order => 'created_at desc').each do |cs|
+      results.root << cs.to_xml_node
+    end
+
+    render :text => results.to_s, :content_type => "text/xml"
+
+  rescue ActiveRecord::RecordNotFound
+    render :nothing => true, :status => :not_found
+  rescue OSM::APIError => ex
+    render ex.render_opts
+  end
+  
+  ##
+  # updates a changeset's tags. none of the changeset's attributes are
+  # user-modifiable, so they will be ignored.
+  #
+  # changesets are not (yet?) versioned, so we don't have to deal with
+  # history tables here. changesets are locked to a single user, however.
+  #
+  # after succesful update, returns the XML of the changeset.
+  def update
+    # request *must* be a PUT.
+    unless request.put?
+      render :nothing => true, :status => :method_not_allowed
+      return
+    end
+    
+    changeset = Changeset.find(params[:id])
+    new_changeset = Changeset.from_xml(request.raw_post)
+
+    unless new_changeset.nil?
+      check_changeset_consistency(changeset, @user)
+      changeset.update_from(new_changeset, @user)
+      render :text => changeset.to_xml, :mime_type => "text/xml"
+    else
+      
+      render :nothing => true, :status => :bad_request
+    end
+      
+  rescue ActiveRecord::RecordNotFound
+    render :nothing => true, :status => :not_found
+  rescue OSM::APIError => ex
+    render ex.render_opts
+  end
+
+private
+  #------------------------------------------------------------
+  # utility functions below.
+  #------------------------------------------------------------  
+
+  ##
+  # merge two conditions
+  def cond_merge(a, b)
+    if a and b
+      a_str = a.shift
+      b_str = b.shift
+      return [ a_str + " and " + b_str ] + a + b
+    elsif a 
+      return a
+    else b
+      return b
+    end
+  end
+
+  ##
+  # if a bounding box was specified then parse it and do some sanity 
+  # checks. this is mostly the same as the map call, but without the 
+  # area restriction.
+  def conditions_bbox(bbox)
+    unless bbox.nil?
+      raise OSM::APIBadUserInput.new("Bounding box should be min_lon,min_lat,max_lon,max_lat") unless bbox.count(',') == 3
+      bbox = sanitise_boundaries(bbox.split(/,/))
+      raise OSM::APIBadUserInput.new("Minimum longitude should be less than maximum.") unless bbox[0] <= bbox[2]
+      raise OSM::APIBadUserInput.new("Minimum latitude should be less than maximum.") unless bbox[1] <= bbox[3]
+      return ['min_lon < ? and max_lon > ? and min_lat < ? and max_lat > ?',
+              bbox[2] * GeoRecord::SCALE, bbox[0] * GeoRecord::SCALE, bbox[3]* GeoRecord::SCALE, bbox[1] * GeoRecord::SCALE]
+    else
+      return nil
+    end
+  end
+
+  ##
+  # restrict changesets to those by a particular user
+  def conditions_user(user)
+    unless user.nil?
+      # user input checking, we don't have any UIDs < 1
+      raise OSM::APIBadUserInput.new("invalid user ID") if user.to_i < 1
+
+      u = User.find(user.to_i)
+      # should be able to get changesets of public users only, or 
+      # our own changesets regardless of public-ness.
+      unless u.data_public?
+        # get optional user auth stuff so that users can see their own
+        # changesets if they're non-public
+        setup_user_auth
+        
+        raise OSM::APINotFoundError if @user.nil? or @user.id != u.id
+      end
+      return ['user_id = ?', u.id]
+    else
+      return nil
+    end
+  end
+
+  ##
+  # restrict changes to those during a particular time period
+  def conditions_time(time) 
+    unless time.nil?
+      # if there is a range, i.e: comma separated, then the first is 
+      # low, second is high - same as with bounding boxes.
+      if time.count(',') == 1
+        # check that we actually have 2 elements in the array
+        times = time.split(/,/)
+        raise OSM::APIBadUserInput.new("bad time range") if times.size != 2 
+
+        from, to = times.collect { |t| DateTime.parse(t) }
+        return ['closed_at >= ? and created_at <= ?', from, to]
+      else
+        # if there is no comma, assume its a lower limit on time
+        return ['closed_at >= ?', DateTime.parse(time)]
+      end
+    else
+      return nil
+    end
+    # stupid DateTime seems to throw both of these for bad parsing, so
+    # we have to catch both and ensure the correct code path is taken.
+  rescue ArgumentError => ex
+    raise OSM::APIBadUserInput.new(ex.message.to_s)
+  rescue RuntimeError => ex
+    raise OSM::APIBadUserInput.new(ex.message.to_s)
+  end
+
+  ##
+  # restrict changes to those which are open
+  def conditions_open(open)
+    return open.nil? ? nil : ['closed_at >= ?', DateTime.now]
+  end
+
+end
diff --git a/app/controllers/changeset_tag_controller.rb b/app/controllers/changeset_tag_controller.rb
new file mode 100644 (file)
index 0000000..3e8db3f
--- /dev/null
@@ -0,0 +1,9 @@
+class ChangesetTagController < ApplicationController
+  layout 'site'
+
+  def search
+    @tags = ChangesetTag.find(:all, :limit => 11, :conditions => ["match(v) against (?)", params[:query][:query].to_s] )
+  end
+
+
+end
index e0d6e44cdd8764f4aacc39bfcf8d664ca38ee83e..3592ccb4fb683ce50558ca14dd76580ac9e98fde 100644 (file)
@@ -38,6 +38,8 @@ class DiaryEntryController < ApplicationController
         redirect_to :controller => 'diary_entry', :action => 'view', :id => params[:id]
       end
     end
+  rescue ActiveRecord::RecordNotFound
+    render :action => "no_such_entry", :status => :not_found
   end
 
   def comment
@@ -54,7 +56,7 @@ class DiaryEntryController < ApplicationController
 
   def list
     if params[:display_name]
-      @this_user = User.find_by_display_name(params[:display_name], :conditions => "visible = 1")
+      @this_user = User.find_by_display_name(params[:display_name], :conditions => {:visible => true})
 
       if @this_user
         @title = @this_user.display_name + "'s diary"
@@ -70,7 +72,7 @@ class DiaryEntryController < ApplicationController
     else
       @title = "Users' diaries"
       @entry_pages, @entries = paginate(:diary_entries, :include => :user,
-                                        :conditions => "users.visible = 1",
+                                        :conditions => ["users.visible = ?", true],
                                         :order => 'created_at DESC',
                                         :per_page => 20)
     end
@@ -78,13 +80,13 @@ class DiaryEntryController < ApplicationController
 
   def rss
     if params[:display_name]
-      user = User.find_by_display_name(params[:display_name], :conditions => "visible = 1")
+      user = User.find_by_display_name(params[:display_name], :conditions => {:visible => true})
 
       if user
         @entries = DiaryEntry.find(:all, :conditions => ['user_id = ?', user.id], :order => 'created_at DESC', :limit => 20)
         @title = "OpenStreetMap diary entries for #{user.display_name}"
         @description = "Recent OpenStreetmap diary entries from #{user.display_name}"
-        @link = "http://www.openstreetmap.org/user/#{user.display_name}/diary"
+        @link = "http://#{SERVER_URL}/user/#{user.display_name}/diary"
 
         render :content_type => Mime::RSS
       else
@@ -92,21 +94,22 @@ class DiaryEntryController < ApplicationController
       end
     else
       @entries = DiaryEntry.find(:all, :include => :user,
-                                 :conditions => "users.visible = 1",
+                                 :conditions => ["users.visible = ?", true],
                                  :order => 'created_at DESC', :limit => 20)
       @title = "OpenStreetMap diary entries"
       @description = "Recent diary entries from users of OpenStreetMap"
-      @link = "http://www.openstreetmap.org/diary"
+      @link = "http://#{SERVER_URL}/diary"
 
       render :content_type => Mime::RSS
     end
   end
 
   def view
-    user = User.find_by_display_name(params[:display_name], :conditions => "visible = 1")
+    user = User.find_by_display_name(params[:display_name], :conditions => {:visible => true})
 
     if user
       @entry = DiaryEntry.find(:first, :conditions => ['user_id = ? AND id = ?', user.id, params[:id]])
+      @title = "Users' diaries | #{params[:display_name]}"
     else
       @not_found_user = params[:display_name]
 
index 85c0ac328f2fc0349bd733518f51dd343410a825..d2fa9bd5fa54744d80749285fb9a137d77b0a734 100644 (file)
@@ -4,11 +4,16 @@ class MessageController < ApplicationController
   before_filter :authorize_web
   before_filter :require_user
 
+  # Allow the user to write a new message to another user. This action also 
+  # deals with the sending of that message to the other user when the user
+  # clicks send.
+  # The user_id param is the id of the user that the message is being sent to.
   def new
     @title = 'send message'
+    @to_user = User.find(params[:user_id])
     if params[:message]
       @message = Message.new(params[:message])
-      @message.to_user_id = params[:user_id]
+      @message.to_user_id = @to_user.id
       @message.from_user_id = @user.id
       @message.sent_on = Time.now
    
@@ -20,8 +25,11 @@ class MessageController < ApplicationController
     else
       @title = params[:title]
     end
+  rescue ActiveRecord::RecordNotFound
+    render :action => 'no_such_user', :status => :not_found
   end
 
+  # Allow the user to reply to another message.
   def reply
     message = Message.find(params[:message_id], :conditions => ["to_user_id = ? or from_user_id = ?", @user.id, @user.id ])
     @body = "On #{message.sent_on} #{message.sender.display_name} wrote:\n\n#{message.body.gsub(/^/, '> ')}" 
@@ -29,18 +37,20 @@ class MessageController < ApplicationController
     @user_id = message.from_user_id
     render :action => 'new'
   rescue ActiveRecord::RecordNotFound
-    render :nothing => true, :status => :not_found
+    render :action => 'no_such_user', :status => :not_found
   end
 
+  # Show a message
   def read
     @title = 'read message'
     @message = Message.find(params[:message_id], :conditions => ["to_user_id = ? or from_user_id = ?", @user.id, @user.id ])
-    @message.message_read = 1 if @message.to_user_id == @user.id
+    @message.message_read = true if @message.to_user_id == @user.id
     @message.save
   rescue ActiveRecord::RecordNotFound
-    render :nothing => true, :status => :not_found
+    render :action => 'no_such_user', :status => :not_found
   end
 
+  # Display the list of messages that have been sent to the user.
   def inbox
     @title = 'inbox'
     if @user and params[:display_name] == @user.display_name
@@ -49,6 +59,7 @@ class MessageController < ApplicationController
     end
   end
 
+  # Display the list of messages that the user has sent to other users.
   def outbox
     @title = 'outbox'
     if @user and params[:display_name] == @user.display_name
@@ -57,15 +68,16 @@ class MessageController < ApplicationController
     end
   end
 
+  # Set the message as being read or unread.
   def mark
     if params[:message_id]
       id = params[:message_id]
       message = Message.find_by_id(id)
       if params[:mark] == 'unread'
-        message_read = 0 
+        message_read = false 
         mark_type = 'unread'
       else
-        message_read = 1
+        message_read = true
         mark_type = 'read'
       end
       message.message_read = message_read
@@ -74,5 +86,7 @@ class MessageController < ApplicationController
         redirect_to :controller => 'message', :action => 'inbox', :display_name => @user.display_name
       end
     end
+  rescue ActiveRecord::RecordNotFound
+    render :action => 'no_such_user', :status => :not_found
   end
 end
index edc3675e58382fce0b8b5801a2e7180ab280cec2..c03f3c4fbf26767135213e4e65c0d2b5de9f11fc 100644 (file)
@@ -11,20 +11,21 @@ class NodeController < ApplicationController
 
   # Create a node from XML.
   def create
-    if request.put?
-      node = Node.from_xml(request.raw_post, true)
-
-      if node
-        node.user_id = @user.id
-        node.visible = true
-        node.save_with_history!
+    begin
+      if request.put?
+        node = Node.from_xml(request.raw_post, true)
 
-        render :text => node.id.to_s, :content_type => "text/plain"
+        if node
+          node.create_with_history @user
+          render :text => node.id.to_s, :content_type => "text/plain"
+        else
+          render :nothing => true, :status => :bad_request
+        end
       else
-        render :nothing => true, :status => :bad_request
+        render :nothing => true, :status => :method_not_allowed
       end
-    else
-      render :nothing => true, :status => :method_not_allowed
+    rescue OSM::APIError => ex
+      render ex.render_opts
     end
   end
 
@@ -32,7 +33,7 @@ class NodeController < ApplicationController
   def read
     begin
       node = Node.find(params[:id])
-      if node.visible
+      if node.visible?
         response.headers['Last-Modified'] = node.timestamp.rfc822
         render :text => node.to_xml.to_s, :content_type => "text/xml"
        else
@@ -42,7 +43,7 @@ class NodeController < ApplicationController
       render :nothing => true, :status => :not_found
     end
   end
-
+  
   # Update a node from given XML
   def update
     begin
@@ -50,49 +51,40 @@ class NodeController < ApplicationController
       new_node = Node.from_xml(request.raw_post)
 
       if new_node and new_node.id == node.id
-        node.user_id = @user.id
-        node.latitude = new_node.latitude 
-        node.longitude = new_node.longitude
-        node.tags = new_node.tags
-        node.visible = true
-        node.save_with_history!
-
-        render :nothing => true
+        node.update_from(new_node, @user)
+        render :text => node.version.to_s, :content_type => "text/plain"
       else
         render :nothing => true, :status => :bad_request
       end
+    rescue OSM::APIError => ex
+      render ex.render_opts
     rescue ActiveRecord::RecordNotFound
       render :nothing => true, :status => :not_found
     end
   end
 
-  # Delete a node. Doesn't actually delete it, but retains its history in a wiki-like way.
-  # FIXME remove all the fricking SQL
+  # Delete a node. Doesn't actually delete it, but retains its history 
+  # in a wiki-like way. We therefore treat it like an update, so the delete
+  # method returns the new version number.
   def delete
     begin
       node = Node.find(params[:id])
-
-      if node.visible
-        if WayNode.find(:first, :joins => "INNER JOIN current_ways ON current_ways.id = current_way_nodes.id", :conditions => [ "current_ways.visible = 1 AND current_way_nodes.node_id = ?", node.id ])
-          render :text => "", :status => :precondition_failed
-        elsif RelationMember.find(:first, :joins => "INNER JOIN current_relations ON current_relations.id=current_relation_members.id", :conditions => [ "visible = 1 AND member_type='node' and member_id=?", params[:id]])
-          render :text => "", :status => :precondition_failed
-        else
-          node.user_id = @user.id
-          node.visible = 0
-          node.save_with_history!
-
-          render :nothing => true
-        end
+      new_node = Node.from_xml(request.raw_post)
+      
+      if new_node and new_node.id == node.id
+        node.delete_with_history!(new_node, @user)
+        render :text => node.version.to_s, :content_type => "text/plain"
       else
-        render :text => "", :status => :gone
+        render :nothing => true, :status => :bad_request
       end
     rescue ActiveRecord::RecordNotFound
       render :nothing => true, :status => :not_found
+    rescue OSM::APIError => ex
+      render ex.render_opts
     end
   end
 
-  # WTF does this do?
+  # Dump the details on many nodes whose ids are given in the "nodes" parameter.
   def nodes
     ids = params['nodes'].split(',').collect { |n| n.to_i }
 
index e27898336361832ed193c9d1b3c8de04ddb14a41..56397625c967490fdd1b365e664bb94588ede6a7 100644 (file)
@@ -22,4 +22,21 @@ class OldNodeController < ApplicationController
       render :nothing => true, :status => :internal_server_error
     end
   end
+  
+  def version
+    begin
+      old_node = OldNode.find(:first, :conditions => {:id => params[:id], :version => params[:version]} )
+      
+      response.headers['Last-Modified'] = old_node.timestamp.rfc822
+
+      doc = OSM::API.new.get_xml_doc
+      doc.root << old_node.to_xml_node
+
+      render :text => doc.to_s, :content_type => "text/xml"
+    rescue ActiveRecord::RecordNotFound
+      render :nothing => true, :status => :not_found
+    rescue
+      render :nothing => true, :status => :internal_server_error
+    end
+  end
 end
index 0b5aa89be885f18c03cf83c46097778efc839610..84d0b0c902c53d84ce4fd09f942ff220a9b87455 100644 (file)
@@ -2,6 +2,7 @@ class OldRelationController < ApplicationController
   require 'xml/libxml'
 
   session :off
+  before_filter :check_read_availability
   after_filter :compress_output
 
   def history
@@ -20,4 +21,21 @@ class OldRelationController < ApplicationController
       render :nothing => true, :status => :internal_server_error
     end
   end
+  
+  def version
+    begin
+      old_relation = OldRelation.find(:first, :conditions => {:id => params[:id], :version => params[:version]} )
+      
+      response.headers['Last-Modified'] = old_relation.timestamp.rfc822
+
+      doc = OSM::API.new.get_xml_doc
+      doc.root << old_relation.to_xml_node
+
+      render :text => doc.to_s, :content_type => "text/xml"
+    rescue ActiveRecord::RecordNotFound
+      render :nothing => true, :status => :not_found
+    rescue
+      render :nothing => true, :status => :internetal_service_error
+    end
+  end
 end
index e72c97a0078022650a8484d08c070b7d67fed6c7..da4e26d67be706e07aebd6297b38838ce73f7813 100644 (file)
@@ -13,7 +13,7 @@ class OldWayController < ApplicationController
 
       way.old_ways.each do |old_way|
         doc.root << old_way.to_xml_node
-     end
+      end
 
       render :text => doc.to_s, :content_type => "text/xml"
     rescue ActiveRecord::RecordNotFound
@@ -22,4 +22,21 @@ class OldWayController < ApplicationController
       render :nothing => true, :status => :internal_server_error
     end
   end
+  
+  def version
+    begin
+      old_way = OldWay.find(:first, :conditions => {:id => params[:id], :version => params[:version]} )
+      
+      response.headers['Last-Modified'] = old_way.timestamp.rfc822
+      
+      doc = OSM::API.new.get_xml_doc
+      doc.root << old_way.to_xml_node
+      
+      render :text => doc.to_s, :content_type => "text/xml"
+    rescue ActiveRecord::RecordNotFound
+      render :nothing => true, :status => :not_found
+    rescue
+      render :nothing => true, :status => :internal_server_error
+    end
+  end
 end
index 2b1ba6c753c70df6579d381facebc7bd451be754..93573b95f88f943e5e2b2b36b48a1aeb3ec3928b 100644 (file)
@@ -8,23 +8,23 @@ class RelationController < ApplicationController
   after_filter :compress_output
 
   def create
-    if request.put?
-      relation = Relation.from_xml(request.raw_post, true)
-
-      if relation
-        if !relation.preconditions_ok?
-          render :text => "", :status => :precondition_failed
-        else
-          relation.user_id = @user.id
-          relation.save_with_history!
-
-         render :text => relation.id.to_s, :content_type => "text/plain"
-        end
+    begin
+      if request.put?
+        relation = Relation.from_xml(request.raw_post, true)
+
+        # We assume that an exception has been thrown if there was an error 
+        # generating the relation
+        #if relation
+          relation.create_with_history @user
+          render :text => relation.id.to_s, :content_type => "text/plain"
+        #else
+         # render :text => "Couldn't get turn the input into a relation.", :status => :bad_request
+        #end
       else
-        render :nothing => true, :status => :bad_request
+        render :nothing => true, :status => :method_not_allowed
       end
-    else
-      render :nothing => true, :status => :method_not_allowed
+    rescue OSM::APIError => ex
+      render ex.render_opts
     end
   end
 
@@ -45,56 +45,38 @@ class RelationController < ApplicationController
   end
 
   def update
+    logger.debug request.raw_post
     begin
       relation = Relation.find(params[:id])
       new_relation = Relation.from_xml(request.raw_post)
 
       if new_relation and new_relation.id == relation.id
-        if !new_relation.preconditions_ok?
-          render :text => "", :status => :precondition_failed
-        else
-          relation.user_id = @user.id
-          relation.tags = new_relation.tags
-          relation.members = new_relation.members
-          relation.visible = true
-          relation.save_with_history!
-
-          render :nothing => true
-        end
+        relation.update_from new_relation, @user
+        render :text => relation.version.to_s, :content_type => "text/plain"
       else
         render :nothing => true, :status => :bad_request
       end
     rescue ActiveRecord::RecordNotFound
       render :nothing => true, :status => :not_found
-    rescue
-      render :nothing => true, :status => :internal_server_error
+    rescue OSM::APIError => ex
+      render ex.render_opts
     end
   end
 
   def delete
-#XXX check if member somewhere!
     begin
       relation = Relation.find(params[:id])
-
-      if relation.visible
-        if RelationMember.find(:first, :joins => "INNER JOIN current_relations ON current_relations.id=current_relation_members.id", :conditions => [ "visible = 1 AND member_type='relation' and member_id=?", params[:id]])
-          render :text => "", :status => :precondition_failed
-        else
-          relation.user_id = @user.id
-          relation.tags = []
-          relation.members = []
-          relation.visible = false
-          relation.save_with_history!
-
-          render :nothing => true
-        end
+      new_relation = Relation.from_xml(request.raw_post)
+      if new_relation and new_relation.id == relation.id
+        relation.delete_with_history!(new_relation, @user)
+        render :text => relation.version.to_s, :content_type => "text/plain"
       else
-        render :text => "", :status => :gone
+        render :nothing => true, :status => :bad_request
       end
+    rescue OSM::APIError => ex
+      render ex.render_opts
     rescue ActiveRecord::RecordNotFound
       render :nothing => true, :status => :not_found
-    rescue
-      render :nothing => true, :status => :internal_server_error
     end
   end
 
@@ -160,8 +142,7 @@ class RelationController < ApplicationController
         render :text => doc.to_s, :content_type => "text/xml"
 
       else
-
-        render :text => "", :status => :gone
+        render :nothing => true, :status => :gone
       end
 
     rescue ActiveRecord::RecordNotFound
@@ -184,8 +165,10 @@ class RelationController < ApplicationController
 
       render :text => doc.to_s, :content_type => "text/xml"
     else
-      render :nothing => true, :status => :bad_request
+      render :text => "You need to supply a comma separated list of ids.", :status => :bad_request
     end
+  rescue ActiveRecord::RecordNotFound
+    render :text => "Could not find one of the relations", :status => :not_found
   end
 
   def relations_for_way
@@ -199,12 +182,12 @@ class RelationController < ApplicationController
   end
 
   def relations_for_object(objtype)
-    relationids = RelationMember.find(:all, :conditions => ['member_type=? and member_id=?', objtype, params[:id]]).collect { |ws| ws.id }.uniq
+    relationids = RelationMember.find(:all, :conditions => ['member_type=? and member_id=?', objtype, params[:id]]).collect { |ws| ws.id[0] }.uniq
 
     doc = OSM::API.new.get_xml_doc
 
     Relation.find(relationids).each do |relation|
-      doc.root << relation.to_xml_node
+      doc.root << relation.to_xml_node if relation.visible
     end
 
     render :text => doc.to_s, :content_type => "text/xml"
index bcac1184454aa1bffd5ab5694bbdbb88398ee85e..022c304fb15b685dfb0bb39b20e097d25d34ec2d 100644 (file)
@@ -13,7 +13,7 @@ class TraceController < ApplicationController
     # from display name, pick up user id if one user's traces only
     display_name = params[:display_name]
     if target_user.nil? and !display_name.blank?
-      target_user = User.find(:first, :conditions => [ "visible = 1 and display_name = ?", display_name])
+      target_user = User.find(:first, :conditions => [ "visible = ? and display_name = ?", true, display_name])
     end
 
     # set title
@@ -34,15 +34,15 @@ class TraceController < ApplicationController
     # 4 - user's traces, not logged in as that user = all user's public traces
     if target_user.nil? # all traces
       if @user
-        conditions = ["(gpx_files.public = 1 OR gpx_files.user_id = ?)", @user.id] #1
+        conditions = ["(gpx_files.public = ? OR gpx_files.user_id = ?)", true, @user.id] #1
       else
-        conditions  = ["gpx_files.public = 1"] #2
+        conditions  = ["gpx_files.public = ?", true] #2
       end
     else
       if @user and @user == target_user
         conditions = ["gpx_files.user_id = ?", @user.id] #3 (check vs user id, so no join + can't pick up non-public traces by changing name)
       else
-        conditions = ["gpx_files.public = 1 AND gpx_files.user_id = ?", target_user.id] #4
+        conditions = ["gpx_files.public = ? AND gpx_files.user_id = ?", true, target_user.id] #4
       end
     end
     
@@ -53,7 +53,8 @@ class TraceController < ApplicationController
       conditions[0] += " AND gpx_files.id IN (#{files.join(',')})"
     end
     
-    conditions[0] += " AND gpx_files.visible = 1"
+    conditions[0] += " AND gpx_files.visible = ?"
+    conditions << true
 
     @trace_pages, @traces = paginate(:traces,
                                      :include => [:user, :tags],
@@ -194,7 +195,7 @@ class TraceController < ApplicationController
   end
 
   def georss
-    conditions = ["gpx_files.public = 1"]
+    conditions = ["gpx_files.public = ?", true]
 
     if params[:display_name]
       conditions[0] += " AND users.display_name = ?"
@@ -309,6 +310,17 @@ private
     else
       FileUtils.rm_f(filename)
     end
+    
+    # Finally save whether the user marked the trace as being public
+    if @trace.public?
+      if @user.trace_public_default.nil?
+        @user.preferences.create(:k => "gps.trace.public", :v => "default")
+      end
+    else
+      pref = @user.trace_public_default
+      pref.destroy unless pref.nil?
+    end
+    
   end
 
 end
index c658b201412a5bf2dda750e292458b38c945c1aa..825c9263582c8f66076cb891c495ef39610f3e3c 100644 (file)
@@ -77,7 +77,7 @@ class UserController < ApplicationController
   def lost_password
     @title = 'lost password'
     if params[:user] and params[:user][:email]
-      user = User.find_by_email(params[:user][:email], :conditions => "visible = 1")
+      user = User.find_by_email(params[:user][:email], :conditions => {:visible => true})
 
       if user
         token = user.tokens.create
@@ -117,6 +117,14 @@ class UserController < ApplicationController
   end
 
   def login
+    if session[:user]
+      # The user is logged in already, if the referer param exists, redirect them to that
+      if params[:referer]
+        redirect_to params[:referer]
+      else
+        redirect_to :controller => 'site', :action => 'index'
+      end
+    end
     @title = 'login'
     if params[:user]
       email_or_display_name = params[:user][:email]
@@ -217,7 +225,7 @@ class UserController < ApplicationController
   end
 
   def view
-    @this_user = User.find_by_display_name(params[:display_name], :conditions => "visible = 1")
+    @this_user = User.find_by_display_name(params[:display_name], :conditions => {:visible => true})
 
     if @this_user
       @title = @this_user.display_name
@@ -230,7 +238,7 @@ class UserController < ApplicationController
   def make_friend
     if params[:display_name]     
       name = params[:display_name]
-      new_friend = User.find_by_display_name(name, :conditions => "visible = 1")
+      new_friend = User.find_by_display_name(name, :conditions => {:visible => true})
       friend = Friend.new
       friend.user_id = @user.id
       friend.friend_user_id = new_friend.id
@@ -252,7 +260,7 @@ class UserController < ApplicationController
   def remove_friend
     if params[:display_name]     
       name = params[:display_name]
-      friend = User.find_by_display_name(name, :conditions => "visible = 1")
+      friend = User.find_by_display_name(name, :conditions => {:visible => true})
       if @user.is_friends_with?(friend)
         Friend.delete_all "user_id = #{@user.id} AND friend_user_id = #{friend.id}"
         flash[:notice] = "#{friend.display_name} was removed from your friends."
index 5594799293a1c9f79d492c54f79e128225369c04..3b56c257be32b822864b08f984c959d8f549fc27 100644 (file)
@@ -5,11 +5,9 @@ class UserPreferenceController < ApplicationController
   def read_one
     pref = UserPreference.find(@user.id, params[:preference_key])
 
-    if pref
-      render :text => pref.v.to_s
-    else
-      render :text => 'OH NOES! PREF NOT FOUND!', :status => 404
-    end
+    render :text => pref.v.to_s
+  rescue ActiveRecord::RecordNotFound => ex
+    render :text => 'OH NOES! PREF NOT FOUND!', :status => :not_found
   end
 
   def update_one
@@ -32,6 +30,8 @@ class UserPreferenceController < ApplicationController
     UserPreference.delete(@user.id, params[:preference_key])
 
     render :nothing => true
+  rescue ActiveRecord::RecordNotFound => ex
+    render :text => "param: #{params[:preference_key]} not found", :status => :not_found
   end
 
   # print out all the preferences as a big xml block
@@ -52,49 +52,43 @@ class UserPreferenceController < ApplicationController
 
   # update the entire set of preferences
   def update
-    begin
-      p = XML::Parser.new
-      p.string = request.raw_post
-      doc = p.parse
-
-      prefs = []
-
-      keyhash = {}
+    p = XML::Parser.new
+    p.string = request.raw_post
+    doc = p.parse
 
-      doc.find('//preferences/preference').each do |pt|
-        pref = UserPreference.new
+    prefs = []
 
-        unless keyhash[pt['k']].nil? # already have that key
-          render :text => 'OH NOES! CAN HAS UNIQUE KEYS?', :status => :not_acceptable
-          return
-        end
+    keyhash = {}
 
-        keyhash[pt['k']] = 1
-
-        pref.k = pt['k']
-        pref.v = pt['v']
-        pref.user_id = @user.id
-        prefs << pref
-      end
+    doc.find('//preferences/preference').each do |pt|
+      pref = UserPreference.new
 
-      if prefs.size > 150
-        render :text => 'Too many preferences', :status => :request_entity_too_large
-        return
+      unless keyhash[pt['k']].nil? # already have that key
+        render :text => 'OH NOES! CAN HAS UNIQUE KEYS?', :status => :not_acceptable
       end
 
-      # kill the existing ones
-      UserPreference.delete_all(['user_id = ?', @user.id])
+      keyhash[pt['k']] = 1
 
-      # save the new ones
-      prefs.each do |pref|
-        pref.save!
-      end
+      pref.k = pt['k']
+      pref.v = pt['v']
+      pref.user_id = @user.id
+      prefs << pref
+    end
 
-    rescue Exception => ex
-      render :text => 'OH NOES! FAIL!: ' + ex.to_s, :status => :internal_server_error
-      return
+    if prefs.size > 150
+      render :text => 'Too many preferences', :status => :request_entity_too_large
     end
 
+    # kill the existing ones
+    UserPreference.delete_all(['user_id = ?', @user.id])
+
+    # save the new ones
+    prefs.each do |pref|
+      pref.save!
+    end
     render :nothing => true
+
+  rescue Exception => ex
+    render :text => 'OH NOES! FAIL!: ' + ex.to_s, :status => :internal_server_error
   end
 end
index 3b6491cf0b3ceda5ed90ee7e1da5e49579c6af21..80c75d91c3644d4c72fb04c8ea8758687217fa5a 100644 (file)
@@ -8,23 +8,22 @@ class WayController < ApplicationController
   after_filter :compress_output
 
   def create
-    if request.put?
-      way = Way.from_xml(request.raw_post, true)
-
-      if way
-        if !way.preconditions_ok?
-          render :text => "", :status => :precondition_failed
-        else
-          way.user_id = @user.id
-          way.save_with_history!
+    begin
+      if request.put?
+        way = Way.from_xml(request.raw_post, true)
 
+        if way
+          way.create_with_history @user
           render :text => way.id.to_s, :content_type => "text/plain"
+        else
+          render :nothing => true, :status => :bad_request
         end
       else
-        render :nothing => true, :status => :bad_request
+        render :nothing => true, :status => :method_not_allowed
       end
-    else
-      render :nothing => true, :status => :method_not_allowed
+    rescue OSM::APIError => ex
+      logger.warn request.raw_post
+      render ex.render_opts
     end
   end
 
@@ -39,6 +38,8 @@ class WayController < ApplicationController
       else
         render :text => "", :status => :gone
       end
+    rescue OSM::APIError => ex
+      render ex.render_opts
     rescue ActiveRecord::RecordNotFound
       render :nothing => true, :status => :not_found
     end
@@ -50,20 +51,14 @@ class WayController < ApplicationController
       new_way = Way.from_xml(request.raw_post)
 
       if new_way and new_way.id == way.id
-        if !new_way.preconditions_ok?
-          render :text => "", :status => :precondition_failed
-        else
-          way.user_id = @user.id
-          way.tags = new_way.tags
-          way.nds = new_way.nds
-          way.visible = true
-          way.save_with_history!
-
-          render :nothing => true
-        end
+        way.update_from(new_way, @user)
+        render :text => way.version.to_s, :content_type => "text/plain"
       else
         render :nothing => true, :status => :bad_request
       end
+    rescue OSM::APIError => ex
+      logger.warn request.raw_post
+      render ex.render_opts
     rescue ActiveRecord::RecordNotFound
       render :nothing => true, :status => :not_found
     end
@@ -73,14 +68,16 @@ class WayController < ApplicationController
   def delete
     begin
       way = Way.find(params[:id])
-      way.delete_with_relations_and_history(@user)
-
-      # if we get here, all is fine, otherwise something will catch below.  
-      render :nothing => true
-    rescue OSM::APIAlreadyDeletedError
-      render :text => "", :status => :gone
-    rescue OSM::APIPreconditionFailedError
-      render :text => "", :status => :precondition_failed
+      new_way = Way.from_xml(request.raw_post)
+
+      if new_way and new_way.id == way.id
+        way.delete_with_history!(new_way, @user)
+        render :text => way.version.to_s, :content_type => "text/plain"
+      else
+        render :nothing => true, :status => :bad_request
+      end
+    rescue OSM::APIError => ex
+      render ex.render_opts
     rescue ActiveRecord::RecordNotFound
       render :nothing => true, :status => :not_found
     end
@@ -92,7 +89,7 @@ class WayController < ApplicationController
 
       if way.visible
         nd_ids = way.nds + [-1]
-        nodes = Node.find(:all, :conditions => "visible = 1 AND id IN (#{nd_ids.join(',')})")
+        nodes = Node.find(:all, :conditions => ["visible = ? AND id IN (#{nd_ids.join(',')})", true])
 
         # Render
         doc = OSM::API.new.get_xml_doc
@@ -130,13 +127,19 @@ class WayController < ApplicationController
     end
   end
 
+  ##
+  # returns all the ways which are currently using the node given in the 
+  # :id parameter. note that this used to return deleted ways as well, but
+  # this seemed not to be the expected behaviour, so it was removed.
   def ways_for_node
-    wayids = WayNode.find(:all, :conditions => ['node_id = ?', params[:id]]).collect { |ws| ws.id[0] }.uniq
+    wayids = WayNode.find(:all, 
+                          :conditions => ['node_id = ?', params[:id]]
+                          ).collect { |ws| ws.id[0] }.uniq
 
     doc = OSM::API.new.get_xml_doc
 
     Way.find(wayids).each do |way|
-      doc.root << way.to_xml_node
+      doc.root << way.to_xml_node if way.visible
     end
 
     render :text => doc.to_s, :content_type => "text/xml"
index c86ad5b71c0701bd1b3f65a7ce7381b4afcc74ad..34302a8af2d7f8af01c963d1b7ba1b2059216742 100644 (file)
@@ -1,2 +1,5 @@
 module BrowseHelper
+  def link_to_page(page, page_param)
+    return link_to(page, page_param => page)
+  end
 end
diff --git a/app/models/changeset.rb b/app/models/changeset.rb
new file mode 100644 (file)
index 0000000..3e0ba9f
--- /dev/null
@@ -0,0 +1,242 @@
+class Changeset < ActiveRecord::Base
+  require 'xml/libxml'
+
+  belongs_to :user
+
+  has_many :changeset_tags, :foreign_key => 'id'
+  
+  has_many :nodes
+  has_many :ways
+  has_many :relations
+  has_many :old_nodes
+  has_many :old_ways
+  has_many :old_relations
+  
+  validates_presence_of :id, :on => :update
+  validates_presence_of :user_id, :created_at, :closed_at, :num_changes
+  validates_uniqueness_of :id
+  validates_numericality_of :id, :on => :update, :integer_only => true
+  validates_numericality_of :min_lat, :max_lat, :min_lon, :max_lat, :allow_nil => true, :integer_only => true
+  validates_numericality_of :user_id,  :integer_only => true
+  validates_numericality_of :num_changes, :integer_only => true, :greater_than_or_equal_to => 0
+  validates_associated :user
+
+  # over-expansion factor to use when updating the bounding box
+  EXPAND = 0.1
+
+  # maximum number of elements allowed in a changeset
+  MAX_ELEMENTS = 50000
+
+  # maximum time a changeset is allowed to be open for (note that this
+  # is in days - so one hour is Rational(1,24)).
+  MAX_TIME_OPEN = 1
+
+  # idle timeout increment, one hour as a rational number of days.
+  # NOTE: DO NOT CHANGE THIS TO 1.hour! when this was done the idle
+  # timeout changed to 1 second, which meant all changesets closed 
+  # almost immediately.
+  IDLE_TIMEOUT = Rational(1,24)
+
+  # Use a method like this, so that we can easily change how we
+  # determine whether a changeset is open, without breaking code in at 
+  # least 6 controllers
+  def is_open?
+    # a changeset is open (that is, it will accept further changes) when
+    # it has not yet run out of time and its capacity is small enough.
+    # note that this may not be a hard limit - due to timing changes and
+    # concurrency it is possible that some changesets may be slightly 
+    # longer than strictly allowed or have slightly more changes in them.
+    return ((closed_at > DateTime.now) and (num_changes <= MAX_ELEMENTS))
+  end
+
+  def set_closed_time_now
+    unless is_open?
+      self.closed_at = DateTime.now
+    end
+  end
+  
+  def self.from_xml(xml, create=false)
+    begin
+      p = XML::Parser.new
+      p.string = xml
+      doc = p.parse
+
+      cs = Changeset.new
+
+      doc.find('//osm/changeset').each do |pt|
+        if create
+          cs.created_at = Time.now
+          # initial close time is 1h ahead, but will be increased on each
+          # modification.
+          cs.closed_at = Time.now + IDLE_TIMEOUT
+          # initially we have no changes in a changeset
+          cs.num_changes = 0
+        end
+
+        pt.find('tag').each do |tag|
+          cs.add_tag_keyval(tag['k'], tag['v'])
+        end
+      end
+    rescue Exception => ex
+      cs = nil
+    end
+
+    return cs
+  end
+
+  ##
+  # returns the bounding box of the changeset. it is possible that some
+  # or all of the values will be nil, indicating that they are undefined.
+  def bbox
+    @bbox ||= [ min_lon, min_lat, max_lon, max_lat ]
+  end
+
+  ##
+  # expand the bounding box to include the given bounding box. also, 
+  # expand a little bit more in the direction of the expansion, so that
+  # further expansions may be unnecessary. this is an optimisation 
+  # suggested on the wiki page by kleptog.
+  def update_bbox!(array)
+    # ensure that bbox is cached and has no nils in it. if there are any
+    # nils, just use the bounding box update to write over them.
+    @bbox = bbox.zip(array).collect { |a, b| a.nil? ? b : a }
+
+    # FIXME - this looks nasty and violates DRY... is there any prettier 
+    # way to do this? 
+    @bbox[0] = array[0] + EXPAND * (@bbox[0] - @bbox[2]) if array[0] < @bbox[0]
+    @bbox[1] = array[1] + EXPAND * (@bbox[1] - @bbox[3]) if array[1] < @bbox[1]
+    @bbox[2] = array[2] + EXPAND * (@bbox[2] - @bbox[0]) if array[2] > @bbox[2]
+    @bbox[3] = array[3] + EXPAND * (@bbox[3] - @bbox[1]) if array[3] > @bbox[3]
+
+    # update active record. rails 2.1's dirty handling should take care of
+    # whether this object needs saving or not.
+    self.min_lon, self.min_lat, self.max_lon, self.max_lat = @bbox
+  end
+
+  ##
+  # the number of elements is also passed in so that we can ensure that
+  # a single changeset doesn't contain too many elements. this, of course,
+  # destroys the optimisation described in the bbox method above.
+  def add_changes!(elements)
+    self.num_changes += elements
+  end
+
+  def tags_as_hash
+    return tags
+  end
+
+  def tags
+    unless @tags
+      @tags = {}
+      self.changeset_tags.each do |tag|
+        @tags[tag.k] = tag.v
+      end
+    end
+    @tags
+  end
+
+  def tags=(t)
+    @tags = t
+  end
+
+  def add_tag_keyval(k, v)
+    @tags = Hash.new unless @tags
+    @tags[k] = v
+  end
+
+  def save_with_tags!
+    t = Time.now
+
+    # do the changeset update and the changeset tags update in the
+    # same transaction to ensure consistency.
+    Changeset.transaction do
+      # set the auto-close time to be one hour in the future unless
+      # that would make it more than 24h long, in which case clip to
+      # 24h, as this has been decided is a reasonable time limit.
+      if (closed_at - created_at) > (MAX_TIME_OPEN - IDLE_TIMEOUT)
+        self.closed_at = created_at + MAX_TIME_OPEN
+      else
+        self.closed_at = DateTime.now + IDLE_TIMEOUT
+      end
+      self.save!
+
+      tags = self.tags
+      ChangesetTag.delete_all(['id = ?', self.id])
+
+      tags.each do |k,v|
+        tag = ChangesetTag.new
+        tag.k = k
+        tag.v = v
+        tag.id = self.id
+        tag.save!
+      end
+    end
+  end
+  
+  def to_xml
+    doc = OSM::API.new.get_xml_doc
+    doc.root << to_xml_node()
+    return doc
+  end
+  
+  def to_xml_node(user_display_name_cache = nil)
+    el1 = XML::Node.new 'changeset'
+    el1['id'] = self.id.to_s
+
+    user_display_name_cache = {} if user_display_name_cache.nil?
+
+    if user_display_name_cache and user_display_name_cache.key?(self.user_id)
+      # use the cache if available
+    elsif self.user.data_public?
+      user_display_name_cache[self.user_id] = self.user.display_name
+    else
+      user_display_name_cache[self.user_id] = nil
+    end
+
+    el1['user'] = user_display_name_cache[self.user_id] unless user_display_name_cache[self.user_id].nil?
+    el1['uid'] = self.user_id.to_s if self.user.data_public?
+
+    self.tags.each do |k,v|
+      el2 = XML::Node.new('tag')
+      el2['k'] = k.to_s
+      el2['v'] = v.to_s
+      el1 << el2
+    end
+    
+    el1['created_at'] = self.created_at.xmlschema
+    el1['closed_at'] = self.closed_at.xmlschema unless is_open?
+    el1['open'] = is_open?.to_s
+
+    el1['min_lon'] = (bbox[0].to_f / GeoRecord::SCALE).to_s unless bbox[0].nil?
+    el1['min_lat'] = (bbox[1].to_f / GeoRecord::SCALE).to_s unless bbox[1].nil?
+    el1['max_lon'] = (bbox[2].to_f / GeoRecord::SCALE).to_s unless bbox[2].nil?
+    el1['max_lat'] = (bbox[3].to_f / GeoRecord::SCALE).to_s unless bbox[3].nil?
+    
+    # NOTE: changesets don't include the XML of the changes within them,
+    # they are just structures for tagging. to get the osmChange of a
+    # changeset, see the download method of the controller.
+
+    return el1
+  end
+
+  ##
+  # update this instance from another instance given and the user who is
+  # doing the updating. note that this method is not for updating the
+  # bounding box, only the tags of the changeset.
+  def update_from(other, user)
+    # ensure that only the user who opened the changeset may modify it.
+    unless user.id == self.user_id 
+      raise OSM::APIUserChangesetMismatchError 
+    end
+    
+    # can't change a closed changeset
+    unless is_open?
+      raise OSM::APIChangesetAlreadyClosedError.new(self)
+    end
+
+    # copy the other's tags
+    self.tags = other.tags
+
+    save_with_tags!
+  end
+end
diff --git a/app/models/changeset_tag.rb b/app/models/changeset_tag.rb
new file mode 100644 (file)
index 0000000..6a414a0
--- /dev/null
@@ -0,0 +1,8 @@
+class ChangesetTag < ActiveRecord::Base
+  belongs_to :changeset, :foreign_key => 'id'
+
+  validates_presence_of :id
+  validates_length_of :k, :v, :maximum => 255, :allow_blank => true
+  validates_uniqueness_of :id, :scope => :k
+  validates_numericality_of :id, :only_integer => true
+end
index dd1f9882a7a4726ffff14147292b1891a0584893..4b2058b9d9b7b684426b5c82d223596b4e371288 100644 (file)
@@ -5,6 +5,8 @@ class DiaryEntry < ActiveRecord::Base
                             :order => "diary_comments.id"
 
   validates_presence_of :title, :body
+  validates_length_of :title, :within => 1..255
+  validates_length_of :language, :within => 2..3, :allow_nil => true
   validates_numericality_of :latitude, :allow_nil => true
   validates_numericality_of :longitude, :allow_nil => true
   validates_associated :user
index 97e411192b0df5cedbd89d7255f236ac7fb2cf35..464c5502837b56c5ffdf409ff32288f6b5779cac 100644 (file)
@@ -1,8 +1,12 @@
+require 'validators'
+
 class Message < ActiveRecord::Base
   belongs_to :sender, :class_name => "User", :foreign_key => :from_user_id
   belongs_to :recipient, :class_name => "User", :foreign_key => :to_user_id
 
-  validates_presence_of :title, :body, :sent_on
+  validates_presence_of :title, :body, :sent_on, :sender, :recipient
+  validates_length_of :title, :within => 1..255
   validates_inclusion_of :message_read, :in => [ true, false ]
   validates_associated :sender, :recipient
+  validates_as_utf8 :title
 end
index cec755f4765bfc35e9679256934512be093f74da..f2ad3a78add2196f117214816489d3c7bdc686cb 100644 (file)
@@ -2,27 +2,34 @@ class Node < ActiveRecord::Base
   require 'xml/libxml'
 
   include GeoRecord
+  include ConsistencyValidations
 
   set_table_name 'current_nodes'
-  
-  validates_presence_of :user_id, :timestamp
-  validates_inclusion_of :visible, :in => [ true, false ]
-  validates_numericality_of :latitude, :longitude
-  validate :validate_position
 
-  belongs_to :user
+  belongs_to :changeset
 
   has_many :old_nodes, :foreign_key => :id
 
   has_many :way_nodes
   has_many :ways, :through => :way_nodes
 
+  has_many :node_tags, :foreign_key => :id
+  
   has_many :old_way_nodes
   has_many :ways_via_history, :class_name=> "Way", :through => :old_way_nodes, :source => :way
 
   has_many :containing_relation_members, :class_name => "RelationMember", :as => :member
   has_many :containing_relations, :class_name => "Relation", :through => :containing_relation_members, :source => :relation, :extend => ObjectFinder
 
+  validates_presence_of :id, :on => :update
+  validates_presence_of :timestamp,:version,  :changeset_id
+  validates_uniqueness_of :id
+  validates_inclusion_of :visible, :in => [ true, false ]
+  validates_numericality_of :latitude, :longitude, :changeset_id, :version, :integer_only => true
+  validates_numericality_of :id, :on => :update, :integer_only => true
+  validate :validate_position
+  validates_associated :changeset
+
   # Sanity check the latitude and longitude and add an error if it's broken
   def validate_position
     errors.add_to_base("Node is not in the world") unless in_world?
@@ -50,7 +57,7 @@ class Node < ActiveRecord::Base
     #conditions = keys.join(' AND ')
  
     find_by_area(min_lat, min_lon, max_lat, max_lon,
-                    :conditions => 'visible = 1',
+                    :conditions => {:visible => true},
                     :limit => APP_CONFIG['max_number_of_nodes']+1)  
   end
 
@@ -60,83 +67,148 @@ class Node < ActiveRecord::Base
       p = XML::Parser.new
       p.string = xml
       doc = p.parse
-  
-      node = Node.new
 
       doc.find('//osm/node').each do |pt|
-        node.lat = pt['lat'].to_f
-        node.lon = pt['lon'].to_f
+        return Node.from_xml_node(pt, create)
+      end
+    rescue LibXML::XML::Error => ex
+      raise OSM::APIBadXMLError.new("node", xml, ex.message)
+    end
+  end
 
-        return nil unless node.in_world?
+  def self.from_xml_node(pt, create=false)
+    node = Node.new
+    
+    raise OSM::APIBadXMLError.new("node", pt, "lat missing") if pt['lat'].nil?
+    raise OSM::APIBadXMLError.new("node", pt, "lon missing") if pt['lon'].nil?
+    node.lat = pt['lat'].to_f
+    node.lon = pt['lon'].to_f
+    raise OSM::APIBadXMLError.new("node", pt, "changeset id missing") if pt['changeset'].nil?
+    node.changeset_id = pt['changeset'].to_i
 
-        unless create
-          if pt['id'] != '0'
-            node.id = pt['id'].to_i
-          end
-        end
+    raise OSM::APIBadUserInput.new("The node is outside this world") unless node.in_world?
 
-        node.visible = pt['visible'] and pt['visible'] == 'true'
+    # version must be present unless creating
+    raise OSM::APIBadXMLError.new("node", pt, "Version is required when updating") unless create or not pt['version'].nil?
+    node.version = create ? 0 : pt['version'].to_i
 
-        if create
-          node.timestamp = Time.now
-        else
-          if pt['timestamp']
-            node.timestamp = Time.parse(pt['timestamp'])
-          end
-        end
+    unless create
+      if pt['id'] != '0'
+        node.id = pt['id'].to_i
+      end
+    end
 
-        tags = []
+    # visible if it says it is, or as the default if the attribute
+    # is missing.
+    # Don't need to set the visibility, when it is set explicitly in the create/update/delete
+    #node.visible = pt['visible'].nil? or pt['visible'] == 'true'
 
-        pt.find('tag').each do |tag|
-          tags << [tag['k'],tag['v']]
-        end
+    # We don't care about the time, as it is explicitly set on create/update/delete
 
-        node.tags = Tags.join(tags)
-      end
-    rescue
-      node = nil
+    tags = []
+
+    pt.find('tag').each do |tag|
+      node.add_tag_key_val(tag['k'],tag['v'])
     end
 
     return node
   end
 
-  # Save this node with the appropriate OldNode object to represent it's history.
-  def save_with_history!
+  ##
+  # the bounding box around a node, which is used for determining the changeset's
+  # bounding box
+  def bbox
+    [ longitude, latitude, longitude, latitude ]
+  end
+
+  # Should probably be renamed delete_from to come in line with update
+  def delete_with_history!(new_node, user)
+    unless self.visible
+      raise OSM::APIAlreadyDeletedError.new
+    end
+
+    # need to start the transaction here, so that the database can 
+    # provide repeatable reads for the used-by checks. this means it
+    # shouldn't be possible to get race conditions.
     Node.transaction do
-      self.timestamp = Time.now
-      self.save!
-      old_node = OldNode.from_node(self)
-      old_node.save!
+      check_consistency(self, new_node, user)
+      if WayNode.find(:first, :joins => "INNER JOIN current_ways ON current_ways.id = current_way_nodes.id", :conditions => [ "current_ways.visible = ? AND current_way_nodes.node_id = ?", true, self.id ])
+        raise OSM::APIPreconditionFailedError.new
+      elsif RelationMember.find(:first, :joins => "INNER JOIN current_relations ON current_relations.id=current_relation_members.id", :conditions => [ "visible = ? AND member_type='node' and member_id=? ", true, self.id])
+        raise OSM::APIPreconditionFailedError.new
+      else
+        self.changeset_id = new_node.changeset_id
+        self.visible = false
+        
+        # update the changeset with the deleted position
+        changeset.update_bbox!(bbox)
+        
+        save_with_history!
+      end
     end
   end
 
-  # Turn this Node in to a complete OSM XML object with <osm> wrapper
+  def update_from(new_node, user)
+    check_consistency(self, new_node, user)
+
+    # update changeset with *old* position first
+    changeset.update_bbox!(bbox);
+
+    # FIXME logic needs to be double checked
+    self.changeset_id = new_node.changeset_id
+    self.latitude = new_node.latitude 
+    self.longitude = new_node.longitude
+    self.tags = new_node.tags
+    self.visible = true
+
+    # update changeset with *new* position
+    changeset.update_bbox!(bbox);
+
+    save_with_history!
+  end
+  
+  def create_with_history(user)
+    check_create_consistency(self, user)
+    self.id = nil
+    self.version = 0
+    self.visible = true
+
+    # update the changeset to include the new location
+    changeset.update_bbox!(bbox)
+
+    save_with_history!
+  end
+
   def to_xml
     doc = OSM::API.new.get_xml_doc
     doc.root << to_xml_node()
     return doc
   end
 
-  # Turn this Node in to an XML Node without the <osm> wrapper.
   def to_xml_node(user_display_name_cache = nil)
     el1 = XML::Node.new 'node'
     el1['id'] = self.id.to_s
     el1['lat'] = self.lat.to_s
     el1['lon'] = self.lon.to_s
-
+    el1['version'] = self.version.to_s
+    el1['changeset'] = self.changeset_id.to_s
+    
     user_display_name_cache = {} if user_display_name_cache.nil?
 
-    if user_display_name_cache and user_display_name_cache.key?(self.user_id)
+    if user_display_name_cache and user_display_name_cache.key?(self.changeset.user_id)
       # use the cache if available
-    elsif self.user.data_public?
-      user_display_name_cache[self.user_id] = self.user.display_name
+    elsif self.changeset.user.data_public?
+      user_display_name_cache[self.changeset.user_id] = self.changeset.user.display_name
     else
-      user_display_name_cache[self.user_id] = nil
+      user_display_name_cache[self.changeset.user_id] = nil
     end
 
-    el1['user'] = user_display_name_cache[self.user_id] unless user_display_name_cache[self.user_id].nil?
+    if not user_display_name_cache[self.changeset.user_id].nil?
+      el1['user'] = user_display_name_cache[self.changeset.user_id]
+      el1['uid'] = self.changeset.user_id.to_s
+    end
 
-    Tags.split(self.tags) do |k,v|
+    self.tags.each do |k,v|
       el2 = XML::Node.new('tag')
       el2['k'] = k.to_s
       el2['v'] = v.to_s
@@ -148,12 +220,79 @@ class Node < ActiveRecord::Base
     return el1
   end
 
-  # Return the node's tags as a Hash of keys and their values
   def tags_as_hash
-    hash = {}
-    Tags.split(self.tags) do |k,v|
-      hash[k] = v
+    return tags
+  end
+
+  def tags
+    unless @tags
+      @tags = {}
+      self.node_tags.each do |tag|
+        @tags[tag.k] = tag.v
+      end
+    end
+    @tags
+  end
+
+  def tags=(t)
+    @tags = t 
+  end 
+
+  def add_tag_key_val(k,v)
+    @tags = Hash.new unless @tags
+
+    # duplicate tags are now forbidden, so we can't allow values
+    # in the hash to be overwritten.
+    raise OSM::APIDuplicateTagsError.new("node", self.id, k) if @tags.include? k
+
+    @tags[k] = v
+  end
+
+  ##
+  # are the preconditions OK? this is mainly here to keep the duck
+  # typing interface the same between nodes, ways and relations.
+  def preconditions_ok?
+    in_world?
+  end
+
+  ##
+  # dummy method to make the interfaces of node, way and relation
+  # more consistent.
+  def fix_placeholders!(id_map)
+    # nodes don't refer to anything, so there is nothing to do here
+  end
+  
+  private
+
+  def save_with_history!
+    t = Time.now
+    Node.transaction do
+      self.version += 1
+      self.timestamp = t
+      self.save!
+
+      # Create a NodeTag
+      tags = self.tags
+      NodeTag.delete_all(['id = ?', self.id])
+      tags.each do |k,v|
+        tag = NodeTag.new
+        tag.k = k 
+        tag.v = v 
+        tag.id = self.id
+        tag.save!
+      end 
+
+      # Create an OldNode
+      old_node = OldNode.from_node(self)
+      old_node.timestamp = t
+      old_node.save_with_dependencies!
+
+      # tell the changeset we updated one element only
+      changeset.add_changes! 1
+
+      # save the changeset in case of bounding box updates
+      changeset.save!
     end
-    hash
   end
+  
 end
diff --git a/app/models/node_tag.rb b/app/models/node_tag.rb
new file mode 100644 (file)
index 0000000..4942601
--- /dev/null
@@ -0,0 +1,10 @@
+class NodeTag < ActiveRecord::Base
+  set_table_name 'current_node_tags'
+
+  belongs_to :node, :foreign_key => 'id'
+  
+  validates_presence_of :id
+  validates_length_of :k, :v, :maximum => 255, :allow_blank => true
+  validates_uniqueness_of :id, :scope => :k
+  validates_numericality_of :id, :only_integer => true
+end
index 76eab8427b2c570cce79846887706eb6c10923b6..be115c53eaaaa026d38559ad63c5316326872968 100644 (file)
@@ -1,25 +1,21 @@
 class OldNode < ActiveRecord::Base
   include GeoRecord
+  include ConsistencyValidations
 
   set_table_name 'nodes'
   
-  validates_presence_of :user_id, :timestamp
+  validates_presence_of :changeset_id, :timestamp
   validates_inclusion_of :visible, :in => [ true, false ]
   validates_numericality_of :latitude, :longitude
   validate :validate_position
+  validates_associated :changeset
 
-  belongs_to :user
+  belongs_to :changeset
  
   def validate_position
     errors.add_to_base("Node is not in the world") unless in_world?
   end
 
-  def in_world?
-    return false if self.lat < -90 or self.lat > 90
-    return false if self.lon < -180 or self.lon > 180
-    return true
-  end
-
   def self.from_node(node)
     old_node = OldNode.new
     old_node.latitude = node.latitude
@@ -27,19 +23,30 @@ class OldNode < ActiveRecord::Base
     old_node.visible = node.visible
     old_node.tags = node.tags
     old_node.timestamp = node.timestamp
-    old_node.user_id = node.user_id
+    old_node.changeset_id = node.changeset_id
     old_node.id = node.id
+    old_node.version = node.version
     return old_node
   end
+  
+  def to_xml
+    doc = OSM::API.new.get_xml_doc
+    doc.root << to_xml_node()
+    return doc
+  end
 
   def to_xml_node
     el1 = XML::Node.new 'node'
     el1['id'] = self.id.to_s
     el1['lat'] = self.lat.to_s
     el1['lon'] = self.lon.to_s
-    el1['user'] = self.user.display_name if self.user.data_public?
+    el1['changeset'] = self.changeset.id.to_s
+    if self.changeset.user.data_public?
+      el1['user'] = self.changeset.user.display_name
+      el1['uid'] = self.changeset.user.id.to_s
+    end
 
-    Tags.split(self.tags) do |k,v|
+    self.tags.each do |k,v|
       el2 = XML::Node.new('tag')
       el2['k'] = k.to_s
       el2['v'] = v.to_s
@@ -48,24 +55,54 @@ class OldNode < ActiveRecord::Base
 
     el1['visible'] = self.visible.to_s
     el1['timestamp'] = self.timestamp.xmlschema
+    el1['version'] = self.version.to_s
     return el1
   end
-  
-  def tags_as_hash
-    hash = {}
-    Tags.split(self.tags) do |k,v|
-      hash[k] = v
+
+  def save_with_dependencies!
+    save!
+    #not sure whats going on here
+    clear_aggregation_cache
+    clear_association_cache
+    #ok from here
+    @attributes.update(OldNode.find(:first, :conditions => ['id = ? AND timestamp = ? AND version = ?', self.id, self.timestamp, self.version]).instance_variable_get('@attributes'))
+   
+    self.tags.each do |k,v|
+      tag = OldNodeTag.new
+      tag.k = k
+      tag.v = v
+      tag.id = self.id
+      tag.version = self.version
+      tag.save!
     end
-    hash
   end
 
-  # Pretend we're not in any ways
-  def ways
-    return []
+  def tags
+    unless @tags
+        @tags = Hash.new
+        OldNodeTag.find(:all, :conditions => ["id = ? AND version = ?", self.id, self.version]).each do |tag|
+            @tags[tag.k] = tag.v
+        end
+    end
+    @tags = Hash.new unless @tags
+    @tags
   end
 
-  # Pretend we're not in any relations
-  def containing_relation_members
-    return []
+  def tags=(t)
+    @tags = t 
   end
+
+  def tags_as_hash 
+    return self.tags
+  end 
+  # Pretend we're not in any ways 
+  def ways 
+    return [] 
+  end 
+  # Pretend we're not in any relations 
+  def containing_relation_members 
+    return [] 
+  end 
 end
diff --git a/app/models/old_node_tag.rb b/app/models/old_node_tag.rb
new file mode 100644 (file)
index 0000000..3fd4bf8
--- /dev/null
@@ -0,0 +1,10 @@
+class OldNodeTag < ActiveRecord::Base
+  set_table_name 'node_tags'
+  
+  belongs_to :user
+
+  validates_presence_of :id, :version
+  validates_length_of :k, :v, :maximum => 255, :allow_blank => true
+  validates_uniqueness_of :id, :scope => [:k, :version]
+  validates_numericality_of :id, :version, :only_integer => true
+end
index bac03c4d2eff6811a9dc4b5d2c32e3bb988e76b3..e2a6505112fadd9b639c899cd3b5d295133e13d4 100644 (file)
@@ -1,14 +1,19 @@
 class OldRelation < ActiveRecord::Base
+  include ConsistencyValidations
+  
   set_table_name 'relations'
 
-  belongs_to :user
+  belongs_to :changeset
+  
+  validates_associated :changeset
 
   def self.from_relation(relation)
     old_relation = OldRelation.new
     old_relation.visible = relation.visible
-    old_relation.user_id = relation.user_id
+    old_relation.changeset_id = relation.changeset_id
     old_relation.timestamp = relation.timestamp
     old_relation.id = relation.id
+    old_relation.version = relation.version
     old_relation.members = relation.members
     old_relation.tags = relation.tags
     return old_relation
@@ -33,14 +38,12 @@ class OldRelation < ActiveRecord::Base
       tag.save!
     end
 
-    i = 1
-    self.members.each do |m|
+    self.members.each_with_index do |m,i|
       member = OldRelationMember.new
-      member.id = self.id
+      member.id = [self.id, self.version, i]
       member.member_type = m[0]
       member.member_id = m[1]
       member.member_role = m[2]
-      member.version = self.version
       member.save!
     end
   end
@@ -48,7 +51,7 @@ class OldRelation < ActiveRecord::Base
   def members
     unless @members
         @members = Array.new
-        OldRelationMember.find(:all, :conditions => ["id = ? AND version = ?", self.id, self.version]).each do |m|
+        OldRelationMember.find(:all, :conditions => ["id = ? AND version = ?", self.id, self.version], :order => "sequence_id").each do |m|
             @members += [[m.type,m.id,m.role]]
         end
     end
@@ -85,12 +88,23 @@ class OldRelation < ActiveRecord::Base
     OldRelationTag.find(:all, :conditions => ['id = ? AND version = ?', self.id, self.version])    
   end
 
+  def to_xml
+    doc = OSM::API.new.get_xml_doc
+    doc.root << to_xml_node()
+    return doc
+  end
+
   def to_xml_node
     el1 = XML::Node.new 'relation'
     el1['id'] = self.id.to_s
     el1['visible'] = self.visible.to_s
     el1['timestamp'] = self.timestamp.xmlschema
-    el1['user'] = self.user.display_name if self.user.data_public?
+    if self.changeset.user.data_public?
+      el1['user'] = self.changeset.user.display_name
+      el1['uid'] = self.changeset.user.id.to_s
+    end
+    el1['version'] = self.version.to_s
+    el1['changeset'] = self.changeset_id.to_s
     
     self.old_members.each do |member|
       e = XML::Node.new 'member'
index d8b68585428da54515eb579d9a31ca4f04f04576..f0294d33952bd015f43c48d860ff30721b7a8b4e 100644 (file)
@@ -1,3 +1,6 @@
 class OldRelationMember < ActiveRecord::Base
   set_table_name 'relation_members'
+
+  set_primary_keys :id, :version, :sequence_id
+  belongs_to :relation, :foreign_key=> :id
 end
index 7ce6f694e633bb2a2110711229b1be8b6886e275..0fcb113269ea475fef4438d747ccee8eb9c69e26 100644 (file)
@@ -1,3 +1,10 @@
 class OldRelationTag < ActiveRecord::Base
   set_table_name 'relation_tags'
+  
+  belongs_to :old_relation, :foreign_key => [:id, :version]
+  
+  validates_presence_of :id, :version
+  validates_length_of :k, :v, :maximum => 255, :allow_blank => true
+  validates_uniqueness_of :id, :scope => [:k, :version]
+  validates_numericality_of :id, :version, :only_integer => true
 end
index 63265d6bf5c77814e90205cc4c0c5138a65a04c3..da9cf0697104ecea39d12db601d17f26e1be8cec 100644 (file)
@@ -1,14 +1,19 @@
 class OldWay < ActiveRecord::Base
+  include ConsistencyValidations
+  
   set_table_name 'ways'
 
-  belongs_to :user
+  belongs_to :changeset
 
+  validates_associated :changeset
+  
   def self.from_way(way)
     old_way = OldWay.new
     old_way.visible = way.visible
-    old_way.user_id = way.user_id
+    old_way.changeset_id = way.changeset_id
     old_way.timestamp = way.timestamp
     old_way.id = way.id
+    old_way.version = way.version
     old_way.nds = way.nds
     old_way.tags = way.tags
     return old_way
@@ -93,7 +98,12 @@ class OldWay < ActiveRecord::Base
     el1['id'] = self.id.to_s
     el1['visible'] = self.visible.to_s
     el1['timestamp'] = self.timestamp.xmlschema
-    el1['user'] = self.user.display_name if self.user.data_public?
+    if self.changeset.user.data_public?
+      el1['user'] = self.changeset.user.display_name
+      el1['uid'] = self.changeset.user.id.to_s
+    end
+    el1['version'] = self.version.to_s
+    el1['changeset'] = self.changeset.id.to_s
     
     self.old_nodes.each do |nd| # FIXME need to make sure they come back in the right order
       e = XML::Node.new 'nd'
index b02fd45b93ad8f23ba3aea7271bcef1ddfce5051..801532dbaa7632d2074e33fbb48ac4a5251285a3 100644 (file)
@@ -1,6 +1,10 @@
 class OldWayTag < ActiveRecord::Base
-  belongs_to :user
-
   set_table_name 'way_tags'
 
+  belongs_to :old_way, :foreign_key => [:id, :version]
+
+  validates_presence_of :id
+  validates_length_of :k, :v, :maximum => 255, :allow_blank => true
+  validates_uniqueness_of :id, :scope => [:k, :version]
+  validates_numericality_of :id, :version, :only_integer => true
 end
index c8516b58a3441c9f3b0ec38262d7628c8888d00f..6be1061591dda7b665b20ceed5b5b81ae79e96d3 100644 (file)
@@ -1,52 +1,84 @@
 class Relation < ActiveRecord::Base
   require 'xml/libxml'
   
+  include ConsistencyValidations
+  
   set_table_name 'current_relations'
 
-  belongs_to :user
+  belongs_to :changeset
 
   has_many :old_relations, :foreign_key => 'id', :order => 'version'
 
-  has_many :relation_members, :foreign_key => 'id'
+  has_many :relation_members, :foreign_key => 'id', :order => 'sequence_id'
   has_many :relation_tags, :foreign_key => 'id'
 
   has_many :containing_relation_members, :class_name => "RelationMember", :as => :member
   has_many :containing_relations, :class_name => "Relation", :through => :containing_relation_members, :source => :relation, :extend => ObjectFinder
 
+  validates_presence_of :id, :on => :update
+  validates_presence_of :timestamp,:version,  :changeset_id 
+  validates_uniqueness_of :id
+  validates_inclusion_of :visible, :in => [ true, false ]
+  validates_numericality_of :id, :on => :update, :integer_only => true
+  validates_numericality_of :changeset_id, :version, :integer_only => true
+  validates_associated :changeset
+  
+  TYPES = ["node", "way", "relation"]
+
   def self.from_xml(xml, create=false)
     begin
       p = XML::Parser.new
       p.string = xml
       doc = p.parse
 
-      relation = Relation.new
-
       doc.find('//osm/relation').each do |pt|
-        if !create and pt['id'] != '0'
-          relation.id = pt['id'].to_i
-        end
+        return Relation.from_xml_node(pt, create)
+      end
+    rescue LibXML::XML::Error => ex
+      raise OSM::APIBadXMLError.new("relation", xml, ex.message)
+    end
+  end
 
-        if create
-          relation.timestamp = Time.now
-          relation.visible = true
-        else
-          if pt['timestamp']
-            relation.timestamp = Time.parse(pt['timestamp'])
-          end
-        end
+  def self.from_xml_node(pt, create=false)
+    relation = Relation.new
 
-        pt.find('tag').each do |tag|
-          relation.add_tag_keyval(tag['k'], tag['v'])
-        end
+    if !create and pt['id'] != '0'
+      relation.id = pt['id'].to_i
+    end
 
-        pt.find('member').each do |member|
-          relation.add_member(member['type'], member['ref'], member['role'])
-        end
+    raise OSM::APIBadXMLError.new("relation", pt, "You are missing the required changeset in the relation") if pt['changeset'].nil?
+    relation.changeset_id = pt['changeset']
+
+    # The follow block does not need to be executed because they are dealt with 
+    # in create_with_history, update_from and delete_with_history
+    if create
+      relation.timestamp = Time.now
+      relation.visible = true
+      relation.version = 0
+    else
+      if pt['timestamp']
+        relation.timestamp = Time.parse(pt['timestamp'])
       end
-    rescue
-      relation = nil
+      relation.version = pt['version']
+    end
+
+    pt.find('tag').each do |tag|
+      relation.add_tag_keyval(tag['k'], tag['v'])
     end
 
+    pt.find('member').each do |member|
+      #member_type = 
+      logger.debug "each member"
+      raise OSM::APIBadXMLError.new("relation", pt, "The #{member['type']} is not allowed only, #{TYPES.inspect} allowed") unless TYPES.include? member['type']
+      logger.debug "after raise"
+      #member_ref = member['ref']
+      #member_role
+      member['role'] ||= "" # Allow  the upload to not include this, in which case we default to an empty string.
+      logger.debug member['role']
+      relation.add_member(member['type'], member['ref'], member['role'])
+    end
+    raise OSM::APIBadUserInput.new("Some bad xml in relation") if relation.nil?
+
     return relation
   end
 
@@ -61,18 +93,23 @@ class Relation < ActiveRecord::Base
     el1['id'] = self.id.to_s
     el1['visible'] = self.visible.to_s
     el1['timestamp'] = self.timestamp.xmlschema
+    el1['version'] = self.version.to_s
+    el1['changeset'] = self.changeset_id.to_s
 
     user_display_name_cache = {} if user_display_name_cache.nil?
     
-    if user_display_name_cache and user_display_name_cache.key?(self.user_id)
+    if user_display_name_cache and user_display_name_cache.key?(self.changeset.user_id)
       # use the cache if available
-    elsif self.user.data_public?
-      user_display_name_cache[self.user_id] = self.user.display_name
+    elsif self.changeset.user.data_public?
+      user_display_name_cache[self.changeset.user_id] = self.changeset.user.display_name
     else
-      user_display_name_cache[self.user_id] = nil
+      user_display_name_cache[self.changeset.user_id] = nil
     end
 
-    el1['user'] = user_display_name_cache[self.user_id] unless user_display_name_cache[self.user_id].nil?
+    if not user_display_name_cache[self.changeset.user_id].nil?
+      el1['user'] = user_display_name_cache[self.changeset.user_id]
+      el1['uid'] = self.changeset.user_id.to_s
+    end
 
     self.relation_members.each do |member|
       p=0
@@ -171,19 +208,164 @@ class Relation < ActiveRecord::Base
 
   def add_tag_keyval(k, v)
     @tags = Hash.new unless @tags
+
+    # duplicate tags are now forbidden, so we can't allow values
+    # in the hash to be overwritten.
+    raise OSM::APIDuplicateTagsError.new("relation", self.id, k) if @tags.include? k
+
     @tags[k] = v
   end
 
+  ##
+  # updates the changeset bounding box to contain the bounding box of 
+  # the element with given +type+ and +id+. this only works with nodes
+  # and ways at the moment, as they're the only elements to respond to
+  # the :bbox call.
+  def update_changeset_element(type, id)
+    element = Kernel.const_get(type.capitalize).find(id)
+    changeset.update_bbox! element.bbox
+  end    
+
+  def delete_with_history!(new_relation, user)
+    unless self.visible
+      raise OSM::APIAlreadyDeletedError.new
+    end
+
+    # need to start the transaction here, so that the database can 
+    # provide repeatable reads for the used-by checks. this means it
+    # shouldn't be possible to get race conditions.
+    Relation.transaction do
+      check_consistency(self, new_relation, user)
+      # This will check to see if this relation is used by another relation
+      if RelationMember.find(:first, :joins => "INNER JOIN current_relations ON current_relations.id=current_relation_members.id", :conditions => [ "visible = ? AND member_type='relation' and member_id=? ", true, self.id ])
+        raise OSM::APIPreconditionFailedError.new("The relation #{new_relation.id} is a used in another relation")
+      end
+      self.changeset_id = new_relation.changeset_id
+      self.tags = {}
+      self.members = []
+      self.visible = false
+      save_with_history!
+    end
+  end
+
+  def update_from(new_relation, user)
+    check_consistency(self, new_relation, user)
+    if !new_relation.preconditions_ok?
+      raise OSM::APIPreconditionFailedError.new
+    end
+    self.changeset_id = new_relation.changeset_id
+    self.tags = new_relation.tags
+    self.members = new_relation.members
+    self.visible = true
+    save_with_history!
+  end
+  
+  def create_with_history(user)
+    check_create_consistency(self, user)
+    if !self.preconditions_ok?
+      raise OSM::APIPreconditionFailedError.new
+    end
+    self.version = 0
+    self.visible = true
+    save_with_history!
+  end
+
+  def preconditions_ok?
+    # These are hastables that store an id in the index of all 
+    # the nodes/way/relations that have already been added.
+    # If the member is valid and visible then we add it to the 
+    # relevant hash table, with the value true as a cache.
+    # Thus if you have nodes with the ids of 50 and 1 already in the
+    # relation, then the hash table nodes would contain:
+    # => {50=>true, 1=>true}
+    elements = { :node => Hash.new, :way => Hash.new, :relation => Hash.new }
+    self.members.each do |m|
+      # find the hash for the element type or die
+      hash = elements[m[0].to_sym] or return false
+
+      # unless its in the cache already
+      unless hash.key? m[1]
+        # use reflection to look up the appropriate class
+        model = Kernel.const_get(m[0].capitalize)
+
+        # get the element with that ID
+        element = model.find(m[1])
+
+        # and check that it is OK to use.
+        unless element and element.visible? and element.preconditions_ok?
+          return false
+        end
+        hash[m[1]] = true
+      end
+    end
+
+    return true
+  rescue
+    return false
+  end
+
+  # Temporary method to match interface to nodes
+  def tags_as_hash
+    return self.tags
+  end
+
+  ##
+  # if any members are referenced by placeholder IDs (i.e: negative) then
+  # this calling this method will fix them using the map from placeholders 
+  # to IDs +id_map+. 
+  def fix_placeholders!(id_map)
+    self.members.map! do |type, id, role|
+      old_id = id.to_i
+      if old_id < 0
+        new_id = id_map[type.to_sym][old_id]
+        raise "invalid placeholder" if new_id.nil?
+        [type, new_id, role]
+      else
+        [type, id, role]
+      end
+    end
+  end
+
+  private
+  
   def save_with_history!
     Relation.transaction do
+      # have to be a little bit clever here - to detect if any tags
+      # changed then we have to monitor their before and after state.
+      tags_changed = false
+
       t = Time.now
+      self.version += 1
       self.timestamp = t
       self.save!
 
       tags = self.tags
+      self.relation_tags.each do |old_tag|
+        key = old_tag.k
+        # if we can match the tags we currently have to the list
+        # of old tags, then we never set the tags_changed flag. but
+        # if any are different then set the flag and do the DB 
+        # update.
+        if tags.has_key? key 
+          # rails 2.1 dirty handling should take care of making this
+          # somewhat efficient... hopefully...
+          old_tag.v = tags[key]
+          tags_changed |= old_tag.changed?
+          old_tag.save!
+
+          # remove from the map, so that we can expect an empty map
+          # at the end if there are no new tags
+          tags.delete key
 
-      RelationTag.delete_all(['id = ?', self.id])
-
+        else
+          # this means a tag was deleted
+          tags_changed = true
+          RelationTag.delete_all ['id = ? and k = ?', self.id, old_tag.k]
+        end
+      end
+      # if there are left-over tags then they are new and will have to
+      # be added.
+      tags_changed |= (not tags.empty?)
       tags.each do |k,v|
         tag = RelationTag.new
         tag.k = k
@@ -192,80 +374,80 @@ class Relation < ActiveRecord::Base
         tag.save!
       end
 
-      members = self.members
-
-      RelationMember.delete_all(['id = ?', self.id])
+      # same pattern as before, but this time we're collecting the
+      # changed members in an array, as the bounding box updates for
+      # elements are per-element, not blanked on/off like for tags.
+      changed_members = Array.new
+      members = Hash.new
+      self.members.each do |m|
+        # should be: h[[m.id, m.type]] = m.role, but someone prefers arrays
+        members[[m[1], m[0]]] = m[2]
+      end
+      relation_members.each do |old_member|
+        key = [old_member.member_id.to_s, old_member.member_type]
+        if members.has_key? key
+          members.delete key
+        else
+          changed_members << key
+        end
+      end
+      # any remaining members must be new additions
+      changed_members += members.keys
 
-      members.each do |n|
+      # update the members. first delete all the old members, as the new
+      # members may be in a different order and i don't feel like implementing
+      # a longest common subsequence algorithm to optimise this.
+      members = self.members
+      RelationMember.delete_all(:id => self.id)
+      members.each_with_index do |m,i|
         mem = RelationMember.new
-        mem.id = self.id
-        mem.member_type = n[0];
-        mem.member_id = n[1];
-        mem.member_role = n[2];
+        mem.id = [self.id, i]
+        mem.member_type = m[0]
+        mem.member_id = m[1]
+        mem.member_role = m[2]
         mem.save!
       end
 
       old_relation = OldRelation.from_relation(self)
       old_relation.timestamp = t
       old_relation.save_with_dependencies!
-    end
-  end
 
-  def preconditions_ok?
-    # These are hastables that store an id in the index of all 
-    # the nodes/way/relations that have already been added.
-    # Once we know the id of the node/way/relation exists
-    # we check to see if it is already existing in the hashtable
-    # if it does, then we return false. Otherwise
-    # we add it to the relevant hash table, with the value true..
-    # Thus if you have nodes with the ids of 50 and 1 already in the
-    # relation, then the hash table nodes would contain:
-    # => {50=>true, 1=>true}
-    nodes = Hash.new
-    ways = Hash.new
-    relations = Hash.new
-    self.members.each do |m|
-      if (m[0] == "node")
-        n = Node.find(:first, :conditions => ["id = ?", m[1]])
-        unless n and n.visible 
-          return false
-        end
-        if nodes[m[1]]
-          return false
-        else
-          nodes[m[1]] = true
-        end
-      elsif (m[0] == "way")
-        w = Way.find(:first, :conditions => ["id = ?", m[1]])
-        unless w and w.visible and w.preconditions_ok?
-          return false
-        end
-        if ways[m[1]]
-          return false
-        else
-          ways[m[1]] = true
-        end
-      elsif (m[0] == "relation")
-        e = Relation.find(:first, :conditions => ["id = ?", m[1]])
-        unless e and e.visible and e.preconditions_ok?
-          return false
-        end
-        if relations[m[1]]
-          return false
-        else
-          relations[m[1]] = true
+      # update the bbox of the changeset and save it too.
+      # discussion on the mailing list gave the following definition for
+      # the bounding box update procedure of a relation:
+      #
+      # adding or removing nodes or ways from a relation causes them to be
+      # added to the changeset bounding box. adding a relation member or
+      # changing tag values causes all node and way members to be added to the
+      # bounding box. this is similar to how the map call does things and is
+      # reasonable on the assumption that adding or removing members doesn't
+      # materially change the rest of the relation.
+      any_relations = 
+        changed_members.collect { |id,type| type == "relation" }.
+        inject(false) { |b,s| b or s }
+
+      if tags_changed or any_relations
+        # add all non-relation bounding boxes to the changeset
+        # FIXME: check for tag changes along with element deletions and
+        # make sure that the deleted element's bounding box is hit.
+        self.members.each do |type, id, role|
+          if type != "relation"
+            update_changeset_element(type, id)
+          end
         end
       else
-        return false
+        # add only changed members to the changeset
+        changed_members.each do |id, type|
+          update_changeset_element(type, id)
+        end
       end
+
+      # tell the changeset we updated one element only
+      changeset.add_changes! 1
+
+      # save the (maybe updated) changeset bounding box
+      changeset.save!
     end
-    return true
-  rescue
-    return false
   end
 
-  # Temporary method to match interface to nodes
-  def tags_as_hash
-    return self.tags
-  end
 end
index 9ff4f46f3b8b0b6ae4fb1b02ba49ccab25a71b66..f3033d1c641494225b15dc77ac108ee623866249 100644 (file)
@@ -1,6 +1,7 @@
 class RelationMember < ActiveRecord::Base
   set_table_name 'current_relation_members'
   
+  set_primary_keys :id, :sequence_id
   belongs_to :member, :polymorphic => true, :foreign_type => :member_class
   belongs_to :relation, :foreign_key => :id
 
@@ -9,7 +10,7 @@ class RelationMember < ActiveRecord::Base
   end
 
   def after_initialize
-    self[:member_class] = self.member_type.capitalize
+    self[:member_class] = self.member_type.capitalize unless self.member_type.nil?
   end
 
   def before_save
index 939165ebd30f9cc136a12d77f4b013165bf0be27..812b2ec3592f7f866a28b27b79259839b8658ad6 100644 (file)
@@ -3,4 +3,8 @@ class RelationTag < ActiveRecord::Base
 
   belongs_to :relation, :foreign_key => 'id'
 
+  validates_presence_of :id
+  validates_length_of :k, :v, :maximum => 255, :allow_blank => true
+  validates_uniqueness_of :id, :scope => :k
+  validates_numericality_of :id, :only_integer => true
 end
index 10e867badc71381fc42ba6c93b4a7071f53ea827..1b44e218717cdea042dc30ce7f0359d719609e37 100644 (file)
@@ -3,6 +3,8 @@ class Trace < ActiveRecord::Base
 
   validates_presence_of :user_id, :name, :timestamp
   validates_presence_of :description, :on => :create
+  validates_length_of :name, :within => 1..255
+  validates_length_of :description, :within => 1..255
 #  validates_numericality_of :latitude, :longitude
   validates_inclusion_of :public, :inserted, :in => [ true, false]
   
index f1d5967d53dd5c38b75d591b792cf6a330cd69ad..f9833e141446125128a07ab5e03a6c21ba2f9bde 100644 (file)
@@ -2,6 +2,7 @@ class Tracetag < ActiveRecord::Base
   set_table_name 'gpx_file_tags'
 
   validates_format_of :tag, :with => /^[^\/;.,?]*$/
+  validates_length_of :tag, :within => 1..255
 
   belongs_to :trace, :foreign_key => 'gpx_id'
 end
index fae037110951ffe64d847fba40b99a0c37d40d9e..ce244fe02448bce114dbb79d25b6a009a4848f28 100644 (file)
@@ -4,19 +4,21 @@ class User < ActiveRecord::Base
   has_many :traces
   has_many :diary_entries, :order => 'created_at DESC'
   has_many :messages, :foreign_key => :to_user_id, :order => 'sent_on DESC'
-  has_many :new_messages, :class_name => "Message", :foreign_key => :to_user_id, :conditions => "message_read = 0", :order => 'sent_on DESC'
+  has_many :new_messages, :class_name => "Message", :foreign_key => :to_user_id, :conditions => {:message_read => false}, :order => 'sent_on DESC'
   has_many :sent_messages, :class_name => "Message", :foreign_key => :from_user_id, :order => 'sent_on DESC'
-  has_many :friends, :include => :befriendee, :conditions => "users.visible = 1"
+  has_many :friends, :include => :befriendee, :conditions => ["users.visible = ?", true]
   has_many :tokens, :class_name => "UserToken"
   has_many :preferences, :class_name => "UserPreference"
+  has_many :changesets
 
   validates_presence_of :email, :display_name
   validates_confirmation_of :email, :message => 'Email addresses must match'
   validates_confirmation_of :pass_crypt, :message => 'Password must match the confirmation password'
   validates_uniqueness_of :display_name, :allow_nil => true
   validates_uniqueness_of :email
-  validates_length_of :pass_crypt, :minimum => 8
-  validates_length_of :display_name, :minimum => 3, :allow_nil => true
+  validates_length_of :pass_crypt, :within => 8..255
+  validates_length_of :display_name, :within => 3..255, :allow_nil => true
+  validates_length_of :email, :within => 6..255
   validates_format_of :email, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i
   validates_format_of :display_name, :with => /^[^\/;.,?]*$/
   validates_numericality_of :home_lat, :allow_nil => true
@@ -80,7 +82,7 @@ class User < ActiveRecord::Base
     if self.home_lon and self.home_lat 
       gc = OSM::GreatCircle.new(self.home_lat, self.home_lon)
       bounds = gc.bounds(radius)
-      nearby = User.find(:all, :conditions => "visible = 1 and home_lat between #{bounds[:minlat]} and #{bounds[:maxlat]} and home_lon between #{bounds[:minlon]} and #{bounds[:maxlon]} and data_public = 1 and id != #{self.id}")
+      nearby = User.find(:all, :conditions => ["visible = ? and home_lat between #{bounds[:minlat]} and #{bounds[:maxlat]} and home_lon between #{bounds[:minlon]} and #{bounds[:maxlon]} and data_public = ? and id != #{self.id}", true, true])
       nearby.delete_if { |u| gc.distance(u.home_lat, u.home_lon) > radius }
       nearby.sort! { |u1,u2| gc.distance(u1.home_lat, u1.home_lon) <=> gc.distance(u2.home_lat, u2.home_lon) }
     else
@@ -104,6 +106,10 @@ class User < ActiveRecord::Base
     return false
   end
 
+  def trace_public_default
+    return self.preferences.find(:first, :conditions => {:k => "gps.trace.public", :v => "default"})
+  end
+
   def delete
     self.active = false
     self.display_name = "user_#{self.id}"
index 3985a527ec5f62e83b53b948b72164a48789a08d..28ef40f1d5c8347597c9f9a790b4d8d002cb63ce 100644 (file)
@@ -1,6 +1,9 @@
 class UserPreference < ActiveRecord::Base
   set_primary_keys :user_id, :k
   belongs_to :user
+  
+  validates_length_of :k, :within => 1..255
+  validates_length_of :v, :within => 1..255
 
   # Turn this Node in to an XML Node without the <osm> wrapper.
   def to_xml_node
index 958944200df628054c67c6c0eb8e60105d99bb38..86b25e08e2fb44a9660bc197221e35d109b46f69 100644 (file)
@@ -1,9 +1,11 @@
 class Way < ActiveRecord::Base
   require 'xml/libxml'
+  
+  include ConsistencyValidations
 
   set_table_name 'current_ways'
-
-  belongs_to :user
+  
+  belongs_to :changeset
 
   has_many :old_ways, :foreign_key => 'id', :order => 'version'
 
@@ -15,38 +17,57 @@ class Way < ActiveRecord::Base
   has_many :containing_relation_members, :class_name => "RelationMember", :as => :member
   has_many :containing_relations, :class_name => "Relation", :through => :containing_relation_members, :source => :relation, :extend => ObjectFinder
 
+  validates_presence_of :id, :on => :update
+  validates_presence_of :changeset_id,:version,  :timestamp
+  validates_uniqueness_of :id
+  validates_inclusion_of :visible, :in => [ true, false ]
+  validates_numericality_of :changeset_id, :version, :integer_only => true
+  validates_numericality_of :id, :on => :update, :integer_only => true
+  validates_associated :changeset
+
   def self.from_xml(xml, create=false)
     begin
       p = XML::Parser.new
       p.string = xml
       doc = p.parse
 
-      way = Way.new
-
       doc.find('//osm/way').each do |pt|
-        if !create and pt['id'] != '0'
-          way.id = pt['id'].to_i
-        end
-
-        if create
-          way.timestamp = Time.now
-          way.visible = true
-        else
-          if pt['timestamp']
-            way.timestamp = Time.parse(pt['timestamp'])
-          end
-        end
+        return Way.from_xml_node(pt, create)
+      end
+    rescue LibXML::XML::Error => ex
+      raise OSM::APIBadXMLError.new("way", xml, ex.message)
+    end
+  end
 
-        pt.find('tag').each do |tag|
-          way.add_tag_keyval(tag['k'], tag['v'])
-        end
+  def self.from_xml_node(pt, create=false)
+    way = Way.new
 
-        pt.find('nd').each do |nd|
-          way.add_nd_num(nd['ref'])
-        end
+    if !create and pt['id'] != '0'
+      way.id = pt['id'].to_i
+    end
+    
+    way.version = pt['version']
+    raise OSM::APIBadXMLError.new("node", pt, "Changeset is required") if pt['changeset'].nil?
+    way.changeset_id = pt['changeset']
+
+    # This next section isn't required for the create, update, or delete of ways
+    if create
+      way.timestamp = Time.now
+      way.visible = true
+    else
+      if pt['timestamp']
+        way.timestamp = Time.parse(pt['timestamp'])
       end
-    rescue
-      way = nil
+      # if visible isn't present then it defaults to true
+      way.visible = (pt['visible'] or true)
+    end
+
+    pt.find('tag').each do |tag|
+      way.add_tag_keyval(tag['k'], tag['v'])
+    end
+
+    pt.find('nd').each do |nd|
+      way.add_nd_num(nd['ref'])
     end
 
     return way
@@ -74,18 +95,23 @@ class Way < ActiveRecord::Base
     el1['id'] = self.id.to_s
     el1['visible'] = self.visible.to_s
     el1['timestamp'] = self.timestamp.xmlschema
+    el1['version'] = self.version.to_s
+    el1['changeset'] = self.changeset_id.to_s
 
     user_display_name_cache = {} if user_display_name_cache.nil?
 
-    if user_display_name_cache and user_display_name_cache.key?(self.user_id)
+    if user_display_name_cache and user_display_name_cache.key?(self.changeset.user_id)
       # use the cache if available
-    elsif self.user.data_public?
-      user_display_name_cache[self.user_id] = self.user.display_name
+    elsif self.changeset.user.data_public?
+      user_display_name_cache[self.changeset.user_id] = self.changeset.user.display_name
     else
-      user_display_name_cache[self.user_id] = nil
+      user_display_name_cache[self.changeset.user_id] = nil
     end
 
-    el1['user'] = user_display_name_cache[self.user_id] unless user_display_name_cache[self.user_id].nil?
+    if not user_display_name_cache[self.changeset.user_id].nil?
+      el1['user'] = user_display_name_cache[self.changeset.user_id]
+      el1['uid'] = self.changeset.user_id.to_s
+    end
 
     # make sure nodes are output in sequence_id order
     ordered_nodes = []
@@ -97,7 +123,7 @@ class Way < ActiveRecord::Base
         end
       else
         # otherwise, manually go to the db to check things
-        if nd.node.visible? and nd.node.visible?
+        if nd.node and nd.node.visible?
           ordered_nodes[nd.sequence_id] = nd.node_id.to_s
         end
       end
@@ -155,99 +181,82 @@ class Way < ActiveRecord::Base
 
   def add_tag_keyval(k, v)
     @tags = Hash.new unless @tags
-    @tags[k] = v
-  end
 
-  def save_with_history!
-    t = Time.now
+    # duplicate tags are now forbidden, so we can't allow values
+    # in the hash to be overwritten.
+    raise OSM::APIDuplicateTagsError.new("way", self.id, k) if @tags.include? k
 
-    Way.transaction do
-      self.timestamp = t
-      self.save!
-    end
-
-    WayTag.transaction do
-      tags = self.tags
+    @tags[k] = v
+  end
 
-      WayTag.delete_all(['id = ?', self.id])
+  ##
+  # the integer coords (i.e: unscaled) bounding box of the way, assuming
+  # straight line segments.
+  def bbox
+    lons = nodes.collect { |n| n.longitude }
+    lats = nodes.collect { |n| n.latitude }
+    [ lons.min, lats.min, lons.max, lats.max ]
+  end
 
-      tags.each do |k,v|
-        tag = WayTag.new
-        tag.k = k
-        tag.v = v
-        tag.id = self.id
-        tag.save!
-      end
+  def update_from(new_way, user)
+    check_consistency(self, new_way, user)
+    if !new_way.preconditions_ok?
+      raise OSM::APIPreconditionFailedError.new
     end
+    self.changeset_id = new_way.changeset_id
+    self.tags = new_way.tags
+    self.nds = new_way.nds
+    self.visible = true
+    save_with_history!
+  end
 
-    WayNode.transaction do
-      nds = self.nds
-
-      WayNode.delete_all(['id = ?', self.id])
-
-      sequence = 1
-      nds.each do |n|
-        nd = WayNode.new
-        nd.id = [self.id, sequence]
-        nd.node_id = n
-        nd.save!
-        sequence += 1
-      end
+  def create_with_history(user)
+    check_create_consistency(self, user)
+    if !self.preconditions_ok?
+      raise OSM::APIPreconditionFailedError.new
     end
-
-    old_way = OldWay.from_way(self)
-    old_way.timestamp = t
-    old_way.save_with_dependencies!
+    self.version = 0
+    self.visible = true
+    save_with_history!
   end
 
   def preconditions_ok?
     return false if self.nds.empty?
+    if self.nds.length > APP_CONFIG['max_number_of_way_nodes']
+      raise OSM::APITooManyWayNodesError.new(self.nds.count, APP_CONFIG['max_number_of_way_nodes'])
+    end
     self.nds.each do |n|
       node = Node.find(:first, :conditions => ["id = ?", n])
       unless node and node.visible
-        return false
+        raise OSM::APIPreconditionFailedError.new("The node with id #{n} either does not exist, or is not visible")
       end
     end
     return true
   end
 
-  # Delete the way and it's relations, but don't really delete it - set its visibility to false and update the history etc to maintain wiki-like functionality.
-  def delete_with_relations_and_history(user)
-    if self.visible
-         # FIXME
-         # this should actually delete the relations,
-         # not just throw a PreconditionFailed if it's a member of a relation!!
+  def delete_with_history!(new_way, user)
+    unless self.visible
+      raise OSM::APIAlreadyDeletedError
+    end
+    
+    # need to start the transaction here, so that the database can 
+    # provide repeatable reads for the used-by checks. this means it
+    # shouldn't be possible to get race conditions.
+    Way.transaction do
+      check_consistency(self, new_way, user)
       if RelationMember.find(:first, :joins => "INNER JOIN current_relations ON current_relations.id=current_relation_members.id",
-                             :conditions => [ "visible = 1 AND member_type='way' and member_id=?", self.id])
-        raise OSM::APIPreconditionFailedError
-      # end FIXME
+                             :conditions => [ "visible = ? AND member_type='way' and member_id=? ", true, self.id])
+        raise OSM::APIPreconditionFailedError.new("You need to make sure that this way is not a member of a relation.")
       else
-        self.user_id = user.id
+        self.changeset_id = new_way.changeset_id
         self.tags = []
         self.nds = []
         self.visible = false
-        self.save_with_history!
+        save_with_history!
       end
-    else
-      raise OSM::APIAlreadyDeletedError
     end
   end
 
-  # delete a way and it's nodes that aren't part of other ways, with history
-  def delete_with_relations_and_nodes_and_history(user)
-    # delete the nodes not used by other ways
-    self.unshared_node_ids.each do |node_id|
-      n = Node.find(node_id)
-      n.user_id = user.id
-      n.visible = false
-      n.save_with_history!
-    end
-    
-    self.user_id = user.id
-
-    self.delete_with_relations_and_history(user)
-  end
-
   # Find nodes that belong to this way only
   def unshared_node_ids
     node_ids = self.nodes.collect { |node| node.id }
@@ -264,4 +273,73 @@ class Way < ActiveRecord::Base
   def tags_as_hash
     return self.tags
   end
+
+  ##
+  # if any referenced nodes are placeholder IDs (i.e: are negative) then
+  # this calling this method will fix them using the map from placeholders 
+  # to IDs +id_map+. 
+  def fix_placeholders!(id_map)
+    self.nds.map! do |node_id|
+      if node_id < 0
+        new_id = id_map[:node][node_id]
+        raise "invalid placeholder for #{node_id.inspect}: #{new_id.inspect}" if new_id.nil?
+        new_id
+      else
+        node_id
+      end
+    end
+  end
+
+  private
+  
+  def save_with_history!
+    t = Time.now
+
+    # update the bounding box, but don't save it as the controller knows the 
+    # lifetime of the change better. note that this has to be done both before 
+    # and after the save, so that nodes from both versions are included in the 
+    # bbox.
+    changeset.update_bbox!(bbox) unless nodes.empty?
+
+    Way.transaction do
+      self.version += 1
+      self.timestamp = t
+      self.save!
+
+      tags = self.tags
+      WayTag.delete_all(['id = ?', self.id])
+      tags.each do |k,v|
+        tag = WayTag.new
+        tag.k = k
+        tag.v = v
+        tag.id = self.id
+        tag.save!
+      end
+
+      nds = self.nds
+      WayNode.delete_all(['id = ?', self.id])
+      sequence = 1
+      nds.each do |n|
+        nd = WayNode.new
+        nd.id = [self.id, sequence]
+        nd.node_id = n
+        nd.save!
+        sequence += 1
+      end
+
+      old_way = OldWay.from_way(self)
+      old_way.timestamp = t
+      old_way.save_with_dependencies!
+
+      # update and commit the bounding box, now that way nodes 
+      # have been updated and we're in a transaction.
+      changeset.update_bbox!(bbox) unless nodes.empty?
+
+      # tell the changeset we updated one element only
+      changeset.add_changes! 1
+
+      changeset.save!
+    end
+  end
+
 end
index 4548674d4b950607e2396ecc079da2e3951bba4c..fa9b4336177061287d00d4b153a9da6cef28c6a9 100644 (file)
@@ -6,4 +6,9 @@ class WayTag < ActiveRecord::Base
   # FIXME add a real multipart key to waytags so that we can do eager loadin
 
   belongs_to :way, :foreign_key => 'id'
+  
+  validates_presence_of :id
+  validates_length_of :k, :v, :maximum => 255, :allow_blank => true
+  validates_uniqueness_of :id, :scope => :k
+  validates_numericality_of :id, :only_integer => true
 end
diff --git a/app/views/browse/_changeset_details.rhtml b/app/views/browse/_changeset_details.rhtml
new file mode 100644 (file)
index 0000000..2b71161
--- /dev/null
@@ -0,0 +1,100 @@
+<table>
+
+  <tr>
+    <th>Created at:</th>
+    <td><%= h(changeset_details.created_at) %></td>
+  </tr>
+  
+  <tr>
+    <th>Closed at:</th>
+    <td><%= h(changeset_details.closed_at) %></td>
+  </tr>
+  
+  <% if changeset_details.user.data_public? %>
+    <tr>
+      <th>Belongs to:</th>
+      <td><%= link_to h(changeset_details.user.display_name), :controller => "user", :action => "view", :display_name => changeset_details.user.display_name %></td>
+    </tr>
+  <% end %>
+  
+  <% unless changeset_details.tags_as_hash.empty? %>
+    <tr valign="top">
+      <th>Tags:</th>
+      <td>
+        <table padding="0">
+          <%= render :partial => "tag", :collection => changeset_details.tags_as_hash %>
+        </table>
+      </td>
+    </tr>
+  <% else %>
+    <tr>
+      <th>Tags</th>
+      <td>There are no tags for this changeset</td>
+    </tr>
+  <% end %>
+
+  <% if changeset_details.max_lat.nil? or changeset_details.min_lat.nil? or changeset_details.max_lon.nil? or changeset_details.min_lon.nil? %>
+    <tr>
+      <td>No bounding box has been stored for this changeset.</td>
+    </tr>
+  <% else %>
+    <table>
+      <tr>
+        <td colspan="2" style="text-align:center"><b>Max Latitude: </b><%= changeset_details.max_lat/GeoRecord::SCALE.to_f -%></td>
+      </tr>
+      <tr>
+        <td><b>Min Longitude: </b><%= changeset_details.min_lon/GeoRecord::SCALE.to_f -%></td>
+        <td><b>Max Longitude: </b><%= changeset_details.max_lon/GeoRecord::SCALE.to_f -%></td>
+      </tr>
+      <tr>
+        <td colspan="2" style="text-align:center"><b>Min Latitude: </b><%= changeset_details.min_lon/GeoRecord::SCALE.to_f -%></td>
+      </tr>
+    </table>
+  <% end %>
+
+  <% unless @nodes.empty? %>
+    <tr valign="top">
+      <th>Has the following <%= @node_pages.item_count %> nodes:</th>
+      <td>
+        <table padding="0">
+          <% @nodes.each do |node| %>
+            <tr><td><%= link_to "Node #{node.id.to_s}, version #{node.version.to_s}", :action => "node", :id => node.id.to_s %></td></tr>
+          <% end %>
+        </table>
+      </td>
+    </tr>
+    <%= render :partial => 'paging_nav', :locals => { :pages => @node_pages, :page_param => "node_page"} %>
+  <% end %>
+  
+  <% unless @ways.empty? %>
+    <tr valign="top">
+      <th>Has the following <%= @way_pages.item_count %> ways:</th>
+      <td>
+        <table padding="0">
+          <% @ways.each do |way| %>
+            <tr><td><%= link_to "Way #{way.id.to_s}, version #{way.version.to_s}", :action => "way", :id => way.id.to_s %></td></tr>
+          <% end %>
+          <%=
+          #render :partial => "containing_relation", :collection => changeset_details.containing_relation_members 
+          %>
+        </table>
+      </td>
+    </tr>
+    <%= render :partial => 'paging_nav', :locals => { :pages => @way_pages, :page_param => "way_page" } %>
+  <% end %>
+  
+  <% unless @relations.empty? %>
+    <tr valign="top">
+      <th>Has the following <%= @relation_pages.item_count %> relations:</th>
+      <td>
+        <table padding="0">
+          <% @relations.each do |relation| %>
+            <tr><td><%= link_to "Relation #{relation.id.to_s}, version #{relation.version.to_s}", :action => "relation", :id => relation.id.to_s %></td></tr>
+          <% end %>
+        </table>
+      </td>
+    </tr>
+    <%= render :partial => 'paging_nav', :locals => { :pages => @relation_pages, :page_param => "relation_page" } %>
+  <% end %>
+
+</table>
index ee5f22ceebee4990058fb8be17b10dbaf43a3c4e..71a9dd3145726476b610e3e3f044136c034e0506 100644 (file)
@@ -3,13 +3,23 @@
   <td><%= h(common_details.timestamp) %></td>
 </tr>
 
-<% if common_details.user.data_public %>
+<% if common_details.changeset.user.data_public? %>
   <tr>
     <th>Edited by:</th>
-    <td><%= link_to h(common_details.user.display_name), :controller => "user", :action => "view", :display_name => common_details.user.display_name %></td>
+    <td><%= link_to h(common_details.changeset.user.display_name), :controller => "user", :action => "view", :display_name => common_details.changeset.user.display_name %></td>
   </tr>
 <% end %>
 
+<tr>
+  <th>Version:</th>
+  <td><%= h(common_details.version) %></td>
+</tr>
+
+<tr>
+  <th>In changeset:</th>
+  <td><%= link_to common_details.changeset_id, :action => :changeset, :id => common_details.changeset_id %></td>
+</tr>
+
 <% unless common_details.tags_as_hash.empty? %>
   <tr valign="top">
     <th>Tags:</th>
diff --git a/app/views/browse/_paging_nav.rhtml b/app/views/browse/_paging_nav.rhtml
new file mode 100644 (file)
index 0000000..fcfbb05
--- /dev/null
@@ -0,0 +1,15 @@
+<tr><td colspan='2'>
+<% current_page = pages.current_page %>
+
+Showing page 
+<%= current_page.number %> (<%= current_page.first_item %><% 
+if (current_page.first_item < current_page.last_item) # if more than 1 trace on page 
+  %>-<%= current_page.last_item %><% 
+end %>
+of <%= pages.item_count %>)
+
+<% if pages.page_count > 1 %>
+| <%= pagination_links_each(pages, {}) { |n| link_to_page(n, page_param) } %>
+<% end %>
+</td>
+</tr>
diff --git a/app/views/browse/changeset.rhtml b/app/views/browse/changeset.rhtml
new file mode 100644 (file)
index 0000000..57e39db
--- /dev/null
@@ -0,0 +1,18 @@
+<table width="100%">
+  <tr>
+    <td>
+      <h2>Changeset: <%= h(@changeset.id) %></h2>
+    </td>
+    <td>
+      <%= render :partial => "navigation" %>
+    </td>
+  </tr>
+  <tr valign="top">
+    <td>
+    <%= render :partial => "changeset_details", :object => @changeset %>
+    <hr />
+      <%= link_to "Download Changeset XML", :controller => "changeset", :action => "read" %> | 
+      <%= link_to "Download osmChange XML", :controller => "changeset", :action => "download" %>
+    </td>
+  </tr>
+</table>
index 2cd5cc9da4c09c1f46655033c93042bc0e94bb5f..e9d830a1063d511fc43e0a81a2adcb172e70c128 100644 (file)
@@ -1,11 +1,11 @@
 <h2><%= @nodes.length %> Recently Changed Nodes</h2> 
-<ul>
+<ul id="recently_changed">
 <% @nodes.each do |node| 
    name = node.tags_as_hash['name'].to_s 
    if name.length == 0:
      name = "(No name)"
    end
-   name = name + " - " + node.id.to_s 
+   name = "#{name} - #{node.id} (#{node.version})"
 %>
    <li><%= link_to h(name), :action => "node", :id => node.id %></li>
 <% end %>
diff --git a/app/views/browse/not_found.rhtml b/app/views/browse/not_found.rhtml
new file mode 100644 (file)
index 0000000..1322a0a
--- /dev/null
@@ -0,0 +1 @@
+<p>Sorry, the <%= @type -%> with the id <%= params[:id] -%>, could not be found.</p>
index c17325ad19c6a94b1b87e245d39ee3fabc8e0f01..f38b1dc80937cb0c9f6355236fd87a2b50b01e49 100644 (file)
@@ -189,7 +189,7 @@ page << <<EOJ
     if (size > 0.25) {
       setStatus("Unable to load: Bounding box size of " + size + " is too large (must be smaller than 0.25)");
     } else {
-      loadGML("/api/0.5/map?bbox=" + projected.toBBOX());
+      loadGML("/api/#{API_VERSION}/map?bbox=" + projected.toBBOX());
     }
   }
 
@@ -393,7 +393,7 @@ page << <<EOJ
     this.link.href = "";
     this.link.innerHTML = "Wait...";
 
-    new Ajax.Request("/api/0.5/" + this.type + "/" + this.feature.osm_id + "/history", {
+    new Ajax.Request("/api/#{API_VERSION}/" + this.type + "/" + this.feature.osm_id + "/history", {
       onComplete: OpenLayers.Function.bind(displayHistory, this)
     });
 
index 87f5e9fac6bddce7c6509049c687e5b7082d0d49..a69f4fd9c9240bc7a666099ac806678ade8032e9 100644 (file)
@@ -5,24 +5,28 @@
 <% form_for :diary_entry do |f| %>
   <table>
     <tr valign="top">
-      <th>Subject</th>
+      <td class="fieldName">Subject:</td>
       <td><%= f.text_field :title, :size => 60 %></td>
     </tr>
     <tr valign="top">
-      <th>Body</th>
+      <td class="fieldName">Body:</td>
       <td><%= f.text_area :body, :cols => 80 %></td>
     </tr>
     <tr valign="top">
-      <th>Location</th>
+      <td class="fieldName">Location:</td>
       <td>
         <div id="map" style="border: 1px solid black; position: relative; width : 90%; height : 400px; display: none;"></div>
         <span class="location">Latitude: <%= f.text_field :latitude, :size => 20, :id => "latitude" %> Longitude: <%= f.text_field :longitude, :size => 20, :id => "longitude" %></span>
         <a href="javascript:openMap()" id="usemap">use map</a>
+        <br/><br/>
       </td>
     </tr>
     <tr>
-      <th></th>
-      <td><%= submit_tag 'Save' %></td>
+      <td></td>
+      <td>
+         <%= submit_tag 'Save' %>
+         <%# TODO: button should say 'publish' or 'save changes' depending on new/edit state %>
+      </td>
     </tr>
   </table>
 <% end %>
index 7a2ccf74d6bea047b5cf9c7659e900306cf103ff..9852313bbd7e669d308af8586fd2ed0c49562580 100644 (file)
@@ -4,29 +4,40 @@
  <%= image_tag url_for_file_column(@this_user, "image") %>
 <% end %>
 
-<br />
 
 <% if @this_user %>
   <% if @user == @this_user %>
-    <%= link_to 'New diary entry', :controller => 'diary_entry', :action => 'new', :display_name => @user.display_name %>
+    <%= link_to image_tag("new.png", :border=>0) + 'New diary entry', {:controller => 'diary_entry', :action => 'new', :display_name => @user.display_name}, {:title => 'Compose a new entry in your user diary'} %>
   <% end %>
 <% else %>
   <% if @user %>
-    <%= link_to 'New diary entry', :controller => 'diary_entry', :action => 'new', :display_name => @user.display_name %>
+    <%= link_to image_tag("new.png", :border=>0) + 'New diary entry', {:controller => 'diary_entry', :action => 'new', :display_name => @user.display_name}, {:title => 'Compose a new entry in your user diary'} %>
   <% end %>
 <% end %>
 
-<h3>Recent diary entries:</h3>
 
-<%= render :partial => 'diary_entry', :collection => @entries %>
+<% if @entries.empty? %>
+       <p>No diary entries</p>
 
-<%= link_to "Older Entries", { :page => @entry_pages.current.next } if @entry_pages.current.next %>
-<% if @entry_pages.current.next and @entry_pages.current.previous %>
-|
+<% else %>
+       
+       <p>Recent diary entries:</p>
+       <hr />
+       <%= render :partial => 'diary_entry', :collection => @entries %>
+       
+       <%= link_to "Older Entries", { :page => @entry_pages.current.next } if @entry_pages.current.next %>
+       <% if @entry_pages.current.next and @entry_pages.current.previous %>
+       |
+       <% end %>
+       <%= link_to "Newer Entries", { :page => @entry_pages.current.previous } if @entry_pages.current.previous %>
+       
+       <br />
+       
 <% end %>
-<%= link_to "Newer Entries", { :page => @entry_pages.current.previous } if @entry_pages.current.previous %>
-
-<br />
 
 <%= rss_link_to :action => 'rss' %>
 <%= auto_discovery_link_tag :atom, :action => 'rss' %>
+
+
+<br />
+<br />
\ No newline at end of file
diff --git a/app/views/diary_entry/no_such_entry.rhtml b/app/views/diary_entry/no_such_entry.rhtml
new file mode 100644 (file)
index 0000000..1ebcf26
--- /dev/null
@@ -0,0 +1,2 @@
+<h2>No entry with the id: <%= h(params[:id]) %></h2>
+<p>Sorry, there is no diary entry or comment with the id <%=h params[:id] -%>, or no id was given. Please check your spelling, or maybe the link you clicked is wrong.</p>
index 7aa2db7b9432cdb5d44a90e7e38d6fa9e0866e33..cf7ad9fc811f839adc9fe2cf413831ebb6954996 100644 (file)
@@ -7,6 +7,7 @@
     <%= stylesheet_link_tag 'site' %>
     <%= stylesheet_link_tag 'print', :media => "print" %>
     <%= tag("link", { :rel => "search", :type => "application/opensearchdescription+xml", :title => "OpenStreetMap Search", :href => "/opensearch/osm.xml" }) %>
+    <%= tag("meta", { :name => "description", :content => "OpenStreetMap is the free wiki world map." }) %>
     <title>OpenStreetMap<%= ' | '+ h(@title) if @title %></title>
   </head>
   <body>
             <input type="hidden" name="encrypted" value="-----BEGIN PKCS7-----MIIHTwYJKoZIhvcNAQcEoIIHQDCCBzwCAQExggEwMIIBLAIBADCBlDCBjjELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtQYXlQYWwgSW5jLjETMBEGA1UECxQKbGl2ZV9jZXJ0czERMA8GA1UEAxQIbGl2ZV9hcGkxHDAaBgkqhkiG9w0BCQEWDXJlQHBheXBhbC5jb20CAQAwDQYJKoZIhvcNAQEBBQAEgYCsNDDDDa7OZFojBzDvG4HSPXOiJSO3VNuLoc8HGwsds3LsZYYtv4cPGw7Z/SoVVda+RELM+5FQn0D3Kv7hjA2Z6QdwEkFH2kDDlXCvyPt53ENHkQrzC1KOueRpimsQMH5hl03nvuVXij0hEYlMFqTH0UZr80vyczB+lJU6ZKYtrDELMAkGBSsOAwIaBQAwgcwGCSqGSIb3DQEHATAUBggqhkiG9w0DBwQIZa12CIRB0geAgahqF6Otz0oY0+Wg56fSuEpZvbUmNGEQznjWqBXkJqTkZT0jOwekOrlEi7bNEU8yVIie2u5L1gOhBDSl6rmgpxxVURSa4Jig5qiSioyK5baH6HjXVPQ+MDEWg1gZ4LtjYYtroZ8SBE/1eikQWmG7EOEgU62Vn/jqJJ77/mgS7mdEQhlEWYMiyJBZs35yCB/pK5FUxhZnrquL4sS+2QKHPPOGPDfRc/dnhMKgggOHMIIDgzCCAuygAwIBAgIBADANBgkqhkiG9w0BAQUFADCBjjELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtQYXlQYWwgSW5jLjETMBEGA1UECxQKbGl2ZV9jZXJ0czERMA8GA1UEAxQIbGl2ZV9hcGkxHDAaBgkqhkiG9w0BCQEWDXJlQHBheXBhbC5jb20wHhcNMDQwMjEzMTAxMzE1WhcNMzUwMjEzMTAxMzE1WjCBjjELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtQYXlQYWwgSW5jLjETMBEGA1UECxQKbGl2ZV9jZXJ0czERMA8GA1UEAxQIbGl2ZV9hcGkxHDAaBgkqhkiG9w0BCQEWDXJlQHBheXBhbC5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMFHTt38RMxLXJyO2SmS+Ndl72T7oKJ4u4uw+6awntALWh03PewmIJuzbALScsTS4sZoS1fKciBGoh11gIfHzylvkdNe/hJl66/RGqrj5rFb08sAABNTzDTiqqNpJeBsYs/c2aiGozptX2RlnBktH+SUNpAajW724Nv2Wvhif6sFAgMBAAGjge4wgeswHQYDVR0OBBYEFJaffLvGbxe9WT9S1wob7BDWZJRrMIG7BgNVHSMEgbMwgbCAFJaffLvGbxe9WT9S1wob7BDWZJRroYGUpIGRMIGOMQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxFDASBgNVBAoTC1BheVBhbCBJbmMuMRMwEQYDVQQLFApsaXZlX2NlcnRzMREwDwYDVQQDFAhsaXZlX2FwaTEcMBoGCSqGSIb3DQEJARYNcmVAcGF5cGFsLmNvbYIBADAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4GBAIFfOlaagFrl71+jq6OKidbWFSE+Q4FqROvdgIONth+8kSK//Y/4ihuE4Ymvzn5ceE3S/iBSQQMjyvb+s2TWbQYDwcp129OPIbD9epdr4tJOUNiSojw7BHwYRiPh58S1xGlFgHFXwrEBb3dgNbMUa+u4qectsMAXpVHnD9wIyfmHMYIBmjCCAZYCAQEwgZQwgY4xCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDQTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEUMBIGA1UEChMLUGF5UGFsIEluYy4xEzARBgNVBAsUCmxpdmVfY2VydHMxETAPBgNVBAMUCGxpdmVfYXBpMRwwGgYJKoZIhvcNAQkBFg1yZUBwYXlwYWwuY29tAgEAMAkGBSsOAwIaBQCgXTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0wNjA4MjYwODQ2NDdaMCMGCSqGSIb3DQEJBDEWBBTyC1ZchvuTMtcYeudPPSP/w8HiEDANBgkqhkiG9w0BAQEFAASBgJPpBf69pRAJfhzv/MfPiMncuq3TSlvpX7VtG9p4dXzSko4i2lWUDD72r5zdF2NwDgZ6avf630PutgpOzYJQ525If1xU2olc9DWI43UZTqY+FArgFuCJ8VnkPsy9mcbXPoSjLRqNwrsA2yoETxMISO3ASELzELJTJgpPk4bU57eZ-----END PKCS7-----" />
           </form>
 
-          <a href="http://creativecommons.org/licenses/by-sa/2.0/"><img src="/images/cc_button.png" border="0" alt="" /></a>
+          <%= link_to (image_tag "cc_button.png", :alt => "CC by-sa 2.0", :border => "0"), "http://creativecommons.org/licenses/by-sa/2.0/" %>
 
         </center>
       </div>
index 6d45d33dd33f0eefca339ad37039ea59d35c9b2f..263e30e64089d2b93e25bd29d89b8bdac848ae70 100644 (file)
@@ -1,9 +1,10 @@
-<% this_colour = cycle('lightgrey', 'white') # can only call once for some dumb reason %>
+<% this_colour = cycle('lightgrey', 'white') # can only call once for some dumb reason 
+%>
 
 <tr class="inbox-row<%= "-unread" if not message_summary.message_read? %>">
-  <td class="inbox-sender" bgcolor='<%= this_colour %>'><%= link_to h(message_summary.sender.display_name), :controller => 'user', :action => message_summary.sender.display_name %></td>
-  <td class="inbox-subject" bgcolor='<%= this_colour %>'><%= link_to h(message_summary.title), :controller => 'message', :action => 'read', :message_id => message_summary.id  %></td>
-  <td class="inbox-sent" bgcolor='<%= this_colour %>'><%= message_summary.sent_on %></td>
+  <td class="inbox-sender" bgcolor="<%= this_colour %>"><%= link_to h(message_summary.sender.display_name), :controller => 'user', :action => message_summary.sender.display_name %></td>
+  <td class="inbox-subject" bgcolor="<%= this_colour %>"><%= link_to h(message_summary.title), :controller => 'message', :action => 'read', :message_id => message_summary.id  %></td>
+  <td class="inbox-sent" bgcolor="<%= this_colour %>"><%= message_summary.sent_on %></td>
   <% if message_summary.message_read? %>
     <td><%= button_to 'Mark as unread', :controller => 'message', :action => 'mark', :message_id => message_summary.id, :mark => 'unread' %></td>
   <% else %>
index f0d87aa27c98f16f348a88dd2b90ccf8f0abb451..91fafe901347fbb0aed847c2f328523a645e4ee4 100644 (file)
@@ -1,7 +1,8 @@
-<% this_colour = cycle('lightgrey', 'white') # can only call once for some dumb reason %>
+<% this_colour = cycle('lightgrey', 'white') # can only call once for some dumb reason
+%>
 
 <tr class="inbox-row">
-  <td class="inbox-sender" bgcolor='<%= this_colour %>'><%= link_to h(sent_message_summary.recipient.display_name), :controller => 'user', :action => sent_message_summary.recipient.display_name %></td>
-  <td class="inbox-subject" bgcolor='<%= this_colour %>'><%= link_to h(sent_message_summary.title), :controller => 'message', :action => 'read', :message_id => sent_message_summary.id  %></td>
-  <td class="inbox-sent" bgcolor='<%= this_colour %>'><%= sent_message_summary.sent_on %></td>
+  <td class="inbox-sender" bgcolor="<%= this_colour %>"><%= link_to h(sent_message_summary.recipient.display_name), :controller => 'user', :action => sent_message_summary.recipient.display_name %></td>
+  <td class="inbox-subject" bgcolor="<%= this_colour %>"><%= link_to h(sent_message_summary.title), :controller => 'message', :action => 'read', :message_id => sent_message_summary.id  %></td>
+  <td class="inbox-sent" bgcolor="<%= this_colour %>"><%= sent_message_summary.sent_on %></td>
 </tr>
index d7bb18f8e59461492982ab5ec2ccea114a2b5499..17f3588bb47b4b3a6668a0c06111066eea6674b5 100644 (file)
@@ -1,7 +1,4 @@
-<% user_id = params[:user_id] || @user_id %>
-<% display_name = User.find_by_id(user_id).display_name %>
-
-<h2>Send a new message to <%= h(display_name) %></h2>
+<h2>Send a new message to <%= h(@to_user.display_name) %></h2>
 
 <% if params[:display_name] %>
 <p>Writing a new message to <%= h(params[:display_name]) %></p>  
@@ -10,7 +7,7 @@
 
 <%= error_messages_for 'message' %>
 
-<% form_for :message, :url => { :action => "new", :user_id => user_id } do |f| %>
+<% form_for :message, :url => { :action => "new", :user_id => @to_user.id } do |f| %>
   <table>
     <tr valign="top">
       <th>Subject</th>
diff --git a/app/views/message/no_such_user.rhtml b/app/views/message/no_such_user.rhtml
new file mode 100644 (file)
index 0000000..c18733a
--- /dev/null
@@ -0,0 +1,2 @@
+<h1>No such user or message</h1>
+<p>Sorry there is no user or message with that name or id</p>
index 501af7494102b7b13a3de2d7cea3347c5084ab1c..8a57ccf52e0be46b0287b74e1a5daba786a01d33 100644 (file)
@@ -1,34 +1,53 @@
-<h2>User details</h2>
+<h2>My settings</h2>
 <%= error_messages_for 'user' %>
 <% form_for :user, @user do |f| %>
-<table style="width : 100%">
-  <tr><td>Email</td><td><%= f.text_field :email %></td></tr>
-  <tr><td>Mapper since</td><td><%= @user.creation_time %> (<%= time_ago_in_words(@user.creation_time) %> ago)</td></tr>
-  <tr><td>Display Name</td><td><%= f.text_field :display_name %></td></tr>
-  <tr><td>Password</td><td><%= f.password_field :pass_crypt, {:value => '', :size => 50, :maxlength => 255} %></td></tr>
-  <tr><td>Confirm Password</td><td><%= f.password_field :pass_crypt_confirmation, {:value => '', :size => 50, :maxlength => 255} %></td></tr>
+<table id="accountForm">
+  <tr><td class="fieldName">Display Name : </td><td><%= f.text_field :display_name %></td></tr>
+  <tr><td class="fieldName">Email : </td><td><%= f.text_field :email, {:size => 50, :maxlength => 255} %> <span class="minorNote">(not displayed publicly)</span></td></tr>
+  <tr><td class="fieldName" style="padding-bottom:0px;">Password : </td><td style="padding-bottom:0px;"><%= f.password_field :pass_crypt, {:value => '', :size => 30, :maxlength => 255} %></td></tr>
+  <tr><td class="fieldName">Confirm Password : </td><td><%= f.password_field :pass_crypt_confirmation, {:value => '', :size => 30, :maxlength => 255} %></td></tr>
+  <tr>
+  <td class="fieldName" valign="top">Public editing :</td>
+  <td>
+<% if @user.data_public? %>
+  Enabled. Not anonymous <span class="minorNote">(<a href="http://wiki.openstreetmap.org/index.php/Disabling_anonymous_edits" target="_new">what's this?</a>)</span>
+<% else %>
+
+  Currently your edits are anonymous and people can't send you messages or see your location. To show what you edited and allow people to contact you through the website, click the button below.
+  <b>You will need to do this if you want to use the online editor and it is encouraged</b> (<a href="http://wiki.openstreetmap.org/index.php/Disabling_anonymous_edits" target="_new">find out why</a>).
+  <br /><br >
+  This action cannot be reversed and all new users are now public by default.
+  <br /><br />
+  <%= button_to "Make all my edits public", :action => :go_public %>
+  <br /><br />
+  
+<% end %>
+  </td>
+  </tr>
 
-  <tr><td valign="top">Description</td><td><%= f.text_area :description, :class => "editDescription" %></td></tr>
+  
+   
+  <tr><td class="fieldName" valign="top">Profile Description : </td><td><%= f.text_area :description, :rows => '5', :cols => '60' %><br /><br /></td></tr>
 
-  <tr id="homerow" <% unless @user.home_lat and @user.home_lon %> class="nohome" <%end%> ><td>Your home</td><td><em class="message">You have not entered your home location.</em><span class="location">Latitude: <%= f.text_field :home_lat, :size => 20, :id => "home_lat" %> Longitude <%= f.text_field :home_lon, :size => 20, :id => "home_lon" %></span></td></tr>
+  <tr id="homerow" <% unless @user.home_lat and @user.home_lon %> class="nohome" <%end%> ><td class="fieldName">Home Location : </td><td><em class="message">You have not entered your home location.</em><span class="location">Latitude: <%= f.text_field :home_lat, :size => 20, :id => "home_lat" %> Longitude <%= f.text_field :home_lon, :size => 20, :id => "home_lon" %></span></td></tr>
 
   <tr><td></td><td>
   <p>Update home location when I click on the map? <input type="checkbox" value="1" <% unless @user.home_lat and @user.home_lon %> checked="checked" <% end %> id="updatehome" /> </p>
-  <div id="map" style="border: 1px solid black; position: relative; width : 90%; height : 400px;"></div>
+  <div id="map" style="border:1px solid black; position:relative; width:500px; height:400px;"></div>
   </td></tr>
+  
+  <tr><td></td><td align=right><br/></br><%= submit_tag 'Save Changes' %></td></tr>
 </table>
-<%= submit_tag 'Save Changes' %>
+<br/>
+
 <% end %>
 
 <%= render :partial => 'friend_map' %>
 
-<h2>Public editing</h2>
-<% if @user.data_public? %>
-  All your edits are public.
-<% else %>
-Currently your edits are anonymous and people can't send you messages or see your location. To show what you edited and allow people to contact you through the website, click the button below.
-<b>You will need to do this if you want to use the online editor and it is encouraged</b> (<a href="http://wiki.openstreetmap.org/index.php/Disabling_anonymous_edits">find out why</a>).
-This action cannot be reversed and all new users are now public by default.
-  <br /><br />
-  <%= button_to "Make all my edits public", :action => :go_public %>
-<% end %>
+<br/>
+<br/>
+<br/>
+<%= link_to 'return to profile', :controller => 'user', :action => @user.display_name %>
+<br/>
+<br/>
\ No newline at end of file
index 5577b7068889c6c2694644ed477bbea77cf21fd7..7953ff8222eaf2837ee9b2cd646b1adf8c09a574 100644 (file)
@@ -4,7 +4,7 @@
 
 <form method="post">
 <input type="hidden" name="confirm_string" value="<%= params[:confirm_string] %>">
-<input type="submit" name="confirm_action" value="Confrm">
+<input type="submit" name="confirm_action" value="Confirm">
 </form>
 
 
index ff988f070be1ff76b97a4e1811e8306ac61fc714..770ad8873d018390d866f6008425a617e12c5184 100644 (file)
@@ -3,11 +3,13 @@ Please login or <%= link_to 'create an account', :controller => 'user', :action
 
 <% form_tag :action => 'login' do %>
 <%= hidden_field_tag('referer', h(params[:referer])) %>
+<br/>
 <table>
-  <tr><td>Email Address or username:</td><td><%= text_field('user', 'email',{:size => 50, :maxlength => 255}) %></td></tr>
-  <tr><td>Password:</td><td><%= password_field('user', 'password',{:size => 50, :maxlength => 255}) %></td></tr>
+  <tr><td class="fieldName">Email Address or username:</td><td><%= text_field('user', 'email',{:size => 50, :maxlength => 255}) %></td></tr>
+  <tr><td class="fieldName">Password:</td><td><%= password_field('user', 'password',{:size => 28, :maxlength => 255}) %> <span class="minorNote">(<%= link_to 'Lost your password?', :controller => 'user', :action => 'lost_password' %>)</span></td></tr>
+  <tr><td colspan=2>&nbsp;<!--vertical spacer--></td></tr>
+  <tr><td></td><td align="right"><%= submit_tag 'Login' %></td></tr>
 </table>
 
 <br />
-<%= submit_tag 'Login' %>
-<% end %> (<%= link_to 'Lost your password?', :controller => 'user', :action => 'lost_password' %>)
+<% end %>
index 5d4687edd2acc0b969d94d6dfb0bbb69178b839d..1b7f6e9b433bf58b30b9f2fc343f4910b3975699 100644 (file)
@@ -1,20 +1,28 @@
 <h1>Create a user account</h1><br>
-Fill in the form and we'll send you a quick email to activate your account.<br><br>
+Fill in the form and we'll send you a quick email to activate your account.
+<br><br>
 
 By creating an account, you agree that all work uploaded to openstreetmap.org and all data created by use of any tools which connect to openstreetmap.org is to be (non-exclusively) licensed under <a href="http://creativecommons.org/licenses/by-sa/2.0/">this Creative Commons license (by-sa)</a>.<br><br>
 
 <%= error_messages_for 'user' %>
 
 <% form_tag :action => 'save' do %>
-<table>
-  <tr><td>Email Address</td><td><%= text_field('user', 'email',{:size => 50, :maxlength => 255}) %></td></tr>
-  <tr><td>Confirm Email Address</td><td><%= text_field('user', 'email_confirmation',{:size => 50, :maxlength => 255}) %></td></tr>
-  <tr><td>Display Name</td><td><%= text_field('user', 'display_name',{:size => 50, :maxlength => 255}) %></td></tr>
-  <tr><td>Password</td><td><%= password_field('user', 'pass_crypt',{:size => 50, :maxlength => 255}) %></td></tr>
-  <tr><td>Confirm Password</td><td><%= password_field('user', 'pass_crypt_confirmation',{:size => 50, :maxlength => 255}) %></td></tr>
+<table id="loginForm">
+  <tr><td class="fieldName">Email Address : </td><td><%= text_field('user', 'email',{:size => 50, :maxlength => 255}) %></td></tr>
+  <tr><td class="fieldName">Confirm Email Address : </td><td><%= text_field('user', 'email_confirmation',{:size => 50, :maxlength => 255}) %></td></tr>
+  <tr><td></td><td><span class="minorNote">Not displayed publicly (see <a href="http://wiki.openstreetmap.org/index.php/Privacy_Policy" title="wiki privacy policy including section on email addresses">privacy policy)</span></td></tr>
+  <tr><td colspan=2>&nbsp;<!--vertical spacer--></td></tr>
+  <tr><td class="fieldName">Display Name : </td><td><%= text_field('user', 'display_name',{:size => 30, :maxlength => 255}) %></td></tr>
+  <tr><td colspan=2>&nbsp;<!--vertical spacer--></td></tr>
+  <tr><td class="fieldName">Password : </td><td><%= password_field('user', 'pass_crypt',{:size => 30, :maxlength => 255}) %></td></tr>
+  <tr><td class="fieldName">Confirm Password : </td><td><%= password_field('user', 'pass_crypt_confirmation',{:size => 30, :maxlength => 255}) %></td></tr>
+  
+  <tr><td colspan=2>&nbsp;<!--vertical spacer--></td></tr>
+  <tr><td></td><td align=right><input type="submit" value="Signup"></td></tr>
 </table>
 <br>
 <br>
-<input type="submit" value="Signup">
-
+<!--
+See also <a href="http://wiki.openstreetmap.org/index.php/Creating_an_Account" title="wiki help information about this screen">'Creating an Account' help</a>
+-->
 <% end %>
index 438de836d463be146c4364213471b9674fb1244c..66a7426f52922031ee60395a18292c05a9045a11 100644 (file)
@@ -2,11 +2,13 @@
 <h2><%= h(@this_user.display_name) %></h2>
 <div id="userinformation">
 <% if @user and @this_user.id == @user.id %>
+<!-- Displaying user's own profile page -->
 <%= link_to 'my diary', :controller => 'diary_entry', :action => 'list', :display_name => @user.display_name %>
 | <%= link_to 'new diary entry', :controller => 'diary_entry', :action => 'new', :display_name => @user.display_name %>
 | <%= link_to 'my traces', :controller => 'trace', :action=>'mine' %>
 | <%= link_to 'my settings', :controller => 'user', :action => 'account', :display_name => @user.display_name %>
 <% else %>
+<!-- Displaying another user's profile page -->
 <%= link_to 'send message', :controller => 'message', :action => 'new', :user_id => @this_user.id %>
 | <%= link_to 'diary', :controller => 'diary_entry', :action => 'list', :display_name => @this_user.display_name %>
 | <%= link_to 'traces', :controller => 'trace', :action => 'view', :display_name => @this_user.display_name %>
 <% end %>
 </div>
 
+<% if @this_user != nil %>
+<P>
+<b>Mapper since : </b><%= @this_user.creation_time %> (<%= time_ago_in_words(@this_user.creation_time) %> ago)
+</P>
+<% end %>
+  
 <h3>User image</h3>
 <% if @this_user.image %>
   <%= image_tag url_for_file_column(@this_user, "image") %>
     <% end %>
   <% end %>
 <% end %>
+
+<br/>
+<br/>
+<% if @user and @this_user.id == @user.id %>
+<%= link_to 'change your settings', :controller => 'user', :action => 'account', :display_name => @user.display_name %>
+<% end %>
\ No newline at end of file
index 85ebe9f2152884b044c2f73c136ffaee0cd642fa..7400a7b9a916088639aa0a092198f9c3000d82f4 100644 (file)
@@ -3,8 +3,10 @@ standard_settings: &standard_settings
   max_request_area: 0.25
   # Number of GPS trace/trackpoints returned per-page
   tracepoints_per_page: 5000
-  # Maximum number of nodes
+  # Maximum number of nodes that will be returned by the api in a map request
   max_number_of_nodes: 50000
+  # Maximum number of nodes that can be in a way (checked on save)
+  max_number_of_way_nodes: 2000
  
 development:
   <<: *standard_settings
index b884f3b938fea8c5ea541e0d4af5fbbc16529dd7..cc3f9a1a5fb26e4fd5b2186728c3e4d5c6b45127 100644 (file)
@@ -16,6 +16,7 @@ development:
   username: openstreetmap
   password: openstreetmap
   host: localhost
+  encoding: utf8
 
 # Warning: The database defined as 'test' will be erased and
 # re-generated from your development database when you run 'rake'.
@@ -23,14 +24,15 @@ development:
 test:
   adapter: mysql
   database: osm_test
-  username: root
-  password:
+  username: osm_test
+  password: osm_test
   host: localhost
+  encoding: utf8
 
 production:
   adapter: mysql
-  database: openstreetmap
-  username: openstreetmap
-  password: openstreetmap
-  host: db.openstreetmap.org
-
+  database: osm
+  username: osm
+  password: osm
+  host: localhost
+  encoding: utf8
index e6af619eb82b03aaaf8400db39ea951dbba227b6..ffed548bd4360f2715b9a83b82dda5fdc171ebd3 100644 (file)
@@ -5,13 +5,16 @@
 ENV['RAILS_ENV'] ||= 'production'
 
 # Specifies gem version of Rails to use when vendor/rails is not present
-RAILS_GEM_VERSION = '2.0.2' unless defined? RAILS_GEM_VERSION
+RAILS_GEM_VERSION = '2.1.2' unless defined? RAILS_GEM_VERSION
 
 # Set the server URL
 SERVER_URL = ENV['OSM_SERVER_URL'] || 'www.openstreetmap.org'
 
+# Set the generator
+GENERATOR = ENV['OSM_SERVER_GENERATOR'] || 'OpenStreetMap server'
+
 # Application constants needed for routes.rb - must go before Initializer call
-API_VERSION = ENV['OSM_API_VERSION'] || '0.5'
+API_VERSION = ENV['OSM_API_VERSION'] || '0.6'
 
 # Set application status - possible settings are:
 #
@@ -37,6 +40,16 @@ Rails::Initializer.run do |config|
     config.frameworks -= [ :active_record ]
   end
 
+  # Specify gems that this application depends on. 
+  # They can then be installed with "rake gems:install" on new installations.
+  # config.gem "bj"
+  # config.gem "hpricot", :version => '0.6', :source => "http://code.whytheluckystiff.net"
+  # config.gem "aws-s3", :lib => "aws/s3"
+  config.gem 'composite_primary_keys', :version => '1.1.0'
+  config.gem 'libxml-ruby', :version => '0.9.4', :lib => 'libxml'
+  config.gem 'rmagick', :lib => 'RMagick'
+  config.gem 'mysql'
+
   # Only load the plugins named here, in the order given. By default, all plugins 
   # in vendor/plugins are loaded in alphabetical order.
   # :all can be used as a placeholder for all plugins not explicitly named
@@ -63,6 +76,12 @@ Rails::Initializer.run do |config|
   # (create the session table with 'rake db:sessions:create')
   config.action_controller.session_store = :sql_session_store
 
+  # We will use the old style of migrations, rather than the newer
+  # timestamped migrations that were introduced with Rails 2.1, as
+  # it will be confusing to have the numbered and timestamped migrations
+  # together in the same folder.
+  config.active_record.timestamped_migrations = false
+
   # Use SQL instead of Active Record's schema dumper when creating the test database.
   # This is necessary if your schema can't be completely dumped by the schema dumper,
   # like if you have constraints or database-specific column types
@@ -72,5 +91,5 @@ Rails::Initializer.run do |config|
   # config.active_record.observers = :cacher, :garbage_collector
 
   # Make Active Record use UTC-base instead of local time
-  config.active_record.default_timezone = :utc
+  config.active_record.default_timezone = :utc
 end
index 09a451f9a336aa17352c3421db1cc593c593155d..d67452f0c8680c25376243a069ff3527582d7a31 100644 (file)
@@ -12,7 +12,6 @@ config.whiny_nils = true
 config.action_controller.consider_all_requests_local = true
 config.action_view.debug_rjs                         = true
 config.action_controller.perform_caching             = false
-config.action_view.cache_template_extensions         = false
 
 # Don't care if the mailer can't send
-config.action_mailer.raise_delivery_errors = false
\ No newline at end of file
+config.action_mailer.raise_delivery_errors = false
diff --git a/config/initializers/composite_primary_keys.rb b/config/initializers/composite_primary_keys.rb
deleted file mode 100644 (file)
index 430bcfa..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-require 'rubygems'
-gem 'composite_primary_keys', '= 0.9.93'
-require 'composite_primary_keys'
index a1870dbab8b4aaaacfe5e5169bcb40e622f3dc11..ae636a9a31039a260539fd2d195fa49e9ce56684 100644 (file)
@@ -1,7 +1,9 @@
-require 'rubygems'
-gem 'libxml-ruby', '>= 0.8.3'
-require 'libxml'
-
-LibXML::XML::Parser.register_error_handler do |message|
+# This is required otherwise libxml writes out memory errors to
+# the standard output and exits uncleanly 
+# Changed method due to deprecation of the old register_error_handler
+# http://libxml.rubyforge.org/rdoc/classes/LibXML/XML/Parser.html#M000076
+# So set_handler is used instead
+# http://libxml.rubyforge.org/rdoc/classes/LibXML/XML/Error.html#M000334
+LibXML::XML::Error.set_handler do |message|
   raise message
 end
index 86b6c62b800a68a3ba7982cd6537dd02e6346d0c..ad6cb0154b8833d252801df1af57ff35660d569f 100755 (executable)
@@ -1,8 +1,9 @@
 # Potlatch autocomplete values
 # each line should be: key / way|point|POI (tab) list_of_values
 # '-' indicates no autocomplete for values
-highway/way            motorway,motorway_link,trunk,trunk_link,primary,primary_link,secondary,tertiary,unclassified,residential,service,bridleway,cycleway,footway,pedestrian,steps,living_street,track
+highway/way            motorway,motorway_link,trunk,trunk_link,primary,primary_link,secondary,tertiary,unclassified,residential,service,bridleway,cycleway,footway,pedestrian,steps,living_street,track,road
 highway/point  mini_roundabout,traffic_signals,crossing,gate,stile,cattle_grid,toll_booth,incline,viaduct,motorway_junction,services,ford,bus_stop,turning_circle
+tracktype/way  grade1,grade2,grade3,grade4,grade5
 junction/way   roundabout
 cycleway/way   lane,track,opposite_lane,opposite_track,opposite
 waterway/way   river,canal,stream,drain,dock,riverbank
index c54247cb162ce62606493ffdd924046ec3cd4ea3..ec1c2de03e6e7238b9d38c944da9b2ab1593d8ca 100644 (file)
@@ -12,6 +12,7 @@ secondary             0xFDBF6F        1       -
 tertiary               0xFEFECB        1       -
 unclassified   0xE8E8E8        1       -
 residential            0xE8E8E8        1       -
+road                   0xAAAAAA        1       -
 footway                        0xFF6644        -       -
 cycleway               0xFF6644        -       -
 bridleway              0xFF6644        -       -
index 464204edb4e721bfe0e7c1a9fa46b0a1895f250f..c46f9d12a04a439ec0bd49e90646cb3299fd6d65 100644 (file)
@@ -9,10 +9,14 @@ residential road: highway=residential,ref=,name=(type road name)
 service road: highway=service,ref=,name=
 
 way/footway
-footpath: highway=footway,foot=yes
-bridleway: highway=bridleway,foot=yes
-byway: highway=unsurfaced,foot=yes
-permissive path: highway=footway,foot=permissive
+public footpath: highway=footway,foot=yes,tracktype=
+permissive path: highway=footway,foot=permissive,tracktype=
+bridleway: highway=bridleway,foot=yes,tracktype=
+paved track: highway=track,foot=,tracktype=grade1
+gravel track: highway=track,foot=,tracktype=grade2
+rough track: highway=track,foot=,tracktype=grade3
+dirt track: highway=track,foot=,tracktype=grade4
+grass track: highway=track,foot=,tracktype=grade5
 
 way/cycleway
 cycle lane: highway=cycleway,cycleway=lane,ncn_ref=
@@ -36,6 +40,7 @@ light railway: railway=light_rail
 preserved railway: railway=preserved
 disused railway tracks: railway=disused
 course of old railway: railway=abandoned
+railway platform: railway=platform
 
 way/natural
 lake: natural=water,landuse=
index b7d570980de7876f3f344848e215527a289e74b2..b99dfd2ca56964c12ba3ba48c69b13aff443aad7 100644 (file)
@@ -1,10 +1,22 @@
 ActionController::Routing::Routes.draw do |map|
 
   # API
+  map.connect "api/capabilities", :controller => 'api', :action => 'capabilities'
+  
+  map.connect "api/#{API_VERSION}/changeset/create", :controller => 'changeset', :action => 'create'
+  map.connect "api/#{API_VERSION}/changeset/:id/upload", :controller => 'changeset', :action => 'upload', :id => /\d+/
+  map.connect "api/#{API_VERSION}/changeset/:id/download", :controller => 'changeset', :action => 'download', :id => /\d+/
+  map.connect "api/#{API_VERSION}/changeset/:id/expand_bbox", :controller => 'changeset', :action => 'expand_bbox', :id => /\d+/
+  map.connect "api/#{API_VERSION}/changeset/:id", :controller => 'changeset', :action => 'read', :id => /\d+/, :conditions => { :method => :get }
+  map.connect "api/#{API_VERSION}/changeset/:id", :controller => 'changeset', :action => 'update', :id => /\d+/, :conditions => { :method => :put }
+  map.connect "api/#{API_VERSION}/changeset/:id/close", :controller => 'changeset', :action => 'close', :id =>/\d+/
+  map.connect "api/#{API_VERSION}/changesets", :controller => 'changeset', :action => 'query'
+  
   map.connect "api/#{API_VERSION}/node/create", :controller => 'node', :action => 'create'
   map.connect "api/#{API_VERSION}/node/:id/ways", :controller => 'way', :action => 'ways_for_node', :id => /\d+/
   map.connect "api/#{API_VERSION}/node/:id/relations", :controller => 'relation', :action => 'relations_for_node', :id => /\d+/
   map.connect "api/#{API_VERSION}/node/:id/history", :controller => 'old_node', :action => 'history', :id => /\d+/
+  map.connect "api/#{API_VERSION}/node/:id/:version", :controller => 'old_node', :action => 'version', :id => /\d+/, :version => /\d+/
   map.connect "api/#{API_VERSION}/node/:id", :controller => 'node', :action => 'read', :id => /\d+/, :conditions => { :method => :get }
   map.connect "api/#{API_VERSION}/node/:id", :controller => 'node', :action => 'update', :id => /\d+/, :conditions => { :method => :put }
   map.connect "api/#{API_VERSION}/node/:id", :controller => 'node', :action => 'delete', :id => /\d+/, :conditions => { :method => :delete }
@@ -14,12 +26,12 @@ ActionController::Routing::Routes.draw do |map|
   map.connect "api/#{API_VERSION}/way/:id/history", :controller => 'old_way', :action => 'history', :id => /\d+/
   map.connect "api/#{API_VERSION}/way/:id/full", :controller => 'way', :action => 'full', :id => /\d+/
   map.connect "api/#{API_VERSION}/way/:id/relations", :controller => 'relation', :action => 'relations_for_way', :id => /\d+/
+  map.connect "api/#{API_VERSION}/way/:id/:version", :controller => 'old_way', :action => 'version', :id => /\d+/, :version => /\d+/
   map.connect "api/#{API_VERSION}/way/:id", :controller => 'way', :action => 'read', :id => /\d+/, :conditions => { :method => :get }
   map.connect "api/#{API_VERSION}/way/:id", :controller => 'way', :action => 'update', :id => /\d+/, :conditions => { :method => :put }
   map.connect "api/#{API_VERSION}/way/:id", :controller => 'way', :action => 'delete', :id => /\d+/, :conditions => { :method => :delete }
   map.connect "api/#{API_VERSION}/ways", :controller => 'way', :action => 'ways', :id => nil
 
-  map.connect "api/#{API_VERSION}/capabilities", :controller => 'api', :action => 'capabilities'
   map.connect "api/#{API_VERSION}/relation/create", :controller => 'relation', :action => 'create'
   map.connect "api/#{API_VERSION}/relation/:id/relations", :controller => 'relation', :action => 'relations_for_relation', :id => /\d+/
   map.connect "api/#{API_VERSION}/relation/:id/history", :controller => 'old_relation', :action => 'history', :id => /\d+/
@@ -52,7 +64,7 @@ ActionController::Routing::Routes.draw do |map|
   map.connect "api/#{API_VERSION}/gpx/:id/details", :controller => 'trace', :action => 'api_details'
   map.connect "api/#{API_VERSION}/gpx/:id/data", :controller => 'trace', :action => 'api_data'
   
-  # Potlatch API
+  # AMF (ActionScript) API
   
   map.connect "api/#{API_VERSION}/amf/read", :controller =>'amf', :action =>'amf_read'
   map.connect "api/#{API_VERSION}/amf/write", :controller =>'amf', :action =>'amf_write'
@@ -67,6 +79,7 @@ ActionController::Routing::Routes.draw do |map|
   map.connect '/browse/node/:id/history', :controller => 'browse', :action => 'node_history', :id => /\d+/
   map.connect '/browse/relation/:id', :controller => 'browse', :action => 'relation', :id => /\d+/
   map.connect '/browse/relation/:id/history', :controller => 'browse', :action => 'relation_history', :id => /\d+/
+  map.connect '/browse/changeset/:id', :controller => 'browse', :action => 'changeset', :id => /\d+/
   
   # web site
 
index 447c63651f538952278eb69e10afb7dcbce6ffc3..f0002933938d842abfa0edf4aa09ac48551d6e03 100644 (file)
--- a/db/README
+++ b/db/README
@@ -25,12 +25,12 @@ $ mysql -u <uid> -p
 > flush privileges;
 > exit
 
-Creating functions
-====================
+Creating functions For MySQL
+==============================
 
 Run this command in the db/functions directory:
 
-$ make
+$ make libmyosm.so
 
 Make sure the db/functions directory is on the MySQL server's library
 path and restart the MySQL server. 
@@ -49,6 +49,22 @@ $ mysql -u <uid> -p openstreetmap
 > create function maptile_for_point returns integer soname 'libmyosm.so';
 > exit
 
+Creating functions for PgSQL
+==============================
+
+Run this command in the db/functions directory:
+
+$ make libmyosm.so
+
+Now create the function as follows:
+
+$ psql openstreetmap
+(This may need authentication or a -u <dbowneruid>)
+
+> CREATE FUNCTION maptile_for_point(int8, int8, int4) RETURNS int4 
+  AS '/path/to/rails-port/db/functions/libpgosm', 'maptile_for_point'
+  LANGUAGE C STRICT;
+
 Creating database skeleton tables
 ===================================
 
index 7652862faf55b5bfad71eddf7e56184e5dc62efc..1bdddce71f9a68ed1fb693791e5319ffacf49518 100644 (file)
@@ -1,5 +1,7 @@
 QTDIR=../../lib/quad_tile
 
+PGSQLINC=/usr/include/postgresql/8.3/server/
+
 OS=$(shell uname -s)
 ifeq (${OS},Darwin)
     LDFLAGS=-bundle
@@ -7,11 +9,22 @@ else
     LDFLAGS=-shared
 endif
 
-libmyosm.so: quadtile.o maptile.o
-       cc ${LDFLAGS} -o libmyosm.so quadtile.o maptile.o
+all: libmyosm.so libpgosm.so
+
+clean:
+       $(RM) *.so *.o
+
+libmyosm.so: quadtile.o maptile-mysql.o
+       cc ${LDFLAGS} -o libmyosm.so quadtile.o maptile-mysql.o
+
+libpgosm.so: maptile-pgsql.o
+       cc ${LDFLAGS} -o libpgosm.so maptile-pgsql.o
 
 quadtile.o: quadtile.c ${QTDIR}/quad_tile.h
        cc `mysql_config --include` -I${QTDIR} -fPIC -O3 -c -o quadtile.o quadtile.c
 
-maptile.o: maptile.c
-       cc `mysql_config --include` -fPIC -O3 -c -o maptile.o maptile.c
+maptile-mysql.o: maptile.c
+       cc `mysql_config --include` -fPIC -O3 -DUSE_MYSQL -c -o maptile-mysql.o maptile.c
+
+maptile-pgsql.o: maptile.c
+       cc -I${PGSQLINC} -O3 -fPIC -DUSE_PGSQL -c -o maptile-pgsql.o maptile.c
\ No newline at end of file
index f96f9c23e5e052078c90c2348ae70852b41bd2d2..c2baac5d467c109c2d392b18313b048a4319d7b4 100644 (file)
@@ -1,3 +1,31 @@
+#ifndef USE_MYSQL
+#ifndef USE_PGSQL
+#error One of USE_MYSQL or USE_PGSQL must be defined
+#endif
+#endif
+
+#include <math.h>
+
+/* The real maptile-for-point functionality is here */
+
+static long long internal_maptile_for_point(double lat, double lon, long long zoom)
+{
+   double       scale = pow(2, zoom);
+   double       r_per_d = M_PI / 180;
+   unsigned int x;
+   unsigned int y;
+
+   x = floor((lon + 180.0) * scale / 360.0);
+   y = floor((1 - log(tan(lat * r_per_d) + 1.0 / cos(lat * r_per_d)) / M_PI) * scale / 2.0);
+
+   return (x << zoom) | y;
+}
+
+#ifdef USE_MYSQL
+#ifdef USE_PGSQL
+#error ONLY one of USE_MYSQL and USE_PGSQL should be defined
+#endif
+
 #include <my_global.h>
 #include <my_sys.h>
 #include <m_string.h>
@@ -27,13 +55,39 @@ long long maptile_for_point(UDF_INIT *initid, UDF_ARGS *args, char *is_null, cha
    double       lat = *(long long *)args->args[0] / 10000000.0;
    double       lon = *(long long *)args->args[1] / 10000000.0;
    long long    zoom = *(long long *)args->args[2];
-   double       scale = pow(2, zoom);
-   double       r_per_d = M_PI / 180;
-   unsigned int x;
-   unsigned int y;
+   
+   return internal_maptile_for_point(lat, lon, zoom);
+}
+#endif
 
-   x = floor((lon + 180.0) * scale / 360.0);
-   y = floor((1 - log(tan(lat * r_per_d) + 1.0 / cos(lat * r_per_d)) / M_PI) * scale / 2.0);
+#ifdef USE_PGSQL
+#include <postgres.h>
+#include <fmgr.h>
 
-   return (x << zoom) | y;
+Datum
+maptile_for_point(PG_FUNCTION_ARGS)
+{
+  double lat = PG_GETARG_INT64(0) / 10000000.0;
+  double lon = PG_GETARG_INT64(1) / 10000000.0;
+  int zoom = PG_GETARG_INT32(2);
+  
+  PG_RETURN_INT32(internal_maptile_for_point(lat, lon, zoom));
 }
+
+PG_FUNCTION_INFO_V1(maptile_for_point);
+
+/*
+ * To bind this into PGSQL, try something like:
+ *
+ * CREATE FUNCTION maptile_for_point(int8, int8, int4) RETURNS int4 
+ *  AS '/path/to/rails-port/db/functions/libpgosm', 'maptile_for_point'
+ *  LANGUAGE C STRICT;
+ *
+ * (without all the *s)
+ */
+
+#ifdef PG_MODULE_MAGIC
+PG_MODULE_MAGIC;
+#endif
+
+#endif
index 2c80dd8adeac4c053a6ce5297d8849a1483d1671..3e3377921df5fa4504c7da242dcce94d648c2521 100644 (file)
@@ -16,7 +16,7 @@ class CreateOsmDb < ActiveRecord::Migration
     add_index "current_nodes", ["latitude", "longitude"], :name => "current_nodes_lat_lon_idx"
     add_index "current_nodes", ["timestamp"], :name => "current_nodes_timestamp_idx"
 
-    change_column "current_nodes", "id", :bigint, :limit => 64, :null => false, :options => "AUTO_INCREMENT"
+    change_column :current_nodes, :id, :bigint_auto_64
 
     create_table "current_segments", innodb_table do |t|
       t.column "id",        :bigint,   :limit => 64,                 :null => false
@@ -32,7 +32,7 @@ class CreateOsmDb < ActiveRecord::Migration
     add_index "current_segments", ["node_a"], :name => "current_segments_a_idx"
     add_index "current_segments", ["node_b"], :name => "current_segments_b_idx"
 
-    change_column "current_segments", "id", :bigint, :limit => 64, :null => false, :options => "AUTO_INCREMENT"
+    change_column :current_segments, :id, :bigint_auto_64
 
     create_table "current_way_segments", innodb_table do |t|
       t.column "id",          :bigint, :limit => 64
@@ -50,21 +50,17 @@ class CreateOsmDb < ActiveRecord::Migration
     end
 
     add_index "current_way_tags", ["id"], :name => "current_way_tags_id_idx"
-    execute "CREATE FULLTEXT INDEX `current_way_tags_v_idx` ON `current_way_tags` (`v`)"
+    add_fulltext_index "current_way_tags", "v"
 
     create_table "current_ways", myisam_table do |t|
-      t.column "id",        :bigint,   :limit => 64, :null => false
+      t.column "id",        :bigint_pk_64, :null => false
       t.column "user_id",   :bigint,   :limit => 20
       t.column "timestamp", :datetime
       t.column "visible",   :boolean
     end
 
-    add_primary_key "current_ways", ["id"]
-
-    change_column "current_ways", "id", :bigint, :limit => 64, :null => false, :options => "AUTO_INCREMENT"
-
     create_table "diary_entries", myisam_table do |t|
-      t.column "id",         :bigint,   :limit => 20, :null => false
+      t.column "id",         :bigint_pk, :null => false
       t.column "user_id",    :bigint,   :limit => 20, :null => false
       t.column "title",      :string
       t.column "body",       :text
@@ -72,21 +68,14 @@ class CreateOsmDb < ActiveRecord::Migration
       t.column "updated_at", :datetime
     end
 
-    add_primary_key "diary_entries", ["id"]
-
-    change_column "diary_entries", "id", :bigint, :limit => 20, :null => false, :options => "AUTO_INCREMENT"
-
     create_table "friends", myisam_table do |t|
-      t.column "id",             :bigint,  :limit => 20, :null => false
+      t.column "id",             :bigint_pk, :null => false
       t.column "user_id",        :bigint,  :limit => 20, :null => false
       t.column "friend_user_id", :bigint,  :limit => 20, :null => false
     end
 
-    add_primary_key "friends", ["id"]
     add_index "friends", ["friend_user_id"], :name => "user_id_idx"
 
-    change_column "friends", "id", :bigint, :limit => 20, :null => false, :options => "AUTO_INCREMENT"
-
     create_table "gps_points", myisam_table do |t|
       t.column "altitude",  :float
       t.column "user_id",   :integer,  :limit => 20
@@ -104,16 +93,13 @@ class CreateOsmDb < ActiveRecord::Migration
     create_table "gpx_file_tags", myisam_table do |t|
       t.column "gpx_id", :bigint,  :limit => 64, :default => 0, :null => false
       t.column "tag",    :string
-      t.column "id",     :integer, :limit => 20, :null => false
+      t.column "id",     :bigint_pk, :null => false
     end
 
-    add_primary_key "gpx_file_tags", ["id"]
     add_index "gpx_file_tags", ["gpx_id"], :name => "gpx_file_tags_gpxid_idx"
 
-    change_column "gpx_file_tags", "id", :integer, :limit => 20, :null => false, :options => "AUTO_INCREMENT"
-
     create_table "gpx_files", myisam_table do |t|
-      t.column "id",          :bigint,   :limit => 64,                   :null => false
+      t.column "id",          :bigint_pk_64,                   :null => false
       t.column "user_id",     :bigint,   :limit => 20
       t.column "visible",     :boolean,                :default => true, :null => false
       t.column "name",        :string,                 :default => "",   :null => false
@@ -126,12 +112,9 @@ class CreateOsmDb < ActiveRecord::Migration
       t.column "inserted",    :boolean
     end
 
-    add_primary_key "gpx_files", ["id"]
     add_index "gpx_files", ["timestamp"], :name => "gpx_files_timestamp_idx"
     add_index "gpx_files", ["visible", "public"], :name => "gpx_files_visible_public_idx"
 
-    change_column "gpx_files", "id", :bigint, :limit => 64, :null => false, :options => "AUTO_INCREMENT"
-
     create_table "gpx_pending_files", myisam_table do |t|
       t.column "originalname", :string
       t.column "tmpname",      :string
@@ -139,7 +122,7 @@ class CreateOsmDb < ActiveRecord::Migration
     end
 
     create_table "messages", myisam_table do |t|
-      t.column "id",                :bigint,   :limit => 20,                    :null => false
+      t.column "id",                :bigint_pk,                                 :null => false
       t.column "user_id",           :bigint,   :limit => 20,                    :null => false
       t.column "from_user_id",      :bigint,   :limit => 20,                    :null => false
       t.column "from_display_name", :string,                 :default => ""
@@ -150,21 +133,14 @@ class CreateOsmDb < ActiveRecord::Migration
       t.column "to_user_id",        :bigint,   :limit => 20,                    :null => false
     end
 
-    add_primary_key "messages", ["id"]
     add_index "messages", ["from_display_name"], :name => "from_name_idx"
 
-    change_column "messages", "id", :bigint, :limit => 20, :null => false, :options => "AUTO_INCREMENT"
-
     create_table "meta_areas", myisam_table do |t|
-      t.column "id",        :bigint,  :limit => 64, :null => false
+      t.column "id",        :bigint_pk_64, :null => false
       t.column "user_id",   :bigint,  :limit => 20
       t.column "timestamp", :datetime
     end
 
-    add_primary_key "meta_areas", ["id"]
-
-    change_column "meta_areas", "id", :bigint, :limit => 64, :null => false, :options => "AUTO_INCREMENT"
-
     create_table "nodes", myisam_table do |t|
       t.column "id",        :bigint,  :limit => 64
       t.column "latitude",  :double
@@ -194,7 +170,7 @@ class CreateOsmDb < ActiveRecord::Migration
 
     create_table "users", innodb_table do |t|
       t.column "email",         :string
-      t.column "id",            :bigint,   :limit => 20,                    :null => false
+      t.column "id",            :bigint_pk,                    :null => false
       t.column "token",         :string
       t.column "active",        :integer,                :default => 0,     :null => false
       t.column "pass_crypt",    :string
@@ -211,12 +187,9 @@ class CreateOsmDb < ActiveRecord::Migration
       t.column "home_zoom",     :integer,  :limit => 2,  :default => 3
     end
 
-    add_primary_key "users", ["id"]
     add_index "users", ["email"], :name => "users_email_idx"
     add_index "users", ["display_name"], :name => "users_display_name_idx"
 
-    change_column "users", "id", :bigint, :limit => 20, :null => false, :options => "AUTO_INCREMENT"
-
     create_table "way_segments", myisam_table do |t|
       t.column "id",          :bigint,  :limit => 64, :default => 0, :null => false
       t.column "segment_id",  :integer
@@ -226,7 +199,7 @@ class CreateOsmDb < ActiveRecord::Migration
 
     add_primary_key "way_segments", ["id", "version", "sequence_id"]
 
-    change_column "way_segments", "sequence_id", :bigint, :limit => 11, :null => false, :options => "AUTO_INCREMENT"
+    change_column "way_segments", "sequence_id", :bigint_auto_11
 
     create_table "way_tags", myisam_table do |t|
       t.column "id",      :bigint,  :limit => 64, :default => 0, :null => false
@@ -248,7 +221,7 @@ class CreateOsmDb < ActiveRecord::Migration
     add_primary_key "ways", ["id", "version"]
     add_index "ways", ["id"], :name => "ways_id_version_idx"
 
-    change_column "ways", "version", :bigint, :limit => 20, :null => false, :options => "AUTO_INCREMENT"
+    change_column "ways", "version", :bigint_auto_20
   end
 
   def self.down
index b99055e5244f1483031513982ac7aa4ca1527ea6..b13a92099d29dde64d2a3c1525465d5ab83e24a3 100644 (file)
@@ -29,7 +29,7 @@ class CleanupOsmDb < ActiveRecord::Migration
     change_column "current_ways", "user_id", :bigint, :limit => 20, :null => false
     change_column "current_ways", "timestamp", :datetime, :null => false
     change_column "current_ways", "visible", :boolean, :null => false
-    execute "ALTER TABLE current_ways ENGINE = InnoDB"
+    change_engine "current_ways", "InnoDB"
 
     change_column "diary_entries", "title", :string, :null => false
     change_column "diary_entries", "body", :text, :null => false
@@ -39,7 +39,9 @@ class CleanupOsmDb < ActiveRecord::Migration
     add_index "friends", ["user_id"], :name => "friends_user_id_idx"
 
     remove_index "gps_points", :name => "points_uid_idx"
+    remove_index "gps_points", :name => "points_idx"
     remove_column "gps_points", "user_id"
+    add_index "gps_points", ["latitude", "longitude"], :name => "points_idx"
     change_column "gps_points", "trackid", :integer, :null => false
     change_column "gps_points", "latitude", :integer, :null => false
     change_column "gps_points", "longitude", :integer, :null => false
@@ -150,15 +152,11 @@ class CleanupOsmDb < ActiveRecord::Migration
     change_column "nodes", "id", :bigint, :limit => 64
 
     create_table "meta_areas", myisam_table do |t|
-      t.column "id",        :bigint,  :limit => 64, :null => false
+      t.column "id",        :bigint_pk_64, :null => false
       t.column "user_id",   :bigint,  :limit => 20
       t.column "timestamp", :datetime
     end
 
-    add_primary_key "meta_areas", ["id"]
-
-    change_column "meta_areas", "id", :bigint, :limit => 64, :null => false, :options => "AUTO_INCREMENT"
-
     remove_index "messages", :name => "messages_to_user_id_idx"
     change_column "messages", "message_read", :boolean, :default => false
     change_column "messages", "sent_on", :datetime
@@ -195,7 +193,7 @@ class CleanupOsmDb < ActiveRecord::Migration
     change_column "diary_entries", "body", :text
     change_column "diary_entries", "title", :string, :default => nil
 
-    execute "ALTER TABLE current_ways ENGINE = MyISAM"
+    change_engine "current_ways", "MyISAM"
     change_column "current_ways", "visible", :boolean
     change_column "current_ways", "timestamp", :datetime
     change_column "current_ways", "user_id", :bigint, :limit => 20
@@ -223,6 +221,6 @@ class CleanupOsmDb < ActiveRecord::Migration
     change_column "current_nodes", "user_id", :bigint, :limit => 20
     change_column "current_nodes", "longitude", :double
     change_column "current_nodes", "latitude", :double
-    change_column "current_nodes", "id", :bigint, :limit => 64, :null => false, :options => "AUTO_INCREMENT"
+    change_column "current_nodes", "id", :bigint_auto_64
   end
 end
index 7b1c75479ac0144798004ddae61f010478aa96a9..4de0dd4b19120e6613528918ea7d96c248e57751 100644 (file)
@@ -1,6 +1,6 @@
 class SqlSessionStoreSetup < ActiveRecord::Migration
   def self.up
-    create_table "sessions", :options => "ENGINE=InnoDB" do |t|
+    create_table "sessions", :options => innodb_option do |t|
       t.column "session_id", :string
       t.column "data",       :text
       t.column "created_at", :timestamp
index 92f01bf5d124b3ce61fe3f84788367e158996c4a..a6e81d222fcd190cef0aa969352ae923a4354b5e 100644 (file)
@@ -13,18 +13,15 @@ class UserEnhancements < ActiveRecord::Migration
     add_primary_key "user_preferences", ["user_id", "k"]
 
     create_table "user_tokens", innodb_table do |t|
-      t.column "id",      :bigint,   :limit => 20, :null => false
+      t.column "id",      :bigint_pk, :null => false
       t.column "user_id", :bigint,   :limit => 20, :null => false
       t.column "token",   :string,   :null => false
       t.column "expiry",  :datetime, :null => false
     end
 
-    add_primary_key "user_tokens", ["id"]
     add_index "user_tokens", ["token"], :name => "user_tokens_token_idx", :unique => true
     add_index "user_tokens", ["user_id"], :name => "user_tokens_user_id_idx"
 
-    change_column "user_tokens", "id", :bigint, :limit => 20, :null => false, :options => "AUTO_INCREMENT"
-
     User.find(:all, :conditions => "token is not null").each do |user|
       UserToken.create(:user_id => user.id, :token => user.token, :expiry => 1.week.from_now)
     end
index 51a4d1376233c28ed88494b24f3c7eca71ee5a30..74d85d195629d7ad731eb54e28956f15c4b176eb 100644 (file)
@@ -1,6 +1,6 @@
 class TileTracepoints < ActiveRecord::Migration
   def self.up
-    add_column "gps_points", "tile", :integer, :null => false, :unsigned => true
+    add_column "gps_points", "tile", :four_byte_unsigned
     add_index "gps_points", ["tile"], :name => "points_tile_idx"
     remove_index "gps_points", :name => "points_idx"
 
index 3a50cc9b05ff0372041681b8a16d2e12973556e5..dc4755ac3bbffeb4c53031bd70c24940437664d1 100644 (file)
@@ -33,10 +33,12 @@ class TileNodes < ActiveRecord::Migration
   end
 
   def self.up
+    remove_index "current_nodes", :name => "current_nodes_timestamp_idx"
+
     rename_table "current_nodes", "current_nodes_v5"
 
     create_table "current_nodes", innodb_table do |t|
-      t.column "id",        :bigint,   :limit => 64,                 :null => false
+      t.column "id",        :bigint_pk_64,                           :null => false
       t.column "latitude",  :integer,                                :null => false
       t.column "longitude", :integer,                                :null => false
       t.column "user_id",   :bigint,   :limit => 20,                 :null => false
@@ -46,17 +48,17 @@ class TileNodes < ActiveRecord::Migration
       t.column "tile",      :integer,                                :null => false
     end
 
-    add_primary_key "current_nodes", ["id"]
     add_index "current_nodes", ["timestamp"], :name => "current_nodes_timestamp_idx"
     add_index "current_nodes", ["tile"], :name => "current_nodes_tile_idx"
 
-    change_column "current_nodes", "id", :bigint, :limit => 64, :null => false, :options => "AUTO_INCREMENT"
-    change_column "current_nodes", "tile", :integer, :null => false, :unsigned => true
+    change_column "current_nodes", "tile", :four_byte_unsigned
 
     upgrade_table "current_nodes_v5", "current_nodes", Node
     
     drop_table "current_nodes_v5"
 
+    remove_index "nodes", :name=> "nodes_uid_idx"
+    remove_index "nodes", :name=> "nodes_timestamp_idx"
     rename_table "nodes", "nodes_v5"
 
     create_table "nodes", myisam_table do |t|
@@ -74,7 +76,7 @@ class TileNodes < ActiveRecord::Migration
     add_index "nodes", ["timestamp"], :name => "nodes_timestamp_idx"
     add_index "nodes", ["tile"], :name => "nodes_tile_idx"
 
-    change_column "nodes", "tile", :integer, :null => false, :unsigned => true
+    change_column "nodes", "tile", :four_byte_unsigned
 
     upgrade_table "nodes_v5", "nodes", OldNode
 
@@ -85,7 +87,7 @@ class TileNodes < ActiveRecord::Migration
     rename_table "current_nodes", "current_nodes_v6"
 
     create_table "current_nodes", innodb_table do |t|
-      t.column "id",        :bigint,   :limit => 64,                 :null => false
+      t.column "id",        :bigint_pk_64,                           :null => false
       t.column "latitude",  :double,                                 :null => false
       t.column "longitude", :double,                                 :null => false
       t.column "user_id",   :bigint,   :limit => 20,                 :null => false
@@ -94,12 +96,9 @@ class TileNodes < ActiveRecord::Migration
       t.column "timestamp", :datetime,                               :null => false
     end
 
-    add_primary_key "current_nodes", ["id"]
     add_index "current_nodes", ["latitude", "longitude"], :name => "current_nodes_lat_lon_idx"
     add_index "current_nodes", ["timestamp"], :name => "current_nodes_timestamp_idx"
 
-    change_column "current_nodes", "id", :bigint, :limit => 64, :null => false, :options => "AUTO_INCREMENT"
-
     downgrade_table "current_nodes_v6", "current_nodes"
 
     drop_table "current_nodes_v6"
index a30642e32a801127e576c21c4a7db0e7c581db61..c265fc3adef1507c8d4e672acf8fcb98a2fc0520 100644 (file)
@@ -11,7 +11,7 @@ class AddRelations < ActiveRecord::Migration
       t.column "member_role", :string
     end
     # enums work like strings but are more efficient
-    execute "alter table current_relation_members change column member_type member_type enum('node','way','relation');"
+    alter_column_nwr_enum :current_relation_members, :member_type
 
     add_primary_key "current_relation_members", ["id", "member_type", "member_id", "member_role"]
     add_index "current_relation_members", ["member_type", "member_id"], :name => "current_relation_members_member_idx"
@@ -24,18 +24,15 @@ class AddRelations < ActiveRecord::Migration
     end
 
     add_index "current_relation_tags", ["id"], :name => "current_relation_tags_id_idx"
-    execute "CREATE FULLTEXT INDEX `current_relation_tags_v_idx` ON `current_relation_tags` (`v`)"
+    add_fulltext_index "current_relation_tags", "v"
 
     create_table "current_relations", innodb_table do |t|
-      t.column "id",        :bigint,   :limit => 64, :null => false
+      t.column "id",        :bigint_pk_64,           :null => false
       t.column "user_id",   :bigint,   :limit => 20, :null => false
       t.column "timestamp", :datetime, :null => false
       t.column "visible",   :boolean,  :null => false
     end
 
-    add_primary_key "current_relations", ["id"]
-    change_column "current_relations", "id", :bigint, :limit => 64, :null => false, :options => "AUTO_INCREMENT"
-
     create_table "relation_members", myisam_table do |t|
       t.column "id",          :bigint,  :limit => 64, :default => 0, :null => false
       t.column "member_type", :string, :limit => 11, :null => false
@@ -44,7 +41,7 @@ class AddRelations < ActiveRecord::Migration
       t.column "version",     :bigint,  :limit => 20, :default => 0, :null => false
     end
 
-    execute "alter table relation_members change column member_type member_type enum('node','way','relation');" 
+    alter_column_nwr_enum :relation_members, :member_type 
     add_primary_key "relation_members", ["id", "version", "member_type", "member_id", "member_role"]
     add_index "relation_members", ["member_type", "member_id"], :name => "relation_members_member_idx"
 
@@ -68,7 +65,7 @@ class AddRelations < ActiveRecord::Migration
     add_primary_key "relations", ["id", "version"]
     add_index "relations", ["timestamp"], :name => "relations_timestamp_idx"
     
-    change_column "relations", "version", :bigint, :limit => 20, :null => false, :options => "AUTO_INCREMENT"
+    change_column "relations", "version", :bigint_auto_20
   end
 
 
index be07d851ae96cfd4918b5ebb4d2c3d2130384ff8..43019a938ff96738d322d39361e6677632871528 100644 (file)
@@ -1,7 +1,7 @@
 class DiaryComments < ActiveRecord::Migration
   def self.up
     create_table "diary_comments", myisam_table do |t|
-      t.column "id",             :bigint,   :limit => 20, :null => false
+      t.column "id",             :bigint_pk,              :null => false
       t.column "diary_entry_id", :bigint,   :limit => 20, :null => false
       t.column "user_id",        :bigint,   :limit => 20, :null => false
       t.column "body",           :text,                   :null => false
@@ -9,10 +9,8 @@ class DiaryComments < ActiveRecord::Migration
       t.column "updated_at",     :datetime,               :null => false
     end
 
-    add_primary_key "diary_comments", ["id"]
     add_index "diary_comments", ["diary_entry_id", "id"], :name => "diary_comments_entry_id_idx", :unique => true
 
-    change_column "diary_comments", "id", :bigint, :limit => 20, :null => false, :options => "AUTO_INCREMENT"
   end
 
   def self.down
index b8af4bf6ad5e7b6c519ba19c2b0f1228d95cd227..a0180970109a8ea212b013c359e75f3832009080 100644 (file)
@@ -1,7 +1,7 @@
 class AddEmailValid < ActiveRecord::Migration
   def self.up
     add_column "users", "email_valid", :boolean, :default => false, :null => false
-    User.update_all("email_valid = active")
+    User.update_all("email_valid = (active != 0)") #email_valid is :boolean, but active is :integer. "email_valid = active" (see r11802 or earlier) will fail for stricter dbs than mysql
   end
 
   def self.down
index d870dfffdae477c646136afc48a9650bbd2f3eab..869f24c3755c12eb792259d8d53e45594f2371dc 100644 (file)
@@ -1,7 +1,7 @@
 class AddUserVisible < ActiveRecord::Migration
   def self.up
     add_column "users", "visible", :boolean, :default => true, :null => false
-    User.update_all("visible = 1")
+    User.update_all(:visible => true)
   end
 
   def self.down
diff --git a/db/migrate/018_add_timestamp_indexes.rb b/db/migrate/018_add_timestamp_indexes.rb
new file mode 100644 (file)
index 0000000..c6b3bc7
--- /dev/null
@@ -0,0 +1,11 @@
+class AddTimestampIndexes < ActiveRecord::Migration
+  def self.up
+    add_index :current_ways, :timestamp, :name => :current_ways_timestamp_idx
+    add_index :current_relations, :timestamp, :name => :current_relations_timestamp_idx
+  end
+
+  def self.down
+    remove_index :current_ways, :name => :current_ways_timestamp_idx
+    remove_index :current_relations, :name => :current_relations_timestamp_idx
+  end
+end
diff --git a/db/migrate/019_populate_node_tags_and_remove.rb b/db/migrate/019_populate_node_tags_and_remove.rb
new file mode 100644 (file)
index 0000000..8603586
--- /dev/null
@@ -0,0 +1,60 @@
+class PopulateNodeTagsAndRemove < ActiveRecord::Migration
+  def self.up
+    have_nodes = select_value("SELECT count(*) FROM current_nodes").to_i != 0
+
+    if have_nodes
+      prefix = File.join Dir.tmpdir, "019_populate_node_tags_and_remove.#{$$}."
+
+      cmd = "db/migrate/019_populate_node_tags_and_remove_helper"
+      src = "#{cmd}.c"
+      if not File.exists? cmd or File.mtime(cmd) < File.mtime(src) then 
+        system 'cc -O3 -Wall `mysql_config --cflags --libs` ' +
+          "#{src} -o #{cmd}" or fail
+      end
+
+      conn_opts = ActiveRecord::Base.connection.instance_eval { @connection_options }
+      args = conn_opts.map { |arg| arg.to_s } + [prefix]
+      fail "#{cmd} failed" unless system cmd, *args
+
+      tempfiles = ['nodes', 'node_tags', 'current_nodes', 'current_node_tags'].
+        map { |base| prefix + base }
+      nodes, node_tags, current_nodes, current_node_tags = tempfiles
+    end
+
+    execute "TRUNCATE nodes"
+    remove_column :nodes, :tags
+    remove_column :current_nodes, :tags
+
+    add_column :nodes, :version, :bigint, :limit => 20, :null => false
+
+    create_table :current_node_tags, innodb_table do |t|
+      t.column :id,          :bigint, :limit => 64, :null => false
+      t.column :k,          :string, :default => "", :null => false
+      t.column :v,          :string, :default => "", :null => false
+    end
+
+    create_table :node_tags, innodb_table do |t|
+      t.column :id,          :bigint, :limit => 64, :null => false
+      t.column :version,     :bigint, :limit => 20, :null => false
+      t.column :k,          :string, :default => "", :null => false
+      t.column :v,          :string, :default => "", :null => false
+    end
+
+    # now get the data back
+    csvopts = "FIELDS TERMINATED BY ',' ENCLOSED BY '\"' ESCAPED BY '\"' LINES TERMINATED BY '\\n'"
+
+    if have_nodes
+      execute "LOAD DATA INFILE '#{nodes}' INTO TABLE nodes #{csvopts} (id, latitude, longitude, user_id, visible, timestamp, tile, version)";
+      execute "LOAD DATA INFILE '#{node_tags}' INTO TABLE node_tags #{csvopts} (id, version, k, v)"
+      execute "LOAD DATA INFILE '#{current_node_tags}' INTO TABLE current_node_tags #{csvopts} (id, k, v)"
+    end
+
+    tempfiles.each { |fn| File.unlink fn } if have_nodes
+  end
+
+  def self.down
+    raise IrreversibleMigration.new
+#    add_column :nodes, "tags", :text, :default => "", :null => false
+#    add_column :current_nodes, "tags", :text, :default => "", :null => false
+  end
+end
diff --git a/db/migrate/019_populate_node_tags_and_remove_helper.c b/db/migrate/019_populate_node_tags_and_remove_helper.c
new file mode 100644 (file)
index 0000000..c41ea33
--- /dev/null
@@ -0,0 +1,241 @@
+#include <mysql.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+static void exit_mysql_err(MYSQL *mysql) {
+  const char *err = mysql_error(mysql);
+  if (err) {
+    fprintf(stderr, "019_populate_node_tags_and_remove_helper: MySQL error: %s\n", err);
+  } else {
+    fprintf(stderr, "019_populate_node_tags_and_remove_helper: MySQL error\n");
+  }
+  abort();
+  exit(EXIT_FAILURE);
+}
+
+static void write_csv_col(FILE *f, const char *str, char end) {
+  char *out = (char *) malloc(2 * strlen(str) + 4);
+  char *o = out;
+  size_t len;
+
+  *(o++) = '\"';
+  for (; *str; str++) {
+    if (*str == '\0') {
+      break;
+    } else if (*str == '\"') {
+      *(o++) = '\"';
+      *(o++) = '\"';
+    } else {
+      *(o++) = *str;
+    }
+  }
+  *(o++) = '\"';
+  *(o++) = end;
+  *(o++) = '\0';
+
+  len = strlen(out);
+  if (fwrite(out, len, 1, f) != 1) {
+    perror("fwrite");
+    exit(EXIT_FAILURE);
+  }
+
+  free(out);
+}
+
+static void unescape(char *str) {
+  char *i = str, *o = str, tmp;
+
+  while (*i) {
+    if (*i == '\\') {
+      i++;
+      switch (tmp = *i++) {
+        case 's': *o++ = ';'; break;
+        case 'e': *o++ = '='; break;
+        case '\\': *o++ = '\\'; break;
+        default: *o++ = tmp; break;
+      }
+    } else {
+      *o++ = *i++;
+    }
+  }
+}
+
+static int read_node_tags(char **tags, char **k, char **v) {
+  if (!**tags) return 0;
+  char *i = strchr(*tags, ';');
+  if (!i) i = *tags + strlen(*tags);
+  char *j = strchr(*tags, '=');
+  *k = *tags;
+  if (j && j < i) {
+    *v = j + 1;
+  } else {
+    *v = i;
+  }
+  *tags = *i ? i + 1 : i;
+  *i = '\0';
+  if (j) *j = '\0';
+
+  unescape(*k);
+  unescape(*v);
+
+  return 1;
+}
+
+struct data {
+  MYSQL *mysql;
+  size_t version_size;
+  uint16_t *version;
+};
+
+static void proc_nodes(struct data *d, const char *tbl, FILE *out, FILE *out_tags, int hist) {
+  MYSQL_RES *res;
+  MYSQL_ROW row;
+  char query[256];
+
+  snprintf(query, sizeof(query),  "SELECT id, latitude, longitude, "
+      "user_id, visible, tags, timestamp, tile FROM %s", tbl);
+  if (mysql_query(d->mysql, query))
+    exit_mysql_err(d->mysql);
+
+  res = mysql_use_result(d->mysql);
+  if (!res) exit_mysql_err(d->mysql);
+
+  while ((row = mysql_fetch_row(res))) {
+    unsigned long id = strtoul(row[0], NULL, 10);
+    uint32_t version;
+
+    if (id >= d->version_size) {
+      fprintf(stderr, "preallocated nodes size exceeded");
+      abort();
+    }
+
+    if (hist) {
+      version = ++(d->version[id]);
+
+      fprintf(out, "\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",\"%u\"\n",
+        row[0], row[1], row[2], row[3], row[4], row[6], row[7], version);
+    } else {
+      /*fprintf(out, "\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",\"%s\"\n",
+       row[0], row[1], row[2], row[3], row[4], row[6], row[7]);*/
+    }
+
+    char *tags_it = row[5], *k, *v;
+    while (read_node_tags(&tags_it, &k, &v)) {
+      if (hist) {
+        fprintf(out_tags, "\"%s\",\"%u\",", row[0], version);
+      } else {
+        fprintf(out_tags, "\"%s\",", row[0]);
+      }
+
+      write_csv_col(out_tags, k, ',');
+      write_csv_col(out_tags, v, '\n');
+    }
+  }
+  if (mysql_errno(d->mysql)) exit_mysql_err(d->mysql);
+
+  mysql_free_result(res);
+}
+
+static size_t select_size(MYSQL *mysql, const char *q) {
+  MYSQL_RES *res;
+  MYSQL_ROW row;
+  size_t ret;
+
+  if (mysql_query(mysql, q))
+    exit_mysql_err(mysql);
+
+  res = mysql_store_result(mysql);
+  if (!res) exit_mysql_err(mysql);
+
+  row = mysql_fetch_row(res);
+  if (!row) exit_mysql_err(mysql);
+
+  if (row[0]) {
+    ret = strtoul(row[0], NULL, 10);
+  } else {
+    ret = 0;
+  }
+
+  mysql_free_result(res);
+
+  return ret;
+}
+
+static MYSQL *connect_to_mysql(char **argv) {
+  MYSQL *mysql = mysql_init(NULL);
+  if (!mysql) exit_mysql_err(mysql);
+
+  if (!mysql_real_connect(mysql, argv[1], argv[2], argv[3], argv[4],
+      argv[5][0] ? atoi(argv[5]) : 0, argv[6][0] ? argv[6] : NULL, 0))
+    exit_mysql_err(mysql);
+
+  if (mysql_set_character_set(mysql, "utf8"))
+    exit_mysql_err(mysql);
+
+  return mysql;
+}
+
+static void open_file(FILE **f, char *fn) {
+  *f = fopen(fn, "w+");
+  if (!*f) {
+    perror("fopen");
+    exit(EXIT_FAILURE);
+  }
+}
+
+int main(int argc, char **argv) {
+  size_t prefix_len;
+  FILE *current_nodes, *current_node_tags, *nodes, *node_tags;
+  char *tempfn;
+  struct data data, *d = &data;
+
+  if (argc != 8) {
+    printf("Usage: 019_populate_node_tags_and_remove_helper host user passwd database port socket prefix\n");
+    exit(EXIT_FAILURE);
+  }
+
+  d->mysql = connect_to_mysql(argv);
+
+  d->version_size = 1 + select_size(d->mysql, "SELECT max(id) FROM current_nodes");
+  d->version = (uint16_t *) malloc(sizeof(uint16_t) * d->version_size);
+  if (!d->version) {
+    perror("malloc");
+    abort();
+    exit(EXIT_FAILURE);
+  }
+  memset(d->version, 0, sizeof(uint16_t) * d->version_size);
+
+  prefix_len = strlen(argv[7]);
+  tempfn = (char *) malloc(prefix_len + 32);
+  strcpy(tempfn, argv[7]);
+
+  strcpy(tempfn + prefix_len, "current_nodes");
+  open_file(&current_nodes, tempfn);
+
+  strcpy(tempfn + prefix_len, "current_node_tags");
+  open_file(&current_node_tags, tempfn);
+
+  strcpy(tempfn + prefix_len, "nodes");
+  open_file(&nodes, tempfn);
+
+  strcpy(tempfn + prefix_len, "node_tags");
+  open_file(&node_tags, tempfn);
+
+  free(tempfn);
+
+  proc_nodes(d, "nodes", nodes, node_tags, 1);
+  proc_nodes(d, "current_nodes", current_nodes, current_node_tags, 0);
+
+  free(d->version);
+
+  mysql_close(d->mysql);
+
+  fclose(current_nodes);
+  fclose(current_node_tags);
+  fclose(nodes);
+  fclose(node_tags);
+
+  exit(EXIT_SUCCESS);
+}
diff --git a/db/migrate/020_move_to_innodb.rb b/db/migrate/020_move_to_innodb.rb
new file mode 100644 (file)
index 0000000..da0488c
--- /dev/null
@@ -0,0 +1,45 @@
+class MoveToInnodb < ActiveRecord::Migration
+  @@conv_tables = ['nodes', 'ways', 'way_tags', 'way_nodes',
+    'current_way_tags', 'relation_members',
+    'relations', 'relation_tags', 'current_relation_tags']
+
+  @@ver_tbl = ['nodes', 'ways', 'relations']
+
+  def self.up
+    remove_index :current_way_tags, :name=> :current_way_tags_v_idx
+    remove_index :current_relation_tags, :name=> :current_relation_tags_v_idx
+
+    @@ver_tbl.each { |tbl|
+      change_column tbl, "version", :bigint, :limit => 20, :null => false
+    }
+
+    @@conv_tables.each { |tbl|
+      change_engine (tbl, "InnoDB")
+    }
+
+    @@ver_tbl.each { |tbl|
+      add_column "current_#{tbl}", "version", :bigint, :limit => 20, :null => false
+      # As the initial version of all nodes, ways and relations is 0, we set the 
+      # current version to something less so that we can update the version in 
+      # batches of 10000
+      tbl.classify.constantize.update_all("version=-1")
+      while tbl.classify.constantize.count(:conditions => {:version => -1}) > 0
+        tbl.classify.constantize.update_all("version=(SELECT max(version) FROM #{tbl} WHERE #{tbl}.id = current_#{tbl}.id)", {:version => -1}, :limit => 10000)
+      end
+     # execute "UPDATE current_#{tbl} SET version = " +
+      #  "(SELECT max(version) FROM #{tbl} WHERE #{tbl}.id = current_#{tbl}.id)"
+        # The above update causes a MySQL error:
+        # -- add_column("current_nodes", "version", :bigint, {:null=>false, :limit=>20})
+        # -> 1410.9152s
+        # -- execute("UPDATE current_nodes SET version = (SELECT max(version) FROM nodes WHERE nodes.id = current_nodes.id)")
+        # rake aborted!
+        # Mysql::Error: The total number of locks exceeds the lock table size: UPDATE current_nodes SET version = (SELECT max(version) FROM nodes WHERE nodes.id = current_nodes.id)
+
+        # The above rails version will take longer, however will no run out of locks
+    }
+  end
+
+  def self.down
+    raise IrreversibleMigration.new
+  end
+end
diff --git a/db/migrate/021_key_constraints.rb b/db/migrate/021_key_constraints.rb
new file mode 100644 (file)
index 0000000..40f98be
--- /dev/null
@@ -0,0 +1,50 @@
+class KeyConstraints < ActiveRecord::Migration
+  def self.up
+    # Primary keys
+    add_primary_key :current_node_tags, [:id, :k]
+    add_primary_key :current_way_tags, [:id, :k]
+    add_primary_key :current_relation_tags, [:id, :k]
+
+    add_primary_key :node_tags, [:id, :version, :k]
+    add_primary_key :way_tags, [:id, :version, :k]
+    add_primary_key :relation_tags, [:id, :version, :k]
+
+    add_primary_key :nodes, [:id, :version]
+
+    # Remove indexes superseded by primary keys
+    remove_index :current_way_tags, :name => :current_way_tags_id_idx
+    remove_index :current_relation_tags, :name => :current_relation_tags_id_idx
+
+    remove_index :way_tags, :name => :way_tags_id_version_idx
+    remove_index :relation_tags, :name => :relation_tags_id_version_idx
+
+    remove_index :nodes, :name => :nodes_uid_idx
+
+    # Foreign keys (between ways, way_tags, way_nodes, etc.)
+    add_foreign_key :current_node_tags, [:id], :current_nodes
+    add_foreign_key :node_tags, [:id, :version], :nodes
+
+    add_foreign_key :current_way_tags, [:id], :current_ways
+    add_foreign_key :current_way_nodes, [:id], :current_ways
+    add_foreign_key :way_tags, [:id, :version], :ways
+    add_foreign_key :way_nodes, [:id, :version], :ways
+
+    add_foreign_key :current_relation_tags, [:id], :current_relations
+    add_foreign_key :current_relation_members, [:id], :current_relations
+    add_foreign_key :relation_tags, [:id, :version], :relations
+    add_foreign_key :relation_members, [:id, :version], :relations
+
+    # Foreign keys (between different types of primitives)
+    add_foreign_key :current_way_nodes, [:node_id], :current_nodes, [:id]
+
+    # FIXME: We don't have foreign keys for relation members since the id
+    # might point to a different table depending on the `type' column.
+    # We'd probably need different current_relation_member_nodes,
+    # current_relation_member_ways and current_relation_member_relations
+    # tables for this to work cleanly.
+  end
+
+  def self.down
+    raise IrreversibleMigration.new
+  end
+end
diff --git a/db/migrate/022_add_changesets.rb b/db/migrate/022_add_changesets.rb
new file mode 100644 (file)
index 0000000..e0cf390
--- /dev/null
@@ -0,0 +1,46 @@
+class AddChangesets < ActiveRecord::Migration
+  @@conv_user_tables = ['current_nodes',
+  'current_relations', 'current_ways', 'nodes', 'relations', 'ways' ]
+  
+  def self.up
+    create_table "changesets", innodb_table do |t|
+      t.column "id",             :bigint_pk,              :null => false
+      t.column "user_id",        :bigint,   :limit => 20, :null => false
+      t.column "created_at",     :datetime,               :null => false
+      t.column "open",           :boolean,                :null => false, :default => true
+      t.column "min_lat",        :integer,                :null => true
+      t.column "max_lat",        :integer,                :null => true
+      t.column "min_lon",        :integer,                :null => true
+      t.column "max_lon",        :integer,                :null => true
+    end
+
+    create_table "changeset_tags", innodb_table do |t|
+      t.column "id", :bigint, :limit => 64, :null => false
+      t.column "k",  :string, :default => "", :null => false
+      t.column "v",  :string, :default => "", :null => false
+    end
+
+    add_index "changeset_tags", ["id"], :name => "changeset_tags_id_idx"
+    
+    #
+    # Initially we will have one changeset for every user containing 
+    # all edits up to the API change,  
+    # all the changesets will have the id of the user that made them.
+    # We need to generate a changeset for each user in the database
+    execute "INSERT INTO changesets (id, user_id, created_at, open)" + 
+      "SELECT id, id, creation_time, false from users;"
+
+    @@conv_user_tables.each { |tbl|
+      rename_column tbl, :user_id, :changeset_id
+      #foreign keys too
+      add_foreign_key tbl, [:changeset_id], :changesets, [:id]
+    }
+  end
+
+  def self.down
+    # It's not easy to generate the user ids from the changesets
+    raise IrreversibleMigration.new
+    #drop_table "changesets"
+    #drop_table "changeset_tags"
+  end
+end
diff --git a/db/migrate/023_order_relation_members.rb b/db/migrate/023_order_relation_members.rb
new file mode 100644 (file)
index 0000000..5500edf
--- /dev/null
@@ -0,0 +1,33 @@
+class OrderRelationMembers < ActiveRecord::Migration
+  def self.up
+    # add sequence column. rails won't let us define an ordering here,
+    # as defaults must be constant.
+    add_column(:relation_members, :sequence_id, :integer,
+               :default => 0, :null => false)
+
+    # update the sequence column with default (partial) ordering by 
+    # element ID. the sequence ID is a smaller int type, so we can't
+    # just copy the member_id.
+    execute("update relation_members set sequence_id = mod(member_id, 16384)")
+
+    # need to update the primary key to include the sequence number, 
+    # otherwise the primary key will barf when we have repeated members.
+    # mysql barfs on this anyway, so we need a single command. this may
+    # not work in postgres... needs testing.
+    alter_primary_key("relation_members", [:id, :version, :member_type, :member_id, :member_role, :sequence_id])
+
+    # do the same for the current tables
+    add_column(:current_relation_members, :sequence_id, :integer,
+               :default => 0, :null => false)
+    execute("update current_relation_members set sequence_id = mod(member_id, 16384)")
+    alter_primary_key("current_relation_members", [:id, :member_type, :member_id, :member_role, :sequence_id])
+  end
+
+  def self.down
+    alter_primary_key("current_relation_members", [:id, :member_type, :member_id, :member_role])
+    remove_column :relation_members, :sequence_id
+
+    alter_primary_key("relation_members", [:id, :version, :member_type, :member_id, :member_role])
+    remove_column :current_relation_members, :sequence_id
+  end
+end
diff --git a/db/migrate/024_add_end_time_to_changesets.rb b/db/migrate/024_add_end_time_to_changesets.rb
new file mode 100644 (file)
index 0000000..b87ce3f
--- /dev/null
@@ -0,0 +1,34 @@
+class AddEndTimeToChangesets < ActiveRecord::Migration
+  def self.up
+    # swap the boolean closed-or-not for a time when the changeset will
+    # close or has closed.
+    add_column(:changesets, :closed_at, :datetime, :null => false)
+    
+    # it appears that execute will only accept string arguments, so
+    # this is an ugly, ugly hack to get some sort of mysql/postgres
+    # independence. now i have to go wash my brain with bleach.
+    execute("update changesets set closed_at=(now()-'1 hour') where open=(1=0)")
+    execute("update changesets set closed_at=(now()+'1 hour') where open=(1=1)")
+
+    # remove the open column as it is unnecessary now and denormalises 
+    # the table.
+    remove_column :changesets, :open
+
+    # add a column to keep track of the number of changes in a changeset.
+    # could probably work out how many changes there are here, but i'm not
+    # sure its actually important.
+    add_column(:changesets, :num_changes, :integer, 
+               :null => false, :default => 0)
+  end
+
+  def self.down
+    # in the reverse direction, we can look at the closed_at to figure out
+    # if changesets are closed or not.
+    add_column(:changesets, :open, :boolean, :null => false, :default => true)
+    execute("update changesets set open=(closed_at > now())")
+    remove_column :changesets, :closed_at
+
+    # remove the column for tracking number of changes
+    remove_column :changesets, :num_changes
+  end
+end
index 129a6f24b58bb7a79f78f5f07dcaa1fc58e646ec..6c4e6b0fcf38ebdef9a39fe7d05b5f0ff2132124 100644 (file)
@@ -1,7 +1,12 @@
-This is the OpenStreetMap rails server codebase. Documentation is currently extremely incomplete. Please help by writing docs and moving any SQL you see to use models etc.
+This is the OpenStreetMap rails server codebase. Documentation is currently
+extremely incomplete. Please help by writing docs and moving any SQL you
+see to use models etc.
 
 =INSTALL
 
+Full information is available at 
+http://wiki.openstreetmap.org/index.php/Rails
+
 * Get rails working (http://www.rubyonrails.org/)
 * Make your db (see db/README)
 * Install ruby libxml bindings:
@@ -18,14 +23,17 @@ This is the OpenStreetMap rails server codebase. Documentation is currently extr
 
 See
 
-http://wiki.openstreetmap.org/index.php/REST#Changes_in_the_upcoming_0.4_API
+The information about the next version of the protocol API 0.6 is available at 
+http://wiki.openstreetmap.org/index.php/OSM_Protocol_Version_0.6
+http://wiki.openstreetmap.org/index.php/REST
 
 =HACKING
 
 * Log in to your site (proably localhost:3000)
-* Create a user and confirm it
-* You want to play with the API (probably at http://localhost:3000/api/0.5/node/create etc)
-* Lots of tests are needed to test the API.
+* Create a user and confirm it (by setting the active flag to true in the users table of the database
+* You want to play with the API (probably at http://localhost:3000/api/0.6/node/create etc)
+* Lots of tests are needed to test the API. To run the tests use 
+    rake test
 * Lots of little things to make the site work like the old one.
 
 =Bugs
diff --git a/lib/consistency_validations.rb b/lib/consistency_validations.rb
new file mode 100644 (file)
index 0000000..4f38815
--- /dev/null
@@ -0,0 +1,45 @@
+module ConsistencyValidations
+  # Generic checks that are run for the updates and deletes of
+  # node, ways and relations. This code is here to avoid duplication, 
+  # and allow the extention of the checks without having to modify the
+  # code in 6 places for all the updates and deletes. Some of these tests are 
+  # needed for creates, but are currently not run :-( 
+  # This will throw an exception if there is an inconsistency
+  def check_consistency(old, new, user)
+    if new.version != old.version
+      raise OSM::APIVersionMismatchError.new(new.id, new.class.to_s, new.version, old.version)
+    elsif new.changeset.nil?
+      raise OSM::APIChangesetMissingError.new
+    elsif new.changeset.user_id != user.id
+      raise OSM::APIUserChangesetMismatchError.new
+    elsif not new.changeset.is_open?
+      raise OSM::APIChangesetAlreadyClosedError.new(new.changeset)
+    end
+  end
+  
+  # This is similar to above, just some validations don't apply
+  def check_create_consistency(new, user)
+    if new.changeset.nil?
+      raise OSM::APIChangesetMissingError.new
+    elsif new.changeset.user_id != user.id
+      raise OSM::APIUserChangesetMismatchError.new
+    elsif not new.changeset.is_open?
+      raise OSM::APIChangesetAlreadyClosedError.new(new.changeset)
+    end
+  end
+
+  ##
+  # subset of consistency checks which should be applied to almost
+  # all the changeset controller's writable methods.
+  def check_changeset_consistency(changeset, user)
+    # check user credentials - only the user who opened a changeset
+    # may alter it.
+    if changeset.nil?
+      raise OSM::APIChangesetMissingError.new
+    elsif user.id != changeset.user_id 
+      raise OSM::APIUserChangesetMismatchError.new
+    elsif not changeset.is_open?
+      raise OSM::APIChangesetAlreadyClosedError.new(changeset)
+    end
+  end
+end
diff --git a/lib/diff_reader.rb b/lib/diff_reader.rb
new file mode 100644 (file)
index 0000000..6a053e4
--- /dev/null
@@ -0,0 +1,169 @@
+##
+# DiffReader reads OSM diffs and applies them to the database.
+#
+# Uses the streaming LibXML "Reader" interface to cut down on memory
+# usage, so hopefully we can process fairly large diffs.
+class DiffReader
+  include ConsistencyValidations
+
+  # maps each element type to the model class which handles it
+  MODELS = { 
+    "node"     => Node, 
+    "way"      => Way, 
+    "relation" => Relation
+  }
+
+  ##
+  # Construct a diff reader by giving it a bunch of XML +data+ to parse
+  # in OsmChange format. All diffs must be limited to a single changeset
+  # given in +changeset+.
+  def initialize(data, changeset)
+    @reader = XML::Reader.new data
+    @changeset = changeset
+  end
+
+  ##
+  # An element-block mapping for using the LibXML reader interface. 
+  #
+  # Since a lot of LibXML reader usage is boilerplate iteration through
+  # elements, it would be better to DRY and do this in a block. This
+  # could also help with error handling...?
+  def with_element
+    # skip the first element, which is our opening element of the block
+    @reader.read
+    # loop over all elements. 
+    # NOTE: XML::Reader#read returns 0 for EOF and -1 for error.
+    while @reader.read == 1
+      break if @reader.node_type == 15 # end element
+      next unless @reader.node_type == 1 # element
+      yield @reader.name
+    end
+  end
+
+  ##
+  # An element-block mapping for using the LibXML reader interface. 
+  #
+  # Since a lot of LibXML reader usage is boilerplate iteration through
+  # elements, it would be better to DRY and do this in a block. This
+  # could also help with error handling...?
+  def with_model
+    with_element do |model_name|
+      model = MODELS[model_name]
+      raise "Unexpected element type #{model_name}, " +
+        "expected node, way, relation." if model.nil?
+      yield model, @reader.expand
+      @reader.next
+    end
+  end
+
+  ##
+  # Checks a few invariants. Others are checked in the model methods
+  # such as save_ and delete_with_history.
+  def check(model, xml, new)
+    raise OSM::APIBadXMLError.new(model, xml) if new.nil?
+    unless new.changeset_id == @changeset.id 
+      raise OSM::APIChangesetMismatchError.new(new.changeset_id, @changeset.id)
+    end
+  end
+
+  ##
+  # Consume the XML diff and try to commit it to the database. This code
+  # is *not* transactional, so code which calls it should ensure that the
+  # appropriate transaction block is in place.
+  #
+  # On a failure to meet preconditions (e.g: optimistic locking fails) 
+  # an exception subclassing OSM::APIError will be thrown.
+  def commit
+
+    node_ids, way_ids, rel_ids = {}, {}, {}
+    ids = { :node => node_ids, :way => way_ids, :relation => rel_ids}
+
+    result = OSM::API.new.get_xml_doc
+    result.root.name = "diffResult"
+
+    # loop at the top level, within the <osmChange> element (although we
+    # don't actually check this...)
+    with_element do |action_name|
+      if action_name == 'create'
+        # create a new element. this code is agnostic of the element type
+        # because all the elements support the methods that we're using.
+        with_model do |model, xml|
+          new = model.from_xml_node(xml, true)
+          check(model, xml, new)
+
+          # when this element is saved it will get a new ID, so we save it
+          # to produce the mapping which is sent to other elements.
+          placeholder_id = xml['id'].to_i
+          raise OSM::APIBadXMLError.new(model, xml) if placeholder_id.nil?
+
+          # some elements may have placeholders for other elements in the
+          # diff, so we must fix these before saving the element.
+          new.fix_placeholders!(ids)
+
+          # create element given user
+          new.create_with_history(@changeset.user)
+          
+          # save placeholder => allocated ID map
+          ids[model.to_s.downcase.to_sym][placeholder_id] = new.id
+
+          # add the result to the document we're building for return.
+          xml_result = XML::Node.new model.to_s.downcase
+          xml_result["old_id"] = placeholder_id.to_s
+          xml_result["new_id"] = new.id.to_s
+          xml_result["new_version"] = new.version.to_s
+          result.root << xml_result
+        end
+        
+      elsif action_name == 'modify'
+        # modify an existing element. again, this code doesn't directly deal
+        # with types, but uses duck typing to handle them transparently.
+        with_model do |model, xml|
+          # get the new element from the XML payload
+          new = model.from_xml_node(xml, false)
+          check(model, xml, new)
+
+          # and the old one from the database
+          old = model.find(new.id)
+
+          new.fix_placeholders!(ids)
+          old.update_from(new, @changeset.user)
+
+          xml_result = XML::Node.new model.to_s.downcase
+          xml_result["old_id"] = old.id.to_s
+          xml_result["new_id"] = new.id.to_s 
+          # version is updated in "old" through the update, so we must not
+          # return new.version here but old.version!
+          xml_result["new_version"] = old.version.to_s
+          result.root << xml_result
+        end
+
+      elsif action_name == 'delete'
+        # delete action. this takes a payload in API 0.6, so we need to do
+        # most of the same checks that are done for the modify.
+        with_model do |model, xml|
+          new = model.from_xml_node(xml, false)
+          check(model, xml, new)
+
+          old = model.find(new.id)
+
+          # can a delete have placeholders under any circumstances?
+          # if a way is modified, then deleted is that a valid diff?
+          new.fix_placeholders!(ids)
+          old.delete_with_history!(new, @changeset.user)
+
+          xml_result = XML::Node.new model.to_s.downcase
+          xml_result["old_id"] = old.id.to_s
+          result.root << xml_result
+        end
+
+      else
+        # no other actions to choose from, so it must be the users fault!
+        raise OSM::APIChangesetActionInvalid.new(action_name)
+      end
+    end
+
+    # return the XML document to be rendered back to the client
+    return result
+  end
+
+end
index f1a923c42c1e48b3b0de75d2e67b744bdecea23a..2740eab0c5472da4c76d95128c5f8253dd440cbb 100644 (file)
@@ -1,4 +1,9 @@
 module GeoRecord
+  # This scaling factor is used to convert between the float lat/lon that is 
+  # returned by the API, and the integer lat/lon equivalent that is stored in
+  # the database.
+  SCALE = 10000000
+  
   def self.included(base)
     base.extend(ClassMethods)
   end
@@ -20,21 +25,21 @@ module GeoRecord
   end
 
   def lat=(l)
-    self.latitude = (l * 10000000).round
+    self.latitude = (l * SCALE).round
   end
 
   def lon=(l)
-    self.longitude = (l * 10000000).round
+    self.longitude = (l * SCALE).round
   end
 
   # Return WGS84 latitude
   def lat
-    return self.latitude.to_f / 10000000
+    return self.latitude.to_f / SCALE
   end
 
   # Return WGS84 longitude
   def lon
-    return self.longitude.to_f / 10000000
+    return self.longitude.to_f / SCALE
   end
 
 private
index 9b39c9342353fd28a693d1052a45bce99048fe07..153d65780da8f80025a3e06dee05fa60603b9888 100644 (file)
@@ -1,10 +1,11 @@
 module MapBoundary
+  # Take an array of length 4, and return the min_lon, min_lat, max_lon and 
+  # max_lat within their respective boundaries.
   def sanitise_boundaries(bbox)
-    min_lon = [bbox[0].to_f,-180].max
-    min_lat = [bbox[1].to_f,-90].max
-    max_lon = [bbox[2].to_f,+180].min
-    max_lat = [bbox[3].to_f,+90].min
-
+    min_lon = [[bbox[0].to_f,-180].max,180].min
+    min_lat = [[bbox[1].to_f,-90].max,90].min
+    max_lon = [[bbox[2].to_f,+180].min,-180].max
+    max_lat = [[bbox[3].to_f,+90].min,-90].max
     return min_lon, min_lat, max_lon, max_lat
   end
 
@@ -17,6 +18,7 @@ module MapBoundary
       raise("The minimum latitude must be less than the maximum latitude, but it wasn't")
     end
     unless min_lon >= -180 && min_lat >= -90 && max_lon <= 180 && max_lat <= 90
+      # Due to sanitize_boundaries, it is highly unlikely we'll actually get here
       raise("The latitudes must be between -90 and 90, and longitudes between -180 and 180")
     end
 
index 1d32d175d77d0fb85055f8f60c19dff7cc86de2c..3b1e46fb8fb608f48b0be269a0533f0cc597122b 100644 (file)
@@ -1,6 +1,10 @@
 module ActiveRecord
   module ConnectionAdapters
     module SchemaStatements
+      def quote_column_names(column_name)
+        Array(column_name).map { |e| quote_column_name(e) }.join(", ")
+      end
+
       def add_primary_key(table_name, column_name, options = {})
         column_names = Array(column_name)
         quoted_column_names = column_names.map { |e| quote_column_name(e) }.join(", ")
@@ -11,6 +15,12 @@ module ActiveRecord
         execute "ALTER TABLE #{table_name} DROP PRIMARY KEY"
       end
 
+      def add_foreign_key(table_name, column_name, reftbl, refcol = nil)
+        execute "ALTER TABLE #{table_name} ADD " +
+         "FOREIGN KEY (#{quote_column_names(column_name)}) " +
+         "REFERENCES #{reftbl} (#{quote_column_names(refcol || column_name)})"
+      end
+
       alias_method :old_options_include_default?, :options_include_default?
 
       def options_include_default?(options)
@@ -28,12 +38,20 @@ module ActiveRecord
     end
 
     class MysqlAdapter
-      alias_method :old_native_database_types, :native_database_types
+      if MysqlAdapter.public_instance_methods(false).include?('native_database_types')
+        alias_method :old_native_database_types, :native_database_types
+      end
 
       def native_database_types
         types = old_native_database_types
         types[:bigint] = { :name => "bigint", :limit => 20 }
         types[:double] = { :name => "double" }
+        types[:bigint_pk] = { :name => "bigint(20) DEFAULT NULL auto_increment PRIMARY KEY" }
+        types[:bigint_pk_64] = { :name => "bigint(64) DEFAULT NULL auto_increment PRIMARY KEY" }
+        types[:bigint_auto_64] = { :name => "bigint(64) DEFAULT NULL auto_increment" }
+        types[:bigint_auto_11] = { :name => "bigint(11) DEFAULT NULL auto_increment" }
+        types[:bigint_auto_20] = { :name => "bigint(20) DEFAULT NULL auto_increment" }
+        types[:four_byte_unsigned] = { :name=> "integer unsigned NOT NULL" }
         types
       end
 
@@ -58,6 +76,76 @@ module ActiveRecord
       def innodb_table
         return { :id => false, :force => true, :options => "ENGINE=InnoDB" }
       end
+
+      def innodb_option
+        return "ENGINE=InnoDB"
+      end
+
+      def change_engine (table_name, engine)
+        execute "ALTER TABLE #{table_name} ENGINE = #{engine}"
+      end
+
+      def add_fulltext_index (table_name, column)
+        execute "CREATE FULLTEXT INDEX `#{table_name}_#{column}_idx` ON `#{table_name}` (`#{column}`)"
+      end
+
+      def alter_column_nwr_enum (table_name, column)
+        execute "alter table #{table_name} change column #{column} #{column} enum('node','way','relation');"
+      end
+
+      def alter_primary_key(table_name, new_columns)
+        execute("alter table #{table_name} drop primary key, add primary key (#{new_columns.join(',')})")
+      end
+    end
+
+    class PostgreSQLAdapter
+      if PostgreSQLAdapter.public_instance_methods(false).include?('native_database_types')
+        alias_method :old_native_database_types, :native_database_types
+      end
+
+      def native_database_types
+        types = old_native_database_types
+        types[:double] = { :name => "double precision" }
+        types[:bigint_pk] = { :name => "bigserial PRIMARY KEY" }
+        types[:bigint_pk_64] = { :name => "bigserial PRIMARY KEY" }
+        types[:bigint_auto_64] = { :name => "bigint" } #fixme: need autoincrement?
+        types[:bigint_auto_11] = { :name => "bigint" } #fixme: need autoincrement?
+        types[:bigint_auto_20] = { :name => "bigint" } #fixme: need autoincrement?
+        types[:four_byte_unsigned] = { :name => "bigint" } # meh
+        types
+      end
+
+      def myisam_table
+        return { :id => false, :force => true, :options => ""}
+      end
+
+      def innodb_table
+        return { :id => false, :force => true, :options => ""}
+      end
+
+      def innodb_option
+        return ""
+      end
+      def change_engine (table_name, engine)
+      end
+
+      def add_fulltext_index (table_name, column)
+        execute "CREATE INDEX #{table_name}_#{column}_idx on #{table_name} (#{column})"
+      end
+
+      def alter_column_nwr_enum (table_name, column)
+        response = select_one("select count(*) as count from pg_type where typname = 'nwr_enum'")
+        if response['count'] == "0" #yep, as a string
+          execute "create type nwr_enum as ENUM ('node', 'way', 'relation')"
+        end
+        execute        "alter table #{table_name} drop #{column}"
+        execute "alter table #{table_name} add #{column} nwr_enum"
+      end
+
+      def alter_primary_key(table_name, new_columns)
+        execute "alter table #{table_name} drop constraint #{table_name}_pkey; alter table #{table_name} add primary key (#{new_columns.join(',')})"
+      end
     end
   end
 end
index 9c271607dc0160d1d7e5b2b78138dc04ed2dedf0..f6646503d0e37e0d68e04efbaf01755be4c6195e 100644 (file)
@@ -10,18 +10,157 @@ module OSM
 
   # The base class for API Errors.
   class APIError < RuntimeError
+    def render_opts
+      { :text => "Generic API Error", :status => :internal_server_error, :content_type => "text/plain" }
+    end
   end
 
   # Raised when an API object is not found.
   class APINotFoundError < APIError
+    def render_opts
+      { :text => "The API wasn't found", :status => :not_found, :content_type => "text/plain" }
+    end
   end
 
   # Raised when a precondition to an API action fails sanity check.
   class APIPreconditionFailedError < APIError
+    def initialize(message = "")
+      @message = message
+    end
+    
+    def render_opts
+      { :text => "Precondition failed: #{@message}", :status => :precondition_failed, :content_type => "text/plain" }
+    end
   end
 
   # Raised when to delete an already-deleted object.
   class APIAlreadyDeletedError < APIError
+    def render_opts
+      { :text => "The object has already been deleted", :status => :gone, :content_type => "text/plain" }
+    end
+  end
+
+  # Raised when the user logged in isn't the same as the changeset
+  class APIUserChangesetMismatchError < APIError
+    def render_opts
+      { :text => "The user doesn't own that changeset", :status => :conflict, :content_type => "text/plain" }
+    end
+  end
+
+  # Raised when the changeset provided is already closed
+  class APIChangesetAlreadyClosedError < APIError
+    def initialize(changeset)
+      @changeset = changeset
+    end
+
+    attr_reader :changeset
+    
+    def render_opts
+      { :text => "The changeset #{@changeset.id} was closed at #{@changeset.closed_at}.", :status => :conflict, :content_type => "text/plain" }
+    end
+  end
+  
+  # Raised when a change is expecting a changeset, but the changeset doesn't exist
+  class APIChangesetMissingError < APIError
+    def render_opts
+      { :text => "You need to supply a changeset to be able to make a change", :status => :conflict, :content_type => "text/plain" }
+    end
+  end
+
+  # Raised when a diff is uploaded containing many changeset IDs which don't match
+  # the changeset ID that the diff was uploaded to.
+  class APIChangesetMismatchError < APIError
+    def initialize(provided, allowed)
+      @provided, @allowed = provided, allowed
+    end
+    
+    def render_opts
+      { :text => "Changeset mismatch: Provided #{@provided} but only " +
+      "#{@allowed} is allowed.", :status => :conflict, :content_type => "text/plain" }
+    end
+  end
+  
+  # Raised when a diff upload has an unknown action. You can only have create,
+  # modify, or delete
+  class APIChangesetActionInvalid < APIError
+    def initialize(provided)
+      @provided = provided
+    end
+    
+    def render_opts
+      { :text => "Unknown action #{@provided}, choices are create, modify, delete.",
+      :status => :bad_request, :content_type => "text/plain" }
+    end
+  end
+
+  # Raised when bad XML is encountered which stops things parsing as
+  # they should.
+  class APIBadXMLError < APIError
+    def initialize(model, xml, message="")
+      @model, @xml, @message = model, xml, message
+    end
+
+    def render_opts
+      { :text => "Cannot parse valid #{@model} from xml string #{@xml}. #{@message}",
+      :status => :bad_request, :content_type => "text/plain" }
+    end
+  end
+
+  # Raised when the provided version is not equal to the latest in the db.
+  class APIVersionMismatchError < APIError
+    def initialize(id, type, provided, latest)
+      @id, @type, @provided, @latest = id, type, provided, latest
+    end
+
+    attr_reader :provided, :latest, :id, :type
+
+    def render_opts
+      { :text => "Version mismatch: Provided " + provided.to_s +
+        ", server had: " + latest.to_s + " of " + type + " " + id.to_s, 
+        :status => :conflict, :content_type => "text/plain" }
+    end
+  end
+
+  # raised when a two tags have a duplicate key string in an element.
+  # this is now forbidden by the API.
+  class APIDuplicateTagsError < APIError
+    def initialize(type, id, tag_key)
+      @type, @id, @tag_key = type, id, tag_key
+    end
+
+    attr_reader :type, :id, :tag_key
+
+    def render_opts
+      { :text => "Element #{@type}/#{@id} has duplicate tags with key #{@tag_key}.",
+        :status => :bad_request, :content_type => "text/plain" }
+    end
+  end
+  
+  # Raised when a way has more than the configured number of way nodes.
+  # This prevents ways from being to long and difficult to work with
+  class APITooManyWayNodesError < APIError
+    def initialize(provided, max)
+      @provided, @max = provided, max
+    end
+    
+    attr_reader :provided, :max
+    
+    def render_opts
+      { :text => "You tried to add #{provided} nodes to the way, however only #{max} are allowed",
+        :status => :bad_request, :content_type => "text/plain" }
+    end
+  end
+
+  ##
+  # raised when user input couldn't be parsed
+  class APIBadUserInput < APIError
+    def initialize(message)
+      @message = message
+    end
+
+    def render_opts
+      { :text => @message, :content_type => "text/plain", :status => :bad_request }
+    end
   end
 
   # Helper methods for going to/from mercator and lat/lng.
@@ -190,7 +329,7 @@ module OSM
       doc.encoding = 'UTF-8' 
       root = XML::Node.new 'osm'
       root['version'] = API_VERSION
-      root['generator'] = 'OpenStreetMap server'
+      root['generator'] = GENERATOR
       doc.root = root
       return doc
     end
index cf8f5903dde5fe72aae759155e5ef89b5273e6d6..ebafbce0086c809e5668899378bf3f4bbb456b85 100644 (file)
@@ -92,6 +92,10 @@ module Potlatch
         0.chr+encodedouble(n)
       when 'NilClass'
         5.chr
+         when 'TrueClass'
+        0.chr+encodedouble(1)
+         when 'FalseClass'
+        0.chr+encodedouble(0)
       else
         RAILS_DEFAULT_LOGGER.error("Unexpected Ruby type for AMF conversion: "+n.class.to_s)
       end
diff --git a/lib/tasks/populate_node_tags.rake b/lib/tasks/populate_node_tags.rake
deleted file mode 100644 (file)
index 86747cf..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-namespace 'db' do
-  desc 'Populate the node_tags table'
-  task :node_tags  do
-    require File.dirname(__FILE__) + '/../../config/environment'
-
-    node_count = Node.count
-    limit = 1000 #the number of nodes to grab in one go
-    offset = 0   
-
-    while offset < node_count
-        Node.find(:all, :limit => limit, :offset => offset).each do |node|
-        seq_id = 1
-        node.tags.split(';').each do |tag|
-          nt = NodeTag.new
-          nt.id = node.id
-          nt.k = tag.split('=')[0] || ''
-          nt.v = tag.split('=')[1] || ''
-          nt.sequence_id = seq_id 
-          nt.save! || raise
-          seq_id += 1
-        end
-
-        version = 1 #version refers to one set of histories
-        node.old_nodes.find(:all, :order => 'timestamp asc').each do |old_node|
-        sequence_id = 1 #sequence_id refers to the sequence of node tags within a history
-        old_node.tags.split(';').each do |tag|
-          ont = OldNodeTag.new
-          ont.id = node.id #the id of the node tag
-          ont.k = tag.split('=')[0] || ''
-          ont.v = tag.split('=')[1] || ''
-          ont.version = version
-          ont.sequence_id = sequence_id
-          ont.save! || raise
-          sequence_id += 1
-          end     
-        version += 1
-        end
-      end
-    offset += limit
-    end
-  end
-end
diff --git a/lib/validators.rb b/lib/validators.rb
new file mode 100644 (file)
index 0000000..095fb7a
--- /dev/null
@@ -0,0 +1,32 @@
+module ActiveRecord
+  module Validations
+    module ClassMethods
+      
+      # error message when invalid UTF-8 is detected
+      @@invalid_utf8_message = " is invalid UTF-8"
+
+      ##
+      # validation method to be included like any other validations methods
+      # in the models definitions. this one checks that the named attribute
+      # is a valid UTF-8 format string.
+      def validates_as_utf8(*attrs)
+        validates_each(attrs) do |record, attr, value|
+          record.errors.add(attr, @@invalid_utf8_message) unless valid_utf8? value
+        end
+      end    
+      
+      ##
+      # Checks that a string is valid UTF-8 by trying to convert it to UTF-8
+      # using the iconv library, which is in the standard library.
+      def valid_utf8?(str)
+        return true if str.nil?
+        Iconv.conv("UTF-8", "UTF-8", str)
+        return true
+
+      rescue
+        return false
+      end  
+      
+    end
+  end
+end
index 0e1845619d8d06bf998b2b2aff7c2241c7abd009..16abbfca0e03f0483709dcbf6e08b06319ac42e5 100644 (file)
@@ -2,7 +2,11 @@
    "http://www.w3.org/TR/html4/loose.dtd">
 <html>
 <body>
-  <h1>File not found</h1>
-  <p>Change this error message for pages not found in public/404.html</p>
+  <img src="http://www.openstreetmap.org/images/osm_logo.png" style="float:left; margin:10px">
+  <div style="float:left;">
+    <h1>File not found</h1>  
+    <p>Couldn't find a file/directory/API operation by that name on the OpenStreetMap server (HTTP 404)</p>
+    <p>Feel free to <a href="http://wiki.openstreetmap.org/index.php/Contact" title="Various contact channels explained">contact</a> the OpenStreetMap community if you have found a broken link / bug. Make a note of the exact URL of your request.</p>
+  </div>
 </body>
 </html>
\ No newline at end of file
index ab95f74c4661b6ebc19908a47d1ce39f4b66087a..552024a2db5cd8829bd528c5ade2022b1564ff91 100644 (file)
@@ -2,7 +2,12 @@
    "http://www.w3.org/TR/html4/loose.dtd">
 <html>
 <body>
-  <h1>Application error</h1>
-  <p>Change this error message for exceptions thrown outside of an action (like in Dispatcher setups or broken Ruby code) in public/500.html</p>
+  <img src="http://www.openstreetmap.org/images/osm_logo.png" style="float:left; margin:10px">
+  <div style="float:left;">
+    <h1>Application error</h1>
+    <p>The OpenStreetMap server encountered an unexpected condition that prevented it from fulfilling the request (HTTP 500)</p>
+    <p>Feel free to <a href="http://wiki.openstreetmap.org/index.php/Contact" title="Various contact channels explained">contact</a> the OpenStreetMap community if your problem persists. Make a note of the exact URL / post data of your request.</p>
+    <p>This may be a problem in our Ruby On Rails code. 500 ocurrs with exceptions thrown outside of an action (like in Dispatcher setups or broken Ruby code)</p>
+  </div>
 </body>
 </html>
\ No newline at end of file
diff --git a/public/images/new.png b/public/images/new.png
new file mode 100644 (file)
index 0000000..202e10e
Binary files /dev/null and b/public/images/new.png differ
index c80ed2242c8147db90177925af3db63eacee971f..fcf34336f7dbb23d55aa1eb151e8dea967967252 100644 (file)
@@ -13,6 +13,8 @@ var nonamekeys = {
 };
 
 OpenLayers._getScriptLocation = function () {
+  // Should really have this file as an erb, so that this can return 
+  // the real rails root
    return "/openlayers/";
 }
 
index 3daebd6dc278defb75dac384cabc0677d3df34e2..3a1b8c4d107ceb7f4a3bd6a4f74317112c1a552f 100755 (executable)
Binary files a/public/potlatch/potlatch.swf and b/public/potlatch/potlatch.swf differ
index 31c61de28c57b38b3e9266ca890142a956566e36..764d8971b84f0b0d6f5dbd72979d2ac33f516180 100644 (file)
@@ -277,6 +277,12 @@ hides rule from IE5-Mac \*/
   font-size: 10px;
 }
 
+hr {
+  border: none;
+  background-color: #ccc;
+  color: #ccc;
+  height: 1px;
+}
 
 .gpxsummary {
   font-size: 12px;
@@ -527,6 +533,16 @@ input[type="submit"] {
   border: 1px solid black;
 }
 
+#accountForm td {
+       padding-bottom:10px;
+}
+
+.fieldName {
+       text-align:right;
+       font-weight:bold;
+}
+
+
 .nohome .location {
   display: none;
 }
@@ -539,9 +555,8 @@ input[type="submit"] {
   display: inline !important;
 }
 
-.editDescription {
-  height: 10ex;
-  width: 30em;
+.minorNote {
+       font-size:0.8em;
 }
 
 .nowrap {
diff --git a/test/fixtures/changeset_tags.yml b/test/fixtures/changeset_tags.yml
new file mode 100644 (file)
index 0000000..34d2bf4
--- /dev/null
@@ -0,0 +1,4 @@
+changeset_1_tag_1: 
+  id: 1
+  k: created_by
+  v: test suite yml
diff --git a/test/fixtures/changesets.yml b/test/fixtures/changesets.yml
new file mode 100644 (file)
index 0000000..defd691
--- /dev/null
@@ -0,0 +1,50 @@
+# FIXME! all of these changesets need their bounding boxes set correctly!
+# 
+<% SCALE = 10000000 unless defined?(SCALE) %>
+
+# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
+normal_user_first_change:
+  id: 1
+  user_id: 1
+  created_at: "2007-01-01 00:00:00"
+  closed_at: <%= DateTime.now + Rational(1,24) %>
+  min_lon: <%= 1 * SCALE %>
+  min_lat: <%= 1 * SCALE %>
+  max_lon: <%= 5 * SCALE %>
+  max_lat: <%= 5 * SCALE %>
+  num_changes: 11
+  
+second_user_first_change:
+  id: 2
+  user_id: 2
+  created_at: "2008-05-01 01:23:45"
+  closed_at: <%= DateTime.now + Rational(1,24) %>
+  num_changes: 0
+
+normal_user_closed_change:
+  id: 3
+  user_id: 1
+  created_at: "2007-01-01 00:00:00"
+  closed_at: "2007-01-02 00:00:00"
+  num_changes: 0
+
+normal_user_version_change:
+  id: 4
+  user_id: 1
+  created_at: "2008-01-01 00:00:00"
+  closed_at: <%= DateTime.now + Rational(1,24) %>
+  min_lon: <%= 1 * SCALE %>
+  min_lat: <%= 1 * SCALE %>
+  max_lon: <%= 4 * SCALE %>
+  max_lat: <%= 4 * SCALE %>
+  num_changes: 8
+
+# changeset to contain all the invalid stuff that is in the
+# fixtures (nodes outside the world, etc...), but needs to have
+# a valid user...
+invalid_changeset:
+  id: 5
+  user_id: 3
+  created_at: "2008-01-01 00:00:00"
+  closed_at: "2008-01-02 00:00:00"
+  num_changes: 9
diff --git a/test/fixtures/current_node_tags.yml b/test/fixtures/current_node_tags.yml
new file mode 100644 (file)
index 0000000..1494daf
--- /dev/null
@@ -0,0 +1,29 @@
+t1:
+  id: 1
+  k: 'testvisible'
+  v: 'yes'
+
+t2:
+  id: 2
+  k: 'testused'
+  v: 'yes'
+
+t3:
+  id: 3
+  k: 'test'
+  v: 'yes'
+
+t4:
+  id: 4
+  k: 'test'
+  v: 'yes'
+
+nv_t1:
+  id: 15
+  k: 'testing'
+  v: 'added in node version 3'
+
+nv_t2:
+  id: 15
+  k: 'testing two'
+  v: 'modified in node version 4'
index dd3bd248772a314f52e0bf8c92a68b0b076fca0c..6f21fd47f30a1267004e5e2116652cd1fe4f9ae2 100644 (file)
 # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
+<% SCALE = 10000000 unless defined?(SCALE) %>
+
 visible_node:
   id: 1
-  latitude: 1
-  longitude: 1
-  user_id: 1
-  visible: 1
-  tags: test=yes
+  latitude: <%= 1*SCALE %>
+  longitude: <%= 1*SCALE %>
+  changeset_id: 1
+  visible: true
+  version: 1
+  tile: <%= QuadTile.tile_for_point(1,1) %>
   timestamp: 2007-01-01 00:00:00
 
 invisible_node:
   id: 2
-  latitude: 2
-  longitude: 2
-  user_id: 1
-  visible: 0
-  tags: test=yes
+  latitude: <%= 2*SCALE %>
+  longitude: <%= 2*SCALE %>
+  changeset_id: 1
+  visible: false
+  version: 1
+  tile: <%= QuadTile.tile_for_point(2,2) %>
   timestamp: 2007-01-01 00:00:00
 
 used_node_1:
   id: 3
-  latitude: 3
-  longitude: 3
-  user_id: 1
-  visible: 1
-  tags: test=yes
+  latitude: <%= 3*SCALE %>
+  longitude: <%= 3*SCALE %>
+  changeset_id: 1
+  visible: true
+  version: 1
+  tile: <%= QuadTile.tile_for_point(3,3) %>
   timestamp: 2007-01-01 00:00:00
 
 used_node_2:
   id: 4
-  latitude: 4
-  longitude: 4
-  user_id: 1
-  visible: 1
-  tags: test=yes
+  latitude: <%= 4*SCALE %>
+  longitude: <%= 4*SCALE %>
+  changeset_id: 1
+  visible: true
+  version: 1
+  tile: <%= QuadTile.tile_for_point(4,4) %>
   timestamp: 2007-01-01 00:00:00
 
 node_used_by_relationship:
   id: 5
-  latitude: 5
-  longitude: 5
-  user_id: 1
-  visible: 1
-  tags: test=yes
+  latitude: <%= 5*SCALE %>
+  longitude: <%= 5*SCALE %>
+  changeset_id: 1
+  visible: true
+  version: 1
+  tile: <%= QuadTile.tile_for_point(5,5) %>
+  timestamp: 2007-01-01 00:00:00
+  
+node_too_far_north:
+  id: 6
+  latitude: <%= 90.01*SCALE %>
+  longitude: <%= 6*SCALE %>
+  changeset_id: 5
+  visible: true
+  version: 1
+  tile: <%= QuadTile.tile_for_point(90.01,6) %>
+  timestamp: 2007-01-01 00:00:00
+  
+node_north_limit:
+  id: 11
+  latitude: <%= 90*SCALE %>
+  longitude: <%= 11*SCALE %>
+  changeset_id: 5
+  visible: true
+  version: 1
+  tile: <%= QuadTile.tile_for_point(90,11) %>
+  timestamp: 2008-07-08 14:50:00
+  
+node_too_far_south:
+  id: 7
+  latitude: <%= -90.01*SCALE %>
+  longitude: <%= 7*SCALE %>
+  changeset_id: 5
+  visible: true
+  version: 1
+  tile: <%= QuadTile.tile_for_point(-90.01,7) %>
+  timestamp: 2007-01-01 00:00:00
+  
+node_south_limit:
+  id: 12
+  latitude: <%= -90*SCALE %>
+  longitude: <%= 12*SCALE %>
+  changeset_id: 5
+  visible: true
+  version: 1
+  tile: <%= QuadTile.tile_for_point(-90,12) %>
+  timestamp: 2008-07-08 15:02:18
+  
+node_too_far_west:
+  id: 8
+  latitude: <%= 8*SCALE %>
+  longitude: <%= -180.01*SCALE %>
+  changeset_id: 5
+  visible: true
+  version: 1
+  tile: <%= QuadTile.tile_for_point(8,-180.01) %>
+  timestamp: 2007-01-01 00:00:00
+  
+node_west_limit:
+  id: 13
+  latitude: <%= 13*SCALE %>
+  longitude: <%= -180*SCALE %>
+  changeset_id: 5
+  visible: true
+  version: 1
+  tile: <%= QuadTile.tile_for_point(13,-180) %>
+  timestamp: 2008-07-08 15:17:37
+  
+node_too_far_east:
+  id: 9
+  latitude: <%= 9*SCALE %>
+  longitude: <%= 180.01*SCALE %>
+  changeset_id: 5
+  visible: true
+  version: 1
+  tile: <%= QuadTile.tile_for_point(9,180.01) %>
+  timestamp: 2007-01-01 00:00:00
+  
+node_east_limit:
+  id: 14
+  latitude: <%= 14*SCALE %>
+  longitude: <%= 180*SCALE %>
+  changeset_id: 5
+  visible: true
+  version: 1
+  tile: <%= QuadTile.tile_for_point(14,180) %>
+  timestamp: 2008-07-08 15:46:16
+  
+node_totally_wrong:
+  id: 10
+  latitude: <%= 200*SCALE %>
+  longitude: <%= 200*SCALE %>
+  changeset_id: 5
+  visible: true
+  version: 1
+  tile: <%= QuadTile.tile_for_point(200,200) %>
   timestamp: 2007-01-01 00:00:00
+  
+node_with_versions:
+  id: 15
+  latitude: <%= 1*SCALE %>
+  longitude: <%= 1*SCALE %>
+  changeset_id: 4
+  visible: true
+  version: 4
+  tile: <%= QuadTile.tile_for_point(1,1) %>
+  timestamp: 2008-01-01 00:04:00
index bddc8a0dd0cf0a77dbd0825194240c9e2eeff40d..5696a365f23ac6f3a32abaaeeb90956ead0812b1 100644 (file)
@@ -21,3 +21,9 @@ t4:
   member_role: "some"
   member_type: "node"
   member_id: 5
+
+t5:
+  id: 2
+  member_role: "some"
+  member_type: "node"
+  member_id: 5
index aaf06a397d54a3321dda12eb054e1a3549e823d0..d2755bdfd4edbe8781399363233f671a5de91f98 100644 (file)
@@ -1,14 +1,14 @@
 t1:
   id: 1
-  k: test
-  v: yes
+  k: 'test'
+  v: 'yes'
 
 t2:
   id: 2
-  k: test
-  v: yes
+  k: 'test'
+  v: 'yes'
 
-t2:
+t3:
   id: 3
-  k: test
-  v: yes
+  k: 'test'
+  v: 'yes'
index c1f77d4282d36a316fe68159f08579ed611b53d9..165f1a21e032a7623a9735cfc9d50d8e2d9eef97 100644 (file)
@@ -1,17 +1,20 @@
 visible_relation:
   id: 1
-  user_id: 1
+  changeset_id: 1
   timestamp: 2007-01-01 00:00:00
-  visible: 1
+  visible: true
+  version: 1
 
 invisible_relation:
   id: 2
-  user_id: 1
+  changeset_id: 1
   timestamp: 2007-01-01 00:00:00
-  visible: 0
+  visible: false
+  version: 1
 
 used_relation:
   id: 3
-  user_id: 1
+  changeset_id: 1
   timestamp: 2007-01-01 00:00:00
-  visible: 1
+  visible: true
+  version: 1
index ce394edbeed760341fd4f4d97e7f387dc2cd875b..66aae0f20af7ab68653288d1f8c4c84b9b10c02b 100644 (file)
@@ -12,3 +12,8 @@ t3:
   id: 3
   node_id: 3
   sequence_id: 1
+
+t4:
+  id: 4
+  node_id: 15
+  sequence_id: 1
index 375247ea2ce24e4ffed0f179dead3828fd77fe15..c1ef21d573a2eecbb9ca46a9f066d129499ce73f 100644 (file)
@@ -1,15 +1,15 @@
 t1:
   id: 1
-  k: test
-  v: yes
+  k: 'test'
+  v: 'yes'
 
 t2:
   id: 2
-  k: test
-  v: yes
+  k: 'test'
+  v: 'yes'
 
 t3:
   id: 3
-  k: test
-  v: yes
+  k: 'test'
+  v: 'yes'
 
index b129d7f45eb7d1a026f361bd0d3b0c5fafb7d22d..44a54caacbc2e429299069b9fa23975dcdf49cd1 100644 (file)
@@ -1,18 +1,27 @@
 visible_way:
   id: 1
-  user_id: 1
+  changeset_id: 1
   timestamp: 2007-01-01 00:00:00
-  visible: 1
+  visible: true
+  version: 1
 
 invisible_way:
   id: 2
-  user_id: 1
+  changeset_id: 1
   timestamp: 2007-01-01 00:00:00
-  visible: 0
+  visible: false
+  version: 1
 
 used_way:
   id: 3
-  user_id: 1
+  changeset_id: 1
   timestamp: 2007-01-01 00:00:00
-  visible: 1
+  visible: true
+  version: 1
 
+way_with_versions:
+  id: 4
+  changeset_id: 4
+  timestamp: 2008-01-01 00:01:00
+  visible: true
+  version: 4
diff --git a/test/fixtures/diary_comments.yml b/test/fixtures/diary_comments.yml
new file mode 100644 (file)
index 0000000..8bb9f49
--- /dev/null
@@ -0,0 +1,7 @@
+comment_for_geo_post:
+  id: 1
+  diary_entry_id: 2
+  user_id: 2
+  body: Some comment text
+  created_at: "2008-11-08 09:45:34"
+  updated_at: "2008-11-08 10:34:34"
diff --git a/test/fixtures/diary_entries.yml b/test/fixtures/diary_entries.yml
new file mode 100644 (file)
index 0000000..5d07e5f
--- /dev/null
@@ -0,0 +1,21 @@
+normal_user_entry_1:
+  id: 1
+  user_id: 1
+  title: Diary Entry 1
+  body: This is the body of diary entry 1.
+  created_at: "2008-11-07 17:43:34"
+  updated_at: "2008-11-07 17:43:34"
+  latitude: 
+  longitude: 
+  language: 
+  
+normal_user_geo_entry:
+  id: 2
+  user_id: 1
+  title: Geo Entry 1
+  body: This is the body of a geo diary entry in London.
+  created_at: "2008-11-07 17:47:34"
+  updated_at: "2008-11-07 17:47:34"
+  latitude: 51.50763
+  longitude: -0.10781
+  language: 
diff --git a/test/fixtures/friends.yml b/test/fixtures/friends.yml
new file mode 100644 (file)
index 0000000..782f1e3
--- /dev/null
@@ -0,0 +1,4 @@
+normal_user_with_second_user:
+  id: 1
+  user_id: 1
+  friend_user_id: 2
diff --git a/test/fixtures/gps_points.yml b/test/fixtures/gps_points.yml
new file mode 100644 (file)
index 0000000..13ee355
--- /dev/null
@@ -0,0 +1,9 @@
+first_trace_1:
+  altitude: 134
+  trackid: 1
+  latitude: 1
+  longitude: 1
+  gpx_id: 1
+  timestamp: "2008-10-01 10:10:10"
+  tile: 1
+  
diff --git a/test/fixtures/gpx_file_tags.yml b/test/fixtures/gpx_file_tags.yml
new file mode 100644 (file)
index 0000000..d914bfb
--- /dev/null
@@ -0,0 +1,4 @@
+first_trace_1:
+  gpx_id: 1
+  tag: London
+  id: 1
diff --git a/test/fixtures/gpx_files.yml b/test/fixtures/gpx_files.yml
new file mode 100644 (file)
index 0000000..3ab74c8
--- /dev/null
@@ -0,0 +1,12 @@
+first_trace_file:
+  id: 1
+  user_id: 1
+  visible: true
+  name: Fist Trace.gpx
+  size:
+  latitude: 1
+  longitude: 1
+  timestamp: "2008-10-29 10:10:10"
+  public: true
+  description: This is a trace
+  inserted: true
index b49c4eb4e1e9d522e9424c822ce1ce80662e94cc..22fab186322bca351586e0297f3d89aa98b4b15a 100644 (file)
@@ -1,5 +1,16 @@
 # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
 one:
-  id: 1
+  from_user_id: 1
+  title: test message 1
+  body: some body text
+  sent_on: "2008-05-01 12:34:56"
+  message_read: false
+  to_user_id: 2
+  
 two:
-  id: 2
+  from_user_id: 2
+  title: test message 2
+  body: some body test
+  sent_on: "2008-05-02 12:45:23"
+  message_read: true
+  to_user_id: 1
diff --git a/test/fixtures/node_tags.yml b/test/fixtures/node_tags.yml
new file mode 100644 (file)
index 0000000..722bc53
--- /dev/null
@@ -0,0 +1,47 @@
+t1:
+  id: 1
+  k: 'testvisible'
+  v: 'yes'
+  version: 1
+
+t2:
+  id: 3
+  k: 'test'
+  v: 'yes'
+  version: 1
+
+t3:
+  id: 4
+  k: 'test'
+  v: 'yes'
+  version: 1
+
+nv3_t1:
+  id: 15
+  k: 'testing'
+  v: 'added in node version 3'
+  version: 3
+
+nv3_t2:
+  id: 15
+  k: 'testing two'
+  v: 'added in node version 3'
+  version: 3
+
+nv3_t3:
+  id: 15
+  k: 'testing three'
+  v: 'added in node version 3'
+  version: 3
+
+nv4_t1:
+  id: 15
+  k: 'testing'
+  v: 'added in node version 3'
+  version: 4
+
+nv4_t2:
+  id: 15
+  k: 'testing two'
+  v: 'modified in node version 4'
+  version: 4
index 37152c4d3ddee9fe1cdbe4537bf8ea637a7a2a32..5b690696e104c86f622bcfd67f93968133a20d91 100644 (file)
 # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
+<% SCALE = 10000000 unless defined?(SCALE) %>
+
 visible_node:
   id: 1
-  latitude: 1
-  longitude: 1
-  user_id: 1
-  visible: 1
-  tags: test=yes
+  latitude: <%= 1*SCALE %>
+  longitude: <%= 1*SCALE %>
+  changeset_id: 1
+  visible: true
+  version: 1
+  tile: <%= QuadTile.tile_for_point(1,1) %>
   timestamp: 2007-01-01 00:00:00
 
 invisible_node:
   id: 2
-  latitude: 2
-  longitude: 2
-  user_id: 1
-  visible: 0
-  tags: test=yes
+  latitude: <%= 2*SCALE %>
+  longitude: <%= 2*SCALE %>
+  changeset_id: 1
+  visible: false
+  version: 1
+  tile: <%= QuadTile.tile_for_point(2,2) %>
   timestamp: 2007-01-01 00:00:00
 
 used_node_1:
   id: 3
-  latitude: 3
-  longitude: 3
-  user_id: 1
-  visible: 1
-  tags: test=yes
+  latitude: <%= 3*SCALE %>
+  longitude: <%= 3*SCALE %>
+  changeset_id: 1
+  visible: true
+  version: 1
+  tile: <%= QuadTile.tile_for_point(3,3) %>
   timestamp: 2007-01-01 00:00:00
 
 used_node_2:
   id: 4
-  latitude: 4
-  longitude: 4
-  user_id: 1
-  visible: 1
-  tags: test=yes
+  latitude: <%= 4*SCALE %>
+  longitude: <%= 4*SCALE %>
+  changeset_id: 1
+  visible: true
+  version: 1
+  tile: <%= QuadTile.tile_for_point(4,4) %>
   timestamp: 2007-01-01 00:00:00
 
 node_used_by_relationship:
   id: 5
-  latitude: 5
-  longitude: 5
-  user_id: 1
-  visible: 1
-  tags: test=yes
+  latitude: <%= 5*SCALE %>
+  longitude: <%= 5*SCALE %>
+  changeset_id: 1
+  visible: true
+  version: 1
+  tile: <%= QuadTile.tile_for_point(5,5) %>
+  timestamp: 2007-01-01 00:00:00
+
+node_too_far_north:
+  id: 6
+  latitude: <%= 90.01*SCALE %>
+  longitude: <%= 6*SCALE %>
+  changeset_id: 5
+  visible: true
+  version: 1
+  tile: <%= QuadTile.tile_for_point(90.01,6) %>
+  timestamp: 2007-01-01 00:00:00
+  
+node_north_limit:
+  id: 11
+  latitude: <%= 90*SCALE %>
+  longitude: <%= 11*SCALE %>
+  changeset_id: 5
+  visible: true
+  version: 1
+  tile: <%= QuadTile.tile_for_point(90,11) %>
+  timestamp: 2008-07-08 14:50:00
+  
+node_too_far_south:
+  id: 7
+  latitude: <%= -90.01*SCALE %>
+  longitude: <%= 7*SCALE %>
+  changeset_id: 5
+  visible: true
+  version: 1
+  tile: <%= QuadTile.tile_for_point(-90.01,7) %>
+  timestamp: 2007-01-01 00:00:00
+  
+node_south_limit:
+  id: 12
+  latitude: <%= -90*SCALE %>
+  longitude: <%= 12*SCALE %>
+  changeset_id: 5
+  visible: true
+  version: 1
+  tile: <%= QuadTile.tile_for_point(-90,12) %>
+  timestamp: 2008-07-08 15:02:18
+  
+node_too_far_west:
+  id: 8
+  latitude: <%= 8*SCALE %>
+  longitude: <%= -180.01*SCALE %>
+  changeset_id: 5
+  visible: true
+  version: 1
+  tile: <%= QuadTile.tile_for_point(8,-180.01) %>
   timestamp: 2007-01-01 00:00:00
+  
+node_west_limit:
+  id: 13
+  latitude: <%= 13*SCALE %>
+  longitude: <%= -180*SCALE %>
+  changeset_id: 5
+  visible: true
+  version: 1
+  tile: <%= QuadTile.tile_for_point(13,-180) %>
+  timestamp: 2008-07-08 15:17:37
+  
+node_too_far_east:
+  id: 9
+  latitude: <%= 9*SCALE %>
+  longitude: <%= 180.01*SCALE %>
+  changeset_id: 5
+  visible: true
+  version: 1
+  tile: <%= QuadTile.tile_for_point(9,180.01) %>
+  timestamp: 2007-01-01 00:00:00
+  
+node_east_limit:
+  id: 14
+  latitude: <%= 14*SCALE %>
+  longitude: <%= 180*SCALE %>
+  changeset_id: 5
+  visible: true
+  version: 1
+  tile: <%= QuadTile.tile_for_point(14,180) %>
+  timestamp: 2008-07-08 15:46:16
+
+node_totally_wrong:
+  id: 10
+  latitude: <%= 200*SCALE %>
+  longitude: <%= 200*SCALE %>
+  changeset_id: 5
+  visible: true
+  version: 1
+  tile: <%= QuadTile.tile_for_point(200,200) %>
+  timestamp: 2007-01-01 00:00:00
+  
+node_with_versions_v1:
+  id: 15
+  latitude: <%= 1*SCALE %>
+  longitude: <%= 1*SCALE %>
+  changeset_id: 4
+  visible: true
+  version: 1
+  tile: <%= QuadTile.tile_for_point(1,1) %>
+  timestamp: 2008-01-01 00:01:00
+
+node_with_versions_v2:
+  id: 15
+  latitude: <%= 2*SCALE %>
+  longitude: <%= 2*SCALE %>
+  changeset_id: 4
+  visible: true
+  version: 2
+  tile: <%= QuadTile.tile_for_point(1,1) %>
+  timestamp: 2008-01-01 00:02:00
+
+node_with_versions_v3:
+  id: 15
+  latitude: <%= 1*SCALE %>
+  longitude: <%= 1*SCALE %>
+  changeset_id: 4
+  visible: true
+  version: 3
+  tile: <%= QuadTile.tile_for_point(1,1) %>
+  timestamp: 2008-01-01 00:03:00
 
+node_with_versions_v4:
+  id: 15
+  latitude: <%= 1*SCALE %>
+  longitude: <%= 1*SCALE %>
+  changeset_id: 4
+  visible: true
+  version: 4
+  tile: <%= QuadTile.tile_for_point(1,1) %>
+  timestamp: 2008-01-01 00:04:00
index 39f4bd5de3172f3bb507416e3c0e0539a9120633..7e671672d5e4079b9ec3926dd1a478cdadbd15ff 100644 (file)
@@ -1,17 +1,17 @@
 t1:
   id: 1
-  k: test
-  v: yes
+  k: 'test'
+  v: 'yes'
   version: 1
 
 t2:
   id: 2
-  k: test
-  v: yes
+  k: 'test'
+  v: 'yes'
   version: 1
 
 t3:
   id: 3
-  k: test
-  v: yes
+  k: 'test'
+  v: 'yes'
   version: 1
index cf1d1ff5600718b5db43998900a1d51a8823d62d..165f1a21e032a7623a9735cfc9d50d8e2d9eef97 100644 (file)
@@ -1,20 +1,20 @@
 visible_relation:
   id: 1
-  user_id: 1
+  changeset_id: 1
   timestamp: 2007-01-01 00:00:00
-  visible: 1
+  visible: true
   version: 1
 
 invisible_relation:
   id: 2
-  user_id: 1
+  changeset_id: 1
   timestamp: 2007-01-01 00:00:00
-  visible: 0
+  visible: false
   version: 1
 
 used_relation:
   id: 3
-  user_id: 1
+  changeset_id: 1
   timestamp: 2007-01-01 00:00:00
-  visible: 1
+  visible: true
   version: 1
index 5bf02933a3a79bfacb88dad9d4f4799f3cf20600..59ebd0542bd9cdcf672414c30a1ec8d91c43497d 100644 (file)
@@ -1,7 +1,11 @@
 # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
 
-# one:
-#   column: value
-#
-# two:
-#   column: value
+a:
+  user_id: 1
+  k: "key"
+  v: "value"
+
+two:
+  user_id: 1
+  k: "some_key"
+  v: "some_value"
index bcce2f7db5b9732ad1ff59109917c83060427711..709139d68e72aca707f94d21aa04eb74debf88db 100644 (file)
@@ -1,13 +1,39 @@
 # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
 normal_user:
-  email: test@openstreetmap.org
   id: 1
-  active: 1
+  email: test@openstreetmap.org
+  active: true
   pass_crypt: <%= Digest::MD5.hexdigest('test') %>
   creation_time: "2007-01-01 00:00:00"
   display_name: test
-  data_public: 0
+  data_public: false
   description: test
-  home_lat: 1
-  home_lon: 1
+  home_lat: 12.1
+  home_lon: 12.1
   home_zoom: 3
+  
+second_user:
+  id: 2
+  email: test@example.com
+  active: true
+  pass_crypt: <%= Digest::MD5.hexdigest('test') %>
+  creation_time: "2008-05-01 01:23:45"
+  display_name: test2
+  data_public: true
+  description: some test description
+  home_lat: 12
+  home_lon: 12
+  home_zoom: 12
+  
+inactive_user:
+  id: 3
+  email: inactive@openstreetmap.org
+  active: false
+  pass_crypt: <%= Digest::MD5::hexdigest('test2') %>
+  creation_time: "2008-07-01 02:23:45"
+  display_name: Inactive User
+  data_public: true
+  description: description
+  home_lat: 123.4
+  home_lon: 12.34
+  home_zoom: 15
index caeac16b1d7b16a1bbc1f580432b6047785d718e..0b43f6a9c1f586ce70c7c64f4276cb3a2e5a452b 100644 (file)
@@ -1,9 +1,9 @@
-t1:
+t1a:
   id: 1
   node_id: 3
   sequence_id: 1
   version: 1
-
+  
 t2:
   id: 2
   node_id: 3
@@ -15,3 +15,51 @@ t3:
   node_id: 3
   sequence_id: 1
   version: 1
+
+w4_v1_n1:
+  id: 4
+  node_id: 3
+  sequence_id: 1
+  version: 1
+
+w4_v1_n2:
+  id: 4
+  node_id: 4
+  sequence_id: 2
+  version: 1
+
+w4_v2_n1:
+  id: 4
+  node_id: 15
+  sequence_id: 1
+  version: 2
+
+w4_v2_n2:
+  id: 4
+  node_id: 3
+  sequence_id: 2
+  version: 2
+
+w4_v2_n3:
+  id: 4
+  node_id: 4
+  sequence_id: 3
+  version: 2
+
+w4_v3_n1:
+  id: 4
+  node_id: 15
+  sequence_id: 1
+  version: 3
+
+w4_v3_n2:
+  id: 4
+  node_id: 3
+  sequence_id: 2
+  version: 3
+
+w4_v4_n1:
+  id: 4
+  node_id: 15
+  sequence_id: 1
+  version: 4
index 39f4bd5de3172f3bb507416e3c0e0539a9120633..7e671672d5e4079b9ec3926dd1a478cdadbd15ff 100644 (file)
@@ -1,17 +1,17 @@
 t1:
   id: 1
-  k: test
-  v: yes
+  k: 'test'
+  v: 'yes'
   version: 1
 
 t2:
   id: 2
-  k: test
-  v: yes
+  k: 'test'
+  v: 'yes'
   version: 1
 
 t3:
   id: 3
-  k: test
-  v: yes
+  k: 'test'
+  v: 'yes'
   version: 1
index c8cf6dcf4d2f6568cffacd5ea0b39336d3b9d151..80b1da6426a8248f5b959be0fc3571eae0f8a963 100644 (file)
@@ -1,21 +1,49 @@
 visible_way:
   id: 1
-  user_id: 1
+  changeset_id: 1
   timestamp: 2007-01-01 00:00:00
-  visible: 1
+  visible: true
   version: 1
 
 invisible_way:
   id: 2
-  user_id: 1
+  changeset_id: 1
   timestamp: 2007-01-01 00:00:00
-  visible: 0
+  visible: false
   version: 1
 
 used_way:
   id: 3
-  user_id: 1
+  changeset_id: 1
   timestamp: 2007-01-01 00:00:00
-  visible: 0
+  visible: true
   version: 1
 
+way_with_versions_v1:
+  id: 4
+  changeset_id: 4
+  timestamp: 2008-01-01 00:01:00
+  visible: true
+  version: 1
+
+way_with_versions_v2:
+  id: 4
+  changeset_id: 4
+  timestamp: 2008-01-01 00:02:00
+  visible: true
+  version: 2
+
+way_with_versions:
+  id: 4
+  changeset_id: 4
+  timestamp: 2008-01-01 00:03:00
+  visible: true
+  version: 3
+
+way_with_versions_v4:
+  id: 4
+  changeset_id: 4
+  timestamp: 2008-01-01 00:04:00
+  visible: true
+  version: 4
+
diff --git a/test/functional/amf_controller_test.rb b/test/functional/amf_controller_test.rb
new file mode 100644 (file)
index 0000000..b1b2212
--- /dev/null
@@ -0,0 +1,483 @@
+require File.dirname(__FILE__) + '/../test_helper'
+require 'stringio'
+include Potlatch
+
+class AmfControllerTest < ActionController::TestCase
+  api_fixtures
+
+  # this should be what AMF controller returns when the bbox of a request
+  # is invalid or too large.
+  BOUNDARY_ERROR = [-2,"Sorry - I can't get the map for that area."]
+
+  def test_getway
+    # check a visible way
+    id = current_ways(:visible_way).id
+    amf_content "getway", "/1", [id]
+    post :amf_read
+    assert_response :success
+    amf_parse_response                                         
+    assert_equal amf_result("/1")[0], id
+  end
+
+  def test_getway_invisible
+    # check an invisible way
+    id = current_ways(:invisible_way).id
+    amf_content "getway", "/1", [id]
+    post :amf_read
+    assert_response :success
+    amf_parse_response
+    way = amf_result("/1")
+    assert_equal way[0], id
+    assert way[1].empty? and way[2].empty?
+  end
+
+  def test_getway_nonexistent
+    # check chat a non-existent way is not returned
+    amf_content "getway", "/1", [0]
+    post :amf_read
+    assert_response :success
+    amf_parse_response
+    way = amf_result("/1")
+    assert_equal way[0], 0
+    assert way[1].empty? and way[2].empty?
+  end
+
+  def test_whichways
+    node = current_nodes(:used_node_1)
+    minlon = node.lon-0.1
+    minlat = node.lat-0.1
+    maxlon = node.lon+0.1
+    maxlat = node.lat+0.1
+    amf_content "whichways", "/1", [minlon, minlat, maxlon, maxlat]
+    post :amf_read
+    assert_response :success
+    amf_parse_response 
+
+    # check contents of message
+    map = amf_result "/1"
+    assert_equal 0, map[0], 'map error code should be 0'
+
+    # check the formatting of the message
+    assert_equal 4, map.length, 'map should have length 4'
+    assert_equal Array, map[1].class, 'map "ways" element should be an array'
+    assert_equal Array, map[2].class, 'map "nodes" element should be an array'
+    assert_equal Array, map[3].class, 'map "relations" element should be an array'
+    map[1].each do |w|
+      assert_equal 2, w.length, 'way should be (id, version) pair'
+      assert w[0] == w[0].floor, 'way ID should be an integer'
+      assert w[1] == w[1].floor, 'way version should be an integer'
+    end
+
+    map[2].each do |n|
+      assert_equal 5, w.length, 'node should be (id, lat, lon, [tags], version) tuple'
+      assert n[0] == n[0].floor, 'node ID should be an integer'
+      assert n[1] >= minlat - 0.01, 'node lat should be greater than min'
+      assert n[1] <= maxlat - 0.01, 'node lat should be less than max'
+      assert n[2] >= minlon - 0.01, 'node lon should be greater than min'
+      assert n[2] <= maxlon - 0.01, 'node lon should be less than max'
+      assert_equal Array, a[3].class, 'node tags should be array'
+      assert n[4] == n[4].floor, 'node version should be an integer'
+    end
+
+    map[3].each do |r|
+      assert_equal 2, r.length, 'relation should be (id, version) pair'
+      assert r[0] == r[0].floor, 'relation ID should be an integer'
+      assert r[1] == r[1].floor, 'relation version should be an integer'
+    end
+
+    # TODO: looks like amf_controller changed since this test was written
+    # so someone who knows what they're doing should check this!
+    ways = map[1].collect { |x| x[0] }
+    assert ways.include?(current_ways(:used_way).id),
+      "map should include used way"
+    assert !ways.include?(current_ways(:invisible_way).id),
+      'map should not include deleted way'
+  end
+
+  ##
+  # checks that too-large a bounding box will not be served.
+  def test_whichways_toobig
+    bbox = [-0.1,-0.1,1.1,1.1]
+    check_bboxes_are_bad [bbox] do |map,bbox|
+      assert_equal BOUNDARY_ERROR, map, "AMF controller should have returned an error."
+    end
+  end
+
+  ##
+  # checks that an invalid bounding box will not be served. in this case
+  # one with max < min latitudes.
+  #
+  # NOTE: the controller expands the bbox by 0.01 in each direction!
+  def test_whichways_badlat
+    bboxes = [[0,0.1,0.1,0], [-0.1,80,0.1,70], [0.24,54.35,0.25,54.33]]
+    check_bboxes_are_bad bboxes do |map, bbox|
+      assert_equal BOUNDARY_ERROR, map, "AMF controller should have returned an error #{bbox.inspect}."
+    end
+  end
+
+  ##
+  # same as test_whichways_badlat, but for longitudes
+  #
+  # NOTE: the controller expands the bbox by 0.01 in each direction!
+  def test_whichways_badlon
+    bboxes = [[80,-0.1,70,0.1], [54.35,0.24,54.33,0.25]]
+    check_bboxes_are_bad bboxes do |map, bbox|
+      assert_equal BOUNDARY_ERROR, map, "AMF controller should have returned an error #{bbox.inspect}."
+    end
+  end
+
+  def test_whichways_deleted
+    node = current_nodes(:used_node_1)
+    minlon = node.lon-0.1
+    minlat = node.lat-0.1
+    maxlon = node.lon+0.1
+    maxlat = node.lat+0.1
+    amf_content "whichways_deleted", "/1", [minlon, minlat, maxlon, maxlat]
+    post :amf_read
+    assert_response :success
+    amf_parse_response
+
+    # check contents of message
+    map = amf_result "/1"
+    assert_equal 0, map[0], 'first map element should be 0'
+    assert_equal Array, map[1].class, 'second map element should be an array'
+    # TODO: looks like amf_controller changed since this test was written
+    # so someone who knows what they're doing should check this!
+    assert !map[1].include?(current_ways(:used_way).id),
+      "map should not include used way"
+    assert map[1].include?(current_ways(:invisible_way).id),
+      'map should include deleted way'
+  end
+
+  def test_whichways_deleted_toobig
+    bbox = [-0.1,-0.1,1.1,1.1]
+    amf_content "whichways_deleted", "/1", bbox
+    post :amf_read
+    assert_response :success
+    amf_parse_response 
+
+    map = amf_result "/1"
+    assert_equal BOUNDARY_ERROR, map, "AMF controller should have returned an error."
+  end
+
+  def test_getrelation
+    id = current_relations(:visible_relation).id
+    amf_content "getrelation", "/1", [id]
+    post :amf_read
+    assert_response :success
+    amf_parse_response
+    assert_equal amf_result("/1")[0], id
+  end
+
+  def test_getrelation_invisible
+    id = current_relations(:invisible_relation).id
+    amf_content "getrelation", "/1", [id]
+    post :amf_read
+    assert_response :success
+    amf_parse_response
+    rel = amf_result("/1")
+    assert_equal rel[0], id
+    assert rel[1].empty? and rel[2].empty?
+  end
+
+  def test_getrelation_nonexistent
+    id = 0
+    amf_content "getrelation", "/1", [id]
+    post :amf_read
+    assert_response :success
+    amf_parse_response
+    rel = amf_result("/1")
+    assert_equal rel[0], id
+    assert rel[1].empty? and rel[2].empty?
+  end
+
+  def test_getway_old
+    # try to get the last visible version (specified by <0) (should be current version)
+    latest = current_ways(:way_with_versions)
+    # try to get version 1
+    v1 = ways(:way_with_versions_v1)
+    {latest => -1, v1 => v1.version}.each do |way, v|
+      amf_content "getway_old", "/1", [way.id, v]
+      post :amf_read
+      assert_response :success
+      amf_parse_response
+      returned_way = amf_result("/1")
+      assert_equal returned_way[1], way.id
+      assert_equal returned_way[4], way.version
+    end
+  end
+
+  def test_getway_old_nonexistent
+    # try to get the last version+10 (shoudn't exist)
+    latest = current_ways(:way_with_versions)
+    # try to get last visible version of non-existent way
+    # try to get specific version of non-existent way
+    {nil => -1, nil => 1, latest => latest.version + 10}.each do |way, v|
+      amf_content "getway_old", "/1", [way.nil? ? 0 : way.id, v]
+      post :amf_read
+      assert_response :success
+      amf_parse_response
+      returned_way = amf_result("/1")
+      assert returned_way[2].empty?
+      assert returned_way[3].empty?
+      assert returned_way[4] < 0
+    end
+  end
+
+  def test_getway_history
+    latest = current_ways(:way_with_versions)
+    amf_content "getway_history", "/1", [latest.id]
+    post :amf_read
+    assert_response :success
+    amf_parse_response
+    history = amf_result("/1")
+
+    # ['way',wayid,history]
+    assert_equal history[0], 'way'
+    assert_equal history[1], latest.id
+    assert_equal history[2].first[0], latest.version
+    assert_equal history[2].last[0], ways(:way_with_versions_v1).version
+  end
+
+  def test_getway_history_nonexistent
+    amf_content "getway_history", "/1", [0]
+    post :amf_read
+    assert_response :success
+    amf_parse_response
+    history = amf_result("/1")
+
+    # ['way',wayid,history]
+    assert_equal history[0], 'way'
+    assert_equal history[1], 0
+    assert history[2].empty?
+  end
+
+  def test_getnode_history
+    latest = current_nodes(:node_with_versions)
+    amf_content "getnode_history", "/1", [latest.id]
+    post :amf_read
+    assert_response :success
+    amf_parse_response
+    history = amf_result("/1")
+
+    # ['node',nodeid,history]
+    assert_equal history[0], 'node', 
+      'first element should be "node"'
+    assert_equal history[1], latest.id,
+      'second element should be the input node ID'
+    # NOTE: changed this test to match what amf_controller actually 
+    # outputs - which may or may not be what potlatch is expecting.
+    # someone who knows potlatch (i.e: richard f) should review this.
+    assert_equal history[2].first[0], latest.version,
+      'first part of third element should be the latest version'
+    assert_equal history[2].last[0], 
+      nodes(:node_with_versions_v1).version,
+      'second part of third element should be the initial version'
+  end
+
+  def test_getnode_history_nonexistent
+    amf_content "getnode_history", "/1", [0]
+    post :amf_read
+    assert_response :success
+    amf_parse_response
+    history = amf_result("/1")
+
+    # ['node',nodeid,history]
+    assert_equal history[0], 'node'
+    assert_equal history[1], 0
+    assert history[2].empty?
+  end
+
+  # ************************************************************
+  # AMF Write tests
+  def test_putpoi_update_valid
+    nd = current_nodes(:visible_node)
+    amf_content "putpoi", "/1", ["test@openstreetmap.org:test", nd.changeset_id, nd.version, nd.id, nd.lon, nd.lat, nd.tags, nd.visible]
+    post :amf_write
+    assert_response :success
+    amf_parse_response
+    result = amf_result("/1")
+    
+    assert_equal 0, result[0]
+    assert_equal nd.id, result[1]
+    assert_equal nd.id, result[2]
+    assert_equal nd.version+1, result[3]
+    
+    # Now try to update again, with a different lat/lon, using the updated version number
+    lat = nd.lat+0.1
+    lon = nd.lon-0.1
+    amf_content "putpoi", "/2", ["test@openstreetmap.org:test", nd.changeset_id, nd.version+1, nd.id, lon, lat, nd.tags, nd.visible]
+    post :amf_write
+    assert_response :success
+    amf_parse_response
+    result = amf_result("/2")
+    
+    assert_equal 0, result[0]
+    assert_equal nd.id, result[1]
+    assert_equal nd.id, result[2]
+    assert_equal nd.version+2, result[3]
+  end
+  
+  # Check that we can create a no valid poi
+  # Using similar method for the node controller test
+  def test_putpoi_create_valid
+    # This node has no tags
+    nd = Node.new
+    # create a node with random lat/lon
+    lat = rand(100)-50 + rand
+    lon = rand(100)-50 + rand
+    # normal user has a changeset open
+    changeset = changesets(:normal_user_first_change)
+    
+    amf_content "putpoi", "/1", ["test@openstreetmap.org:test", changeset.id, nil, nil, lon, lat, {}, nil]
+    post :amf_write
+    assert_response :success
+    amf_parse_response
+    result = amf_result("/1")
+    
+    # check the array returned by the amf
+    assert_equal 4, result.size
+    assert_equal 0, result[0], "expected to get the status ok from the amf"
+    assert_equal 0, result[1], "The old id should be 0"
+    assert result[2] > 0, "The new id should be greater than 0"
+    assert_equal 1, result[3], "The new version should be 1"
+    
+    # Finally check that the node that was saved has saved the data correctly 
+    # in both the current and history tables
+    # First check the current table
+    current_node = Node.find(result[2])
+    assert_in_delta lat, current_node.lat, 0.00001, "The latitude was not retreieved correctly"
+    assert_in_delta lon, current_node.lon, 0.00001, "The longitude was not retreived correctly"
+    assert_equal 0, current_node.tags.size, "There seems to be a tag that has been added to the node"
+    assert_equal result[3], current_node.version, "The version returned, is different to the one returned by the amf"
+    # Now check the history table
+    historic_nodes = Node.find(:all, :conditions => { :id => result[2] })
+    assert_equal 1, historic_nodes.size, "There should only be one historic node created"
+    first_historic_node = historic_nodes.first
+    assert_in_delta lat, first_historic_node.lat, 0.00001, "The latitude was not retreived correctly"
+    assert_in_delta lon, first_historic_node.lon, 0.00001, "The longitude was not retreuved correctly"
+    assert_equal 0, first_historic_node.tags.size, "There seems to be a tag that have been attached to this node"
+    assert_equal result[3], first_historic_node.version, "The version returned, is different to the one returned by the amf"
+    
+    ####
+    # This node has some tags
+    tnd = Node.new
+    # create a node with random lat/lon
+    lat = rand(100)-50 + rand
+    lon = rand(100)-50 + rand
+    # normal user has a changeset open
+    changeset = changesets(:normal_user_first_change)
+    
+    amf_content "putpoi", "/2", ["test@openstreetmap.org:test", changeset.id, nil, nil, lon, lat, { "key" => "value", "ping" => "pong" }, nil]
+    post :amf_write
+    assert_response :success
+    amf_parse_response
+    result = amf_result("/2")
+
+    # check the array returned by the amf
+    assert_equal 4, result.size
+    assert_equal 0, result[0], "Expected to get the status ok in the amf"
+    assert_equal 0, result[1], "The old id should be 0"
+    assert result[2] > 0, "The new id should be greater than 0"
+    assert_equal 1, result[3], "The new version should be 1"
+    
+    # Finally check that the node that was saved has saved the data correctly 
+    # in both the current and history tables
+    # First check the current table
+    current_node = Node.find(result[2])
+    assert_in_delta lat, current_node.lat, 0.00001, "The latitude was not retreieved correctly"
+    assert_in_delta lon, current_node.lon, 0.00001, "The longitude was not retreived correctly"
+    assert_equal 2, current_node.tags.size, "There seems to be a tag that has been added to the node"
+    assert_equal({ "key" => "value", "ping" => "pong" }, current_node.tags, "tags are different")
+    assert_equal result[3], current_node.version, "The version returned, is different to the one returned by the amf"
+    # Now check the history table
+    historic_nodes = Node.find(:all, :conditions => { :id => result[2] })
+    assert_equal 1, historic_nodes.size, "There should only be one historic node created"
+    first_historic_node = historic_nodes.first
+    assert_in_delta lat, first_historic_node.lat, 0.00001, "The latitude was not retreived correctly"
+    assert_in_delta lon, first_historic_node.lon, 0.00001, "The longitude was not retreuved correctly"
+    assert_equal 2, first_historic_node.tags.size, "There seems to be a tag that have been attached to this node"
+    assert_equal({ "key" => "value", "ping" => "pong" }, first_historic_node.tags, "tags are different")
+    assert_equal result[3], first_historic_node.version, "The version returned, is different to the one returned by the amf"
+
+  end
+
+  # ************************************************************
+  # AMF Helper functions
+
+  # Get the result record for the specified ID
+  # It's an assertion FAIL if the record does not exist
+  def amf_result ref
+    assert @amf_result.has_key?("#{ref}/onResult")
+    @amf_result["#{ref}/onResult"]
+  end
+
+  # Encode the AMF message to invoke "target" with parameters as
+  # the passed data. The ref is used to retrieve the results.
+  def amf_content(target, ref, data)
+    a,b=1.divmod(256)
+    c = StringIO.new()
+    c.write 0.chr+0.chr   # version 0
+    c.write 0.chr+0.chr   # n headers
+    c.write a.chr+b.chr   # n bodies
+    c.write AMF.encodestring(target)
+    c.write AMF.encodestring(ref)
+    c.write [-1].pack("N")
+    c.write AMF.encodevalue(data)
+
+    @request.env["RAW_POST_DATA"] = c.string
+  end
+
+  # Parses the @response object as an AMF messsage.
+  # The result is a hash of message_ref => data.
+  # The attribute @amf_result is initialised to this hash.
+  def amf_parse_response
+    if @response.body.class.to_s == 'Proc'
+      res = StringIO.new()
+      @response.body.call @response, res
+      req = StringIO.new(res.string)
+    else
+      req = StringIO.new(@response.body)
+    end
+    req.read(2)   # version
+
+    # parse through any headers
+       headers=AMF.getint(req)                                 # Read number of headers
+       headers.times do                                                # Read each header
+         name=AMF.getstring(req)                               #  |
+         req.getc                                                              #  | skip boolean
+         value=AMF.getvalue(req)                               #  |
+       end
+
+    # parse through responses
+    results = {}
+    bodies=AMF.getint(req)                                     # Read number of bodies
+       bodies.times do                                                 # Read each body
+         message=AMF.getstring(req)                    #  | get message name
+         index=AMF.getstring(req)                              #  | get index in response sequence
+         bytes=AMF.getlong(req)                                #  | get total size in bytes
+         args=AMF.getvalue(req)                                #  | get response (probably an array)
+      results[message] = args
+    end
+    @amf_result = results
+    results
+  end
+
+  ##
+  # given an array of bounding boxes (each an array of 4 floats), call the
+  # AMF "whichways" controller for each and pass the result back to the
+  # caller's block for assertion testing.
+  def check_bboxes_are_bad(bboxes)
+    bboxes.each do |bbox|
+      amf_content "whichways", "/1", bbox
+      post :amf_read
+      assert_response :success
+      amf_parse_response
+
+      # pass the response back to the caller's block to be tested
+      # against what the caller expected.
+      map = amf_result "/1"
+      yield map, bbox
+    end
+  end
+end
index 05cbe2af0ac5d8ab4a33816dfb30485e3dde1c72..a8e8087166cb3bbf975a5dffab51717ed98b8d38 100644 (file)
@@ -1,16 +1,21 @@
 require File.dirname(__FILE__) + '/../test_helper'
 require 'api_controller'
 
-# Re-raise errors caught by the controller.
-class ApiController; def rescue_action(e) raise e end; end
-
-class ApiControllerTest < Test::Unit::TestCase
+class ApiControllerTest < ActionController::TestCase
   api_fixtures
-
+  
   def setup
-    @controller = ApiController.new
-    @request    = ActionController::TestRequest.new
-    @response   = ActionController::TestResponse.new
+    super
+    @badbigbbox = %w{ -0.1,-0.1,1.1,1.1  10,10,11,11 }
+    @badmalformedbbox = %w{ -0.1  hello 
+    10N2W10.1N2.1W }
+    @badlatmixedbbox = %w{ 0,0.1,0.1,0  -0.1,80,0.1,70  0.24,54.34,0.25,54.33 }
+    @badlonmixedbbox = %w{ 80,-0.1,70,0.1  54.34,0.24,54.33,0.25 }  
+    #@badlatlonoutboundsbbox = %w{ 191,-0.1,193,0.1  -190.1,89.9,-190,90 }
+    @goodbbox = %w{ -0.1,-0.1,0.1,0.1  51.1,-0.1,51.2,0 
+    -0.1,%20-0.1,%200.1,%200.1  -0.1edcd,-0.1d,0.1,0.1  -0.1E,-0.1E,0.1S,0.1N S0.1,W0.1,N0.1,E0.1}
+    # That last item in the goodbbox really shouldn't be there, as the API should
+    # reall reject it, however this is to test to see if the api changes.
   end
 
   def basic_authorization(user, pass)
@@ -23,12 +28,201 @@ class ApiControllerTest < Test::Unit::TestCase
 
   def test_map
     node = current_nodes(:used_node_1)
-    bbox = "#{node.latitude-0.1},#{node.longitude-0.1},#{node.latitude+0.1},#{node.longitude+0.1}"
+    # Need to split the min/max lat/lon out into their own variables here
+    # so that we can test they are returned later.
+    minlon = node.lon-0.1
+    minlat = node.lat-0.1
+    maxlon = node.lon+0.1
+    maxlat = node.lat+0.1
+    bbox = "#{minlon},#{minlat},#{maxlon},#{maxlat}"
     get :map, :bbox => bbox
     if $VERBOSE
-        print @response.body
+      print @request.to_yaml
+      print @response.body
+    end
+    assert_response :success, "Expected scucess with the map call"
+    assert_select "osm[version='#{API_VERSION}'][generator='#{GENERATOR}']:root", :count => 1 do
+      assert_select "bounds[minlon=#{minlon}][minlat=#{minlat}][maxlon=#{maxlon}][maxlat=#{maxlat}]", :count => 1
+      assert_select "node[id=#{node.id}][lat=#{node.lat}][lon=#{node.lon}][version=#{node.version}][changeset=#{node.changeset_id}][visible=#{node.visible}][timestamp=#{node.timestamp.xmlschema}]", :count => 1 do
+        # This should really be more generic
+        assert_select "tag[k='test'][v='yes']"
+      end
+      # Should also test for the ways and relation
     end
+  end
+  
+  # This differs from the above test in that we are making the bbox exactly
+  # the same as the node we are looking at
+  def test_map_inclusive
+    node = current_nodes(:used_node_1)
+    bbox = "#{node.lon},#{node.lat},#{node.lon},#{node.lat}"
+    get :map, :bbox => bbox
+    #print @response.body
+    assert_response :success, "The map call should have succeeded"
+    assert_select "osm[version='#{API_VERSION}'][generator='#{GENERATOR}']:root:empty", :count => 1
+  end
+  
+  def test_tracepoints
+    point = gpx_files(:first_trace_file)
+    minlon = point.longitude-0.1
+    minlat = point.latitude-0.1
+    maxlon = point.longitude+0.1
+    maxlat = point.latitude+0.1
+    bbox = "#{minlon},#{minlat},#{maxlon},#{maxlat}"
+    get :trackpoints, :bbox => bbox
+    #print @response.body
     assert_response :success
+    assert_select "gpx[version=1.0][creator=OpenStreetMap.org][xmlns=http://www.topografix.com/GPX/1/0/]:root", :count => 1 do
+      assert_select "trk" do
+        assert_select "trkseg"
+      end
+    end
+  end
+  
+  def test_map_without_bbox
+    ["trackpoints", "map"].each do |tq|
+      get tq
+      assert_response :bad_request
+      assert_equal "The parameter bbox is required, and must be of the form min_lon,min_lat,max_lon,max_lat", @response.body, "A bbox param was expected"
+    end
+  end
+  
+  def test_traces_page_less_than_0
+    -10.upto(-1) do |i|
+      get :trackpoints, :page => i, :bbox => "-0.1,-0.1,0.1,0.1"
+      assert_response :bad_request
+      assert_equal "Page number must be greater than or equal to 0", @response.body, "The page number was #{i}"
+    end
+    0.upto(10) do |i|
+      get :trackpoints, :page => i, :bbox => "-0.1,-0.1,0.1,0.1"
+      assert_response :success, "The page number was #{i} and should have been accepted"
+    end
+  end
+  
+  def test_bbox_too_big
+    @badbigbbox.each do |bbox|
+      [ "trackpoints", "map" ].each do |tq|
+        get tq, :bbox => bbox
+        assert_response :bad_request, "The bbox:#{bbox} was expected to be too big"
+        assert_equal "The maximum bbox size is #{APP_CONFIG['max_request_area']}, and your request was too large. Either request a smaller area, or use planet.osm", @response.body, "bbox: #{bbox}"
+      end
+    end
+  end
+  
+  def test_bbox_malformed
+    @badmalformedbbox.each do |bbox|
+      [ "trackpoints", "map" ].each do |tq|
+        get tq, :bbox => bbox
+        assert_response :bad_request, "The bbox:#{bbox} was expected to be malformed"
+        assert_equal "The parameter bbox is required, and must be of the form min_lon,min_lat,max_lon,max_lat", @response.body, "bbox: #{bbox}"
+      end
+    end
+  end
+  
+  def test_bbox_lon_mixedup
+    @badlonmixedbbox.each do |bbox|
+      [ "trackpoints", "map" ].each do |tq|
+        get tq, :bbox => bbox
+        assert_response :bad_request, "The bbox:#{bbox} was expected to have the longitude mixed up"
+        assert_equal "The minimum longitude must be less than the maximum longitude, but it wasn't", @response.body, "bbox: #{bbox}"
+      end
+    end
+  end
+  
+  def test_bbox_lat_mixedup
+    @badlatmixedbbox.each do |bbox|
+      ["trackpoints", "map"].each do |tq|
+        get tq, :bbox => bbox
+        assert_response :bad_request, "The bbox:#{bbox} was expected to have the latitude mixed up"
+        assert_equal "The minimum latitude must be less than the maximum latitude, but it wasn't", @response.body, "bbox: #{bbox}"
+      end
+    end
+  end
+  
+  # We can't actually get an out of bounds error, as the bbox is sanitised.
+  #def test_latlon_outofbounds
+  #  @badlatlonoutboundsbbox.each do |bbox|
+  #    [ "trackpoints", "map" ].each do |tq|
+  #      get tq, :bbox => bbox
+  #      #print @request.to_yaml
+  #      assert_response :bad_request, "The bbox #{bbox} was expected to be out of range"
+  #      assert_equal "The latitudes must be between -90 an 90, and longitudes between -180 and 180", @response.body, "bbox: #{bbox}"
+  #    end
+  #  end
+  #end
+  
+  # MySQL requires that the C based functions are installed for this test to 
+  # work. More information is available from:
+  # http://wiki.openstreetmap.org/index.php/Rails#Installing_the_quadtile_functions
+  def test_changes_simple
+    get :changes
+    assert_response :success
+    #print @response.body
+    # As we have loaded the fixtures, we can assume that there are no 
+    # changes recently
+    now = Time.now
+    hourago = now - 1.hour
+    # Note that this may fail on a very slow machine, so isn't a great test
+    assert_select "osm[version='#{API_VERSION}'][generator='#{GENERATOR}']:root", :count => 1 do
+      assert_select "changes[starttime='#{hourago.xmlschema}'][endtime='#{now.xmlschema}']", :count => 1
+    end
+  end
+  
+  def test_changes_zoom_invalid
+    zoom_to_test = %w{ p -1 0 17 one two }
+    zoom_to_test.each do |zoom|
+      get :changes, :zoom => zoom
+      assert_response :bad_request
+      assert_equal @response.body, "Requested zoom is invalid, or the supplied start is after the end time, or the start duration is more than 24 hours"
+    end
+  end
+  
+  def test_changes_zoom_valid
+    1.upto(16) do |zoom|
+      get :changes, :zoom => zoom
+      assert_response :success
+      now = Time.now
+      hourago = now - 1.hour
+      # Note that this may fail on a very slow machine, so isn't a great test
+      assert_select "osm[version='#{API_VERSION}'][generator='#{GENERATOR}']:root", :count => 1 do
+        assert_select "changes[starttime='#{hourago.xmlschema}'][endtime='#{now.xmlschema}']", :count => 1
+      end
+    end
+  end
+  
+  def test_start_end_time_invalid
+    
+  end
+  
+  def test_start_end_time_invalid
+    
+  end
+  
+  def test_hours_invalid
+    invalid = %w{ -21 335 -1 0 25 26 100 one two three ping pong : }
+    invalid.each do |hour|
+      get :changes, :hours => hour
+      assert_response :bad_request, "Problem with the hour: #{hour}"
+      assert_equal @response.body, "Requested zoom is invalid, or the supplied start is after the end time, or the start duration is more than 24 hours", "Problem with the hour: #{hour}."
+    end
+  end
+  
+  def test_hours_valid
+    1.upto(24) do |hour|
+      get :changes, :hours => hour
+      assert_response :success
+    end
+  end
+  
+  def test_capabilities
+    get :capabilities
+    assert_response :success
+    assert_select "osm:root[version='#{API_VERSION}'][generator='#{GENERATOR}']", :count => 1 do
+      assert_select "api", :count => 1 do
+        assert_select "version[minimum=#{API_VERSION}][maximum=#{API_VERSION}]", :count => 1
+        assert_select "area[maximum=#{APP_CONFIG['max_request_area']}]", :count => 1
+        assert_select "tracepoints[per_page=#{APP_CONFIG['tracepoints_per_page']}]", :count => 1
+      end
+    end
   end
-
 end
diff --git a/test/functional/browse_controller_test.rb b/test/functional/browse_controller_test.rb
new file mode 100644 (file)
index 0000000..65e8510
--- /dev/null
@@ -0,0 +1,66 @@
+require File.dirname(__FILE__) + '/../test_helper'
+require 'browse_controller'
+
+class BrowseControllerTest < ActionController::TestCase
+  api_fixtures
+
+  def basic_authorization(user, pass)
+    @request.env["HTTP_AUTHORIZATION"] = "Basic %s" % Base64.encode64("#{user}:#{pass}")
+  end
+
+  def content(c)
+    @request.env["RAW_POST_DATA"] = c.to_s
+  end
+
+  # We need to load the home page, then activate the start rjs method
+  # and finally check that the new panel has loaded.
+  def test_start
+  
+  end
+  
+  # This should display the last 20 nodes that were edited.
+  def test_index
+    @nodes = Node.find(:all, :order => "timestamp DESC", :limit => 20)
+    assert @nodes.size <= 20
+    get :index
+    assert_response :success
+    assert_template "index"
+    # Now check that all 20 (or however many were returned) nodes are in the html
+    assert_select "h2", :text => "#{@nodes.size} Recently Changed Nodes", :count => 1
+    assert_select "ul[id='recently_changed'] li a", :count => @nodes.size
+    @nodes.each do |node|
+      name = node.tags_as_hash['name'].to_s
+      name = "(No name)" if name.length == 0
+      assert_select "ul[id='recently_changed'] li a[href=/browse/node/#{node.id}]", :text => "#{name} - #{node.id} (#{node.version})"
+    end
+  end
+  
+  # Test reading a relation
+  def test_read_relation
+    
+  end
+  
+  def test_read_relation_history
+    
+  end
+  
+  def test_read_way
+    
+  end
+  
+  def test_read_way_history
+    
+  end
+  
+  def test_read_node
+    
+  end
+  
+  def test_read_node_history
+    
+  end
+  
+  def test_read_changeset
+    
+  end
+end
diff --git a/test/functional/changeset_controller_test.rb b/test/functional/changeset_controller_test.rb
new file mode 100644 (file)
index 0000000..e8648e5
--- /dev/null
@@ -0,0 +1,892 @@
+require File.dirname(__FILE__) + '/../test_helper'
+require 'changeset_controller'
+
+class ChangesetControllerTest < ActionController::TestCase
+  api_fixtures
+
+  def basic_authorization(user, pass)
+    @request.env["HTTP_AUTHORIZATION"] = "Basic %s" % Base64.encode64("#{user}:#{pass}")
+  end
+
+  def content(c)
+    @request.env["RAW_POST_DATA"] = c.to_s
+  end
+  
+  # -----------------------
+  # Test simple changeset creation
+  # -----------------------
+  
+  def test_create
+    basic_authorization "test@openstreetmap.org", "test"
+    
+    # Create the first user's changeset
+    content "<osm><changeset>" +
+      "<tag k='created_by' v='osm test suite checking changesets'/>" + 
+      "</changeset></osm>"
+    put :create
+    
+    assert_response :success, "Creation of changeset did not return sucess status"
+    newid = @response.body.to_i
+
+    # check end time, should be an hour ahead of creation time
+    cs = Changeset.find(newid)
+    duration = cs.closed_at - cs.created_at
+    # the difference can either be a rational, or a floating point number
+    # of seconds, depending on the code path taken :-(
+    if duration.class == Rational
+      assert_equal Rational(1,24), duration , "initial idle timeout should be an hour (#{cs.created_at} -> #{cs.closed_at})"
+    else
+      # must be number of seconds...
+      assert_equal 3600.0, duration , "initial idle timeout should be an hour (#{cs.created_at} -> #{cs.closed_at})"
+    end
+  end
+  
+  def test_create_invalid
+    basic_authorization "test@openstreetmap.org", "test"
+    content "<osm><changeset></osm>"
+    put :create
+    assert_response :bad_request, "creating a invalid changeset should fail"
+  end
+
+  ##
+  # check that the changeset can be read and returns the correct
+  # document structure.
+  def test_read
+    changeset_id = changesets(:normal_user_first_change).id
+    get :read, :id => changeset_id
+    assert_response :success, "cannot get first changeset"
+    
+    assert_select "osm[version=#{API_VERSION}][generator=\"OpenStreetMap server\"]", 1
+    assert_select "osm>changeset[id=#{changeset_id}]", 1
+  end
+  
+  ##
+  # test that the user who opened a change can close it
+  def test_close
+    basic_authorization "test@openstreetmap.org", "test"
+
+    put :close, :id => changesets(:normal_user_first_change).id
+    assert_response :success
+  end
+
+  ##
+  # test that a different user can't close another user's changeset
+  def test_close_invalid
+    basic_authorization "test@example.com", "test"
+
+    put :close, :id => changesets(:normal_user_first_change).id
+    assert_response :conflict
+    assert_equal "The user doesn't own that changeset", @response.body
+  end
+
+  ##
+  # upload something simple, but valid and check that it can 
+  # be read back ok.
+  def test_upload_simple_valid
+    basic_authorization "test@openstreetmap.org", "test"
+
+    # simple diff to change a node, way and relation by removing 
+    # their tags
+    diff = <<EOF
+<osmChange>
+ <modify>
+  <node id='1' lon='0' lat='0' changeset='1' version='1'/>
+  <way id='1' changeset='1' version='1'>
+   <nd ref='3'/>
+  </way>
+ </modify>
+ <modify>
+  <relation id='1' changeset='1' version='1'>
+   <member type='way' role='some' ref='3'/>
+   <member type='node' role='some' ref='5'/>
+   <member type='relation' role='some' ref='3'/>
+  </relation>
+ </modify>
+</osmChange>
+EOF
+
+    # upload it
+    content diff
+    post :upload, :id => 1
+    assert_response :success, 
+      "can't upload a simple valid diff to changeset: #{@response.body}"
+
+    # check that the changes made it into the database
+    assert_equal 0, Node.find(1).tags.size, "node 1 should now have no tags"
+    assert_equal 0, Way.find(1).tags.size, "way 1 should now have no tags"
+    assert_equal 0, Relation.find(1).tags.size, "relation 1 should now have no tags"
+  end
+    
+  ##
+  # upload something which creates new objects using placeholders
+  def test_upload_create_valid
+    basic_authorization "test@openstreetmap.org", "test"
+
+    # simple diff to create a node way and relation using placeholders
+    diff = <<EOF
+<osmChange>
+ <create>
+  <node id='-1' lon='0' lat='0' changeset='1'>
+   <tag k='foo' v='bar'/>
+   <tag k='baz' v='bat'/>
+  </node>
+  <way id='-1' changeset='1'>
+   <nd ref='3'/>
+  </way>
+ </create>
+ <create>
+  <relation id='-1' changeset='1'>
+   <member type='way' role='some' ref='3'/>
+   <member type='node' role='some' ref='5'/>
+   <member type='relation' role='some' ref='3'/>
+  </relation>
+ </create>
+</osmChange>
+EOF
+
+    # upload it
+    content diff
+    post :upload, :id => 1
+    assert_response :success, 
+      "can't upload a simple valid creation to changeset: #{@response.body}"
+
+    # check the returned payload
+    assert_select "diffResult[version=#{API_VERSION}][generator=\"OpenStreetMap server\"]", 1
+    assert_select "diffResult>node", 1
+    assert_select "diffresult>way", 1
+    assert_select "diffResult>relation", 1
+
+    # inspect the response to find out what the new element IDs are
+    doc = XML::Parser.string(@response.body).parse
+    new_node_id = doc.find("//diffResult/node").first["new_id"].to_i
+    new_way_id = doc.find("//diffResult/way").first["new_id"].to_i
+    new_rel_id = doc.find("//diffResult/relation").first["new_id"].to_i
+
+    # check the old IDs are all present and negative one
+    assert_equal -1, doc.find("//diffResult/node").first["old_id"].to_i
+    assert_equal -1, doc.find("//diffResult/way").first["old_id"].to_i
+    assert_equal -1, doc.find("//diffResult/relation").first["old_id"].to_i
+
+    # check the versions are present and equal one
+    assert_equal 1, doc.find("//diffResult/node").first["new_version"].to_i
+    assert_equal 1, doc.find("//diffResult/way").first["new_version"].to_i
+    assert_equal 1, doc.find("//diffResult/relation").first["new_version"].to_i
+
+    # check that the changes made it into the database
+    assert_equal 2, Node.find(new_node_id).tags.size, "new node should have two tags"
+    assert_equal 0, Way.find(new_way_id).tags.size, "new way should have no tags"
+    assert_equal 0, Relation.find(new_rel_id).tags.size, "new relation should have no tags"
+  end
+    
+  ##
+  # test a complex delete where we delete elements which rely on eachother
+  # in the same transaction.
+  def test_upload_delete
+    basic_authorization "test@openstreetmap.org", "test"
+
+    diff = XML::Document.new
+    diff.root = XML::Node.new "osmChange"
+    delete = XML::Node.new "delete"
+    diff.root << delete
+    delete << current_relations(:visible_relation).to_xml_node
+    delete << current_relations(:used_relation).to_xml_node
+    delete << current_ways(:used_way).to_xml_node
+    delete << current_nodes(:node_used_by_relationship).to_xml_node
+
+    # upload it
+    content diff
+    post :upload, :id => 1
+    assert_response :success, 
+      "can't upload a deletion diff to changeset: #{@response.body}"
+
+    # check that everything was deleted
+    assert_equal false, Node.find(current_nodes(:node_used_by_relationship).id).visible
+    assert_equal false, Way.find(current_ways(:used_way).id).visible
+    assert_equal false, Relation.find(current_relations(:visible_relation).id).visible
+    assert_equal false, Relation.find(current_relations(:used_relation).id).visible
+  end
+
+  ##
+  # test that deleting stuff in a transaction doesn't bypass the checks
+  # to ensure that used elements are not deleted.
+  def test_upload_delete_invalid
+    basic_authorization "test@openstreetmap.org", "test"
+
+    diff = XML::Document.new
+    diff.root = XML::Node.new "osmChange"
+    delete = XML::Node.new "delete"
+    diff.root << delete
+    delete << current_relations(:visible_relation).to_xml_node
+    delete << current_ways(:used_way).to_xml_node
+    delete << current_nodes(:node_used_by_relationship).to_xml_node
+
+    # upload it
+    content diff
+    post :upload, :id => 1
+    assert_response :precondition_failed, 
+      "shouldn't be able to upload a invalid deletion diff: #{@response.body}"
+
+    # check that nothing was, in fact, deleted
+    assert_equal true, Node.find(current_nodes(:node_used_by_relationship).id).visible
+    assert_equal true, Way.find(current_ways(:used_way).id).visible
+    assert_equal true, Relation.find(current_relations(:visible_relation).id).visible
+  end
+
+  ##
+  # upload something which creates new objects and inserts them into
+  # existing containers using placeholders.
+  def test_upload_complex
+    basic_authorization "test@openstreetmap.org", "test"
+
+    # simple diff to create a node way and relation using placeholders
+    diff = <<EOF
+<osmChange>
+ <create>
+  <node id='-1' lon='0' lat='0' changeset='1'>
+   <tag k='foo' v='bar'/>
+   <tag k='baz' v='bat'/>
+  </node>
+ </create>
+ <modify>
+  <way id='1' changeset='1' version='1'>
+   <nd ref='-1'/>
+   <nd ref='3'/>
+  </way>
+  <relation id='1' changeset='1' version='1'>
+   <member type='way' role='some' ref='3'/>
+   <member type='node' role='some' ref='-1'/>
+   <member type='relation' role='some' ref='3'/>
+  </relation>
+ </modify>
+</osmChange>
+EOF
+
+    # upload it
+    content diff
+    post :upload, :id => 1
+    assert_response :success, 
+      "can't upload a complex diff to changeset: #{@response.body}"
+
+    # check the returned payload
+    assert_select "diffResult[version=#{API_VERSION}][generator=\"#{GENERATOR}\"]", 1
+    assert_select "diffResult>node", 1
+    assert_select "diffResult>way", 1
+    assert_select "diffResult>relation", 1
+
+    # inspect the response to find out what the new element IDs are
+    doc = XML::Parser.string(@response.body).parse
+    new_node_id = doc.find("//diffResult/node").first["new_id"].to_i
+
+    # check that the changes made it into the database
+    assert_equal 2, Node.find(new_node_id).tags.size, "new node should have two tags"
+    assert_equal [new_node_id, 3], Way.find(1).nds, "way nodes should match"
+    Relation.find(1).members.each do |type,id,role|
+      if type == 'node'
+        assert_equal new_node_id, id, "relation should contain new node"
+      end
+    end
+  end
+    
+  ##
+  # create a diff which references several changesets, which should cause
+  # a rollback and none of the diff gets committed
+  def test_upload_invalid_changesets
+    basic_authorization "test@openstreetmap.org", "test"
+
+    # simple diff to create a node way and relation using placeholders
+    diff = <<EOF
+<osmChange>
+ <modify>
+  <node id='1' lon='0' lat='0' changeset='1' version='1'/>
+  <way id='1' changeset='1' version='1'>
+   <nd ref='3'/>
+  </way>
+ </modify>
+ <modify>
+  <relation id='1' changeset='1' version='1'>
+   <member type='way' role='some' ref='3'/>
+   <member type='node' role='some' ref='5'/>
+   <member type='relation' role='some' ref='3'/>
+  </relation>
+ </modify>
+ <create>
+  <node id='-1' lon='0' lat='0' changeset='4'>
+   <tag k='foo' v='bar'/>
+   <tag k='baz' v='bat'/>
+  </node>
+ </create>
+</osmChange>
+EOF
+    # cache the objects before uploading them
+    node = current_nodes(:visible_node)
+    way = current_ways(:visible_way)
+    rel = current_relations(:visible_relation)
+
+    # upload it
+    content diff
+    post :upload, :id => 1
+    assert_response :conflict, 
+      "uploading a diff with multiple changsets should have failed"
+
+    # check that objects are unmodified
+    assert_nodes_are_equal(node, Node.find(1))
+    assert_ways_are_equal(way, Way.find(1))
+  end
+    
+  ##
+  # upload multiple versions of the same element in the same diff.
+  def test_upload_multiple_valid
+    basic_authorization "test@openstreetmap.org", "test"
+
+    # change the location of a node multiple times, each time referencing
+    # the last version. doesn't this depend on version numbers being
+    # sequential?
+    diff = <<EOF
+<osmChange>
+ <modify>
+  <node id='1' lon='0' lat='0' changeset='1' version='1'/>
+  <node id='1' lon='1' lat='0' changeset='1' version='2'/>
+  <node id='1' lon='1' lat='1' changeset='1' version='3'/>
+  <node id='1' lon='1' lat='2' changeset='1' version='4'/>
+  <node id='1' lon='2' lat='2' changeset='1' version='5'/>
+  <node id='1' lon='3' lat='2' changeset='1' version='6'/>
+  <node id='1' lon='3' lat='3' changeset='1' version='7'/>
+  <node id='1' lon='9' lat='9' changeset='1' version='8'/>
+ </modify>
+</osmChange>
+EOF
+
+    # upload it
+    content diff
+    post :upload, :id => 1
+    assert_response :success, 
+      "can't upload multiple versions of an element in a diff: #{@response.body}"
+  end
+
+  ##
+  # upload multiple versions of the same element in the same diff, but
+  # keep the version numbers the same.
+  def test_upload_multiple_duplicate
+    basic_authorization "test@openstreetmap.org", "test"
+
+    diff = <<EOF
+<osmChange>
+ <modify>
+  <node id='1' lon='0' lat='0' changeset='1' version='1'/>
+  <node id='1' lon='1' lat='1' changeset='1' version='1'/>
+ </modify>
+</osmChange>
+EOF
+
+    # upload it
+    content diff
+    post :upload, :id => 1
+    assert_response :conflict, 
+      "shouldn't be able to upload the same element twice in a diff: #{@response.body}"
+  end
+
+  ##
+  # try to upload some elements without specifying the version
+  def test_upload_missing_version
+    basic_authorization "test@openstreetmap.org", "test"
+
+    diff = <<EOF
+<osmChange>
+ <modify>
+  <node id='1' lon='1' lat='1' changeset='1'/>
+ </modify>
+</osmChange>
+EOF
+
+    # upload it
+    content diff
+    post :upload, :id => 1
+    assert_response :bad_request, 
+      "shouldn't be able to upload an element without version: #{@response.body}"
+  end
+  
+  ##
+  # try to upload with commands other than create, modify, or delete
+  def test_action_upload_invalid
+    basic_authorization "test@openstreetmap.org", "test"
+    
+    diff = <<EOF
+<osmChange>
+  <ping>
+    <node id='1' lon='1' lat='1' changeset='1' />
+  </ping>
+</osmChange>
+EOF
+  content diff
+  post :upload, :id => 1
+  assert_response :bad_request, "Shouldn't be able to upload a diff with the action ping"
+  assert_equal @response.body, "Unknown action ping, choices are create, modify, delete."
+  end
+
+  ##
+  # when we make some simple changes we get the same changes back from the 
+  # diff download.
+  def test_diff_download_simple
+    basic_authorization(users(:normal_user).email, "test")
+
+    # create a temporary changeset
+    content "<osm><changeset>" +
+      "<tag k='created_by' v='osm test suite checking changesets'/>" + 
+      "</changeset></osm>"
+    put :create
+    assert_response :success
+    changeset_id = @response.body.to_i
+
+    # add a diff to it
+    diff = <<EOF
+<osmChange>
+ <modify>
+  <node id='1' lon='0' lat='0' changeset='#{changeset_id}' version='1'/>
+  <node id='1' lon='1' lat='0' changeset='#{changeset_id}' version='2'/>
+  <node id='1' lon='1' lat='1' changeset='#{changeset_id}' version='3'/>
+  <node id='1' lon='1' lat='2' changeset='#{changeset_id}' version='4'/>
+  <node id='1' lon='2' lat='2' changeset='#{changeset_id}' version='5'/>
+  <node id='1' lon='3' lat='2' changeset='#{changeset_id}' version='6'/>
+  <node id='1' lon='3' lat='3' changeset='#{changeset_id}' version='7'/>
+  <node id='1' lon='9' lat='9' changeset='#{changeset_id}' version='8'/>
+ </modify>
+</osmChange>
+EOF
+
+    # upload it
+    content diff
+    post :upload, :id => changeset_id
+    assert_response :success, 
+      "can't upload multiple versions of an element in a diff: #{@response.body}"
+    
+    get :download, :id => changeset_id
+    assert_response :success
+
+    assert_select "osmChange", 1
+    assert_select "osmChange>modify", 8
+    assert_select "osmChange>modify>node", 8
+  end
+  
+  ##
+  # culled this from josm to ensure that nothing in the way that josm
+  # is formatting the request is causing it to fail.
+  #
+  # NOTE: the error turned out to be something else completely!
+  def test_josm_upload
+    basic_authorization(users(:normal_user).email, "test")
+
+    # create a temporary changeset
+    content "<osm><changeset>" +
+      "<tag k='created_by' v='osm test suite checking changesets'/>" + 
+      "</changeset></osm>"
+    put :create
+    assert_response :success
+    changeset_id = @response.body.to_i
+
+    diff = <<OSM
+<osmChange version="0.6" generator="JOSM">
+<create version="0.6" generator="JOSM">
+  <node id='-1' visible='true' changeset='#{changeset_id}' lat='51.49619982187321' lon='-0.18722061869438314' />
+  <node id='-2' visible='true' changeset='#{changeset_id}' lat='51.496359883909605' lon='-0.18653093576241928' />
+  <node id='-3' visible='true' changeset='#{changeset_id}' lat='51.49598132358285' lon='-0.18719613290981638' />
+  <node id='-4' visible='true' changeset='#{changeset_id}' lat='51.4961591711078' lon='-0.18629015888084607' />
+  <node id='-5' visible='true' changeset='#{changeset_id}' lat='51.49582126021711' lon='-0.18708186591517145' />
+  <node id='-6' visible='true' changeset='#{changeset_id}' lat='51.49591018437858' lon='-0.1861432441734455' />
+  <node id='-7' visible='true' changeset='#{changeset_id}' lat='51.49560784152179' lon='-0.18694719410005425' />
+  <node id='-8' visible='true' changeset='#{changeset_id}' lat='51.49567389979617' lon='-0.1860289771788006' />
+  <node id='-9' visible='true' changeset='#{changeset_id}' lat='51.49543761398892' lon='-0.186820684213126' />
+  <way id='-10' action='modiy' visible='true' changeset='#{changeset_id}'>
+    <nd ref='-1' />
+    <nd ref='-2' />
+    <nd ref='-3' />
+    <nd ref='-4' />
+    <nd ref='-5' />
+    <nd ref='-6' />
+    <nd ref='-7' />
+    <nd ref='-8' />
+    <nd ref='-9' />
+    <tag k='highway' v='residential' />
+    <tag k='name' v='Foobar Street' />
+  </way>
+</create>
+</osmChange>
+OSM
+
+    # upload it
+    content diff
+    post :upload, :id => changeset_id
+    assert_response :success, 
+      "can't upload a diff from JOSM: #{@response.body}"
+    
+    get :download, :id => changeset_id
+    assert_response :success
+
+    assert_select "osmChange", 1
+    assert_select "osmChange>create>node", 9
+    assert_select "osmChange>create>way", 1
+    assert_select "osmChange>create>way>nd", 9
+    assert_select "osmChange>create>way>tag", 2
+  end
+
+  ##
+  # when we make some complex changes we get the same changes back from the 
+  # diff download.
+  def test_diff_download_complex
+    basic_authorization(users(:normal_user).email, "test")
+
+    # create a temporary changeset
+    content "<osm><changeset>" +
+      "<tag k='created_by' v='osm test suite checking changesets'/>" + 
+      "</changeset></osm>"
+    put :create
+    assert_response :success
+    changeset_id = @response.body.to_i
+
+    # add a diff to it
+    diff = <<EOF
+<osmChange>
+ <delete>
+  <node id='1' lon='0' lat='0' changeset='#{changeset_id}' version='1'/>
+ </delete>
+ <create>
+  <node id='-1' lon='9' lat='9' changeset='#{changeset_id}' version='0'/>
+  <node id='-2' lon='8' lat='9' changeset='#{changeset_id}' version='0'/>
+  <node id='-3' lon='7' lat='9' changeset='#{changeset_id}' version='0'/>
+ </create>
+ <modify>
+  <node id='3' lon='20' lat='15' changeset='#{changeset_id}' version='1'/>
+  <way id='1' changeset='#{changeset_id}' version='1'>
+   <nd ref='3'/>
+   <nd ref='-1'/>
+   <nd ref='-2'/>
+   <nd ref='-3'/>
+  </way>
+ </modify>
+</osmChange>
+EOF
+
+    # upload it
+    content diff
+    post :upload, :id => changeset_id
+    assert_response :success, 
+      "can't upload multiple versions of an element in a diff: #{@response.body}"
+    
+    get :download, :id => changeset_id
+    assert_response :success
+
+    assert_select "osmChange", 1
+    assert_select "osmChange>create", 3
+    assert_select "osmChange>delete", 1
+    assert_select "osmChange>modify", 2
+    assert_select "osmChange>create>node", 3
+    assert_select "osmChange>delete>node", 1 
+    assert_select "osmChange>modify>node", 1
+    assert_select "osmChange>modify>way", 1
+  end
+
+  ##
+  # check that the bounding box of a changeset gets updated correctly
+  def test_changeset_bbox
+    basic_authorization "test@openstreetmap.org", "test"
+
+    # create a new changeset
+    content "<osm><changeset/></osm>"
+    put :create
+    assert_response :success, "Creating of changeset failed."
+    changeset_id = @response.body.to_i
+    
+    # add a single node to it
+    with_controller(NodeController.new) do
+      content "<osm><node lon='1' lat='2' changeset='#{changeset_id}'/></osm>"
+      put :create
+      assert_response :success, "Couldn't create node."
+    end
+
+    # get the bounding box back from the changeset
+    get :read, :id => changeset_id
+    assert_response :success, "Couldn't read back changeset."
+    assert_select "osm>changeset[min_lon=1.0]", 1
+    assert_select "osm>changeset[max_lon=1.0]", 1
+    assert_select "osm>changeset[min_lat=2.0]", 1
+    assert_select "osm>changeset[max_lat=2.0]", 1
+
+    # add another node to it
+    with_controller(NodeController.new) do
+      content "<osm><node lon='2' lat='1' changeset='#{changeset_id}'/></osm>"
+      put :create
+      assert_response :success, "Couldn't create second node."
+    end
+
+    # get the bounding box back from the changeset
+    get :read, :id => changeset_id
+    assert_response :success, "Couldn't read back changeset for the second time."
+    assert_select "osm>changeset[min_lon=1.0]", 1
+    assert_select "osm>changeset[max_lon=2.0]", 1
+    assert_select "osm>changeset[min_lat=1.0]", 1
+    assert_select "osm>changeset[max_lat=2.0]", 1
+
+    # add (delete) a way to it
+    with_controller(WayController.new) do
+      content update_changeset(current_ways(:visible_way).to_xml,
+                               changeset_id)
+      put :delete, :id => current_ways(:visible_way).id
+      assert_response :success, "Couldn't delete a way."
+    end
+
+    # get the bounding box back from the changeset
+    get :read, :id => changeset_id
+    assert_response :success, "Couldn't read back changeset for the third time."
+    assert_select "osm>changeset[min_lon=1.0]", 1
+    assert_select "osm>changeset[max_lon=3.1]", 1
+    assert_select "osm>changeset[min_lat=1.0]", 1
+    assert_select "osm>changeset[max_lat=3.1]", 1    
+  end
+
+  ##
+  # test that the changeset :include method works as it should
+  def test_changeset_include
+    basic_authorization "test@openstreetmap.org", "test"
+
+    # create a new changeset
+    content "<osm><changeset/></osm>"
+    put :create
+    assert_response :success, "Creating of changeset failed."
+    changeset_id = @response.body.to_i
+
+    # NOTE: the include method doesn't over-expand, like inserting
+    # a real method does. this is because we expect the client to 
+    # know what it is doing!
+    check_after_include(changeset_id,  1,  1, [ 1,  1,  1,  1])
+    check_after_include(changeset_id,  3,  3, [ 1,  1,  3,  3])
+    check_after_include(changeset_id,  4,  2, [ 1,  1,  4,  3])
+    check_after_include(changeset_id,  2,  2, [ 1,  1,  4,  3])
+    check_after_include(changeset_id, -1, -1, [-1, -1,  4,  3])
+    check_after_include(changeset_id, -2,  5, [-2, -1,  4,  5])
+  end
+
+  ##
+  # test the query functionality of changesets
+  def test_query
+    get :query, :bbox => "-10,-10, 10, 10"
+    assert_response :success, "can't get changesets in bbox"
+    assert_changesets [1,4]
+
+    get :query, :bbox => "4.5,4.5,4.6,4.6"
+    assert_response :success, "can't get changesets in bbox"
+    assert_changesets [1]
+
+    # can't get changesets of user 1 without authenticating
+    get :query, :user => users(:normal_user).id
+    assert_response :not_found, "shouldn't be able to get changesets by non-public user"
+
+    # but this should work
+    basic_authorization "test@openstreetmap.org", "test"
+    get :query, :user => users(:normal_user).id
+    assert_response :success, "can't get changesets by user"
+    assert_changesets [1,3,4]
+
+    get :query, :user => users(:normal_user).id, :open => true
+    assert_response :success, "can't get changesets by user and open"
+    assert_changesets [1,4]
+
+    get :query, :time => '2007-12-31'
+    assert_response :success, "can't get changesets by time-since"
+    assert_changesets [1,2,4,5]
+
+    get :query, :time => '2008-01-01T12:34Z'
+    assert_response :success, "can't get changesets by time-since with hour"
+    assert_changesets [1,2,4,5]
+
+    get :query, :time => '2007-12-31T23:59Z,2008-01-01T00:01Z'
+    assert_response :success, "can't get changesets by time-range"
+    assert_changesets [1,4,5]
+
+    get :query, :open => 'true'
+    assert_response :success, "can't get changesets by open-ness"
+    assert_changesets [1,2,4]
+  end
+
+  ##
+  # check that errors are returned if garbage is inserted 
+  # into query strings
+  def test_query_invalid
+    [ "abracadabra!",
+      "1,2,3,F",
+      ";drop table users;"
+      ].each do |bbox|
+      get :query, :bbox => bbox
+      assert_response :bad_request, "'#{bbox}' isn't a bbox"
+    end
+
+    [ "now()",
+      "00-00-00",
+      ";drop table users;",
+      ",",
+      "-,-"
+      ].each do |time|
+      get :query, :time => time
+      assert_response :bad_request, "'#{time}' isn't a valid time range"
+    end
+
+    [ "me",
+      "foobar",
+      "-1",
+      "0"
+      ].each do |uid|
+      get :query, :user => uid
+      assert_response :bad_request, "'#{uid}' isn't a valid user ID"
+    end
+  end
+
+  ##
+  # check updating tags on a changeset
+  def test_changeset_update
+    changeset = changesets(:normal_user_first_change)
+    new_changeset = changeset.to_xml
+    new_tag = XML::Node.new "tag"
+    new_tag['k'] = "tagtesting"
+    new_tag['v'] = "valuetesting"
+    new_changeset.find("//osm/changeset").first << new_tag
+    content new_changeset
+
+    # try without any authorization
+    put :update, :id => changeset.id
+    assert_response :unauthorized
+
+    # try with the wrong authorization
+    basic_authorization "test@example.com", "test"
+    put :update, :id => changeset.id
+    assert_response :conflict
+
+    # now this should work...
+    basic_authorization "test@openstreetmap.org", "test"
+    put :update, :id => changeset.id
+    assert_response :success
+
+    assert_select "osm>changeset[id=#{changeset.id}]", 1
+    assert_select "osm>changeset>tag", 2
+    assert_select "osm>changeset>tag[k=tagtesting][v=valuetesting]", 1
+  end
+  
+  ##
+  # check that a user different from the one who opened the changeset
+  # can't modify it.
+  def test_changeset_update_invalid
+    basic_authorization "test@example.com", "test"
+
+    changeset = changesets(:normal_user_first_change)
+    new_changeset = changeset.to_xml
+    new_tag = XML::Node.new "tag"
+    new_tag['k'] = "testing"
+    new_tag['v'] = "testing"
+    new_changeset.find("//osm/changeset").first << new_tag
+
+    content new_changeset
+    put :update, :id => changeset.id
+    assert_response :conflict
+  end
+
+  ##
+  # check that a changeset can contain a certain max number of changes.
+  def test_changeset_limits
+    basic_authorization "test@openstreetmap.org", "test"
+
+    # open a new changeset
+    content "<osm><changeset/></osm>"
+    put :create
+    assert_response :success, "can't create a new changeset"
+    cs_id = @response.body.to_i
+
+    # start the counter just short of where the changeset should finish.
+    offset = 10
+    # alter the database to set the counter on the changeset directly, 
+    # otherwise it takes about 6 minutes to fill all of them.
+    changeset = Changeset.find(cs_id)
+    changeset.num_changes = Changeset::MAX_ELEMENTS - offset
+    changeset.save!
+
+    with_controller(NodeController.new) do
+      # create a new node
+      content "<osm><node changeset='#{cs_id}' lat='0.0' lon='0.0'/></osm>"
+      put :create
+      assert_response :success, "can't create a new node"
+      node_id = @response.body.to_i
+
+      get :read, :id => node_id
+      assert_response :success, "can't read back new node"
+      node_doc = XML::Parser.string(@response.body).parse
+      node_xml = node_doc.find("//osm/node").first
+
+      # loop until we fill the changeset with nodes
+      offset.times do |i|
+        node_xml['lat'] = rand.to_s
+        node_xml['lon'] = rand.to_s
+        node_xml['version'] = (i+1).to_s
+
+        content node_doc
+        put :update, :id => node_id
+        assert_response :success, "attempt #{i} should have succeeded"
+      end
+
+      # trying again should fail
+      node_xml['lat'] = rand.to_s
+      node_xml['lon'] = rand.to_s
+      node_xml['version'] = offset.to_s
+      
+      content node_doc
+      put :update, :id => node_id
+      assert_response :conflict, "final attempt should have failed"
+    end
+
+    changeset = Changeset.find(cs_id)
+    assert_equal Changeset::MAX_ELEMENTS + 1, changeset.num_changes
+  end
+  
+  #------------------------------------------------------------
+  # utility functions
+  #------------------------------------------------------------
+
+  ##
+  # boilerplate for checking that certain changesets exist in the
+  # output.
+  def assert_changesets(ids)
+    assert_select "osm>changeset", ids.size
+    ids.each do |id|
+      assert_select "osm>changeset[id=#{id}]", 1
+    end
+  end
+
+  ##
+  # call the include method and assert properties of the bbox
+  def check_after_include(changeset_id, lon, lat, bbox)
+    content "<osm><node lon='#{lon}' lat='#{lat}'/></osm>"
+    post :expand_bbox, :id => changeset_id
+    assert_response :success, "Setting include of changeset failed: #{@response.body}"
+
+    # check exactly one changeset
+    assert_select "osm>changeset", 1
+    assert_select "osm>changeset[id=#{changeset_id}]", 1
+
+    # check the bbox
+    doc = XML::Parser.string(@response.body).parse
+    changeset = doc.find("//osm/changeset").first
+    assert_equal bbox[0], changeset['min_lon'].to_f, "min lon"
+    assert_equal bbox[1], changeset['min_lat'].to_f, "min lat"
+    assert_equal bbox[2], changeset['max_lon'].to_f, "max lon"
+    assert_equal bbox[3], changeset['max_lat'].to_f, "max lat"
+  end
+
+  ##
+  # update the changeset_id of a way element
+  def update_changeset(xml, changeset_id)
+    xml_attr_rewrite(xml, 'changeset', changeset_id)
+  end
+
+  ##
+  # update an attribute in a way element
+  def xml_attr_rewrite(xml, name, value)
+    xml.find("//osm/way").first[name] = value.to_s
+    return xml
+  end
+
+end
diff --git a/test/functional/changeset_tag_controller_test.rb b/test/functional/changeset_tag_controller_test.rb
new file mode 100644 (file)
index 0000000..db9710e
--- /dev/null
@@ -0,0 +1,8 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class ChangesetTagControllerTest < ActionController::TestCase
+  # Replace this with your real tests.
+  def test_truth
+    assert true
+  end
+end
diff --git a/test/functional/diary_entry_controller_test.rb b/test/functional/diary_entry_controller_test.rb
new file mode 100644 (file)
index 0000000..c0bd4b9
--- /dev/null
@@ -0,0 +1,162 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class DiaryEntryControllerTest < ActionController::TestCase
+  fixtures :users, :diary_entries, :diary_comments
+
+  def test_showing_new_diary_entry
+    get :new
+    assert_response :redirect
+    assert_redirected_to :controller => :user, :action => "login", :referer => "/diary_entry/new"
+    # Now pretend to login by using the session hash, with the 
+    # id of the person we want to login as through session(:user)=user.id
+    get(:new, nil, {'user' => users(:normal_user).id})
+    assert_response :success
+    #print @response.body
+    
+    #print @response.to_yaml
+    assert_select "html:root", :count => 1 do
+      assert_select "head", :count => 1 do
+        assert_select "title", :text => /New diary entry/, :count => 1
+      end
+      assert_select "body", :count => 1 do
+        assert_select "div#content", :count => 1 do
+          assert_select "h1", "New diary entry", :count => 1
+          # We don't care about the layout, we just care about the form fields
+          # that are available
+          assert_select "form[action='/diary_entry/new']", :count => 1 do
+            assert_select "input[id=diary_entry_title][name='diary_entry[title]']", :count => 1
+            assert_select "textarea#diary_entry_body[name='diary_entry[body]']", :count => 1
+            assert_select "input#latitude[name='diary_entry[latitude]'][type=text]", :count => 1
+            assert_select "input#longitude[name='diary_entry[longitude]'][type=text]", :count => 1
+            assert_select "input[name=commit][type=submit][value=Save]", :count => 1
+          end
+        end
+      end
+    end
+        
+  end
+  
+  def test_editing_diary_entry
+    # Make sure that you are redirected to the login page when you are 
+    # not logged in, without and with the id of the entry you want to edit
+    get :edit
+    assert_response :redirect
+    assert_redirected_to :controller => :user, :action => "login", :referer => "/diary_entry/edit"
+    
+    get :edit, :id => diary_entries(:normal_user_entry_1).id
+    assert_response :redirect
+    assert_redirected_to :controller => :user, :action => "login", :referer => "/diary_entry/edit"
+    
+    # Verify that you get a not found error, when you don't pass an id
+    get(:edit, nil, {'user' => users(:normal_user).id})
+    assert_response :not_found
+    assert_select "html:root", :count => 1 do
+      assert_select "body", :count => 1 do
+        assert_select "div#content", :count => 1 do
+          assert_select "h2", :text => "No entry with the id:", :count => 1 
+        end
+      end
+    end
+    
+    # Now pass the id, and check that you can edit it, when using the same 
+    # user as the person who created the entry
+    get(:edit, {:id => diary_entries(:normal_user_entry_1).id}, {'user' => users(:normal_user).id})
+    assert_response :success
+    assert_select "html:root", :count => 1 do
+      assert_select "head", :count => 1 do
+        assert_select "title", :text => /Edit diary entry/, :count => 1
+      end
+      assert_select "body", :count => 1 do
+        assert_select "div#content", :count => 1 do 
+          assert_select "h1", :text => /Edit diary entry/, :count => 1
+          assert_select "form[action='/diary_entry/#{diary_entries(:normal_user_entry_1).id}/edit'][method=post]", :count => 1 do
+            assert_select "input#diary_entry_title[name='diary_entry[title]'][value='#{diary_entries(:normal_user_entry_1).title}']", :count => 1
+            assert_select "textarea#diary_entry_body[name='diary_entry[body]']", :text => diary_entries(:normal_user_entry_1).body, :count => 1
+            assert_select "input#latitude[name='diary_entry[latitude]']", :count => 1
+            assert_select "input#longitude[name='diary_entry[longitude]']", :count => 1
+            assert_select "input[name=commit][type=submit][value=Save]", :count => 1
+            assert_select "input", :count => 4
+          end
+        end
+      end
+    end
+    
+    # Now lets see if you can edit the diary entry
+    new_title = "New Title"
+    new_body = "This is a new body for the diary entry"
+    new_latitude = "1.1"
+    new_longitude = "2.2"
+    post(:edit, {:id => diary_entries(:normal_user_entry_1).id, 'commit' => 'save', 
+      'diary_entry'=>{'title' => new_title, 'body' => new_body, 'latitude' => new_latitude, 'longitude' => new_longitude} },
+         {'user' => users(:normal_user).id})
+    assert_response :redirect
+    assert_redirected_to :action => :view, :id => diary_entries(:normal_user_entry_1).id
+    
+    # Now check that the new data is rendered, when logged in
+    get :view, {:id => diary_entries(:normal_user_entry_1).id, :display_name => 'test'}, {'user' => users(:normal_user).id}
+    assert_response :success
+    assert_template 'diary_entry/view'
+    assert_select "html:root", :count => 1 do
+      assert_select "head", :count => 1 do
+        assert_select "title", :text => /Users' diaries | /, :count => 1
+      end
+      assert_select "body", :count => 1 do
+        assert_select "div#content", :count => 1 do
+          assert_select "h2", :text => /#{users(:normal_user).display_name}'s diary/, :count => 1
+          assert_select "b", :text => /#{new_title}/, :count => 1
+          # This next line won't work if the text has been run through the htmlize function
+          # due to formatting that could be introduced
+          assert_select "p", :text => /#{new_body}/, :count => 1
+          assert_select "span.latitude", :text => new_latitude, :count => 1
+          assert_select "span.longitude", :text => new_longitude, :count => 1
+          # As we're not logged in, check that you cannot edit
+          #print @response.body
+          assert_select "a[href='/user/#{users(:normal_user).display_name}/diary/#{diary_entries(:normal_user_entry_1).id}/edit']", :text => "Edit this entry", :count => 1
+        end
+      end
+    end
+    
+    # and when not logged in as the user who wrote the entry
+    get :view, {:id => diary_entries(:normal_user_entry_1).id, :display_name => 'test'}, {'user' => users(:second_user).id}
+    assert_response :success
+    assert_template 'diary_entry/view'
+    assert_select "html:root", :count => 1 do
+      assert_select "head", :count => 1 do
+        assert_select "title", :text => /Users' diaries | /, :count => 1
+      end
+      assert_select "body", :count => 1 do
+        assert_select "div#content", :count => 1 do
+          assert_select "h2", :text => /#{users(:normal_user).display_name}'s diary/, :count => 1
+          assert_select "b", :text => /#{new_title}/, :count => 1
+          # This next line won't work if the text has been run through the htmlize function
+          # due to formatting that could be introduced
+          assert_select "p", :text => /#{new_body}/, :count => 1
+          assert_select "span.latitude", :text => new_latitude, :count => 1
+          assert_select "span.longitude", :text => new_longitude, :count => 1
+          # As we're not logged in, check that you cannot edit
+          assert_select "a[href='/user/#{users(:normal_user).display_name}/diary/#{diary_entries(:normal_user_entry_1).id}/edit']", :text => "Edit this entry", :count => 0
+        end
+      end
+    end
+    #print @response.body
+    
+  end
+  
+  def test_editing_creating_diary_comment
+    
+  end
+  
+  def test_listing_diary_entries
+    
+  end
+  
+  def test_rss
+    get :rss
+    assert :success
+    
+  end
+  
+  def test_viewing_diary_entry
+    
+  end
+end
diff --git a/test/functional/export_controller_test.rb b/test/functional/export_controller_test.rb
new file mode 100644 (file)
index 0000000..8a97941
--- /dev/null
@@ -0,0 +1,8 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class ExportControllerTest < ActionController::TestCase
+  # Replace this with your real tests.
+  def test_truth
+    assert true
+  end
+end
diff --git a/test/functional/friend_controller_test.rb b/test/functional/friend_controller_test.rb
new file mode 100644 (file)
index 0000000..d1f0e7d
--- /dev/null
@@ -0,0 +1,8 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class FriendControllerTest < ActionController::TestCase
+  # Replace this with your real tests.
+  def test_truth
+    assert true
+  end
+end
index 3faadc7404aab06ec9fb202d3505bb7175d633a5..f63fe518d0aa04925c04f91b2e45ad905f145013 100644 (file)
@@ -1,15 +1,7 @@
 require File.dirname(__FILE__) + '/../test_helper'
 require 'geocoder_controller'
 
-# Re-raise errors caught by the controller.
-class GeocoderController; def rescue_action(e) raise e end; end
-
-class GeocoderControllerTest < Test::Unit::TestCase
-  def setup
-    @controller = GeocoderController.new
-    @request    = ActionController::TestRequest.new
-    @response   = ActionController::TestResponse.new
-  end
+class GeocoderControllerTest < ActionController::TestCase
 
   # Replace this with your real tests.
   def test_truth
index 54c8a18d12952265bd681b8fdbf00ce2999dc9e1..96f509cb0208b3548753e4fd5f351bdf65822b4c 100644 (file)
@@ -1,15 +1,7 @@
 require File.dirname(__FILE__) + '/../test_helper'
 require 'message_controller'
 
-# Re-raise errors caught by the controller.
-class MessageController; def rescue_action(e) raise e end; end
-
-class MessageControllerTest < Test::Unit::TestCase
-  def setup
-    @controller = MessageController.new
-    @request    = ActionController::TestRequest.new
-    @response   = ActionController::TestResponse.new
-  end
+class MessageControllerTest < ActionController::TestCase
 
   # Replace this with your real tests.
   def test_truth
index a380eeb208313f08672104595eef0d188ec72e06..bc9ffa489f1d1673bc297c67c191c6e60e29e708 100644 (file)
@@ -1,28 +1,23 @@
 require File.dirname(__FILE__) + '/../test_helper'
-require 'node_controller'
 
-# Re-raise errors caught by the controller.
-class NodeController; def rescue_action(e) raise e end; end
-
-class NodeControllerTest < Test::Unit::TestCase
+class NodeControllerTest < ActionController::TestCase
   api_fixtures
 
-  def setup
-    @controller = NodeController.new
-    @request    = ActionController::TestRequest.new
-    @response   = ActionController::TestResponse.new
-  end
-
   def test_create
     # cannot read password from fixture as it is stored as MD5 digest
-    basic_authorization("test@openstreetmap.org", "test");  
+    basic_authorization(users(:normal_user).email, "test")
+    
     # create a node with random lat/lon
     lat = rand(100)-50 + rand
     lon = rand(100)-50 + rand
-    content("<osm><node lat='#{lat}' lon='#{lon}' /></osm>")
+    # normal user has a changeset open, so we'll use that.
+    changeset = changesets(:normal_user_first_change)
+    # create a minimal xml file
+    content("<osm><node lat='#{lat}' lon='#{lon}' changeset='#{changeset.id}'/></osm>")
     put :create
     # hope for success
     assert_response :success, "node upload did not return success status"
+
     # read id of created node and search for it
     nodeid = @response.body
     checknode = Node.find(nodeid)
@@ -30,10 +25,36 @@ class NodeControllerTest < Test::Unit::TestCase
     # compare values
     assert_in_delta lat * 10000000, checknode.latitude, 1, "saved node does not match requested latitude"
     assert_in_delta lon * 10000000, checknode.longitude, 1, "saved node does not match requested longitude"
-    assert_equal users(:normal_user).id, checknode.user_id, "saved node does not belong to user that created it"
+    assert_equal changesets(:normal_user_first_change).id, checknode.changeset_id, "saved node does not belong to changeset that it was created in"
     assert_equal true, checknode.visible, "saved node is not visible"
   end
 
+  def test_create_invalid_xml
+    # Initial setup
+    basic_authorization(users(:normal_user).email, "test")
+    # normal user has a changeset open, so we'll use that.
+    changeset = changesets(:normal_user_first_change)
+    lat = 3.434
+    lon = 3.23
+    
+    # test that the upload is rejected when no lat is supplied
+    # create a minimal xml file
+    content("<osm><node lon='#{lon}' changeset='#{changeset.id}'/></osm>")
+    put :create
+    # hope for success
+    assert_response :bad_request, "node upload did not return bad_request status"
+    assert_equal 'Cannot parse valid node from xml string <node lon="3.23" changeset="1"/>. lat missing', @response.body
+
+    # test that the upload is rejected when no lon is supplied
+    # create a minimal xml file
+    content("<osm><node lat='#{lat}' changeset='#{changeset.id}'/></osm>")
+    put :create
+    # hope for success
+    assert_response :bad_request, "node upload did not return bad_request status"
+    assert_equal 'Cannot parse valid node from xml string <node lat="3.434" changeset="1"/>. lon missing', @response.body
+
+  end
+
   def test_read
     # check that a visible node is returned properly
     get :read, :id => current_nodes(:visible_node).id
@@ -51,19 +72,36 @@ class NodeControllerTest < Test::Unit::TestCase
   # this tests deletion restrictions - basic deletion is tested in the unit
   # tests for node!
   def test_delete
-
     # first try to delete node without auth
     delete :delete, :id => current_nodes(:visible_node).id
     assert_response :unauthorized
 
     # now set auth
-    basic_authorization("test@openstreetmap.org", "test");  
+    basic_authorization(users(:normal_user).email, "test");  
 
-    # this should work
+    # try to delete with an invalid (closed) changeset
+    content update_changeset(current_nodes(:visible_node).to_xml,
+                             changesets(:normal_user_closed_change).id)
+    delete :delete, :id => current_nodes(:visible_node).id
+    assert_response :conflict
+
+    # try to delete with an invalid (non-existent) changeset
+    content update_changeset(current_nodes(:visible_node).to_xml,0)
+    delete :delete, :id => current_nodes(:visible_node).id
+    assert_response :conflict
+
+    # valid delete now takes a payload
+    content(nodes(:visible_node).to_xml)
     delete :delete, :id => current_nodes(:visible_node).id
     assert_response :success
 
+    # valid delete should return the new version number, which should
+    # be greater than the old version number
+    assert @response.body.to_i > current_nodes(:visible_node).version,
+       "delete request should return a new version number for node"
+
     # this won't work since the node is already deleted
+    content(nodes(:invisible_node).to_xml)
     delete :delete, :id => current_nodes(:invisible_node).id
     assert_response :gone
 
@@ -71,17 +109,175 @@ class NodeControllerTest < Test::Unit::TestCase
     delete :delete, :id => 0
     assert_response :not_found
 
-    # this won't work since the node is in use
+    ## these test whether nodes which are in-use can be deleted:
+    # in a way...
+    content(nodes(:used_node_1).to_xml)
     delete :delete, :id => current_nodes(:used_node_1).id
-    assert_response :precondition_failed
+    assert_response :precondition_failed,
+       "shouldn't be able to delete a node used in a way (#{@response.body})"
+
+    # in a relation...
+    content(nodes(:node_used_by_relationship).to_xml)
+    delete :delete, :id => current_nodes(:node_used_by_relationship).id
+    assert_response :precondition_failed,
+       "shouldn't be able to delete a node used in a relation (#{@response.body})"
+  end
+
+  ##
+  # tests whether the API works and prevents incorrect use while trying
+  # to update nodes.
+  def test_update
+    # try and update a node without authorisation
+    # first try to delete node without auth
+    content current_nodes(:visible_node).to_xml
+    put :update, :id => current_nodes(:visible_node).id
+    assert_response :unauthorized
+    
+    # setup auth
+    basic_authorization(users(:normal_user).email, "test")
+
+    ## trying to break changesets
+
+    # try and update in someone else's changeset
+    content update_changeset(current_nodes(:visible_node).to_xml,
+                             changesets(:second_user_first_change).id)
+    put :update, :id => current_nodes(:visible_node).id
+    assert_response :conflict, "update with other user's changeset should be rejected"
+
+    # try and update in a closed changeset
+    content update_changeset(current_nodes(:visible_node).to_xml,
+                             changesets(:normal_user_closed_change).id)
+    put :update, :id => current_nodes(:visible_node).id
+    assert_response :conflict, "update with closed changeset should be rejected"
+
+    # try and update in a non-existant changeset
+    content update_changeset(current_nodes(:visible_node).to_xml, 0)
+    put :update, :id => current_nodes(:visible_node).id
+    assert_response :conflict, "update with changeset=0 should be rejected"
+
+    ## try and submit invalid updates
+    content xml_attr_rewrite(current_nodes(:visible_node).to_xml, 'lat', 91.0);
+    put :update, :id => current_nodes(:visible_node).id
+    assert_response :bad_request, "node at lat=91 should be rejected"
+
+    content xml_attr_rewrite(current_nodes(:visible_node).to_xml, 'lat', -91.0);
+    put :update, :id => current_nodes(:visible_node).id
+    assert_response :bad_request, "node at lat=-91 should be rejected"
+    
+    content xml_attr_rewrite(current_nodes(:visible_node).to_xml, 'lon', 181.0);
+    put :update, :id => current_nodes(:visible_node).id
+    assert_response :bad_request, "node at lon=181 should be rejected"
+
+    content xml_attr_rewrite(current_nodes(:visible_node).to_xml, 'lon', -181.0);
+    put :update, :id => current_nodes(:visible_node).id
+    assert_response :bad_request, "node at lon=-181 should be rejected"
+
+    ## next, attack the versioning
+    current_node_version = current_nodes(:visible_node).version
+
+    # try and submit a version behind
+    content xml_attr_rewrite(current_nodes(:visible_node).to_xml, 
+                             'version', current_node_version - 1);
+    put :update, :id => current_nodes(:visible_node).id
+    assert_response :conflict, "should have failed on old version number"
+    
+    # try and submit a version ahead
+    content xml_attr_rewrite(current_nodes(:visible_node).to_xml, 
+                             'version', current_node_version + 1);
+    put :update, :id => current_nodes(:visible_node).id
+    assert_response :conflict, "should have failed on skipped version number"
+
+    # try and submit total crap in the version field
+    content xml_attr_rewrite(current_nodes(:visible_node).to_xml, 
+                             'version', 'p1r4t3s!');
+    put :update, :id => current_nodes(:visible_node).id
+    assert_response :conflict, 
+       "should not be able to put 'p1r4at3s!' in the version field"
+    
+    ## finally, produce a good request which should work
+    content current_nodes(:visible_node).to_xml
+    put :update, :id => current_nodes(:visible_node).id
+    assert_response :success, "a valid update request failed"
   end
 
+  ##
+  # test adding tags to a node
+  def test_duplicate_tags
+    # setup auth
+    basic_authorization(users(:normal_user).email, "test")
+
+    # add an identical tag to the node
+    tag_xml = XML::Node.new("tag")
+    tag_xml['k'] = current_node_tags(:t1).k
+    tag_xml['v'] = current_node_tags(:t1).v
+
+    # add the tag into the existing xml
+    node_xml = current_nodes(:visible_node).to_xml
+    node_xml.find("//osm/node").first << tag_xml
+
+    # try and upload it
+    content node_xml
+    put :update, :id => current_nodes(:visible_node).id
+    assert_response :bad_request, 
+      "adding duplicate tags to a node should fail with 'bad request'"
+    assert_equal "Element node/#{current_nodes(:visible_node).id} has duplicate tags with key #{current_node_tags(:t1).k}.", @response.body
+  end
+
+  # test whether string injection is possible
+  def test_string_injection
+    basic_authorization(users(:normal_user).email, "test")
+    changeset_id = changesets(:normal_user_first_change).id
+
+    # try and put something into a string that the API might 
+    # use unquoted and therefore allow code injection...
+    content "<osm><node lat='0' lon='0' changeset='#{changeset_id}'>" +
+      '<tag k="#{@user.inspect}" v="0"/>' +
+      '</node></osm>'
+    put :create
+    assert_response :success
+    nodeid = @response.body
+
+    # find the node in the database
+    checknode = Node.find(nodeid)
+    assert_not_nil checknode, "node not found in data base after upload"
+    
+    # and grab it using the api
+    get :read, :id => nodeid
+    assert_response :success
+    apinode = Node.from_xml(@response.body)
+    assert_not_nil apinode, "downloaded node is nil, but shouldn't be"
+    
+    # check the tags are not corrupted
+    assert_equal checknode.tags, apinode.tags
+    assert apinode.tags.include?('#{@user.inspect}')
+  end
 
   def basic_authorization(user, pass)
     @request.env["HTTP_AUTHORIZATION"] = "Basic %s" % Base64.encode64("#{user}:#{pass}")
   end
 
   def content(c)
-    @request.env["RAW_POST_DATA"] = c
+    @request.env["RAW_POST_DATA"] = c.to_s
+  end
+
+  ##
+  # update the changeset_id of a node element
+  def update_changeset(xml, changeset_id)
+    xml_attr_rewrite(xml, 'changeset', changeset_id)
+  end
+
+  ##
+  # update an attribute in the node element
+  def xml_attr_rewrite(xml, name, value)
+    xml.find("//osm/node").first[name] = value.to_s
+    return xml
+  end
+
+  ##
+  # parse some xml
+  def xml_parse(xml)
+    parser = XML::Parser.new
+    parser.string = xml
+    parser.parse
   end
 end
diff --git a/test/functional/old_node_controller_test.rb b/test/functional/old_node_controller_test.rb
new file mode 100644 (file)
index 0000000..f1328e6
--- /dev/null
@@ -0,0 +1,133 @@
+require File.dirname(__FILE__) + '/../test_helper'
+require 'old_node_controller'
+
+class OldNodeControllerTest < ActionController::TestCase
+  api_fixtures
+
+  #
+  # TODO: test history
+  #
+
+  ##
+  # test the version call by submitting several revisions of a new node
+  # to the API and ensuring that later calls to version return the 
+  # matching versions of the object.
+  def test_version
+    basic_authorization(users(:normal_user).email, "test")
+    changeset_id = changesets(:normal_user_first_change).id
+
+    # setup a simple XML node
+    xml_doc = current_nodes(:visible_node).to_xml
+    xml_node = xml_doc.find("//osm/node").first
+    nodeid = current_nodes(:visible_node).id
+
+    # keep a hash of the versions => string, as we'll need something
+    # to test against later
+    versions = Hash.new
+
+    # save a version for later checking
+    versions[xml_node['version']] = xml_doc.to_s
+
+    # randomly move the node about
+    20.times do 
+      # move the node somewhere else
+      xml_node['lat'] = precision(rand * 180 -  90).to_s
+      xml_node['lon'] = precision(rand * 360 - 180).to_s
+      with_controller(NodeController.new) do
+        content xml_doc
+        put :update, :id => nodeid
+        assert_response :success
+        xml_node['version'] = @response.body.to_s
+      end
+      # save a version for later checking
+      versions[xml_node['version']] = xml_doc.to_s
+    end
+
+    # add a bunch of random tags
+    30.times do 
+      xml_tag = XML::Node.new("tag")
+      xml_tag['k'] = random_string
+      xml_tag['v'] = random_string
+      xml_node << xml_tag
+      with_controller(NodeController.new) do
+        content xml_doc
+        put :update, :id => nodeid
+        assert_response :success,
+        "couldn't update node #{nodeid} (#{@response.body})"
+        xml_node['version'] = @response.body.to_s
+      end
+      # save a version for later checking
+      versions[xml_node['version']] = xml_doc.to_s
+    end
+
+    # check all the versions
+    versions.keys.each do |key|
+      get :version, :id => nodeid, :version => key.to_i
+
+      assert_response :success,
+         "couldn't get version #{key.to_i} of node #{nodeid}"
+
+      check_node = Node.from_xml(versions[key])
+      api_node = Node.from_xml(@response.body.to_s)
+
+      assert_nodes_are_equal check_node, api_node
+    end
+  end
+
+  ##
+  # Test that getting the current version is identical to picking
+  # that version with the version URI call.
+  def test_current_version
+    check_current_version(current_nodes(:visible_node))
+    check_current_version(current_nodes(:used_node_1))
+    check_current_version(current_nodes(:used_node_2))
+    check_current_version(current_nodes(:node_used_by_relationship))
+    check_current_version(current_nodes(:node_with_versions))
+  end
+  
+  def check_current_version(node_id)
+    # get the current version of the node
+    current_node = with_controller(NodeController.new) do
+      get :read, :id => node_id
+      assert_response :success, "cant get current node #{node_id}" 
+      Node.from_xml(@response.body)
+    end
+    assert_not_nil current_node, "getting node #{node_id} returned nil"
+
+    # get the "old" version of the node from the old_node interface
+    get :version, :id => node_id, :version => current_node.version
+    assert_response :success, "cant get old node #{node_id}, v#{current_node.version}" 
+    old_node = Node.from_xml(@response.body)
+
+    # check the nodes are the same
+    assert_nodes_are_equal current_node, old_node
+  end
+
+  ##
+  # returns a 16 character long string with some nasty characters in it.
+  # this ought to stress-test the tag handling as well as the versioning.
+  def random_string
+    letters = [['!','"','$','&',';','@'],
+               ('a'..'z').to_a,
+               ('A'..'Z').to_a,
+               ('0'..'9').to_a].flatten
+    (1..16).map { |i| letters[ rand(letters.length) ] }.join
+  end
+
+  ##
+  # truncate a floating point number to the scale that it is stored in
+  # the database. otherwise rounding errors can produce failing unit
+  # tests when they shouldn't.
+  def precision(f)
+    return (f * GeoRecord::SCALE).round.to_f / GeoRecord::SCALE
+  end
+
+  def basic_authorization(user, pass)
+    @request.env["HTTP_AUTHORIZATION"] = "Basic %s" % Base64.encode64("#{user}:#{pass}")
+  end
+
+  def content(c)
+    @request.env["RAW_POST_DATA"] = c.to_s
+  end
+
+end
index b8bf464b6030fd180dced5c4f75dc88efdfed56e..a52211e2e15485ee28c7acb1eacfacf538601c18 100644 (file)
@@ -1,22 +1,12 @@
 require File.dirname(__FILE__) + '/../test_helper'
 require 'old_relation_controller'
 
-# Re-raise errors caught by the controller.
-#class OldRelationController; def rescue_action(e) raise e end; end
-
-class OldRelationControllerTest < Test::Unit::TestCase
+class OldRelationControllerTest < ActionController::TestCase
   api_fixtures
 
-  def setup
-    @controller = OldRelationController.new
-    @request    = ActionController::TestRequest.new
-    @response   = ActionController::TestResponse.new
-  end
-
   # -------------------------------------
   # Test reading old relations.
   # -------------------------------------
-
   def test_history
     # check that a visible relations is returned properly
     get :history, :id => relations(:visible_relation).id
index 374ea7dc2de42bbba429d9c2946d24b0d601ccf5..31da1d2c784bb18d6598cbc52a486b77760343d7 100644 (file)
@@ -1,31 +1,89 @@
 require File.dirname(__FILE__) + '/../test_helper'
 require 'old_way_controller'
 
-# Re-raise errors caught by the controller.
-class OldWayController; def rescue_action(e) raise e end; end
-
-class OldWayControllerTest < Test::Unit::TestCase
+class OldWayControllerTest < ActionController::TestCase
   api_fixtures
 
-  def setup
-    @controller = OldWayController.new
-    @request    = ActionController::TestRequest.new
-    @response   = ActionController::TestResponse.new
-  end
-
   # -------------------------------------
   # Test reading old ways.
   # -------------------------------------
 
-  def test_history
+  def test_history_visible
     # check that a visible way is returned properly
     get :history, :id => ways(:visible_way).id
     assert_response :success
-
+  end
+  
+  def test_history_invisible
+    # check that an invisible way's history is returned properly
+    get :history, :id => ways(:invisible_way).id
+    assert_response :success
+  end
+  
+  def test_history_invalid
     # check chat a non-existent way is not returned
     get :history, :id => 0
     assert_response :not_found
+  end
+  
+  ##
+  # check that we can retrieve versions of a way
+  def test_version
+    check_current_version(current_ways(:visible_way).id)
+    check_current_version(current_ways(:used_way).id)
+    check_current_version(current_ways(:way_with_versions).id)
+  end
+
+  ##
+  # check that returned history is the same as getting all 
+  # versions of a way from the api.
+  def test_history_equals_versions
+    check_history_equals_versions(current_ways(:visible_way).id)
+    check_history_equals_versions(current_ways(:used_way).id)
+    check_history_equals_versions(current_ways(:way_with_versions).id)
+  end
+
+  ##
+  # check that the current version of a way is equivalent to the
+  # version which we're getting from the versions call.
+  def check_current_version(way_id)
+    # get the current version
+    current_way = with_controller(WayController.new) do
+      get :read, :id => way_id
+      assert_response :success, "can't get current way #{way_id}"
+      Way.from_xml(@response.body)
+    end
+    assert_not_nil current_way, "getting way #{way_id} returned nil"
+
+    # get the "old" version of the way from the version method
+    get :version, :id => way_id, :version => current_way.version
+    assert_response :success, "can't get old way #{way_id}, v#{current_way.version}"
+    old_way = Way.from_xml(@response.body)
+
+    # check that the ways are identical
+    assert_ways_are_equal current_way, old_way
+  end
+
+  ##
+  # look at all the versions of the way in the history and get each version from
+  # the versions call. check that they're the same.
+  def check_history_equals_versions(way_id)
+    get :history, :id => way_id
+    assert_response :success, "can't get way #{way_id} from API"
+    history_doc = XML::Parser.string(@response.body).parse
+    assert_not_nil history_doc, "parsing way #{way_id} history failed"
+
+    history_doc.find("//osm/way").each do |way_doc|
+      history_way = Way.from_xml_node(way_doc)
+      assert_not_nil history_way, "parsing way #{way_id} version failed"
 
+      get :version, :id => way_id, :version => history_way.version
+      assert_response :success, "couldn't get way #{way_id}, v#{history_way.version}"
+      version_way = Way.from_xml(@response.body)
+      assert_not_nil version_way, "failed to parse #{way_id}, v#{history_way.version}"
+      
+      assert_ways_are_equal history_way, version_way
+    end
   end
 
 end
index 202a015a87f737459b13ec30a4055e9dc5c67a37..4ace316a4e96ab479abf152b9a669d1e4fa13d5f 100644 (file)
@@ -1,27 +1,15 @@
 require File.dirname(__FILE__) + '/../test_helper'
 require 'relation_controller'
 
-# Re-raise errors caught by the controller.
-class RelationController; def rescue_action(e) raise e end; end
-
-class RelationControllerTest < Test::Unit::TestCase
+class RelationControllerTest < ActionController::TestCase
   api_fixtures
-  fixtures :relations, :current_relations, :relation_members, :current_relation_members, :relation_tags, :current_relation_tags
-  set_fixture_class :current_relations => :Relation
-  set_fixture_class :relations => :OldRelation
-
-  def setup
-    @controller = RelationController.new
-    @request    = ActionController::TestRequest.new
-    @response   = ActionController::TestResponse.new
-  end
 
   def basic_authorization(user, pass)
     @request.env["HTTP_AUTHORIZATION"] = "Basic %s" % Base64.encode64("#{user}:#{pass}")
   end
 
   def content(c)
-    @request.env["RAW_POST_DATA"] = c
+    @request.env["RAW_POST_DATA"] = c.to_s
   end
 
   # -------------------------------------
@@ -40,31 +28,49 @@ class RelationControllerTest < Test::Unit::TestCase
     # check chat a non-existent relation is not returned
     get :read, :id => 0
     assert_response :not_found
+  end
 
-    # check the "relations for node" mode
-    get :relations_for_node, :id => current_nodes(:node_used_by_relationship).id
-    assert_response :success
-    # FIXME check whether this contains the stuff we want!
-    if $VERBOSE
-        print @response.body
-    end
+  ##
+  # check that all relations containing a particular node, and no extra
+  # relations, are returned from the relations_for_node call.
+  def test_relations_for_node
+    check_relations_for_element(:relations_for_node, "node", 
+                                current_nodes(:node_used_by_relationship).id,
+                                [ :visible_relation, :used_relation ])
+  end
 
-    # check the "relations for way" mode
-    get :relations_for_way, :id => current_ways(:used_way).id
-    assert_response :success
-    # FIXME check whether this contains the stuff we want!
-    if $VERBOSE
-        print @response.body
-    end
+  def test_relations_for_way
+    check_relations_for_element(:relations_for_way, "way",
+                                current_ways(:used_way).id,
+                                [ :visible_relation ])
+  end
 
+  def test_relations_for_relation
+    check_relations_for_element(:relations_for_relation, "relation",
+                                current_relations(:used_relation).id,
+                                [ :visible_relation ])
+  end
+
+  def check_relations_for_element(method, type, id, expected_relations)
     # check the "relations for relation" mode
-    get :relations_for_relation, :id => current_relations(:used_relation).id
+    get method, :id => id
     assert_response :success
-    # FIXME check whether this contains the stuff we want!
-    if $VERBOSE
-        print @response.body
+
+    # count one osm element
+    assert_select "osm[version=#{API_VERSION}][generator=\"OpenStreetMap server\"]", 1
+
+    # we should have only the expected number of relations
+    assert_select "osm>relation", expected_relations.size
+
+    # and each of them should contain the node we originally searched for
+    expected_relations.each do |r|
+      relation_id = current_relations(r).id
+      assert_select "osm>relation#?", relation_id
+      assert_select "osm>relation#?>member[type=\"#{type}\"][ref=#{id}]", relation_id
     end
+  end
 
+  def test_full
     # check the "full" mode
     get :full, :id => current_relations(:visible_relation).id
     assert_response :success
@@ -80,9 +86,12 @@ class RelationControllerTest < Test::Unit::TestCase
 
   def test_create
     basic_authorization "test@openstreetmap.org", "test"
+    
+    # put the relation in a dummy fixture changset
+    changeset_id = changesets(:normal_user_first_change).id
 
     # create an relation without members
-    content "<osm><relation><tag k='test' v='yes' /></relation></osm>"
+    content "<osm><relation changeset='#{changeset_id}'><tag k='test' v='yes' /></relation></osm>"
     put :create
     # hope for success
     assert_response :success, 
@@ -97,7 +106,9 @@ class RelationControllerTest < Test::Unit::TestCase
         "saved relation contains members but should not"
     assert_equal checkrelation.tags.length, 1, 
         "saved relation does not contain exactly one tag"
-    assert_equal users(:normal_user).id, checkrelation.user_id, 
+    assert_equal changeset_id, checkrelation.changeset.id,
+        "saved relation does not belong in the changeset it was assigned to"
+    assert_equal users(:normal_user).id, checkrelation.changeset.user_id, 
         "saved relation does not belong to user that created it"
     assert_equal true, checkrelation.visible, 
         "saved relation is not visible"
@@ -106,10 +117,46 @@ class RelationControllerTest < Test::Unit::TestCase
     assert_response :success
 
 
+    ###
     # create an relation with a node as member
+    # This time try with a role attribute in the relation
+    nid = current_nodes(:used_node_1).id
+    content "<osm><relation changeset='#{changeset_id}'>" +
+      "<member  ref='#{nid}' type='node' role='some'/>" +
+      "<tag k='test' v='yes' /></relation></osm>"
+    put :create
+    # hope for success
+    assert_response :success, 
+        "relation upload did not return success status"
+    # read id of created relation and search for it
+    relationid = @response.body
+    checkrelation = Relation.find(relationid)
+    assert_not_nil checkrelation, 
+        "uploaded relation not found in data base after upload"
+    # compare values
+    assert_equal checkrelation.members.length, 1, 
+        "saved relation does not contain exactly one member"
+    assert_equal checkrelation.tags.length, 1, 
+        "saved relation does not contain exactly one tag"
+    assert_equal changeset_id, checkrelation.changeset.id,
+        "saved relation does not belong in the changeset it was assigned to"
+    assert_equal users(:normal_user).id, checkrelation.changeset.user_id, 
+        "saved relation does not belong to user that created it"
+    assert_equal true, checkrelation.visible, 
+        "saved relation is not visible"
+    # ok the relation is there but can we also retrieve it?
+    
+    get :read, :id => relationid
+    assert_response :success
+    
+    
+    ###
+    # create an relation with a node as member, this time test that we don't 
+    # need a role attribute to be included
     nid = current_nodes(:used_node_1).id
-    content "<osm><relation><member type='node' ref='#{nid}' role='some'/>" +
-        "<tag k='test' v='yes' /></relation></osm>"
+    content "<osm><relation changeset='#{changeset_id}'>" +
+      "<member  ref='#{nid}' type='node'/>"+
+      "<tag k='test' v='yes' /></relation></osm>"
     put :create
     # hope for success
     assert_response :success, 
@@ -124,7 +171,9 @@ class RelationControllerTest < Test::Unit::TestCase
         "saved relation does not contain exactly one member"
     assert_equal checkrelation.tags.length, 1, 
         "saved relation does not contain exactly one tag"
-    assert_equal users(:normal_user).id, checkrelation.user_id, 
+    assert_equal changeset_id, checkrelation.changeset.id,
+        "saved relation does not belong in the changeset it was assigned to"
+    assert_equal users(:normal_user).id, checkrelation.changeset.user_id, 
         "saved relation does not belong to user that created it"
     assert_equal true, checkrelation.visible, 
         "saved relation is not visible"
@@ -133,12 +182,14 @@ class RelationControllerTest < Test::Unit::TestCase
     get :read, :id => relationid
     assert_response :success
 
+    ###
     # create an relation with a way and a node as members
     nid = current_nodes(:used_node_1).id
     wid = current_ways(:used_way).id
-    content "<osm><relation><member type='node' ref='#{nid}' role='some'/>" +
-        "<member type='way' ref='#{wid}' role='other'/>" +
-        "<tag k='test' v='yes' /></relation></osm>"
+    content "<osm><relation changeset='#{changeset_id}'>" +
+      "<member type='node' ref='#{nid}' role='some'/>" +
+      "<member type='way' ref='#{wid}' role='other'/>" +
+      "<tag k='test' v='yes' /></relation></osm>"
     put :create
     # hope for success
     assert_response :success, 
@@ -153,7 +204,9 @@ class RelationControllerTest < Test::Unit::TestCase
         "saved relation does not have exactly two members"
     assert_equal checkrelation.tags.length, 1, 
         "saved relation does not contain exactly one tag"
-    assert_equal users(:normal_user).id, checkrelation.user_id, 
+    assert_equal changeset_id, checkrelation.changeset.id,
+        "saved relation does not belong in the changeset it was assigned to"
+    assert_equal users(:normal_user).id, checkrelation.changeset.user_id, 
         "saved relation does not belong to user that created it"
     assert_equal true, checkrelation.visible, 
         "saved relation is not visible"
@@ -170,21 +223,45 @@ class RelationControllerTest < Test::Unit::TestCase
   def test_create_invalid
     basic_authorization "test@openstreetmap.org", "test"
 
+    # put the relation in a dummy fixture changset
+    changeset_id = changesets(:normal_user_first_change).id
+
     # create a relation with non-existing node as member
-    content "<osm><relation><member type='node' ref='0'/><tag k='test' v='yes' /></relation></osm>"
+    content "<osm><relation changeset='#{changeset_id}'>" +
+      "<member type='node' ref='0'/><tag k='test' v='yes' />" +
+      "</relation></osm>"
     put :create
     # expect failure
     assert_response :precondition_failed, 
         "relation upload with invalid node did not return 'precondition failed'"
   end
 
+  # -------------------------------------
+  # Test creating a relation, with some invalid XML
+  # -------------------------------------
+  def test_create_invalid_xml
+    basic_authorization "test@openstreetmap.org", "test"
+    
+    # put the relation in a dummy fixture changeset that works
+    changeset_id = changesets(:normal_user_first_change).id
+    
+    # create some xml that should return an error
+    content "<osm><relation changeset='#{changeset_id}'>" +
+    "<member type='type' ref='#{current_nodes(:used_node_1).id}' role=''/>" +
+    "<tag k='tester' v='yep'/></relation></osm>"
+    put :create
+    # expect failure
+    assert_response :bad_request
+    assert_match(/Cannot parse valid relation from xml string/, @response.body)
+    assert_match(/The type is not allowed only, /, @response.body)
+  end
+  
+  
   # -------------------------------------
   # Test deleting relations.
   # -------------------------------------
   
   def test_delete
-  return true
-
     # first try to delete relation without auth
     delete :delete, :id => current_relations(:visible_relation).id
     assert_response :unauthorized
@@ -192,17 +269,279 @@ class RelationControllerTest < Test::Unit::TestCase
     # now set auth
     basic_authorization("test@openstreetmap.org", "test");  
 
-    # this should work
+    # this shouldn't work, as we should need the payload...
+    delete :delete, :id => current_relations(:visible_relation).id
+    assert_response :bad_request
+
+    # try to delete without specifying a changeset
+    content "<osm><relation id='#{current_relations(:visible_relation).id}'/></osm>"
+    delete :delete, :id => current_relations(:visible_relation).id
+    assert_response :bad_request
+    assert_match(/You are missing the required changeset in the relation/, @response.body)
+
+    # try to delete with an invalid (closed) changeset
+    content update_changeset(current_relations(:visible_relation).to_xml,
+                             changesets(:normal_user_closed_change).id)
+    delete :delete, :id => current_relations(:visible_relation).id
+    assert_response :conflict
+
+    # try to delete with an invalid (non-existent) changeset
+    content update_changeset(current_relations(:visible_relation).to_xml,0)
+    delete :delete, :id => current_relations(:visible_relation).id
+    assert_response :conflict
+
+    # this won't work because the relation is in-use by another relation
+    content(relations(:used_relation).to_xml)
+    delete :delete, :id => current_relations(:used_relation).id
+    assert_response :precondition_failed, 
+       "shouldn't be able to delete a relation used in a relation (#{@response.body})"
+
+    # this should work when we provide the appropriate payload...
+    content(relations(:visible_relation).to_xml)
     delete :delete, :id => current_relations(:visible_relation).id
     assert_response :success
 
+    # valid delete should return the new version number, which should
+    # be greater than the old version number
+    assert @response.body.to_i > current_relations(:visible_relation).version,
+       "delete request should return a new version number for relation"
+
     # this won't work since the relation is already deleted
+    content(relations(:invisible_relation).to_xml)
     delete :delete, :id => current_relations(:invisible_relation).id
     assert_response :gone
 
+    # this works now because the relation which was using this one 
+    # has been deleted.
+    content(relations(:used_relation).to_xml)
+    delete :delete, :id => current_relations(:used_relation).id
+    assert_response :success, 
+       "should be able to delete a relation used in an old relation (#{@response.body})"
+
     # this won't work since the relation never existed
     delete :delete, :id => 0
     assert_response :not_found
   end
 
+  ##
+  # when a relation's tag is modified then it should put the bounding
+  # box of all its members into the changeset.
+  def test_tag_modify_bounding_box
+    # in current fixtures, relation 5 contains nodes 3 and 5 (node 3
+    # indirectly via way 3), so the bbox should be [3,3,5,5].
+    check_changeset_modify([3,3,5,5]) do |changeset_id|
+      # add a tag to an existing relation
+      relation_xml = current_relations(:visible_relation).to_xml
+      relation_element = relation_xml.find("//osm/relation").first
+      new_tag = XML::Node.new("tag")
+      new_tag['k'] = "some_new_tag"
+      new_tag['v'] = "some_new_value"
+      relation_element << new_tag
+      
+      # update changeset ID to point to new changeset
+      update_changeset(relation_xml, changeset_id)
+      
+      # upload the change
+      content relation_xml
+      put :update, :id => current_relations(:visible_relation).id
+      assert_response :success, "can't update relation for tag/bbox test"
+    end
+  end
+
+  ##
+  # add a member to a relation and check the bounding box is only that
+  # element.
+  def test_add_member_bounding_box
+    check_changeset_modify([4,4,4,4]) do |changeset_id|
+      # add node 4 (4,4) to an existing relation
+      relation_xml = current_relations(:visible_relation).to_xml
+      relation_element = relation_xml.find("//osm/relation").first
+      new_member = XML::Node.new("member")
+      new_member['ref'] = current_nodes(:used_node_2).id.to_s
+      new_member['type'] = "node"
+      new_member['role'] = "some_role"
+      relation_element << new_member
+      
+      # update changeset ID to point to new changeset
+      update_changeset(relation_xml, changeset_id)
+      
+      # upload the change
+      content relation_xml
+      put :update, :id => current_relations(:visible_relation).id
+      assert_response :success, "can't update relation for add node/bbox test"
+    end
+  end
+  
+  ##
+  # remove a member from a relation and check the bounding box is 
+  # only that element.
+  def test_remove_member_bounding_box
+    check_changeset_modify([5,5,5,5]) do |changeset_id|
+      # remove node 5 (5,5) from an existing relation
+      relation_xml = current_relations(:visible_relation).to_xml
+      relation_xml.
+        find("//osm/relation/member[@type='node'][@ref='5']").
+        first.remove!
+      
+      # update changeset ID to point to new changeset
+      update_changeset(relation_xml, changeset_id)
+      
+      # upload the change
+      content relation_xml
+      put :update, :id => current_relations(:visible_relation).id
+      assert_response :success, "can't update relation for remove node/bbox test"
+    end
+  end
+  
+  ##
+  # check that relations are ordered
+  def test_relation_member_ordering
+    basic_authorization("test@openstreetmap.org", "test");  
+
+    doc_str = <<OSM
+<osm>
+ <relation changeset='1'>
+  <member ref='1' type='node' role='first'/>
+  <member ref='3' type='node' role='second'/>
+  <member ref='1' type='way' role='third'/>
+  <member ref='3' type='way' role='fourth'/>
+ </relation>
+</osm>
+OSM
+    doc = XML::Parser.string(doc_str).parse
+
+    content doc
+    put :create
+    assert_response :success, "can't create a relation: #{@response.body}"
+    relation_id = @response.body.to_i
+
+    # get it back and check the ordering
+    get :read, :id => relation_id
+    assert_response :success, "can't read back the relation: #{@response.body}"
+    check_ordering(doc, @response.body)
+
+    # insert a member at the front
+    new_member = XML::Node.new "member"
+    new_member['ref'] = 5.to_s
+    new_member['type'] = 'node'
+    new_member['role'] = 'new first'
+    doc.find("//osm/relation").first.child.prev = new_member
+    # update the version, should be 1?
+    doc.find("//osm/relation").first['id'] = relation_id.to_s
+    doc.find("//osm/relation").first['version'] = 1.to_s
+
+    # upload the next version of the relation
+    content doc
+    put :update, :id => relation_id
+    assert_response :success, "can't update relation: #{@response.body}"
+    new_version = @response.body.to_i
+
+    # get it back again and check the ordering again
+    get :read, :id => relation_id
+    assert_response :success, "can't read back the relation: #{@response.body}"
+    check_ordering(doc, @response.body)
+  end
+
+  ## 
+  # check that relations can contain duplicate members
+  def test_relation_member_duplicates
+    basic_authorization("test@openstreetmap.org", "test");  
+
+    doc_str = <<OSM
+<osm>
+ <relation changeset='1'>
+  <member ref='1' type='node' role='forward'/>
+  <member ref='3' type='node' role='forward'/>
+  <member ref='1' type='node' role='forward'/>
+  <member ref='3' type='node' role='forward'/>
+ </relation>
+</osm>
+OSM
+    doc = XML::Parser.string(doc_str).parse
+
+    content doc
+    put :create
+    assert_response :success, "can't create a relation: #{@response.body}"
+    relation_id = @response.body.to_i
+
+    # get it back and check the ordering
+    get :read, :id => relation_id
+    assert_response :success, "can't read back the relation: #{@response.body}"
+    check_ordering(doc, @response.body)
+  end
+
+  # ============================================================
+  # utility functions
+  # ============================================================
+
+  ##
+  # checks that the XML document and the string arguments have
+  # members in the same order.
+  def check_ordering(doc, xml)
+    new_doc = XML::Parser.string(xml).parse
+
+    doc_members = doc.find("//osm/relation/member").collect do |m|
+      [m['ref'].to_i, m['type'].to_sym, m['role']]
+    end
+
+    new_members = new_doc.find("//osm/relation/member").collect do |m|
+      [m['ref'].to_i, m['type'].to_sym, m['role']]
+    end
+
+    doc_members.zip(new_members).each do |d, n|
+      assert_equal d, n, "members are not equal - ordering is wrong? (#{doc}, #{xml})"
+    end
+  end
+
+  ##
+  # create a changeset and yield to the caller to set it up, then assert
+  # that the changeset bounding box is +bbox+.
+  def check_changeset_modify(bbox)
+    basic_authorization("test@openstreetmap.org", "test");  
+
+    # create a new changeset for this operation, so we are assured
+    # that the bounding box will be newly-generated.
+    changeset_id = with_controller(ChangesetController.new) do
+      content "<osm><changeset/></osm>"
+      put :create
+      assert_response :success, "couldn't create changeset for modify test"
+      @response.body.to_i
+    end
+
+    # go back to the block to do the actual modifies
+    yield changeset_id
+
+    # now download the changeset to check its bounding box
+    with_controller(ChangesetController.new) do
+      get :read, :id => changeset_id
+      assert_response :success, "can't re-read changeset for modify test"
+      assert_select "osm>changeset", 1
+      assert_select "osm>changeset[id=#{changeset_id}]", 1
+      assert_select "osm>changeset[min_lon=#{bbox[0].to_f}]", 1
+      assert_select "osm>changeset[min_lat=#{bbox[1].to_f}]", 1
+      assert_select "osm>changeset[max_lon=#{bbox[2].to_f}]", 1
+      assert_select "osm>changeset[max_lat=#{bbox[3].to_f}]", 1
+    end
+  end
+
+  ##
+  # update the changeset_id of a node element
+  def update_changeset(xml, changeset_id)
+    xml_attr_rewrite(xml, 'changeset', changeset_id)
+  end
+
+  ##
+  # update an attribute in the node element
+  def xml_attr_rewrite(xml, name, value)
+    xml.find("//osm/relation").first[name] = value.to_s
+    return xml
+  end
+
+  ##
+  # parse some xml
+  def xml_parse(xml)
+    parser = XML::Parser.new
+    parser.string = xml
+    parser.parse
+  end
 end
diff --git a/test/functional/search_controller_test.rb b/test/functional/search_controller_test.rb
new file mode 100644 (file)
index 0000000..a213253
--- /dev/null
@@ -0,0 +1,8 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class SearchControllerTest < ActionController::TestCase
+  # Replace this with your real tests.
+  def test_truth
+    assert true
+  end
+end
diff --git a/test/functional/site_controller_test.rb b/test/functional/site_controller_test.rb
new file mode 100644 (file)
index 0000000..39a6464
--- /dev/null
@@ -0,0 +1,8 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class SiteControllerTest < ActionController::TestCase
+  # Replace this with your real tests.
+  def test_truth
+    assert true
+  end
+end
diff --git a/test/functional/swf_controller_test.rb b/test/functional/swf_controller_test.rb
new file mode 100644 (file)
index 0000000..862d3a8
--- /dev/null
@@ -0,0 +1,8 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class SwfControllerTest < ActionController::TestCase
+  # Replace this with your real tests.
+  def test_truth
+    assert true
+  end
+end
diff --git a/test/functional/trace_controller_test.rb b/test/functional/trace_controller_test.rb
new file mode 100644 (file)
index 0000000..6b46dbc
--- /dev/null
@@ -0,0 +1,8 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class TraceControllerTest < ActionController::TestCase
+  # Replace this with your real tests.
+  def test_truth
+    assert true
+  end
+end
diff --git a/test/functional/user_controller_test.rb b/test/functional/user_controller_test.rb
new file mode 100644 (file)
index 0000000..2278aed
--- /dev/null
@@ -0,0 +1,8 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class UserControllerTest < ActionController::TestCase
+  # Replace this with your real tests.
+  def test_truth
+    assert true
+  end
+end
index 7ff64b30ebb7a5150674b80ea335409bdd06e23d..714f45c5ddb82456a9eb8fc71bc24cb1d313ecf0 100644 (file)
@@ -1,8 +1,25 @@
 require File.dirname(__FILE__) + '/../test_helper'
 
 class UserPreferenceControllerTest < ActionController::TestCase
-  # Replace this with your real tests.
-  def test_truth
-    assert true
+  fixtures :users, :user_preferences
+  
+  def test_read
+    # first try without auth
+    get :read
+    assert_response :unauthorized, "should be authenticated"
+    
+    # now set the auth
+    basic_authorization("test@openstreetmap.org", "test")
+    
+    get :read
+    assert_response :success
+    assert_select "osm:root" do
+      assert_select "preferences", :count => 1 do
+        assert_select "preference", :count => 2
+        assert_select "preference[k=\"#{user_preferences(:a).k}\"][v=\"#{user_preferences(:a).v}\"]", :count => 1
+        assert_select "preference[k=\"#{user_preferences(:two).k}\"][v=\"#{user_preferences(:two).v}\"]", :count => 1
+      end
+    end
   end
+
 end
index 933dfb542edc9be778923735378a9052439b0de0..40ac0bd71c2388447c7fae9d4b44d58d92f18d85 100644 (file)
@@ -1,24 +1,15 @@
 require File.dirname(__FILE__) + '/../test_helper'
 require 'way_controller'
 
-# Re-raise errors caught by the controller.
-class WayController; def rescue_action(e) raise e end; end
-
-class WayControllerTest < Test::Unit::TestCase
+class WayControllerTest < ActionController::TestCase
   api_fixtures
 
-  def setup
-    @controller = WayController.new
-    @request    = ActionController::TestRequest.new
-    @response   = ActionController::TestResponse.new
-  end
-
   def basic_authorization(user, pass)
     @request.env["HTTP_AUTHORIZATION"] = "Basic %s" % Base64.encode64("#{user}:#{pass}")
   end
 
   def content(c)
-    @request.env["RAW_POST_DATA"] = c
+    @request.env["RAW_POST_DATA"] = c.to_s
   end
 
   # -------------------------------------
@@ -37,18 +28,35 @@ class WayControllerTest < Test::Unit::TestCase
     # check chat a non-existent way is not returned
     get :read, :id => 0
     assert_response :not_found
+  end
 
-    # check the "ways for node" mode
-    get :ways_for_node, :id => current_nodes(:used_node_1).id
-    assert_response :success
-    # FIXME check whether this contains the stuff we want!
-    print @response.body
+  ##
+  # check the "full" mode
+  def test_full
+    Way.find(:all).each do |way|
+      get :full, :id => way.id
 
-    # check the "full" mode
-    get :full, :id => current_ways(:visible_way).id
-    assert_response :success
-    # FIXME check whether this contains the stuff we want!
-    print @response.body
+      # full call should say "gone" for non-visible ways...
+      unless way.visible
+        assert_response :gone
+        next
+      end
+
+      # otherwise it should say success
+      assert_response :success
+      
+      # Check the way is correctly returned
+      assert_select "osm way[id=#{way.id}][version=#{way.version}][visible=#{way.visible}]", 1
+      
+      # check that each node in the way appears once in the output as a 
+      # reference and as the node element. note the slightly dodgy assumption
+      # that nodes appear only once. this is currently the case with the
+      # fixtures, but it doesn't have to be.
+      way.nodes.each do |n|
+        assert_select "osm way nd[ref=#{n.id}]", 1
+        assert_select "osm node[id=#{n.id}][version=#{n.version}][lat=#{n.lat}][lon=#{n.lon}]", 1
+      end
+    end
   end
 
   # -------------------------------------
@@ -60,8 +68,13 @@ class WayControllerTest < Test::Unit::TestCase
     nid2 = current_nodes(:used_node_2).id
     basic_authorization "test@openstreetmap.org", "test"
 
+    # use the first user's open changeset
+    changeset_id = changesets(:normal_user_first_change).id
+    
     # create a way with pre-existing nodes
-    content "<osm><way><nd ref='#{nid1}'/><nd ref='#{nid2}'/><tag k='test' v='yes' /></way></osm>"
+    content "<osm><way changeset='#{changeset_id}'>" +
+      "<nd ref='#{nid1}'/><nd ref='#{nid2}'/>" + 
+      "<tag k='test' v='yes' /></way></osm>"
     put :create
     # hope for success
     assert_response :success, 
@@ -78,7 +91,9 @@ class WayControllerTest < Test::Unit::TestCase
         "saved way does not contain the right node on pos 0"
     assert_equal checkway.nds[1], nid2, 
         "saved way does not contain the right node on pos 1"
-    assert_equal users(:normal_user).id, checkway.user_id, 
+    assert_equal checkway.changeset_id, changeset_id,
+        "saved way does not belong to the correct changeset"
+    assert_equal users(:normal_user).id, checkway.changeset.user_id, 
         "saved way does not belong to user that created it"
     assert_equal true, checkway.visible, 
         "saved way is not visible"
@@ -91,19 +106,34 @@ class WayControllerTest < Test::Unit::TestCase
   def test_create_invalid
     basic_authorization "test@openstreetmap.org", "test"
 
+    # use the first user's open changeset
+    open_changeset_id = changesets(:normal_user_first_change).id
+    closed_changeset_id = changesets(:normal_user_closed_change).id
+    nid1 = current_nodes(:used_node_1).id
+
     # create a way with non-existing node
-    content "<osm><way><nd ref='0'/><tag k='test' v='yes' /></way></osm>"
+    content "<osm><way changeset='#{open_changeset_id}'>" + 
+      "<nd ref='0'/><tag k='test' v='yes' /></way></osm>"
     put :create
     # expect failure
     assert_response :precondition_failed, 
         "way upload with invalid node did not return 'precondition failed'"
 
     # create a way with no nodes
-    content "<osm><way><tag k='test' v='yes' /></way></osm>"
+    content "<osm><way changeset='#{open_changeset_id}'>" +
+      "<tag k='test' v='yes' /></way></osm>"
     put :create
     # expect failure
     assert_response :precondition_failed, 
         "way upload with no node did not return 'precondition failed'"
+
+    # create a way inside a closed changeset
+    content "<osm><way changeset='#{closed_changeset_id}'>" +
+      "<nd ref='#{nid1}'/></way></osm>"
+    put :create
+    # expect failure
+    assert_response :conflict, 
+        "way upload to closed changeset did not return 'conflict'"    
   end
 
   # -------------------------------------
@@ -111,7 +141,6 @@ class WayControllerTest < Test::Unit::TestCase
   # -------------------------------------
   
   def test_delete
-
     # first try to delete way without auth
     delete :delete, :id => current_ways(:visible_way).id
     assert_response :unauthorized
@@ -119,17 +148,164 @@ class WayControllerTest < Test::Unit::TestCase
     # now set auth
     basic_authorization("test@openstreetmap.org", "test");  
 
-    # this should work
+    # this shouldn't work as with the 0.6 api we need pay load to delete
+    delete :delete, :id => current_ways(:visible_way).id
+    assert_response :bad_request
+    
+    # Now try without having a changeset
+    content "<osm><way id='#{current_ways(:visible_way).id}'></osm>"
+    delete :delete, :id => current_ways(:visible_way).id
+    assert_response :bad_request
+    
+    # try to delete with an invalid (closed) changeset
+    content update_changeset(current_ways(:visible_way).to_xml,
+                             changesets(:normal_user_closed_change).id)
+    delete :delete, :id => current_ways(:visible_way).id
+    assert_response :conflict
+
+    # try to delete with an invalid (non-existent) changeset
+    content update_changeset(current_ways(:visible_way).to_xml,0)
+    delete :delete, :id => current_ways(:visible_way).id
+    assert_response :conflict
+
+    # Now try with a valid changeset
+    content current_ways(:visible_way).to_xml
     delete :delete, :id => current_ways(:visible_way).id
     assert_response :success
 
+    # check the returned value - should be the new version number
+    # valid delete should return the new version number, which should
+    # be greater than the old version number
+    assert @response.body.to_i > current_ways(:visible_way).version,
+       "delete request should return a new version number for way"
+
     # this won't work since the way is already deleted
+    content current_ways(:invisible_way).to_xml
     delete :delete, :id => current_ways(:invisible_way).id
     assert_response :gone
 
+    # this shouldn't work as the way is used in a relation
+    content current_ways(:used_way).to_xml
+    delete :delete, :id => current_ways(:used_way).id
+    assert_response :precondition_failed, 
+       "shouldn't be able to delete a way used in a relation (#{@response.body})"
+
     # this won't work since the way never existed
     delete :delete, :id => 0
     assert_response :not_found
   end
 
+  # ------------------------------------------------------------
+  # test tags handling
+  # ------------------------------------------------------------
+
+  ##
+  # Try adding a duplicate of an existing tag to a way
+  def test_add_duplicate_tags
+    # setup auth
+    basic_authorization(users(:normal_user).email, "test")
+
+    # add an identical tag to the way
+    tag_xml = XML::Node.new("tag")
+    tag_xml['k'] = current_way_tags(:t1).k
+    tag_xml['v'] = current_way_tags(:t1).v
+
+    # add the tag into the existing xml
+    way_xml = current_ways(:visible_way).to_xml
+    way_xml.find("//osm/way").first << tag_xml
+
+    # try and upload it
+    content way_xml
+    put :update, :id => current_ways(:visible_way).id
+    assert_response :bad_request, 
+       "adding a duplicate tag to a way should fail with 'bad request'"
+    assert_equal "Element way/#{current_ways(:visible_way).id} has duplicate tags with key #{current_way_tags(:t1).k}.", @response.body
+  end
+
+  ##
+  # Try adding a new duplicate tags to a way
+  def test_new_duplicate_tags
+    # setup auth
+    basic_authorization(users(:normal_user).email, "test")
+
+    # create duplicate tag
+    tag_xml = XML::Node.new("tag")
+    tag_xml['k'] = "i_am_a_duplicate"
+    tag_xml['v'] = "foobar"
+
+    # add the tag into the existing xml
+    way_xml = current_ways(:visible_way).to_xml
+
+    # add two copies of the tag
+    way_xml.find("//osm/way").first << tag_xml.copy(true) << tag_xml
+
+    # try and upload it
+    content way_xml
+    put :update, :id => current_ways(:visible_way).id
+    assert_response :bad_request, 
+       "adding new duplicate tags to a way should fail with 'bad request'"
+    assert_equal "Element way/#{current_ways(:visible_way).id} has duplicate tags with key i_am_a_duplicate.", @response.body
+  end
+
+  ##
+  # Try adding a new duplicate tags to a way.
+  # But be a bit subtle - use unicode decoding ambiguities to use different
+  # binary strings which have the same decoding.
+  def test_invalid_duplicate_tags
+    # setup auth
+    basic_authorization(users(:normal_user).email, "test")
+
+    # add the tag into the existing xml
+    way_str = "<osm><way changeset='1'>"
+    way_str << "<tag k='addr:housenumber' v='1'/>"
+    way_str << "<tag k='addr:housenumber' v='2'/>"
+    way_str << "</way></osm>";
+
+    # try and upload it
+    content way_str
+    put :create
+    assert_response :bad_request, 
+    "adding new duplicate tags to a way should fail with 'bad request'"
+    assert_equal "Element way/ has duplicate tags with key addr:housenumber.", @response.body
+  end
+
+  ##
+  # test that a call to ways_for_node returns all ways that contain the node
+  # and none that don't.
+  def test_ways_for_node
+    # in current fixtures ways 1 and 3 all use node 3. ways 2 and 4 
+    # *used* to use it but doesn't.
+    get :ways_for_node, :id => current_nodes(:used_node_1).id
+    assert_response :success
+    ways_xml = XML::Parser.string(@response.body).parse
+    assert_not_nil ways_xml, "failed to parse ways_for_node response"
+
+    # check that the set of IDs match expectations
+    expected_way_ids = [ current_ways(:visible_way).id,
+                         current_ways(:used_way).id
+                       ]
+    found_way_ids = ways_xml.find("//osm/way").collect { |w| w["id"].to_i }
+    assert_equal expected_way_ids, found_way_ids,
+      "expected ways for node #{current_nodes(:used_node_1).id} did not match found"
+    
+    # check the full ways to ensure we're not missing anything
+    expected_way_ids.each do |id|
+      way_xml = ways_xml.find("//osm/way[@id=#{id}]").first
+      assert_ways_are_equal(Way.find(id),
+                            Way.from_xml_node(way_xml))
+    end
+  end
+
+  ##
+  # update the changeset_id of a node element
+  def update_changeset(xml, changeset_id)
+    xml_attr_rewrite(xml, 'changeset', changeset_id)
+  end
+
+  ##
+  # update an attribute in the node element
+  def xml_attr_rewrite(xml, name, value)
+    xml.find("//osm/way").first[name] = value.to_s
+    return xml
+  end
 end
diff --git a/test/integration/user_diaries_test.rb b/test/integration/user_diaries_test.rb
new file mode 100644 (file)
index 0000000..2e7a010
--- /dev/null
@@ -0,0 +1,50 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class UserDiariesTest < ActionController::IntegrationTest
+  fixtures :users, :diary_entries
+
+  def test_showing_create_diary_entry
+    get_via_redirect '/user/test/diary/new'
+    # We should now be at the login page
+    assert_response :success
+    assert_template 'user/login'
+    # We can now login
+    post  '/login', {'user[email]' => "test@openstreetmap.org", 'user[password]' => "test", :referer => '/user/test/diary/new'}
+    assert_response :redirect
+    #print @response.body
+    # Check that there is some payload alerting the user to the redirect
+    # and allowing them to get to the page they are being directed to
+    assert_select "html:root" do
+      assert_select "body" do
+        assert_select "a[href='http://www.example.com/user/test/diary/new']"
+      end
+    end
+    # Required due to a bug in the rails testing framework
+    # http://markmail.org/message/wnslvi5xv5moqg7g
+    @html_document = nil
+    follow_redirect!
+    
+    assert_response :success
+    assert_template 'diary_entry/edit'
+    #print @response.body
+    #print @html_document.to_yaml
+
+    # We will make sure that the form exists here, full 
+    # assert testing of the full form should be done in the
+    # functional tests rather than this integration test
+    # There are some things that are specific to the integratio
+    # that need to be tested, which can't be tested in the functional tests
+    assert_select "html:root" do
+      assert_select "body" do
+        assert_select "div#content" do
+          assert_select "h1", "New diary entry" 
+          assert_select "form[action='/user/#{users(:normal_user).display_name}/diary/new']" do
+            assert_select "input[id=diary_entry_title]"
+          end
+        end
+      end
+    end
+    
+    
+  end
+end
index b1d7a8fcc280dec9ac39ddbbad7c90de4adf0c2c..88a6fbe4ac9ad139f549f5bb19c6d42bee7d083b 100644 (file)
@@ -1,6 +1,7 @@
 ENV["RAILS_ENV"] = "test"
 require File.expand_path(File.dirname(__FILE__) + "/../config/environment")
 require 'test_help'
+load 'composite_primary_keys/fixtures.rb'
 
 class Test::Unit::TestCase
   # Transactional fixtures accelerate your tests by wrapping each test method
@@ -26,31 +27,91 @@ class Test::Unit::TestCase
 
   # Load standard fixtures needed to test API methods
   def self.api_fixtures
-    fixtures :users
+    #print "setting up the api_fixtures"
+    fixtures :users, :changesets, :changeset_tags
 
     fixtures :current_nodes, :nodes
-    set_fixture_class :current_nodes => :Node
-    set_fixture_class :nodes => :OldNode
+    set_fixture_class :current_nodes => Node
+    set_fixture_class :nodes => OldNode
+
+    fixtures  :current_node_tags,:node_tags
+    set_fixture_class :current_node_tags => NodeTag
+    set_fixture_class :node_tags => OldNodeTag
 
     fixtures :current_ways, :current_way_nodes, :current_way_tags
-    set_fixture_class :current_ways => :Way
-    set_fixture_class :current_way_nodes => :WayNode
-    set_fixture_class :current_way_tags => :WayTag
+    set_fixture_class :current_ways => Way
+    set_fixture_class :current_way_nodes => WayNode
+    set_fixture_class :current_way_tags => WayTag
 
     fixtures :ways, :way_nodes, :way_tags
-    set_fixture_class :ways => :OldWay
-    set_fixture_class :way_nodes => :OldWayNode
-    set_fixture_class :way_tags => :OldWayTag
+    set_fixture_class :ways => OldWay
+    set_fixture_class :way_nodes => OldWayNode
+    set_fixture_class :way_tags => OldWayTag
 
     fixtures :current_relations, :current_relation_members, :current_relation_tags
-    set_fixture_class :current_relations => :Relation
-    set_fixture_class :current_relation_members => :RelationMember
-    set_fixture_class :current_relation_tags => :RelationTag
+    set_fixture_class :current_relations => Relation
+    set_fixture_class :current_relation_members => RelationMember
+    set_fixture_class :current_relation_tags => RelationTag
 
     fixtures :relations, :relation_members, :relation_tags
-    set_fixture_class :relations => :OldRelation
-    set_fixture_class :relation_members => :OldRelationMember
-    set_fixture_class :relation_tags => :OldRelationTag
+    set_fixture_class :relations => OldRelation
+    set_fixture_class :relation_members => OldRelationMember
+    set_fixture_class :relation_tags => OldRelationTag
+    
+    fixtures :gpx_files, :gps_points, :gpx_file_tags
+    set_fixture_class :gpx_files => Trace
+    set_fixture_class :gps_points => Tracepoint
+    set_fixture_class :gpx_file_tags => Tracetag
+  end
+
+  ##
+  # takes a block which is executed in the context of a different 
+  # ActionController instance. this is used so that code can call methods
+  # on the node controller whilst testing the old_node controller.
+  def with_controller(new_controller)
+    controller_save = @controller
+    begin
+      @controller = new_controller
+      yield
+    ensure
+      @controller = controller_save
+    end
+  end
+
+  ##
+  # for some reason assert_equal a, b fails when the ways are actually
+  # equal, so this method manually checks the fields...
+  def assert_ways_are_equal(a, b)
+    assert_not_nil a, "first way is not allowed to be nil"
+    assert_not_nil b, "second way #{a.id} is not allowed to be nil"
+    assert_equal a.id, b.id, "way IDs"
+    assert_equal a.changeset_id, b.changeset_id, "changeset ID on way #{a.id}"
+    assert_equal a.visible, b.visible, "visible on way #{a.id}, #{a.visible.inspect} != #{b.visible.inspect}"
+    assert_equal a.version, b.version, "version on way #{a.id}"
+    assert_equal a.tags, b.tags, "tags on way #{a.id}"
+    assert_equal a.nds, b.nds, "node references on way #{a.id}"
+  end
+
+  ##
+  # for some reason a==b is false, but there doesn't seem to be any 
+  # difference between the nodes, so i'm checking all the attributes 
+  # manually and blaming it on ActiveRecord
+  def assert_nodes_are_equal(a, b)
+    assert_equal a.id, b.id, "node IDs"
+    assert_equal a.latitude, b.latitude, "latitude on node #{a.id}"
+    assert_equal a.longitude, b.longitude, "longitude on node #{a.id}"
+    assert_equal a.changeset_id, b.changeset_id, "changeset ID on node #{a.id}"
+    assert_equal a.visible, b.visible, "visible on node #{a.id}"
+    assert_equal a.version, b.version, "version on node #{a.id}"
+    assert_equal a.tags, b.tags, "tags on node #{a.id}"
+  end
+
+  def basic_authorization(user, pass)
+    @request.env["HTTP_AUTHORIZATION"] = "Basic %s" % Base64.encode64("#{user}:#{pass}")
+  end
+
+  def content(c)
+    @request.env["RAW_POST_DATA"] = c.to_s
   end
 
   # Add more helper methods to be used by all tests here...
diff --git a/test/unit/changeset_tag_test.rb b/test/unit/changeset_tag_test.rb
new file mode 100644 (file)
index 0000000..e0201d5
--- /dev/null
@@ -0,0 +1,70 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class ChangesetTagTest < Test::Unit::TestCase
+  fixtures :changeset_tags
+
+  def test_changeset_tag_count
+    assert_equal 1, ChangesetTag.count
+  end
+  
+  def test_length_key_valid
+    key = "k"
+    (0..255).each do |i|
+      tag = ChangesetTag.new
+      tag.id = 1
+      tag.k = key*i
+      tag.v = "v"
+      assert_valid tag
+    end
+  end
+  
+  def test_length_value_valid
+    val = "v"
+    (0..255).each do |i|
+      tag = ChangesetTag.new
+      tag.id = 1
+      tag.k = "k"
+      tag.v = val*i
+      assert_valid tag
+    end
+  end
+  
+  def test_length_key_invalid
+    ["k"*256].each do |k|
+      tag = ChangesetTag.new
+      tag.id = 1
+      tag.k = k
+      tag.v = "v"
+      assert !tag.valid?, "Key #{k} should be too long"
+      assert tag.errors.invalid?(:k)
+    end
+  end
+  
+  def test_length_value_invalid
+    ["v"*256].each do |v|
+      tag = ChangesetTag.new
+      tag.id = 1
+      tag.k = "k"
+      tag.v = v
+      assert !tag.valid?, "Value #{v} should be too long"
+      assert tag.errors.invalid?(:v)
+    end
+  end
+  
+  def test_empty_tag_invalid
+    tag = ChangesetTag.new
+    assert !tag.valid?, "Empty tag should be invalid"
+    assert tag.errors.invalid?(:id)
+  end
+  
+  def test_uniqueness
+    tag = ChangesetTag.new
+    tag.id = changeset_tags(:changeset_1_tag_1).id
+    tag.k = changeset_tags(:changeset_1_tag_1).k
+    tag.v = changeset_tags(:changeset_1_tag_1).v
+    assert tag.new_record?
+    assert !tag.valid?
+    assert_raise(ActiveRecord::RecordInvalid) {tag.save!}
+    assert tag.new_record?
+  end
+end
diff --git a/test/unit/changeset_test.rb b/test/unit/changeset_test.rb
new file mode 100644 (file)
index 0000000..ee9d092
--- /dev/null
@@ -0,0 +1,11 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class ChangesetTest < Test::Unit::TestCase
+  fixtures :changesets
+  
+  
+  def test_changeset_count
+    assert_equal 5, Changeset.count
+  end
+  
+end
diff --git a/test/unit/diary_comment_test.rb b/test/unit/diary_comment_test.rb
new file mode 100644 (file)
index 0000000..d7f30a6
--- /dev/null
@@ -0,0 +1,11 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class DiaryCommentTest < Test::Unit::TestCase
+  fixtures :diary_comments
+  
+  
+  def test_diary_comment_count
+    assert_equal 1, DiaryComment.count
+  end
+  
+end
diff --git a/test/unit/diary_entry_test.rb b/test/unit/diary_entry_test.rb
new file mode 100644 (file)
index 0000000..0e10f8a
--- /dev/null
@@ -0,0 +1,11 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class DiaryEntryTest < Test::Unit::TestCase
+  fixtures :diary_entries
+  
+  
+  def test_diary_entry_count
+    assert_equal 2, DiaryEntry.count
+  end
+  
+end
diff --git a/test/unit/friend_test.rb b/test/unit/friend_test.rb
new file mode 100644 (file)
index 0000000..fd8b503
--- /dev/null
@@ -0,0 +1,11 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class FriendTest < Test::Unit::TestCase
+  fixtures :friends
+  
+  
+  def test_friend_count
+    assert_equal 1, Friend.count
+  end
+  
+end
index 8804fe003b6c54f794acfa8f56aaec792d44b08b..3b83bf95aec0bca8d81661be765922954cd7101f 100644 (file)
@@ -1,10 +1,96 @@
 require File.dirname(__FILE__) + '/../test_helper'
 
 class MessageTest < Test::Unit::TestCase
-  fixtures :messages
+  fixtures :messages, :users
 
-  # Replace this with your real tests.
-  def test_truth
-    assert true
+  EURO = "\xe2\x82\xac" #euro symbol
+
+  # This needs to be updated when new fixtures are added
+  # or removed.
+  def test_check_message_count
+    assert_equal 2, Message.count
+  end
+
+  def test_check_empty_message_fails
+    message = Message.new
+    assert !message.valid?
+    assert message.errors.invalid?(:title)
+    assert message.errors.invalid?(:body)
+    assert message.errors.invalid?(:sent_on)
+    assert true, message.message_read
+  end
+  
+  def test_validating_msgs
+    message = messages(:one)
+    assert message.valid?
+    massage = messages(:two)
+    assert message.valid?
+  end
+  
+  def test_invalid_send_recipient
+    message = messages(:one)
+    message.sender = nil
+    message.recipient = nil
+    assert !message.valid?
+
+    assert_raise(ActiveRecord::RecordNotFound) { User.find(0) }
+    message.from_user_id = 0
+    message.to_user_id = 0
+    assert_raise(ActiveRecord::RecordInvalid) {message.save!}
+  end
+
+  def test_utf8_roundtrip
+    (1..255).each do |i|
+      assert_message_ok('c', i)
+      assert_message_ok(EURO, i)
+    end
+  end
+
+  def test_length_oversize
+    assert_raise(ActiveRecord::RecordInvalid) { make_message('c', 256).save! }
+    assert_raise(ActiveRecord::RecordInvalid) { make_message(EURO, 256).save! }
   end
+
+  def test_invalid_utf8
+    # See e.g http://en.wikipedia.org/wiki/UTF-8 for byte sequences
+    # FIXME - Invalid Unicode characters can still be encoded into "valid" utf-8 byte sequences - maybe check this too?
+    invalid_sequences = ["\xC0",         # always invalid utf8
+                         "\xC2\x4a",     # 2-byte multibyte identifier, followed by plain ASCII
+                         "\xC2\xC2",     # 2-byte multibyte identifier, followed by another one
+                         "\x4a\x82",     # plain ASCII, followed by multibyte continuation
+                         "\x82\x82",     # multibyte continuations without multibyte identifier
+                         "\xe1\x82\x4a", # three-byte identifier, contination and (incorrectly) plain ASCII
+                        ]
+    invalid_sequences.each do |char|
+      begin
+        # create a message and save to the database
+        msg = make_message(char, 1)
+        # if the save throws, thats fine and the test should pass, as we're
+        # only testing invalid sequences anyway.
+        msg.save! 
+
+        # get the saved message back and check that it is identical - i.e: 
+        # its OK to accept invalid UTF-8 as long as we return it unmodified.
+        db_msg = msg.class.find(msg.id)
+        assert_equal char, db_msg.title, "Database silently truncated message title"
+
+      rescue ActiveRecord::RecordInvalid
+        # because we only test invalid sequences it is OK to barf on them
+      end
+    end
+  end  
+
+  def make_message(char, count)
+    message = messages(:one)
+    message.title = char * count
+    return message
+  end
+
+  def assert_message_ok(char, count)
+    message = make_message(char, count)
+    assert message.save!
+    response = message.class.find(message.id) # stand by for some über-generalisation...
+    assert_equal char * count, response.title, "message with #{count} #{char} chars (i.e. #{char.length*count} bytes) fails"
+  end
+
 end
diff --git a/test/unit/node_tag_test.rb b/test/unit/node_tag_test.rb
new file mode 100644 (file)
index 0000000..2ff9f9f
--- /dev/null
@@ -0,0 +1,82 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class NodeTagTest < Test::Unit::TestCase
+  fixtures :current_node_tags, :current_nodes
+  set_fixture_class :current_nodes => Node
+  set_fixture_class :current_node_tags => NodeTag
+  
+  def test_tag_count
+    assert_equal 6, NodeTag.count
+    node_tag_count(:visible_node, 1)
+    node_tag_count(:invisible_node, 1)
+    node_tag_count(:used_node_1, 1)
+    node_tag_count(:used_node_2, 1)
+    node_tag_count(:node_with_versions, 2)
+  end
+  
+  def node_tag_count (node, count)
+    nod = current_nodes(node)
+    assert_equal count, nod.node_tags.count
+  end
+  
+  def test_length_key_valid
+    key = "k"
+    (0..255).each do |i|
+      tag = NodeTag.new
+      tag.id = current_node_tags(:t1).id
+      tag.k = key*i
+      tag.v = "v"
+      assert_valid tag
+    end
+  end
+  
+  def test_length_value_valid
+    val = "v"
+    (0..255).each do |i|
+      tag = NodeTag.new
+      tag.id = current_node_tags(:t1).id
+      tag.k = "k"
+      tag.v = val*i
+      assert_valid tag
+    end
+  end
+  
+  def test_length_key_invalid
+    ["k"*256].each do |i|
+      tag = NodeTag.new
+      tag.id = current_node_tags(:t1).id
+      tag.k = i
+      tag.v = "v"
+      assert !tag.valid?, "Key should be too long"
+      assert tag.errors.invalid?(:k)
+    end
+  end
+  
+  def test_length_value_invalid
+    ["k"*256].each do |i|
+      tag = NodeTag.new
+      tag.id = current_node_tags(:t1).id
+      tag.k = "k"
+      tag.v = i
+      assert !tag.valid?, "Value should be too long"
+      assert tag.errors.invalid?(:v)
+    end
+  end
+  
+  def test_empty_node_tag_invalid
+    tag = NodeTag.new
+    assert !tag.valid?, "Empty tag should be invalid"
+    assert tag.errors.invalid?(:id)
+  end
+  
+  def test_uniqueness
+    tag = NodeTag.new
+    tag.id = current_node_tags(:t1).id
+    tag.k = current_node_tags(:t1).k
+    tag.v = current_node_tags(:t1).v
+    assert tag.new_record?
+    assert !tag.valid?
+    assert_raise(ActiveRecord::RecordInvalid) {tag.save!}
+    assert tag.new_record?
+  end
+end
index 95321b5cf0cb6d2e8803c484de0069163faa7c41..13dea88dac5a7788fd6cbd688b1cd7d5c56115c1 100644 (file)
@@ -1,25 +1,95 @@
 require File.dirname(__FILE__) + '/../test_helper'
 
 class NodeTest < Test::Unit::TestCase
-  fixtures :current_nodes, :nodes, :users
-  set_fixture_class :current_nodes => :Node
-  set_fixture_class :nodes => :OldNode
-
+  fixtures :changesets, :current_nodes, :users, :current_node_tags, :nodes, :node_tags
+  set_fixture_class :current_nodes => Node
+  set_fixture_class :nodes => OldNode
+  set_fixture_class :node_tags => OldNodeTag
+  set_fixture_class :current_node_tags => NodeTag
+    
+  def test_node_too_far_north
+         invalid_node_test(:node_too_far_north)
+  end
+  
+  def test_node_north_limit
+    valid_node_test(:node_north_limit)
+  end
+  
+  def test_node_too_far_south
+    invalid_node_test(:node_too_far_south)
+  end
+  
+  def test_node_south_limit
+    valid_node_test(:node_south_limit)
+  end
+  
+  def test_node_too_far_west
+    invalid_node_test(:node_too_far_west)
+  end
+  
+  def test_node_west_limit
+    valid_node_test(:node_west_limit)
+  end
+  
+  def test_node_too_far_east
+    invalid_node_test(:node_too_far_east)
+  end
+  
+  def test_node_east_limit
+    valid_node_test(:node_east_limit)
+  end
+  
+  def test_totally_wrong
+    invalid_node_test(:node_totally_wrong)
+  end
+  
+  # This helper method will check to make sure that a node is within the world, and
+  # has the the same lat, lon and timestamp than what was put into the db by 
+  # the fixture
+  def valid_node_test(nod)
+    node = current_nodes(nod)
+    dbnode = Node.find(node.id)
+    assert_equal dbnode.lat, node.latitude.to_f/SCALE
+    assert_equal dbnode.lon, node.longitude.to_f/SCALE
+    assert_equal dbnode.changeset_id, node.changeset_id
+    assert_equal dbnode.timestamp, node.timestamp
+    assert_equal dbnode.version, node.version
+    assert_equal dbnode.visible, node.visible
+    #assert_equal node.tile, QuadTile.tile_for_point(node.lat, node.lon)
+    assert_valid node
+  end
+  
+  # This helper method will check to make sure that a node is outwith the world, 
+  # and has the same lat, lon and timesamp than what was put into the db by the
+  # fixture
+  def invalid_node_test(nod)
+    node = current_nodes(nod)
+    dbnode = Node.find(node.id)
+    assert_equal dbnode.lat, node.latitude.to_f/SCALE
+    assert_equal dbnode.lon, node.longitude.to_f/SCALE
+    assert_equal dbnode.changeset_id, node.changeset_id
+    assert_equal dbnode.timestamp, node.timestamp
+    assert_equal dbnode.version, node.version
+    assert_equal dbnode.visible, node.visible
+    #assert_equal node.tile, QuadTile.tile_for_point(node.lat, node.lon)
+    assert_equal false, dbnode.valid?
+  end
+  
+  # Check that you can create a node and store it
   def test_create
     node_template = Node.new(:latitude => 12.3456,
                              :longitude => 65.4321,
-                             :user_id => users(:normal_user).id,
-                             :visible => 1,
-                             :tags => "")
-    assert node_template.save_with_history!
+                             :changeset_id => changesets(:normal_user_first_change).id,
+                             :visible => 1, 
+                             :version => 1)
+    assert node_template.create_with_history(users(:normal_user))
 
     node = Node.find(node_template.id)
     assert_not_nil node
     assert_equal node_template.latitude, node.latitude
     assert_equal node_template.longitude, node.longitude
-    assert_equal node_template.user_id, node.user_id
+    assert_equal node_template.changeset_id, node.changeset_id
     assert_equal node_template.visible, node.visible
-    assert_equal node_template.tags, node.tags
     assert_equal node_template.timestamp.to_i, node.timestamp.to_i
 
     assert_equal OldNode.find(:all, :conditions => [ "id = ?", node_template.id ]).length, 1
@@ -27,14 +97,14 @@ class NodeTest < Test::Unit::TestCase
     assert_not_nil old_node
     assert_equal node_template.latitude, old_node.latitude
     assert_equal node_template.longitude, old_node.longitude
-    assert_equal node_template.user_id, old_node.user_id
+    assert_equal node_template.changeset_id, old_node.changeset_id
     assert_equal node_template.visible, old_node.visible
     assert_equal node_template.tags, old_node.tags
     assert_equal node_template.timestamp.to_i, old_node.timestamp.to_i
   end
 
   def test_update
-    node_template = Node.find(1)
+    node_template = Node.find(current_nodes(:visible_node).id)
     assert_not_nil node_template
 
     assert_equal OldNode.find(:all, :conditions => [ "id = ?", node_template.id ]).length, 1
@@ -43,16 +113,16 @@ class NodeTest < Test::Unit::TestCase
 
     node_template.latitude = 12.3456
     node_template.longitude = 65.4321
-    node_template.tags = "updated=yes"
-    assert node_template.save_with_history!
+    #node_template.tags = "updated=yes"
+    assert node_template.update_from(old_node_template, users(:normal_user))
 
     node = Node.find(node_template.id)
     assert_not_nil node
     assert_equal node_template.latitude, node.latitude
     assert_equal node_template.longitude, node.longitude
-    assert_equal node_template.user_id, node.user_id
+    assert_equal node_template.changeset_id, node.changeset_id
     assert_equal node_template.visible, node.visible
-    assert_equal node_template.tags, node.tags
+    #assert_equal node_template.tags, node.tags
     assert_equal node_template.timestamp.to_i, node.timestamp.to_i
 
     assert_equal OldNode.find(:all, :conditions => [ "id = ?", node_template.id ]).length, 2
@@ -61,30 +131,29 @@ class NodeTest < Test::Unit::TestCase
     assert_not_nil old_node
     assert_equal node_template.latitude, old_node.latitude
     assert_equal node_template.longitude, old_node.longitude
-    assert_equal node_template.user_id, old_node.user_id
+    assert_equal node_template.changeset_id, old_node.changeset_id
     assert_equal node_template.visible, old_node.visible
-    assert_equal node_template.tags, old_node.tags
+    #assert_equal node_template.tags, old_node.tags
     assert_equal node_template.timestamp.to_i, old_node.timestamp.to_i
   end
 
   def test_delete
-    node_template = Node.find(1)
+    node_template = Node.find(current_nodes(:visible_node))
     assert_not_nil node_template
 
     assert_equal OldNode.find(:all, :conditions => [ "id = ?", node_template.id ]).length, 1
     old_node_template = OldNode.find(:first, :conditions => [ "id = ?", node_template.id ])
     assert_not_nil old_node_template
 
-    node_template.visible = 0
-    assert node_template.save_with_history!
+    assert node_template.delete_with_history!(old_node_template, users(:normal_user))
 
     node = Node.find(node_template.id)
     assert_not_nil node
     assert_equal node_template.latitude, node.latitude
     assert_equal node_template.longitude, node.longitude
-    assert_equal node_template.user_id, node.user_id
+    assert_equal node_template.changeset_id, node.changeset_id
     assert_equal node_template.visible, node.visible
-    assert_equal node_template.tags, node.tags
+    #assert_equal node_template.tags, node.tags
     assert_equal node_template.timestamp.to_i, node.timestamp.to_i
 
     assert_equal OldNode.find(:all, :conditions => [ "id = ?", node_template.id ]).length, 2
@@ -93,9 +162,9 @@ class NodeTest < Test::Unit::TestCase
     assert_not_nil old_node
     assert_equal node_template.latitude, old_node.latitude
     assert_equal node_template.longitude, old_node.longitude
-    assert_equal node_template.user_id, old_node.user_id
+    assert_equal node_template.changeset_id, old_node.changeset_id
     assert_equal node_template.visible, old_node.visible
-    assert_equal node_template.tags, old_node.tags
+    #assert_equal node_template.tags, old_node.tags
     assert_equal node_template.timestamp.to_i, old_node.timestamp.to_i
   end
 end
diff --git a/test/unit/old_node_tag_test.rb b/test/unit/old_node_tag_test.rb
new file mode 100644 (file)
index 0000000..4971843
--- /dev/null
@@ -0,0 +1,78 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class OldNodeTest < Test::Unit::TestCase
+  set_fixture_class :nodes => OldNode
+  set_fixture_class :node_tags => OldNodeTag
+  fixtures  :users, :nodes, :node_tags
+  
+  def test_old_node_tag_count
+    assert_equal 8, OldNodeTag.count, "Unexpected number of fixtures loaded."
+  end
+  
+  def test_length_key_valid
+    key = "k"
+    (0..255).each do |i|
+      tag = OldNodeTag.new
+      tag.id = node_tags(:t1).id
+      tag.version = node_tags(:t1).version
+      tag.k = key*i
+      tag.v = "v"
+      assert_valid tag
+    end
+  end
+  
+  def test_length_value_valid
+    val = "v"
+    (0..255).each do |i|
+      tag = OldNodeTag.new
+      tag.id = node_tags(:t1).id
+      tag.version = node_tags(:t1).version
+      tag.k = "k"
+      tag.v = val*i
+      assert_valid tag
+    end
+  end
+  
+  def test_length_key_invalid
+    ["k"*256].each do |i|
+      tag = OldNodeTag.new
+      tag.id = node_tags(:t1).id
+      tag.version = node_tags(:t1).version
+      tag.k = i
+      tag.v = "v", "Key should be too long"
+      assert !tag.valid?
+      assert tag.errors.invalid?(:k)
+    end
+  end
+  
+  def test_length_value_invalid
+    ["k"*256].each do |i|
+      tag = OldNodeTag.new
+      tag.id = node_tags(:t1).id
+      tag.version = node_tags(:t1).version
+      tag.k = "k"
+      tag.v = i
+      assert !tag.valid?, "Value should be too long"
+      assert tag.errors.invalid?(:v)
+    end
+  end
+  
+  def test_empty_old_node_tag_invalid
+    tag = OldNodeTag.new
+    assert !tag.valid?, "Empty tag should be invalid"
+    assert tag.errors.invalid?(:id)
+    assert tag.errors.invalid?(:version)
+  end
+  
+  def test_uniqueness
+    tag = OldNodeTag.new
+    tag.id = node_tags(:t1).id
+    tag.version = node_tags(:t1).version
+    tag.k = node_tags(:t1).k
+    tag.v = node_tags(:t1).v
+    assert tag.new_record?
+    assert !tag.valid?
+    assert_raise(ActiveRecord::RecordInvalid) {tag.save!}
+    assert tag.new_record?
+  end
+end
diff --git a/test/unit/old_node_test.rb b/test/unit/old_node_test.rb
new file mode 100644 (file)
index 0000000..bdd6853
--- /dev/null
@@ -0,0 +1,79 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class OldNodeTest < Test::Unit::TestCase
+  set_fixture_class :current_nodes => Node
+  set_fixture_class :nodes => OldNode
+  set_fixture_class :node_tags => OldNodeTag
+  set_fixture_class :current_node_tags => NodeTag
+  fixtures :current_nodes, :users, :current_node_tags, :nodes, :node_tags
+    
+  def test_node_too_far_north
+         invalid_node_test(:node_too_far_north)
+  end
+  
+  def test_node_north_limit
+    valid_node_test(:node_north_limit)
+  end
+  
+  def test_node_too_far_south
+    invalid_node_test(:node_too_far_south)
+  end
+  
+  def test_node_south_limit
+    valid_node_test(:node_south_limit)
+  end
+  
+  def test_node_too_far_west
+    invalid_node_test(:node_too_far_west)
+  end
+  
+  def test_node_west_limit
+    valid_node_test(:node_west_limit)
+  end
+  
+  def test_node_too_far_east
+    invalid_node_test(:node_too_far_east)
+  end
+  
+  def test_node_east_limit
+    valid_node_test(:node_east_limit)
+  end
+  
+  def test_totally_wrong
+    invalid_node_test(:node_totally_wrong)
+  end
+  
+  # This helper method will check to make sure that a node is within the world, and
+  # has the the same lat, lon and timestamp than what was put into the db by 
+  # the fixture
+  def valid_node_test(nod)
+    node = nodes(nod)
+    dbnode = Node.find(node.id)
+    assert_equal dbnode.lat, node.latitude.to_f/SCALE
+    assert_equal dbnode.lon, node.longitude.to_f/SCALE
+    assert_equal dbnode.changeset_id, node.changeset_id
+    assert_equal dbnode.version, node.version
+    assert_equal dbnode.visible, node.visible
+    assert_equal dbnode.timestamp, node.timestamp
+    #assert_equal node.tile, QuadTile.tile_for_point(nodes(nod).lat, nodes(nod).lon)
+    assert_valid node
+  end
+  
+  # This helpermethod will check to make sure that a node is outwith the world, 
+  # and has the same lat, lon and timesamp than what was put into the db by the
+  # fixture
+  def invalid_node_test(nod)
+    node = nodes(nod)
+    dbnode = Node.find(node.id)
+    assert_equal dbnode.lat, node.latitude.to_f/SCALE
+    assert_equal dbnode.lon, node.longitude.to_f/SCALE
+    assert_equal dbnode.changeset_id, node.changeset_id
+    assert_equal dbnode.version, node.version
+    assert_equal dbnode.visible, node.visible
+    assert_equal dbnode.timestamp, node.timestamp
+    #assert_equal node.tile, QuadTile.tile_for_point(nodes(nod).lat, nodes(nod).lon)
+    assert_equal false, node.valid?
+  end
+  
+
+end
diff --git a/test/unit/old_relation_tag_test.rb b/test/unit/old_relation_tag_test.rb
new file mode 100644 (file)
index 0000000..d651810
--- /dev/null
@@ -0,0 +1,76 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class OldRelationTagTest < Test::Unit::TestCase
+  fixtures :relation_tags
+  set_fixture_class :relation_tags => OldRelationTag
+  
+  def test_tag_count
+    assert_equal 3, OldRelationTag.count
+  end
+  
+  def test_length_key_valid
+    key = "k"
+    (0..255).each do |i|
+      tag = OldRelationTag.new
+      tag.id = relation_tags(:t1).id
+      tag.version = 1
+      tag.k = key*i
+      tag.v = "v"
+      assert_valid tag
+    end
+  end
+  
+  def test_length_value_valid
+    val = "v"
+    (0..255).each do |i|
+      tag = OldRelationTag.new
+      tag.id = relation_tags(:t1).id
+      tag.version = 1
+      tag.k = "k"
+      tag.v = val*i
+      assert_valid tag
+    end
+  end
+  
+  def test_length_key_invalid
+    ["k"*256].each do |i|
+      tag = OldRelationTag.new
+      tag.id = relation_tags(:t1).id
+      tag.version = 1
+      tag.k = i
+      tag.v = "v"
+      assert !tag.valid?, "Key should be too long"
+      assert tag.errors.invalid?(:k)
+    end
+  end
+  
+  def test_length_value_invalid
+    ["k"*256].each do |i|
+      tag = OldRelationTag.new
+      tag.id = relation_tags(:t1).id
+      tag.version = 1
+      tag.k = "k"
+      tag.v = i
+      assert !tag.valid?, "Value should be too long"
+      assert tag.errors.invalid?(:v)
+    end
+  end
+  
+  def test_empty_node_tag_invalid
+    tag = OldRelationTag.new
+    assert !tag.valid?, "Empty tag should be invalid"
+    assert tag.errors.invalid?(:id)
+  end
+  
+  def test_uniqueness
+    tag = OldRelationTag.new
+    tag.id = relation_tags(:t1).id
+    tag.version = relation_tags(:t1).version
+    tag.k = relation_tags(:t1).k
+    tag.v = relation_tags(:t1).v
+    assert tag.new_record?
+    assert !tag.valid?
+    assert_raise(ActiveRecord::RecordInvalid) {tag.save!}
+    assert tag.new_record?
+  end
+end
diff --git a/test/unit/old_way_tag_test.rb b/test/unit/old_way_tag_test.rb
new file mode 100644 (file)
index 0000000..8210ef0
--- /dev/null
@@ -0,0 +1,76 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class WayTagTest < Test::Unit::TestCase
+  fixtures :way_tags
+  set_fixture_class :way_tags => OldWayTag
+  
+  def test_tag_count
+    assert_equal 3, OldWayTag.count
+  end
+  
+  def test_length_key_valid
+    key = "k"
+    (0..255).each do |i|
+      tag = OldWayTag.new
+      tag.id = way_tags(:t1).id
+      tag.version = 1
+      tag.k = key*i
+      tag.v = "v"
+      assert_valid tag
+    end
+  end
+  
+  def test_length_value_valid
+    val = "v"
+    (0..255).each do |i|
+      tag = OldWayTag.new
+      tag.id = way_tags(:t1).id
+      tag.version = 1
+      tag.k = "k"
+      tag.v = val*i
+      assert_valid tag
+    end
+  end
+  
+  def test_length_key_invalid
+    ["k"*256].each do |i|
+      tag = OldWayTag.new
+      tag.id = way_tags(:t1).id
+      tag.version = 1
+      tag.k = i
+      tag.v = "v"
+      assert !tag.valid?, "Key should be too long"
+      assert tag.errors.invalid?(:k)
+    end
+  end
+  
+  def test_length_value_invalid
+    ["k"*256].each do |i|
+      tag = OldWayTag.new
+      tag.id = way_tags(:t1).id
+      tag.version = 1
+      tag.k = "k"
+      tag.v = i
+      assert !tag.valid?, "Value should be too long"
+      assert tag.errors.invalid?(:v)
+    end
+  end
+  
+  def test_empty_node_tag_invalid
+    tag = OldNodeTag.new
+    assert !tag.valid?, "Empty tag should be invalid"
+    assert tag.errors.invalid?(:id)
+  end
+  
+  def test_uniqueness
+    tag = OldWayTag.new
+    tag.id = way_tags(:t1).id
+    tag.version = way_tags(:t1).version
+    tag.k = way_tags(:t1).k
+    tag.v = way_tags(:t1).v
+    assert tag.new_record?
+    assert !tag.valid?
+    assert_raise(ActiveRecord::RecordInvalid) {tag.save!}
+    assert tag.new_record?
+  end
+end
diff --git a/test/unit/relation_member_test.rb b/test/unit/relation_member_test.rb
new file mode 100644 (file)
index 0000000..d67ac34
--- /dev/null
@@ -0,0 +1,11 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class RelationMemberTest < Test::Unit::TestCase
+  fixtures :current_relation_members
+  set_fixture_class :current_relation_members => RelationMember
+  
+  def test_relation_member_count
+    assert_equal 5, RelationMember.count
+  end
+  
+end
diff --git a/test/unit/relation_tag_test.rb b/test/unit/relation_tag_test.rb
new file mode 100644 (file)
index 0000000..f93e689
--- /dev/null
@@ -0,0 +1,71 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class RelationTagTest < Test::Unit::TestCase
+  fixtures :current_relation_tags
+  set_fixture_class :current_relation_tags => RelationTag
+  
+  def test_relation_tag_count
+    assert_equal 3, RelationTag.count
+  end
+  
+  def test_length_key_valid
+    key = "k"
+    (0..255).each do |i|
+      tag = RelationTag.new
+      tag.id = 1
+      tag.k = key*i
+      tag.v = "v"
+      assert_valid tag
+    end
+  end
+  
+  def test_length_value_valid
+    val = "v"
+    (0..255).each do |i|
+      tag = RelationTag.new
+      tag.id = 1
+      tag.k = "k"
+      tag.v = val*i
+      assert_valid tag
+    end
+  end
+  
+  def test_length_key_invalid
+    ["k"*256].each do |i|
+      tag = RelationTag.new
+      tag.id = 1
+      tag.k = i
+      tag.v = "v"
+      assert !tag.valid?, "Key #{i} should be too long"
+      assert tag.errors.invalid?(:k)
+    end
+  end
+  
+  def test_length_value_invalid
+    ["v"*256].each do |i|
+      tag = RelationTag.new
+      tag.id = 1
+      tag.k = "k"
+      tag.v = i
+      assert !tag.valid?, "Value #{i} should be too long"
+      assert tag.errors.invalid?(:v)
+    end
+  end
+  
+  def test_empty_tag_invalid
+    tag = RelationTag.new
+    assert !tag.valid?, "Empty relation tag should be invalid"
+    assert tag.errors.invalid?(:id)
+  end
+  
+  def test_uniquness
+    tag = RelationTag.new
+    tag.id = current_relation_tags(:t1).id
+    tag.k = current_relation_tags(:t1).k
+    tag.v = current_relation_tags(:t1).v
+    assert tag.new_record?
+    assert !tag.valid?
+    assert_raise(ActiveRecord::RecordInvalid) {tag.save!}
+    assert tag.new_record?
+  end
+end
diff --git a/test/unit/relation_test.rb b/test/unit/relation_test.rb
new file mode 100644 (file)
index 0000000..b5b6391
--- /dev/null
@@ -0,0 +1,11 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class RelationTest < Test::Unit::TestCase
+  fixtures :current_relations
+  set_fixture_class :current_relations => Relation
+  
+  def test_relation_count
+    assert_equal 3, Relation.count
+  end
+  
+end
diff --git a/test/unit/trace_test.rb b/test/unit/trace_test.rb
new file mode 100644 (file)
index 0000000..706455a
--- /dev/null
@@ -0,0 +1,11 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class TraceTest < Test::Unit::TestCase
+  fixtures :gpx_files
+  set_fixture_class :gpx_files => Trace
+  
+  def test_trace_count
+    assert_equal 1, Trace.count
+  end
+  
+end
diff --git a/test/unit/tracepoint_test.rb b/test/unit/tracepoint_test.rb
new file mode 100644 (file)
index 0000000..5d41005
--- /dev/null
@@ -0,0 +1,11 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class TracepointTest < Test::Unit::TestCase
+  fixtures :gps_points
+  set_fixture_class :gps_points => Tracepoint
+  
+  def test_tracepoint_count
+    assert_equal 1, Tracepoint.count
+  end
+  
+end
diff --git a/test/unit/tracetag_test.rb b/test/unit/tracetag_test.rb
new file mode 100644 (file)
index 0000000..4eaf41e
--- /dev/null
@@ -0,0 +1,11 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class TracetagTest < Test::Unit::TestCase
+  fixtures :gpx_file_tags
+  set_fixture_class :gpx_file_tags => Tracetag
+  
+  def test_tracetag_count
+    assert_equal 1, Tracetag.count
+  end
+  
+end
index bd4e800150c89d8200c20855ee78919cfeff4b98..2118fcd395df252d0c40d6cc6188cb35109b650c 100644 (file)
@@ -1,8 +1,54 @@
 require File.dirname(__FILE__) + '/../test_helper'
 
 class UserPreferenceTest < ActiveSupport::TestCase
-  # Replace this with your real tests.
-  def test_truth
-    assert true
+  fixtures :users, :user_preferences
+
+  # This checks to make sure that there are two user preferences
+  # stored in the test database.
+  # This test needs to be updated for every addition/deletion from
+  # the fixture file
+  def test_check_count
+    assert_equal 2, UserPreference.count
+  end
+
+  # Checks that you cannot add a new preference, that is a duplicate
+  def test_add_duplicate_preference
+    up = user_preferences(:a)
+    newUP = UserPreference.new
+    newUP.user = users(:normal_user)
+    newUP.k = up.k
+    newUP.v = "some other value"
+    assert_not_equal newUP.v, up.v
+    assert_raise (ActiveRecord::StatementInvalid) {newUP.save}
   end
+  
+  def test_check_valid_length
+    key = "k"
+    val = "v"
+    (1..255).each do |i|
+      up = UserPreference.new
+      up.user = users(:normal_user)
+      up.k = key*i
+      up.v = val*i
+      assert up.valid?
+      assert up.save!
+      resp = UserPreference.find(up.id)
+      assert_equal key*i, resp.k, "User preference with #{i} #{key} chars (i.e. #{key.length*i} bytes) fails"
+      assert_equal val*i, resp.v, "User preference with #{i} #{val} chars (i.e. #{val.length*i} bytes) fails"
+    end
+  end
+  
+  def test_check_invalid_length
+    key = "k"
+    val = "v"
+    [0,256].each do |i|
+      up = UserPreference.new
+      up.user = users(:normal_user)
+      up.k = key*i
+      up.v = val*i
+      assert_equal false, up.valid?
+      assert_raise(ActiveRecord::RecordInvalid) {up.save!}
+    end
+  end
+
 end
index 5468f7a2d90fc88f295f8beb1cfc595699bfce10..c0df4b716984184aeaa49db5dca0dea810c0b85b 100644 (file)
@@ -2,9 +2,138 @@ require File.dirname(__FILE__) + '/../test_helper'
 
 class UserTest < Test::Unit::TestCase
   fixtures :users
-
-  # Replace this with your real tests.
-  def test_truth
-    assert true
+  
+  def test_invalid_with_empty_attributes
+    user = User.new
+    assert !user.valid?
+    assert user.errors.invalid?(:email)
+    assert user.errors.invalid?(:pass_crypt)
+    assert user.errors.invalid?(:display_name)
+    assert user.errors.invalid?(:email)
+    assert !user.errors.invalid?(:home_lat)
+    assert !user.errors.invalid?(:home_lon)
+    assert !user.errors.invalid?(:home_zoom)
+  end
+  
+  def test_unique_email
+    new_user = User.new(:email => users(:normal_user).email,
+      :active => 1, 
+      :pass_crypt => Digest::MD5.hexdigest('test'),
+      :display_name => "new user",
+      :data_public => 1,
+      :description => "desc")
+    assert !new_user.save
+    assert_equal ActiveRecord::Errors.default_error_messages[:taken], new_user.errors.on(:email)
+  end
+  
+  def test_unique_display_name
+    new_user = User.new(:email => "tester@openstreetmap.org",
+      :active => 0,
+      :pass_crypt => Digest::MD5.hexdigest('test'),
+      :display_name => users(:normal_user).display_name, 
+      :data_public => 1,
+      :description => "desc")
+    assert !new_user.save
+    assert_equal ActiveRecord::Errors.default_error_messages[:taken], new_user.errors.on(:display_name)
+  end
+  
+  def test_email_valid
+    ok = %w{ a@s.com test@shaunmcdonald.me.uk hello_local@ping-d.ng 
+    test_local@openstreetmap.org test-local@example.com
+    輕觸搖晃的遊戲@ah.com も対応します@s.name }
+    bad = %w{ hi ht@ n@ @.com help@.me.uk help"hi.me.uk も対@応します }
+    
+    ok.each do |name|
+      user = users(:normal_user)
+      user.email = name
+      assert user.valid?, user.errors.full_messages
+    end
+    
+    bad.each do |name|
+      user = users(:normal_user)
+      user.email = name
+      assert !user.valid?, "#{name} is valid when it shouldn't be" 
+    end
+  end
+  
+  def test_display_name_length
+    user = users(:normal_user)
+    user.display_name = "123"
+    assert user.valid?, " should allow nil display name"
+    user.display_name = "12"
+    assert !user.valid?, "should not allow 2 char name"
+    user.display_name = ""
+    assert !user.valid?
+    user.display_name = nil
+    # Don't understand why it isn't allowing a nil value, 
+    # when the validates statements specifically allow it
+    # It appears the database does not allow null values
+    assert !user.valid?
+  end
+  
+  def test_display_name_valid
+    # Due to sanitisation in the view some of these that you might not 
+    # expact are allowed
+    # However, would they affect the xml planet dumps?
+    ok = [ "Name", "'me", "he\"", "#ping", "<hr>", "*ho", "\"help\"@", 
+           "vergrößern", "ルシステムにも対応します", "輕觸搖晃的遊戲" ]
+    # These need to be 3 chars in length, otherwise the length test above
+    # should be used.
+    bad = [ "<hr/>", "test@example.com", "s/f", "aa/", "aa;", "aa.",
+            "aa,", "aa?", "/;.,?", "も対応します/" ]
+    ok.each do |display_name|
+      user = users(:normal_user)
+      user.display_name = display_name
+      assert user.valid?, "#{display_name} is invalid, when it should be"
+    end
+    
+    bad.each do |display_name|
+      user = users(:normal_user)
+      user.display_name = display_name
+      assert !user.valid?, "#{display_name} is valid when it shouldn't be"
+      assert_equal "is invalid", user.errors.on(:display_name)
+    end
+  end
+  
+  def test_friend_with
+    assert_equal true, users(:normal_user).is_friends_with?(users(:second_user))
+    assert_equal false, users(:normal_user).is_friends_with?(users(:inactive_user))
+    assert_equal false, users(:second_user).is_friends_with?(users(:normal_user))
+    assert_equal false, users(:second_user).is_friends_with?(users(:inactive_user))
+    assert_equal false, users(:inactive_user).is_friends_with?(users(:normal_user))
+    assert_equal false, users(:inactive_user).is_friends_with?(users(:second_user))
+  end
+  
+  def test_users_nearby
+    # second user has their data public and is close by normal user
+    assert_equal [users(:second_user)], users(:normal_user).nearby
+    # second_user has normal user nearby, but normal user has their data private
+    assert_equal [], users(:second_user).nearby
+    # inactive_user has no user nearby
+    assert_equal [], users(:inactive_user).nearby
+  end
+  
+  def test_friends_with
+    # normal user is a friend of second user
+    # it should be a one way friend accossitation
+    assert_equal 1, Friend.count
+    norm = users(:normal_user)
+    sec = users(:second_user)
+    #friend = Friend.new
+    #friend.befriender = norm
+    #friend.befriendee = sec
+    #friend.save
+    assert_equal [sec], norm.nearby
+    assert_equal 1, norm.nearby.size
+    assert_equal 1, Friend.count
+    assert_equal true, norm.is_friends_with?(sec)
+    assert_equal false, sec.is_friends_with?(norm)
+    assert_equal false, users(:normal_user).is_friends_with?(users(:inactive_user))
+    assert_equal false, users(:second_user).is_friends_with?(users(:normal_user))
+    assert_equal false, users(:second_user).is_friends_with?(users(:inactive_user))
+    assert_equal false, users(:inactive_user).is_friends_with?(users(:normal_user))
+    assert_equal false, users(:inactive_user).is_friends_with?(users(:second_user))
+    #Friend.delete(friend)
+    #assert_equal 0, Friend.count
   end
 end
diff --git a/test/unit/user_token_test.rb b/test/unit/user_token_test.rb
new file mode 100644 (file)
index 0000000..2bc1a2d
--- /dev/null
@@ -0,0 +1,10 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class UserTokenTest < Test::Unit::TestCase
+  fixtures :users
+  
+  def test_user_token_count
+    assert_equal 0, UserToken.count
+  end
+  
+end
diff --git a/test/unit/way_node_test.rb b/test/unit/way_node_test.rb
new file mode 100644 (file)
index 0000000..1871eae
--- /dev/null
@@ -0,0 +1,12 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class WayNodeTest < Test::Unit::TestCase
+  fixtures :way_nodes, :current_way_nodes
+  set_fixture_class :way_nodes=>OldWayNode
+  set_fixture_class :current_way_nodes=>WayNode
+  
+  def test_way_nodes_count
+    assert_equal 4, WayNode.count
+  end
+  
+end
diff --git a/test/unit/way_tag_test.rb b/test/unit/way_tag_test.rb
new file mode 100644 (file)
index 0000000..b1a7d22
--- /dev/null
@@ -0,0 +1,71 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class WayTagTest < Test::Unit::TestCase
+  fixtures :current_way_tags
+  set_fixture_class :current_way_tags => WayTag
+  
+  def test_way_tag_count
+    assert_equal 3, WayTag.count
+  end
+  
+  def test_length_key_valid
+    key = "k"
+    (0..255).each do |i|
+      tag = WayTag.new
+      tag.id = current_way_tags(:t1).id
+      tag.k = key*i
+      tag.v = current_way_tags(:t1).v
+      assert_valid tag
+    end
+  end
+  
+  def test_length_value_valid
+    val = "v"
+    (0..255).each do |i|
+      tag = WayTag.new
+      tag.id = current_way_tags(:t1).id
+      tag.k = "k"
+      tag.v = val*i
+      assert_valid tag
+    end
+  end
+  
+  def test_length_key_invalid
+    ["k"*256].each do |i|
+      tag = WayTag.new
+      tag.id = current_way_tags(:t1).id
+      tag.k = i
+      tag.v = "v"
+      assert !tag.valid?, "Key #{i} should be too long"
+      assert tag.errors.invalid?(:k)
+    end
+  end
+  
+  def test_length_value_invalid
+    ["v"*256].each do |i|
+      tag = WayTag.new
+      tag.id = current_way_tags(:t1).id
+      tag.k = "k"
+      tag.v = i
+      assert !tag.valid?, "Value #{i} should be too long"
+      assert tag.errors.invalid?(:v)
+    end
+  end
+  
+  def test_empty_tag_invalid
+    tag = WayTag.new
+    assert !tag.valid?, "Empty way tag should be invalid"
+    assert tag.errors.invalid?(:id)
+  end
+  
+  def test_uniqueness
+    tag = WayTag.new
+    tag.id = current_way_tags(:t1).id
+    tag.k = current_way_tags(:t1).k
+    tag.v = current_way_tags(:t1).v
+    assert tag.new_record?
+    assert !tag.valid?
+    assert_raise(ActiveRecord::RecordInvalid) {tag.save!}
+    assert tag.new_record?
+  end
+end
diff --git a/test/unit/way_test.rb b/test/unit/way_test.rb
new file mode 100644 (file)
index 0000000..584a30d
--- /dev/null
@@ -0,0 +1,40 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class WayTest < Test::Unit::TestCase
+  api_fixtures
+
+  
+  # Check that we have the correct number of currnet ways in the db
+  # This will need to updated whenever the current_ways.yml is updated
+  def test_db_count
+    assert_equal 4, Way.count
+  end
+  
+  def test_bbox
+    node = current_nodes(:used_node_1)
+    [ :visible_way,
+      :invisible_way,
+      :used_way ].each do |way_symbol|
+      way = current_ways(way_symbol)
+      assert_equal node.bbox, way.bbox
+    end
+  end
+  
+  # Check that the preconditions fail when you are over the defined limit of 
+  # the maximum number of nodes in each way.
+  def test_max_nodes_per_way_limit
+    # Take one of the current ways and add nodes to it until we are near the limit
+    way = Way.find(current_ways(:visible_way).id)
+    assert way.valid?
+    # it already has 1 node
+    1.upto((APP_CONFIG['max_number_of_way_nodes'])/2) {
+      way.add_nd_num(current_nodes(:used_node_1).id)
+      way.add_nd_num(current_nodes(:used_node_2).id)
+    }
+    way.save
+    #print way.nds.size
+    assert way.valid?
+    way.add_nd_num(current_nodes(:visible_node).id)
+    assert way.valid?
+  end
+end
diff --git a/vendor/gems/composite_primary_keys-1.1.0/History.txt b/vendor/gems/composite_primary_keys-1.1.0/History.txt
new file mode 100644 (file)
index 0000000..7016020
--- /dev/null
@@ -0,0 +1,148 @@
+== 1.1.0 2008-10-29
+
+* fixes to get cpk working for Rails 2.1.2
+
+== 1.0.10 2008-10-22
+
+* add composite key where clause creator method [timurv]
+
+== 1.0.9 2008-09-08
+
+* fix postgres tests
+* fix for delete_records when has_many association has composite keys [darxriggs]
+* more consistent table/column name quoting [pbrant]
+
+== 1.0.8 2008-08-27
+
+* fix has_many :through for non composite models [thx rcarver]
+
+== 1.0.7 2008-08-12
+
+* fix for the last fix -- when has_many is composite and belongs_to is single
+
+== 1.0.6 2008-08-06
+
+* fix associations create
+
+== 1.0.5 2008-07-25
+
+* fix for calculations with a group by clause [thx Sirius Black]
+
+== 1.0.4 2008-07-15
+
+* support for oracle_enhanced adapter [thx Raimonds Simanovskis] 
+
+== 1.0.3 2008-07-13
+
+* more fixes and tests for has many through [thx Menno van der Sman]
+
+== 1.0.2 2008-06-07
+
+* fix for has many through when through association has composite keys
+
+== 1.0.1 2008-06-06
+
+* Oracle fixes
+
+== 1.0.0 2008-06-05
+
+* Support for Rails 2.1
+
+== 0.9.93 2008-06-01
+
+* set fixed dependency on activerecord 2.0.2
+
+== 0.9.92 2008-02-22
+
+* Support for has_and_belongs_to_many
+
+== 0.9.91 2008-01-27
+
+* Incremented activerecord dependency to 2.0.2 [thx emmanuel.pirsch]
+
+== 0.9.90 2008-01-27
+
+* Trial release for rails/activerecord 2.0.2 supported
+
+== 0.9.1 2007-10-28
+
+* Migrations fix - allow :primary_key => [:name] to work [no unit test] [thx Shugo Maeda]
+
+== 0.9.0 2007-09-28
+
+* Added support for polymorphs [thx nerdrew]
+* init.rb file so gem can be installed as a plugin for Rails [thx nerdrew]
+* Added ibm_db support [thx K Venkatasubramaniyan]
+* Support for cleaning dependents [thx K Venkatasubramaniyan]
+* Rafactored db rake tasks into namespaces
+* Added namespaced tests (e.g. mysql:test for test_mysql)
+
+== 0.8.6 / 2007-6-12
+
+* 1 emergency fix due to Rails Core change
+  * Rails v7004 removed #quote; fixed with connection.quote_column_name [thx nerdrew]
+
+== 0.8.5 / 2007-6-5
+
+* 1 change due to Rails Core change
+  * Can no longer use RAILS_CONNECTION_ADAPTERS from Rails core
+* 7 dev improvement:
+  * Changed History.txt syntax to rdoc format
+  * Added deploy tasks
+  * Removed CHANGELOG + migrated into History.txt
+  * Changed PKG_NAME -> GEM_NAME in Rakefile
+  * Renamed README -> README.txt for :publish_docs task
+  * Added :check_version task
+  * VER => VERS in rakefile
+* 1 website improvement:
+  * website/index.txt includes link to "8 steps to fix other ppls code"
+
+== 0.8.4 / 2007-5-3
+
+* 1 bugfix
+  * Corrected ids_list => ids in the exception message. That'll teach me for not adding unit tests before fixing bugs. 
+
+== 0.8.3 / 2007-5-3
+
+* 1 bugfix
+  * Explicit reference to ::ActiveRecord::RecordNotFound
+* 1 website addition:
+  * Added routing help [Pete Sumskas]
+
+== 0.8.2 / 2007-4-11
+
+* 1 major enhancement:
+  * Oracle unit tests!! [Darrin Holst]
+  * And they work too
+
+== 0.8.1 / 2007-4-10
+
+* 1 bug fix:
+  * Fixed the distinct(count) for oracle (removed 'as')
+
+== 0.8.0 / 2007-4-6
+
+* 1 major enhancement:
+  * Support for calcualtions on associations
+* 2 new DB supported:
+  * Tests run on sqlite
+  * Tests run on postgresql
+* History.txt to keep track of changes like these
+* Using Hoe for Rakefile
+* Website generator rake tasks
+
+== 0.3.3
+* id=
+* create now work
+
+== 0.1.4
+* it was important that #{primary_key} for composites --> 'key1,key2' and not 'key1key2' so created PrimaryKeys class
+
+== 0.0.1 
+* Initial version
+* set_primary_keys(*keys) is the activation class method to transform an ActiveRecord into a composite primary key AR
+* find(*ids) supports the passing of 
+  * id sets: Foo.find(2,1), 
+  * lists of id sets: Foo.find([2,1], [7,3], [8,12]), 
+  * and even stringified versions of the above:
+  * Foo.find '2,1' or Foo.find '2,1;7,3'
diff --git a/vendor/gems/composite_primary_keys-1.1.0/Manifest.txt b/vendor/gems/composite_primary_keys-1.1.0/Manifest.txt
new file mode 100644 (file)
index 0000000..2ca2fc8
--- /dev/null
@@ -0,0 +1,121 @@
+History.txt
+Manifest.txt
+README.txt
+README_DB2.txt
+Rakefile
+init.rb
+install.rb
+lib/adapter_helper/base.rb
+lib/adapter_helper/mysql.rb
+lib/adapter_helper/oracle.rb
+lib/adapter_helper/postgresql.rb
+lib/adapter_helper/sqlite3.rb
+lib/composite_primary_keys.rb
+lib/composite_primary_keys/association_preload.rb
+lib/composite_primary_keys/associations.rb
+lib/composite_primary_keys/attribute_methods.rb
+lib/composite_primary_keys/base.rb
+lib/composite_primary_keys/calculations.rb
+lib/composite_primary_keys/composite_arrays.rb
+lib/composite_primary_keys/connection_adapters/ibm_db_adapter.rb
+lib/composite_primary_keys/connection_adapters/oracle_adapter.rb
+lib/composite_primary_keys/connection_adapters/postgresql_adapter.rb
+lib/composite_primary_keys/connection_adapters/sqlite3_adapter.rb
+lib/composite_primary_keys/fixtures.rb
+lib/composite_primary_keys/migration.rb
+lib/composite_primary_keys/reflection.rb
+lib/composite_primary_keys/version.rb
+loader.rb
+local/database_connections.rb.sample
+local/paths.rb.sample
+local/tasks.rb.sample
+scripts/console.rb
+scripts/txt2html
+scripts/txt2js
+tasks/activerecord_selection.rake
+tasks/databases.rake
+tasks/databases/mysql.rake
+tasks/databases/oracle.rake
+tasks/databases/postgresql.rake
+tasks/databases/sqlite3.rake
+tasks/deployment.rake
+tasks/local_setup.rake
+tasks/website.rake
+test/README_tests.txt
+test/abstract_unit.rb
+test/connections/native_ibm_db/connection.rb
+test/connections/native_mysql/connection.rb
+test/connections/native_oracle/connection.rb
+test/connections/native_postgresql/connection.rb
+test/connections/native_sqlite/connection.rb
+test/fixtures/article.rb
+test/fixtures/articles.yml
+test/fixtures/comment.rb
+test/fixtures/comments.yml
+test/fixtures/db_definitions/db2-create-tables.sql
+test/fixtures/db_definitions/db2-drop-tables.sql
+test/fixtures/db_definitions/mysql.sql
+test/fixtures/db_definitions/oracle.drop.sql
+test/fixtures/db_definitions/oracle.sql
+test/fixtures/db_definitions/postgresql.sql
+test/fixtures/db_definitions/sqlite.sql
+test/fixtures/department.rb
+test/fixtures/departments.yml
+test/fixtures/employee.rb
+test/fixtures/employees.yml
+test/fixtures/group.rb
+test/fixtures/groups.yml
+test/fixtures/hack.rb
+test/fixtures/hacks.yml
+test/fixtures/membership.rb
+test/fixtures/membership_status.rb
+test/fixtures/membership_statuses.yml
+test/fixtures/memberships.yml
+test/fixtures/product.rb
+test/fixtures/product_tariff.rb
+test/fixtures/product_tariffs.yml
+test/fixtures/products.yml
+test/fixtures/reading.rb
+test/fixtures/readings.yml
+test/fixtures/reference_code.rb
+test/fixtures/reference_codes.yml
+test/fixtures/reference_type.rb
+test/fixtures/reference_types.yml
+test/fixtures/street.rb
+test/fixtures/streets.yml
+test/fixtures/suburb.rb
+test/fixtures/suburbs.yml
+test/fixtures/tariff.rb
+test/fixtures/tariffs.yml
+test/fixtures/user.rb
+test/fixtures/users.yml
+test/hash_tricks.rb
+test/plugins/pagination.rb
+test/plugins/pagination_helper.rb
+test/test_associations.rb
+test/test_attribute_methods.rb
+test/test_attributes.rb
+test/test_clone.rb
+test/test_composite_arrays.rb
+test/test_create.rb
+test/test_delete.rb
+test/test_dummy.rb
+test/test_find.rb
+test/test_ids.rb
+test/test_miscellaneous.rb
+test/test_pagination.rb
+test/test_polymorphic.rb
+test/test_santiago.rb
+test/test_tutorial_examle.rb
+test/test_update.rb
+tmp/test.db
+website/index.html
+website/index.txt
+website/javascripts/rounded_corners_lite.inc.js
+website/stylesheets/screen.css
+website/template.js
+website/template.rhtml
+website/version-raw.js
+website/version-raw.txt
+website/version.js
+website/version.txt
diff --git a/vendor/gems/composite_primary_keys-1.1.0/README.txt b/vendor/gems/composite_primary_keys-1.1.0/README.txt
new file mode 100644 (file)
index 0000000..11daeb9
--- /dev/null
@@ -0,0 +1,41 @@
+= Composite Primary Keys for ActiveRecords\r
+\r
+== Summary\r
+\r
+ActiveRecords/Rails famously doesn't support composite primary keys. \r
+This RubyGem extends the activerecord gem to provide CPK support.\r
+\r
+== Installation\r
+\r
+    gem install composite_primary_keys\r
+    \r
+== Usage\r
+  \r
+    require 'composite_primary_keys'\r
+    class ProductVariation\r
+      set_primary_keys :product_id, :variation_seq\r
+    end\r
+    \r
+    pv = ProductVariation.find(345, 12)\r
+    \r
+It even supports composite foreign keys for associations.\r
+\r
+See http://compositekeys.rubyforge.org for more.\r
+\r
+== Running Tests\r
+\r
+See test/README.tests.txt\r
+\r
+== Url\r
+\r
+http://compositekeys.rubyforge.org\r
+\r
+== Questions, Discussion and Contributions\r
+\r
+http://groups.google.com/compositekeys\r
+\r
+== Author\r
+\r
+Written by Dr Nic Williams, drnicwilliams@gmail\r
+Contributions by many!\r
+\r
diff --git a/vendor/gems/composite_primary_keys-1.1.0/README_DB2.txt b/vendor/gems/composite_primary_keys-1.1.0/README_DB2.txt
new file mode 100644 (file)
index 0000000..b69505f
--- /dev/null
@@ -0,0 +1,33 @@
+Composite Primary key support for db2 
+
+== Driver Support ==
+
+DB2 support requires the IBM_DB driver provided by http://rubyforge.org/projects/rubyibm/
+project. Install using gem install ibm_db. Tested against version 0.60 of the driver.
+This rubyforge project appears to be permenant location for the IBM adapter.
+Older versions of the driver available from IBM Alphaworks will not work. 
+
+== Driver Bug and workaround provided as part of this plugin ==
+
+Unlike the basic quote routine available for Rails AR, the DB2 adapter's quote
+method doesn't return " column_name = 1 " when string values (integers in string type variable) 
+are passed for quoting numeric column. Rather it returns "column_name = '1'. 
+DB2 doesn't accept single quoting numeric columns in SQL. Currently, as part of 
+this plugin a fix is provided for the DB2 adapter since this plugin does 
+pass string values like this. Perhaps a patch should be sent to the DB2 adapter
+project for a permanant fix.
+
+== Database Setup ==
+
+Database must be manually created using a separate command. Read the rake task
+for creating tables and change the db name, user and passwords accordingly.
+
+== Tested Database Server version ==
+
+This is tested against DB2 v9.1 in Ubuntu Feisty Fawn (7.04)
+
+== Tested Database Client version ==
+
+This is tested against DB2 v9.1 in Ubuntu Feisty Fawn (7.04)
+
+
diff --git a/vendor/gems/composite_primary_keys-1.1.0/Rakefile b/vendor/gems/composite_primary_keys-1.1.0/Rakefile
new file mode 100644 (file)
index 0000000..22c1fb6
--- /dev/null
@@ -0,0 +1,65 @@
+require 'rubygems'\r
+require 'rake'\r
+require 'rake/clean'\r
+require 'rake/testtask'\r
+require 'rake/rdoctask'\r
+require 'rake/packagetask'\r
+require 'rake/gempackagetask'\r
+require 'rake/contrib/rubyforgepublisher'\r
+require 'fileutils'\r
+require 'hoe'\r
+include FileUtils\r
+require File.join(File.dirname(__FILE__), 'lib', 'composite_primary_keys', 'version')\r
+\r
+AUTHOR = "Dr Nic Williams"\r
+EMAIL = "drnicwilliams@gmail.com"\r
+DESCRIPTION = "Composite key support for ActiveRecords"\r
+GEM_NAME = "composite_primary_keys" # what ppl will type to install your gem\r
+if File.exists?("~/.rubyforge/user-config.yml")\r
+  # TODO this should prob go in a local/ file\r
+  config = YAML.load(File.read(File.expand_path("~/.rubyforge/user-config.yml")))\r
+  RUBYFORGE_USERNAME = config["username"]\r
+end\r
+RUBYFORGE_PROJECT = "compositekeys"\r
+HOMEPATH = "http://#{RUBYFORGE_PROJECT}.rubyforge.org"\r
+\r
+REV = nil #File.read(".svn/entries")[/committed-rev="(\d+)"/, 1] rescue nil\r
+VERS = ENV['VERSION'] || (CompositePrimaryKeys::VERSION::STRING + (REV ? ".#{REV}" : ""))\r
+CLEAN.include ['**/.*.sw?', '*.gem', '.config','debug.log','*.db','logfile','log/**/*','**/.DS_Store', '.project']\r
+RDOC_OPTS = ['--quiet', '--title', "newgem documentation",\r
+    "--opname", "index.html",\r
+    "--line-numbers", \r
+    "--main", "README",\r
+    "--inline-source"]\r
+\r
+class Hoe\r
+  def extra_deps \r
+    @extra_deps.reject { |x| Array(x).first == 'hoe' } \r
+  end \r
+end\r
+\r
+# Generate all the Rake tasks\r
+# Run 'rake -T' to see list of generated tasks (from gem root directory)\r
+hoe = Hoe.new(GEM_NAME, VERS) do |p|\r
+  p.author = AUTHOR \r
+  p.description = DESCRIPTION\r
+  p.email = EMAIL\r
+  p.summary = DESCRIPTION\r
+  p.url = HOMEPATH\r
+  p.rubyforge_name = RUBYFORGE_PROJECT if RUBYFORGE_PROJECT\r
+  p.test_globs = ["test/**/test*.rb"]\r
+  p.clean_globs |= CLEAN  #An array of file patterns to delete on clean.\r
+\r
+  # == Optional\r
+  p.changes = p.paragraphs_of("History.txt", 0..1).join("\n\n")\r
+  p.extra_deps = [['activerecord', '>= 2.1.2']]  #An array of rubygem dependencies.\r
+  #p.spec_extras    - A hash of extra values to set in the gemspec.\r
+end\r
+\r
+CHANGES = hoe.paragraphs_of('History.txt', 0..1).join("\n\n")\r
+PATH    = RUBYFORGE_PROJECT\r
+hoe.remote_rdoc_dir = File.join(PATH.gsub(/^#{RUBYFORGE_PROJECT}\/?/,''), 'rdoc')\r
+\r
+PROJECT_ROOT = File.expand_path(".")\r
+\r
+require 'loader'\r
diff --git a/vendor/gems/composite_primary_keys-1.1.0/init.rb b/vendor/gems/composite_primary_keys-1.1.0/init.rb
new file mode 100644 (file)
index 0000000..7ae5e5d
--- /dev/null
@@ -0,0 +1,2 @@
+# Include hook code here
+require_dependency 'composite_primary_keys'
diff --git a/vendor/gems/composite_primary_keys-1.1.0/install.rb b/vendor/gems/composite_primary_keys-1.1.0/install.rb
new file mode 100644 (file)
index 0000000..5be89cf
--- /dev/null
@@ -0,0 +1,30 @@
+require 'rbconfig'\r
+require 'find'\r
+require 'ftools'\r
+\r
+include Config\r
+\r
+# this was adapted from rdoc's install.rb by ways of Log4r\r
+\r
+$sitedir = CONFIG["sitelibdir"]\r
+unless $sitedir\r
+  version = CONFIG["MAJOR"] + "." + CONFIG["MINOR"]\r
+  $libdir = File.join(CONFIG["libdir"], "ruby", version)\r
+  $sitedir = $:.find {|x| x =~ /site_ruby/ }\r
+  if !$sitedir\r
+    $sitedir = File.join($libdir, "site_ruby")\r
+  elsif $sitedir !~ Regexp.quote(version)\r
+    $sitedir = File.join($sitedir, version)\r
+  end\r
+end\r
+\r
+# the acual gruntwork\r
+Dir.chdir("lib")\r
+\r
+Find.find("composite_primary_keys", "composite_primary_keys.rb") { |f|\r
+  if f[-3..-1] == ".rb"\r
+    File::install(f, File.join($sitedir, *f.split(/\//)), 0644, true)\r
+  else\r
+    File::makedirs(File.join($sitedir, *f.split(/\//)))\r
+  end\r
+}\r
diff --git a/vendor/gems/composite_primary_keys-1.1.0/lib/adapter_helper/base.rb b/vendor/gems/composite_primary_keys-1.1.0/lib/adapter_helper/base.rb
new file mode 100644 (file)
index 0000000..36ed05a
--- /dev/null
@@ -0,0 +1,63 @@
+module AdapterHelper
+  class Base
+    class << self
+      attr_accessor :adapter
+
+      def load_connection_from_env(adapter)
+        self.adapter = adapter
+        unless ENV['cpk_adapters']
+          puts error_msg_setup_helper
+          exit
+        end
+
+        ActiveRecord::Base.configurations = YAML.load(ENV['cpk_adapters'])
+        unless spec = ActiveRecord::Base.configurations[adapter]
+          puts error_msg_adapter_helper
+          exit
+        end
+        spec[:adapter] = adapter
+        spec
+      end
+    
+      def error_msg_setup_helper
+        <<-EOS
+Setup Helper:
+  CPK now has a place for your individual testing configuration.
+  That is, instead of hardcoding it in the Rakefile and test/connections files,
+  there is now a local/database_connections.rb file that is NOT in the
+  repository. Your personal DB information (username, password etc) can
+  be stored here without making it difficult to submit patches etc.
+
+Installation:
+  i)   cp locals/database_connections.rb.sample locals/database_connections.rb
+  ii)  For #{adapter} connection details see "Adapter Setup Helper" below.
+  iii) Rerun this task
+  
+#{error_msg_adapter_helper}
+  
+Current ENV:
+  #{ENV.inspect}
+        EOS
+      end
+        
+      def error_msg_adapter_helper
+        <<-EOS
+Adapter Setup Helper:
+  To run #{adapter} tests, you need to setup your #{adapter} connections.
+  In your local/database_connections.rb file, within the ENV['cpk_adapter'] hash, add:
+      "#{adapter}" => { adapter settings }
+
+  That is, it will look like:
+    ENV['cpk_adapters'] = {
+      "#{adapter}" => {
+        :adapter  => "#{adapter}",
+        :username => "root",
+        :password => "root",
+        # ...
+      }
+    }.to_yaml
+        EOS
+      end
+    end
+  end
+end
\ No newline at end of file
diff --git a/vendor/gems/composite_primary_keys-1.1.0/lib/adapter_helper/mysql.rb b/vendor/gems/composite_primary_keys-1.1.0/lib/adapter_helper/mysql.rb
new file mode 100644 (file)
index 0000000..8762e1d
--- /dev/null
@@ -0,0 +1,13 @@
+require File.join(File.dirname(__FILE__), 'base')
+
+module AdapterHelper
+  class MySQL < Base
+    class << self
+      def load_connection_from_env
+        spec = super('mysql')
+        spec[:database] ||= 'composite_primary_keys_unittest'
+        spec
+      end
+    end
+  end
+end
\ No newline at end of file
diff --git a/vendor/gems/composite_primary_keys-1.1.0/lib/adapter_helper/oracle.rb b/vendor/gems/composite_primary_keys-1.1.0/lib/adapter_helper/oracle.rb
new file mode 100644 (file)
index 0000000..76a9d19
--- /dev/null
@@ -0,0 +1,12 @@
+require File.join(File.dirname(__FILE__), 'base')
+
+module AdapterHelper
+  class Oracle < Base
+    class << self
+      def load_connection_from_env
+        spec = super('oracle')
+        spec
+      end
+    end
+  end
+end
\ No newline at end of file
diff --git a/vendor/gems/composite_primary_keys-1.1.0/lib/adapter_helper/postgresql.rb b/vendor/gems/composite_primary_keys-1.1.0/lib/adapter_helper/postgresql.rb
new file mode 100644 (file)
index 0000000..ea2c4be
--- /dev/null
@@ -0,0 +1,13 @@
+require File.join(File.dirname(__FILE__), 'base')
+
+module AdapterHelper
+  class Postgresql < Base
+    class << self
+      def load_connection_from_env
+        spec = super('postgresql')
+        spec[:database] ||= 'composite_primary_keys_unittest'
+        spec
+      end
+    end
+  end
+end
\ No newline at end of file
diff --git a/vendor/gems/composite_primary_keys-1.1.0/lib/adapter_helper/sqlite3.rb b/vendor/gems/composite_primary_keys-1.1.0/lib/adapter_helper/sqlite3.rb
new file mode 100644 (file)
index 0000000..7a45d9f
--- /dev/null
@@ -0,0 +1,13 @@
+require File.join(File.dirname(__FILE__), 'base')
+
+module AdapterHelper
+  class Sqlite3 < Base
+    class << self
+      def load_connection_from_env
+        spec = super('sqlite3')
+        spec[:dbfile] ||= "tmp/test.db"
+        spec
+      end
+    end
+  end
+end
\ No newline at end of file
diff --git a/vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys.rb b/vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys.rb
new file mode 100644 (file)
index 0000000..99b6140
--- /dev/null
@@ -0,0 +1,55 @@
+#--\r
+# Copyright (c) 2006 Nic Williams\r
+#\r
+# Permission is hereby granted, free of charge, to any person obtaining\r
+# a copy of this software and associated documentation files (the\r
+# "Software"), to deal in the Software without restriction, including\r
+# without limitation the rights to use, copy, modify, merge, publish,\r
+# distribute, sublicense, and/or sell copies of the Software, and to\r
+# permit persons to whom the Software is furnished to do so, subject to\r
+# the following conditions:\r
+#\r
+# The above copyright notice and this permission notice shall be\r
+# included in all copies or substantial portions of the Software.\r
+#\r
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,\r
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\r
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\r
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\r
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\r
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\r
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\r
+#++\r
+\r
+$:.unshift(File.dirname(__FILE__)) unless\r
+  $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))\r
+\r
+unless defined?(ActiveRecord)\r
+  begin\r
+    require 'active_record'  \r
+  rescue LoadError\r
+    require 'rubygems'\r
+    require_gem 'activerecord'\r
+  end\r
+end\r
+\r
+require 'composite_primary_keys/fixtures'\r
+require 'composite_primary_keys/composite_arrays'\r
+require 'composite_primary_keys/associations'\r
+require 'composite_primary_keys/association_preload'\r
+require 'composite_primary_keys/reflection'\r
+require 'composite_primary_keys/base'\r
+require 'composite_primary_keys/calculations'\r
+require 'composite_primary_keys/migration'\r
+require 'composite_primary_keys/attribute_methods'\r
+\r
+ActiveRecord::Base.class_eval do\r
+  include CompositePrimaryKeys::ActiveRecord::Base\r
+end\r
+\r
+Dir[File.dirname(__FILE__) + '/composite_primary_keys/connection_adapters/*.rb'].each do |adapter|\r
+  begin\r
+    require adapter.gsub('.rb','')\r
+  rescue MissingSourceFile\r
+  end\r
+end\r
diff --git a/vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/association_preload.rb b/vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/association_preload.rb
new file mode 100644 (file)
index 0000000..54e5eeb
--- /dev/null
@@ -0,0 +1,236 @@
+module CompositePrimaryKeys
+  module ActiveRecord
+    module AssociationPreload
+      def self.append_features(base)
+        super
+        base.send(:extend, ClassMethods)
+      end
+
+      # Composite key versions of Association functions
+      module ClassMethods
+        def preload_has_and_belongs_to_many_association(records, reflection, preload_options={})
+          table_name = reflection.klass.quoted_table_name
+          id_to_record_map, ids = construct_id_map(records)
+          records.each {|record| record.send(reflection.name).loaded}
+          options = reflection.options
+
+          if composite?
+            primary_key = reflection.primary_key_name.to_s.split(CompositePrimaryKeys::ID_SEP)
+            where = (primary_key * ids.size).in_groups_of(primary_key.size).map do |keys|
+              "(" + keys.map{|key| "t0.#{connection.quote_column_name(key)} = ?"}.join(" AND ") + ")"
+            end.join(" OR ")
+
+            conditions = [where, ids].flatten
+            joins = "INNER JOIN #{connection.quote_table_name options[:join_table]} t0 ON #{full_composite_join_clause(reflection, reflection.klass.table_name, reflection.klass.primary_key, 't0', reflection.association_foreign_key)}"
+            parent_primary_keys = reflection.primary_key_name.to_s.split(CompositePrimaryKeys::ID_SEP).map{|k| "t0.#{connection.quote_column_name(k)}"}
+            parent_record_id = connection.concat(*parent_primary_keys.zip(["','"] * (parent_primary_keys.size - 1)).flatten.compact)
+          else
+            conditions = ["t0.#{connection.quote_column_name(reflection.primary_key_name)}  IN (?)", ids]
+            joins = "INNER JOIN #{connection.quote_table_name options[:join_table]} t0 ON #{reflection.klass.quoted_table_name}.#{connection.quote_column_name(reflection.klass.primary_key)} = t0.#{connection.quote_column_name(reflection.association_foreign_key)})"
+            parent_record_id = reflection.primary_key_name
+          end
+
+          conditions.first << append_conditions(reflection, preload_options)
+
+          associated_records = reflection.klass.find(:all,
+            :conditions => conditions,
+            :include    => options[:include],
+            :joins      => joins,
+            :select     => "#{options[:select] || table_name+'.*'}, #{parent_record_id} as parent_record_id_",
+            :order      => options[:order])
+
+          set_association_collection_records(id_to_record_map, reflection.name, associated_records, 'parent_record_id_')
+        end
+
+        def preload_has_many_association(records, reflection, preload_options={})
+          id_to_record_map, ids = construct_id_map(records)
+          records.each {|record| record.send(reflection.name).loaded}
+          options = reflection.options
+
+          if options[:through]
+            through_records = preload_through_records(records, reflection, options[:through])
+            through_reflection = reflections[options[:through]]
+            through_primary_key = through_reflection.primary_key_name
+
+            unless through_records.empty?
+              source = reflection.source_reflection.name
+              #add conditions from reflection!
+              through_records.first.class.preload_associations(through_records, source, reflection.options)
+              through_records.each do |through_record|
+                key = through_primary_key.to_s.split(CompositePrimaryKeys::ID_SEP).map{|k| through_record.send(k)}.join(CompositePrimaryKeys::ID_SEP)
+                add_preloaded_records_to_collection(id_to_record_map[key], reflection.name, through_record.send(source))
+              end
+            end
+          else
+            associated_records = find_associated_records(ids, reflection, preload_options)
+            set_association_collection_records(id_to_record_map, reflection.name, associated_records, reflection.primary_key_name.to_s.split(CompositePrimaryKeys::ID_SEP))
+          end
+        end
+
+        def preload_through_records(records, reflection, through_association)
+          through_reflection = reflections[through_association]
+          through_primary_key = through_reflection.primary_key_name
+
+          if reflection.options[:source_type]
+            interface = reflection.source_reflection.options[:foreign_type]
+            preload_options = {:conditions => ["#{connection.quote_column_name interface} = ?", reflection.options[:source_type]]}
+
+            records.compact!
+            records.first.class.preload_associations(records, through_association, preload_options)
+
+            # Dont cache the association - we would only be caching a subset
+            through_records = []
+            records.each do |record|
+              proxy = record.send(through_association)
+
+              if proxy.respond_to?(:target)
+                through_records << proxy.target
+                proxy.reset
+              else # this is a has_one :through reflection
+                through_records << proxy if proxy
+              end
+            end
+            through_records.flatten!
+          else
+            records.first.class.preload_associations(records, through_association)
+            through_records = records.map {|record| record.send(through_association)}.flatten
+          end
+
+          through_records.compact!
+          through_records
+        end
+
+        def preload_belongs_to_association(records, reflection, preload_options={})
+          options = reflection.options
+          primary_key_name = reflection.primary_key_name.to_s.split(CompositePrimaryKeys::ID_SEP)
+
+          if options[:polymorphic]
+            raise AssociationNotSupported, "Polymorphic joins not supported for composite keys"
+          else
+            # I need to keep the original ids for each record (as opposed to the stringified) so
+            # that they get properly converted for each db so the id_map ends up looking like:
+            #
+            # { '1,2' => {:id => [1,2], :records => [...records...]}}
+            id_map = {}
+
+            records.each do |record|
+              key = primary_key_name.map{|k| record.attributes[k]}
+              key_as_string = key.join(CompositePrimaryKeys::ID_SEP)
+
+              if key_as_string
+                mapped_records = (id_map[key_as_string] ||= {:id => key, :records => []})
+                mapped_records[:records] << record
+              end
+            end
+
+
+            klasses_and_ids = [[reflection.klass.name, id_map]]
+          end
+
+          klasses_and_ids.each do |klass_and_id|
+            klass_name, id_map = *klass_and_id
+            klass = klass_name.constantize
+            table_name = klass.quoted_table_name
+            connection = reflection.active_record.connection
+
+            if composite?
+              primary_key = klass.primary_key.to_s.split(CompositePrimaryKeys::ID_SEP)
+              ids = id_map.keys.uniq.map {|id| id_map[id][:id]}
+
+              where = (primary_key * ids.size).in_groups_of(primary_key.size).map do |keys|
+                 "(" + keys.map{|key| "#{table_name}.#{connection.quote_column_name(key)} = ?"}.join(" AND ") + ")"
+              end.join(" OR ")
+
+              conditions = [where, ids].flatten
+            else
+              conditions = ["#{table_name}.#{connection.quote_column_name(primary_key)} IN (?)", id_map.keys.uniq]
+            end
+
+            conditions.first << append_conditions(reflection, preload_options)
+
+            associated_records = klass.find(:all,
+              :conditions => conditions,
+              :include    => options[:include],
+              :select     => options[:select],
+              :joins      => options[:joins],
+              :order      => options[:order])
+
+            set_association_single_records(id_map, reflection.name, associated_records, primary_key)
+          end
+        end
+
+        def set_association_collection_records(id_to_record_map, reflection_name, associated_records, key)
+          associated_records.each do |associated_record|
+            associated_record_key = associated_record[key]
+            associated_record_key = associated_record_key.is_a?(Array) ? associated_record_key.join(CompositePrimaryKeys::ID_SEP) : associated_record_key.to_s
+            mapped_records = id_to_record_map[associated_record_key]
+            add_preloaded_records_to_collection(mapped_records, reflection_name, associated_record)
+          end
+        end
+
+        def set_association_single_records(id_to_record_map, reflection_name, associated_records, key)
+          seen_keys = {}
+          associated_records.each do |associated_record|
+            associated_record_key = associated_record[key]
+            associated_record_key = associated_record_key.is_a?(Array) ? associated_record_key.join(CompositePrimaryKeys::ID_SEP) : associated_record_key.to_s
+
+            #this is a has_one or belongs_to: there should only be one record.
+            #Unfortunately we can't (in portable way) ask the database for 'all records where foo_id in (x,y,z), but please
+            # only one row per distinct foo_id' so this where we enforce that
+            next if seen_keys[associated_record_key]
+            seen_keys[associated_record_key] = true
+            mapped_records = id_to_record_map[associated_record_key][:records]
+            mapped_records.each do |mapped_record|
+              mapped_record.send("set_#{reflection_name}_target", associated_record)
+            end
+          end
+        end
+
+        def find_associated_records(ids, reflection, preload_options)
+          options = reflection.options
+          table_name = reflection.klass.quoted_table_name
+
+          if interface = reflection.options[:as]
+            raise AssociationNotSupported, "Polymorphic joins not supported for composite keys"
+          else
+            connection = reflection.active_record.connection
+            foreign_key = reflection.primary_key_name
+            conditions = ["#{table_name}.#{connection.quote_column_name(foreign_key)} IN (?)", ids]
+            
+            if composite?
+              foreign_keys = foreign_key.to_s.split(CompositePrimaryKeys::ID_SEP)
+            
+              where = (foreign_keys * ids.size).in_groups_of(foreign_keys.size).map do |keys|
+                "(" + keys.map{|key| "#{table_name}.#{connection.quote_column_name(key)} = ?"}.join(" AND ") + ")"
+              end.join(" OR ")
+
+              conditions = [where, ids].flatten
+            end
+          end
+
+          conditions.first << append_conditions(reflection, preload_options)
+
+          reflection.klass.find(:all,
+            :select     => (preload_options[:select] || options[:select] || "#{table_name}.*"),
+            :include    => preload_options[:include] || options[:include],
+            :conditions => conditions,
+            :joins      => options[:joins],
+            :group      => preload_options[:group] || options[:group],
+            :order      => preload_options[:order] || options[:order])
+        end        
+        
+        def full_composite_join_clause(reflection, table1, full_keys1, table2, full_keys2)
+          connection = reflection.active_record.connection
+          full_keys1 = full_keys1.split(CompositePrimaryKeys::ID_SEP) if full_keys1.is_a?(String)
+          full_keys2 = full_keys2.split(CompositePrimaryKeys::ID_SEP) if full_keys2.is_a?(String)
+          where_clause = [full_keys1, full_keys2].transpose.map do |key_pair|
+            quoted1 = connection.quote_table_name(table1)
+            quoted2 = connection.quote_table_name(table2)
+            "#{quoted1}.#{connection.quote_column_name(key_pair.first)}=#{quoted2}.#{connection.quote_column_name(key_pair.last)}"
+          end.join(" AND ")
+          "(#{where_clause})"
+        end
+      end
+    end
+  end
+end
diff --git a/vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/associations.rb b/vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/associations.rb
new file mode 100644 (file)
index 0000000..4ea4a7b
--- /dev/null
@@ -0,0 +1,428 @@
+module CompositePrimaryKeys
+  module ActiveRecord
+    module Associations
+      def self.append_features(base)
+        super
+        base.send(:extend, ClassMethods)
+      end
+
+      # Composite key versions of Association functions
+      module ClassMethods
+
+        def construct_counter_sql_with_included_associations(options, join_dependency)
+          scope = scope(:find)
+          sql = "SELECT COUNT(DISTINCT #{quoted_table_columns(primary_key)})"
+
+          # A (slower) workaround if we're using a backend, like sqlite, that doesn't support COUNT DISTINCT.
+          if !self.connection.supports_count_distinct?
+            sql = "SELECT COUNT(*) FROM (SELECT DISTINCT #{quoted_table_columns(primary_key)}"
+          end
+          
+          sql << " FROM #{quoted_table_name} "
+          sql << join_dependency.join_associations.collect{|join| join.association_join }.join
+
+          add_joins!(sql, options, scope)
+          add_conditions!(sql, options[:conditions], scope)
+          add_limited_ids_condition!(sql, options, join_dependency) if !using_limitable_reflections?(join_dependency.reflections) && ((scope && scope[:limit]) || options[:limit])
+
+          add_limit!(sql, options, scope) if using_limitable_reflections?(join_dependency.reflections)
+
+          if !self.connection.supports_count_distinct?
+            sql << ")"
+          end
+
+          return sanitize_sql(sql)
+        end
+
+        def construct_finder_sql_with_included_associations(options, join_dependency)
+          scope = scope(:find)
+          sql = "SELECT #{column_aliases(join_dependency)} FROM #{(scope && scope[:from]) || options[:from] || quoted_table_name} "
+          sql << join_dependency.join_associations.collect{|join| join.association_join }.join
+
+          add_joins!(sql, options, scope)
+          add_conditions!(sql, options[:conditions], scope)
+          add_limited_ids_condition!(sql, options, join_dependency) if !using_limitable_reflections?(join_dependency.reflections) && options[:limit]
+
+          sql << "ORDER BY #{options[:order]} " if options[:order]
+
+          add_limit!(sql, options, scope) if using_limitable_reflections?(join_dependency.reflections)
+
+          return sanitize_sql(sql)
+        end
+
+        def table_columns(columns)
+          columns.collect {|column| "#{self.quoted_table_name}.#{connection.quote_column_name(column)}"}
+        end
+
+        def quoted_table_columns(columns)
+          table_columns(columns).join(ID_SEP)
+        end
+
+      end
+
+    end
+  end
+end
+
+module ActiveRecord::Associations::ClassMethods
+  class JoinDependency
+    def construct_association(record, join, row)
+      case join.reflection.macro
+        when :has_many, :has_and_belongs_to_many
+          collection = record.send(join.reflection.name)
+          collection.loaded
+
+          join_aliased_primary_keys = join.active_record.composite? ?
+            join.aliased_primary_key : [join.aliased_primary_key]
+          return nil if
+            record.id.to_s != join.parent.record_id(row).to_s or not
+            join_aliased_primary_keys.select {|key| row[key].nil?}.blank?
+          association = join.instantiate(row)
+          collection.target.push(association) unless collection.target.include?(association)
+        when :has_one, :belongs_to
+          return if record.id.to_s != join.parent.record_id(row).to_s or
+                    [*join.aliased_primary_key].any? { |key| row[key].nil? }
+          association = join.instantiate(row)
+          record.send("set_#{join.reflection.name}_target", association)
+        else
+          raise ConfigurationError, "unknown macro: #{join.reflection.macro}"
+      end
+      return association
+    end
+
+    class JoinBase
+      def aliased_primary_key
+        active_record.composite? ?
+          primary_key.inject([]) {|aliased_keys, key| aliased_keys << "#{ aliased_prefix }_r#{aliased_keys.length}"} :
+          "#{ aliased_prefix }_r0"
+      end
+
+      def record_id(row)
+        active_record.composite? ?
+          aliased_primary_key.map {|key| row[key]}.to_composite_ids :
+          row[aliased_primary_key]
+      end
+
+      def column_names_with_alias
+        unless @column_names_with_alias
+          @column_names_with_alias = []
+          keys = active_record.composite? ? primary_key.map(&:to_s) : [primary_key]
+          (keys + (column_names - keys)).each_with_index do |column_name, i|
+            @column_names_with_alias << [column_name, "#{ aliased_prefix }_r#{ i }"]
+          end
+        end
+        return @column_names_with_alias
+      end
+    end
+
+    class JoinAssociation < JoinBase
+      alias single_association_join association_join
+      def association_join
+        reflection.active_record.composite? ? composite_association_join : single_association_join
+      end
+
+      def composite_association_join
+        join = case reflection.macro
+          when :has_and_belongs_to_many
+            " LEFT OUTER JOIN %s ON %s " % [
+              table_alias_for(options[:join_table], aliased_join_table_name),
+                composite_join_clause(
+                  full_keys(aliased_join_table_name, options[:foreign_key] || reflection.active_record.to_s.classify.foreign_key),
+                  full_keys(reflection.active_record.table_name, reflection.active_record.primary_key)
+                )
+              ] +
+             " LEFT OUTER JOIN %s ON %s " % [
+                table_name_and_alias,
+                composite_join_clause(
+                  full_keys(aliased_table_name, klass.primary_key),
+                  full_keys(aliased_join_table_name, options[:association_foreign_key] || klass.table_name.classify.foreign_key)
+                )
+              ]
+          when :has_many, :has_one
+            case
+              when reflection.macro == :has_many && reflection.options[:through]
+                through_conditions = through_reflection.options[:conditions] ? "AND #{interpolate_sql(sanitize_sql(through_reflection.options[:conditions]))}" : ''
+                if through_reflection.options[:as] # has_many :through against a polymorphic join
+                  raise AssociationNotSupported, "Polymorphic joins not supported for composite keys"
+                else
+                  if source_reflection.macro == :has_many && source_reflection.options[:as]
+                    raise AssociationNotSupported, "Polymorphic joins not supported for composite keys"
+                  else
+                    case source_reflection.macro
+                      when :belongs_to
+                        first_key  = primary_key
+                        second_key = options[:foreign_key] || klass.to_s.classify.foreign_key
+                      when :has_many
+                        first_key  = through_reflection.klass.to_s.classify.foreign_key
+                        second_key = options[:foreign_key] || primary_key
+                    end
+
+                    " LEFT OUTER JOIN %s ON %s "  % [
+                      table_alias_for(through_reflection.klass.table_name, aliased_join_table_name),
+                       composite_join_clause(
+                         full_keys(aliased_join_table_name, through_reflection.primary_key_name),
+                         full_keys(parent.aliased_table_name, parent.primary_key)
+                       )
+                     ] +
+                     " LEFT OUTER JOIN %s ON %s " % [
+                       table_name_and_alias,
+                       composite_join_clause(
+                         full_keys(aliased_table_name, first_key),
+                         full_keys(aliased_join_table_name, second_key)
+                       )
+                    ]
+                  end
+                end
+
+              when reflection.macro == :has_many && reflection.options[:as]
+                raise AssociationNotSupported, "Polymorphic joins not supported for composite keys"
+              when reflection.macro == :has_one && reflection.options[:as]
+                raise AssociationNotSupported, "Polymorphic joins not supported for composite keys"
+              else
+                foreign_key = options[:foreign_key] || reflection.active_record.name.foreign_key
+                " LEFT OUTER JOIN %s ON %s " % [
+                  table_name_and_alias,
+                  composite_join_clause(
+                    full_keys(aliased_table_name, foreign_key),
+                    full_keys(parent.aliased_table_name, parent.primary_key)),
+                ]
+            end
+          when :belongs_to
+            " LEFT OUTER JOIN %s ON %s " % [
+               table_name_and_alias,
+               composite_join_clause(
+                 full_keys(aliased_table_name, reflection.klass.primary_key),
+                 full_keys(parent.aliased_table_name, options[:foreign_key] || klass.to_s.foreign_key)),
+              ]
+          else
+            ""
+        end || ''
+        join << %(AND %s.%s = %s ) % [
+          aliased_table_name,
+          reflection.active_record.connection.quote_column_name(reflection.active_record.inheritance_column),
+          klass.connection.quote(klass.name)] unless klass.descends_from_active_record?
+        join << "AND #{interpolate_sql(sanitize_sql(reflection.options[:conditions]))} " if reflection.options[:conditions]
+        join
+      end
+
+      def full_keys(table_name, keys)
+        connection = reflection.active_record.connection
+        quoted_table_name = connection.quote_table_name(table_name)
+        if keys.is_a?(Array) 
+          keys.collect {|key| "#{quoted_table_name}.#{connection.quote_column_name(key)}"}.join(CompositePrimaryKeys::ID_SEP) 
+        else
+          "#{quoted_table_name}.#{connection.quote_column_name(keys)}"
+        end
+      end
+
+      def composite_join_clause(full_keys1, full_keys2)
+        full_keys1 = full_keys1.split(CompositePrimaryKeys::ID_SEP) if full_keys1.is_a?(String)
+        full_keys2 = full_keys2.split(CompositePrimaryKeys::ID_SEP) if full_keys2.is_a?(String)
+        where_clause = [full_keys1, full_keys2].transpose.map do |key1, key2|
+          "#{key1}=#{key2}"
+        end.join(" AND ")
+        "(#{where_clause})"
+      end
+    end
+  end
+end
+
+module ActiveRecord::Associations
+  class AssociationProxy #:nodoc:
+
+    def composite_where_clause(full_keys, ids)
+      full_keys = full_keys.split(CompositePrimaryKeys::ID_SEP) if full_keys.is_a?(String)
+
+      if ids.is_a?(String)
+        ids = [[ids]]
+      elsif not ids.first.is_a?(Array) # if single comp key passed, turn into an array of 1
+        ids = [ids.to_composite_ids]
+      end
+
+      where_clause = ids.map do |id_set|
+        transposed = id_set.size == 1 ? [[full_keys, id_set.first]] : [full_keys, id_set].transpose
+        transposed.map do |full_key, id|
+          "#{full_key.to_s}=#{@reflection.klass.sanitize(id)}"
+        end.join(" AND ")
+      end.join(") OR (")
+
+      "(#{where_clause})"
+    end
+
+    def composite_join_clause(full_keys1, full_keys2)
+      full_keys1 = full_keys1.split(CompositePrimaryKeys::ID_SEP) if full_keys1.is_a?(String)
+      full_keys2 = full_keys2.split(CompositePrimaryKeys::ID_SEP) if full_keys2.is_a?(String)
+
+      where_clause = [full_keys1, full_keys2].transpose.map do |key1, key2|
+        "#{key1}=#{key2}"
+      end.join(" AND ")
+
+      "(#{where_clause})"
+    end
+
+    def full_composite_join_clause(table1, full_keys1, table2, full_keys2)
+      connection = @reflection.active_record.connection
+      full_keys1 = full_keys1.split(CompositePrimaryKeys::ID_SEP) if full_keys1.is_a?(String)
+      full_keys2 = full_keys2.split(CompositePrimaryKeys::ID_SEP) if full_keys2.is_a?(String)
+
+      quoted1 = connection.quote_table_name(table1)
+      quoted2 = connection.quote_table_name(table2)
+      
+      where_clause = [full_keys1, full_keys2].transpose.map do |key_pair|
+        "#{quoted1}.#{connection.quote_column_name(key_pair.first)}=#{quoted2}.#{connection.quote_column_name(key_pair.last)}"
+      end.join(" AND ")
+
+      "(#{where_clause})"
+    end
+
+    def full_keys(table_name, keys)
+      connection = @reflection.active_record.connection
+      quoted_table_name = connection.quote_table_name(table_name)
+      keys = keys.split(CompositePrimaryKeys::ID_SEP) if keys.is_a?(String)
+      if keys.is_a?(Array) 
+        keys.collect {|key| "#{quoted_table_name}.#{connection.quote_column_name(key)}"}.join(CompositePrimaryKeys::ID_SEP) 
+      else
+        "#{quoted_table_name}.#{connection.quote_column_name(keys)}"
+      end
+    end
+
+    def full_columns_equals(table_name, keys, quoted_ids)
+      connection = @reflection.active_record.connection
+      quoted_table_name = connection.quote_table_name(table_name)
+      if keys.is_a?(Symbol) or (keys.is_a?(String) and keys == keys.to_s.split(CompositePrimaryKeys::ID_SEP))
+        return "#{quoted_table_name}.#{connection.quote_column_name(keys)} = #{quoted_ids}"
+      end
+      keys = keys.split(CompositePrimaryKeys::ID_SEP) if keys.is_a?(String)
+      quoted_ids = quoted_ids.split(CompositePrimaryKeys::ID_SEP) if quoted_ids.is_a?(String)
+      keys_ids = [keys, quoted_ids].transpose
+      keys_ids.collect {|key, id| "(#{quoted_table_name}.#{connection.quote_column_name(key)} = #{id})"}.join(' AND ')
+    end 
+
+    def set_belongs_to_association_for(record)
+      if @reflection.options[:as]
+        record["#{@reflection.options[:as]}_id"]   = @owner.id unless @owner.new_record?
+        record["#{@reflection.options[:as]}_type"] = @owner.class.base_class.name.to_s
+      else
+        key_values = @reflection.primary_key_name.to_s.split(CompositePrimaryKeys::ID_SEP).zip([@owner.id].flatten)
+        key_values.each{|key, value| record[key] = value} unless @owner.new_record?
+      end
+    end
+  end
+
+  class HasAndBelongsToManyAssociation < AssociationCollection #:nodoc:
+    def construct_sql
+      @reflection.options[:finder_sql] &&= interpolate_sql(@reflection.options[:finder_sql])
+
+      if @reflection.options[:finder_sql]
+        @finder_sql = @reflection.options[:finder_sql]
+      else
+        @finder_sql = full_columns_equals(@reflection.options[:join_table], @reflection.primary_key_name, @owner.quoted_id)
+        @finder_sql << " AND (#{conditions})" if conditions
+      end
+
+      @join_sql = "INNER JOIN #{@reflection.active_record.connection.quote_table_name(@reflection.options[:join_table])} ON " +
+      full_composite_join_clause(@reflection.klass.table_name, @reflection.klass.primary_key, @reflection.options[:join_table], @reflection.association_foreign_key)
+    end
+  end
+
+  class HasManyAssociation < AssociationCollection #:nodoc:
+    def construct_sql
+      case
+        when @reflection.options[:finder_sql]
+          @finder_sql = interpolate_sql(@reflection.options[:finder_sql])
+
+        when @reflection.options[:as]
+          @finder_sql = 
+            "#{@reflection.klass.quoted_table_name}.#{@reflection.options[:as]}_id = #{@owner.quoted_id} AND " + 
+            "#{@reflection.klass.quoted_table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote_value(@owner.class.base_class.name.to_s)}"
+          @finder_sql << " AND (#{conditions})" if conditions
+
+        else
+          @finder_sql = full_columns_equals(@reflection.klass.table_name, @reflection.primary_key_name, @owner.quoted_id)
+          @finder_sql << " AND (#{conditions})" if conditions
+      end
+
+      if @reflection.options[:counter_sql]
+        @counter_sql = interpolate_sql(@reflection.options[:counter_sql])
+      elsif @reflection.options[:finder_sql]
+        # replace the SELECT clause with COUNT(*), preserving any hints within /* ... */
+        @reflection.options[:counter_sql] = @reflection.options[:finder_sql].sub(/SELECT (\/\*.*?\*\/ )?(.*)\bFROM\b/im) { "SELECT #{$1}COUNT(*) FROM" }
+        @counter_sql = interpolate_sql(@reflection.options[:counter_sql])
+      else
+        @counter_sql = @finder_sql
+      end
+    end
+
+    def delete_records(records)
+      if @reflection.options[:dependent]
+        records.each { |r| r.destroy }
+      else
+        connection = @reflection.active_record.connection
+        field_names = @reflection.primary_key_name.split(',')
+        field_names.collect! {|n| connection.quote_column_name(n) + " = NULL"}
+        records.each do |r|
+          where_clause = nil
+          
+          if r.quoted_id.to_s.include?(CompositePrimaryKeys::ID_SEP)
+            where_clause_terms = [@reflection.klass.primary_key, r.quoted_id].transpose.map do |pair|
+              "(#{connection.quote_column_name(pair[0])} = #{pair[1]})"
+            end
+            where_clause = where_clause_terms.join(" AND ")
+          else
+            where_clause = connection.quote_column_name(@reflection.klass.primary_key) + ' = ' +  r.quoted_id
+          end
+          
+          @reflection.klass.update_all(  field_names.join(',') , where_clause)
+        end
+      end
+    end
+  end
+
+  class HasOneAssociation < BelongsToAssociation #:nodoc:
+    def construct_sql
+      case
+        when @reflection.options[:as]
+          @finder_sql = 
+            "#{@reflection.klass.quoted_table_name}.#{@reflection.options[:as]}_id = #{@owner.quoted_id} AND " + 
+            "#{@reflection.klass.quoted_table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote_value(@owner.class.base_class.name.to_s)}"
+        else
+          @finder_sql = full_columns_equals(@reflection.klass.table_name, @reflection.primary_key_name, @owner.quoted_id)
+      end
+
+      @finder_sql << " AND (#{conditions})" if conditions
+    end
+  end
+
+  class HasManyThroughAssociation < HasManyAssociation #:nodoc:
+    def construct_conditions_with_composite_keys
+      if @reflection.through_reflection.options[:as]
+        construct_conditions_without_composite_keys
+      else
+        conditions = full_columns_equals(@reflection.through_reflection.table_name, @reflection.through_reflection.primary_key_name, @owner.quoted_id)
+        conditions << " AND (#{sql_conditions})" if sql_conditions
+        conditions
+      end
+    end
+    alias_method_chain :construct_conditions, :composite_keys
+
+    def construct_joins_with_composite_keys(custom_joins = nil)
+      if @reflection.through_reflection.options[:as] || @reflection.source_reflection.options[:as]
+        construct_joins_without_composite_keys(custom_joins)
+      else
+        if @reflection.source_reflection.macro == :belongs_to
+          reflection_primary_key = @reflection.klass.primary_key
+          source_primary_key     = @reflection.source_reflection.primary_key_name
+        else
+          reflection_primary_key = @reflection.source_reflection.primary_key_name
+          source_primary_key     = @reflection.klass.primary_key
+        end
+
+        "INNER JOIN %s ON %s #{@reflection.options[:joins]} #{custom_joins}" % [
+          @reflection.through_reflection.quoted_table_name,
+          composite_join_clause(full_keys(@reflection.table_name, reflection_primary_key), full_keys(@reflection.through_reflection.table_name, source_primary_key))
+        ]
+      end
+    end
+    alias_method_chain :construct_joins, :composite_keys
+  end
+end
diff --git a/vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/attribute_methods.rb b/vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/attribute_methods.rb
new file mode 100644 (file)
index 0000000..a0e3331
--- /dev/null
@@ -0,0 +1,84 @@
+module CompositePrimaryKeys
+  module ActiveRecord
+    module AttributeMethods #:nodoc:
+      def self.append_features(base)
+        super
+        base.send(:extend, ClassMethods)
+      end
+
+      module ClassMethods
+        # Define an attribute reader method.  Cope with nil column.
+        def define_read_method(symbol, attr_name, column)
+          cast_code = column.type_cast_code('v') if column
+          cast_code = "::#{cast_code}" if cast_code && cast_code.match('ActiveRecord::.*')
+          access_code = cast_code ? "(v=@attributes['#{attr_name}']) && #{cast_code}" : "@attributes['#{attr_name}']"
+
+          unless self.primary_keys.include?(attr_name.to_sym)
+            access_code = access_code.insert(0, "missing_attribute('#{attr_name}', caller) unless @attributes.has_key?('#{attr_name}'); ")
+          end
+
+          if cache_attribute?(attr_name)
+            access_code = "@attributes_cache['#{attr_name}'] ||= (#{access_code})"
+          end
+
+          evaluate_attribute_method attr_name, "def #{symbol}; #{access_code}; end"
+        end
+
+        # Evaluate the definition for an attribute related method
+        def evaluate_attribute_method(attr_name, method_definition, method_name=attr_name)
+          unless primary_keys.include?(method_name.to_sym)
+            generated_methods << method_name
+          end
+
+          begin
+            class_eval(method_definition, __FILE__, __LINE__)
+          rescue SyntaxError => err
+            generated_methods.delete(attr_name)
+            if logger
+              logger.warn "Exception occurred during reader method compilation."
+              logger.warn "Maybe #{attr_name} is not a valid Ruby identifier?"
+              logger.warn "#{err.message}"
+            end
+          end
+        end
+      end
+
+      # Allows access to the object attributes, which are held in the @attributes hash, as though they
+      # were first-class methods. So a Person class with a name attribute can use Person#name and
+      # Person#name= and never directly use the attributes hash -- except for multiple assigns with
+      # ActiveRecord#attributes=. A Milestone class can also ask Milestone#completed? to test that
+      # the completed attribute is not nil or 0.
+      #
+      # It's also possible to instantiate related objects, so a Client class belonging to the clients
+      # table with a master_id foreign key can instantiate master through Client#master.
+      def method_missing(method_id, *args, &block)
+        method_name = method_id.to_s
+
+        # If we haven't generated any methods yet, generate them, then
+        # see if we've created the method we're looking for.
+        if !self.class.generated_methods?
+          self.class.define_attribute_methods
+
+          if self.class.generated_methods.include?(method_name)
+            return self.send(method_id, *args, &block)
+          end
+        end
+
+        if self.class.primary_keys.include?(method_name.to_sym)
+          ids[self.class.primary_keys.index(method_name.to_sym)]
+        elsif md = self.class.match_attribute_method?(method_name)
+          attribute_name, method_type = md.pre_match, md.to_s
+          if @attributes.include?(attribute_name)
+            __send__("attribute#{method_type}", attribute_name, *args, &block)
+          else
+            super
+          end
+        elsif @attributes.include?(method_name)
+          read_attribute(method_name)
+        else
+          super
+        end
+      end
+    end
+  end
+end
diff --git a/vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/base.rb b/vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/base.rb
new file mode 100644 (file)
index 0000000..42ec475
--- /dev/null
@@ -0,0 +1,337 @@
+module CompositePrimaryKeys\r
+  module ActiveRecord #:nodoc:\r
+    class CompositeKeyError < StandardError #:nodoc:\r
+    end\r
+\r
+    module Base #:nodoc:\r
+\r
+      INVALID_FOR_COMPOSITE_KEYS = 'Not appropriate for composite primary keys'\r
+      NOT_IMPLEMENTED_YET        = 'Not implemented for composite primary keys yet'\r
+\r
+      def self.append_features(base)\r
+        super\r
+        base.send(:include, InstanceMethods)\r
+        base.extend(ClassMethods)\r
+      end\r
+\r
+      module ClassMethods\r
+        def set_primary_keys(*keys)\r
+          keys = keys.first if keys.first.is_a?(Array)\r
+          keys = keys.map { |k| k.to_sym }\r
+          cattr_accessor :primary_keys\r
+          self.primary_keys = keys.to_composite_keys\r
+\r
+          class_eval <<-EOV\r
+            extend CompositeClassMethods\r
+            include CompositeInstanceMethods\r
+\r
+            include CompositePrimaryKeys::ActiveRecord::Associations\r
+            include CompositePrimaryKeys::ActiveRecord::AssociationPreload\r
+            include CompositePrimaryKeys::ActiveRecord::Calculations\r
+            include CompositePrimaryKeys::ActiveRecord::AttributeMethods\r
+          EOV\r
+        end\r
+\r
+        def composite?\r
+          false\r
+        end\r
+      end\r
+\r
+      module InstanceMethods\r
+        def composite?; self.class.composite?; end\r
+      end\r
+\r
+      module CompositeInstanceMethods\r
+\r
+        # A model instance's primary keys is always available as model.ids\r
+        # whether you name it the default 'id' or set it to something else.\r
+        def id\r
+          attr_names = self.class.primary_keys\r
+          CompositeIds.new(attr_names.map { |attr_name| read_attribute(attr_name) })\r
+        end\r
+        alias_method :ids, :id\r
+\r
+        def to_param\r
+          id.to_s\r
+        end\r
+\r
+        def id_before_type_cast #:nodoc:\r
+          raise CompositeKeyError, CompositePrimaryKeys::ActiveRecord::Base::NOT_IMPLEMENTED_YET\r
+        end\r
+\r
+        def quoted_id #:nodoc:\r
+          [self.class.primary_keys, ids].\r
+            transpose.\r
+            map {|attr_name,id| quote_value(id, column_for_attribute(attr_name))}.\r
+            to_composite_ids\r
+        end\r
+\r
+        # Sets the primary ID.\r
+        def id=(ids)\r
+          ids = ids.split(ID_SEP) if ids.is_a?(String)\r
+          ids.flatten!\r
+          unless ids.is_a?(Array) and ids.length == self.class.primary_keys.length\r
+            raise "#{self.class}.id= requires #{self.class.primary_keys.length} ids"\r
+          end\r
+          [primary_keys, ids].transpose.each {|key, an_id| write_attribute(key , an_id)}\r
+          id\r
+        end\r
+\r
+        # Returns a clone of the record that hasn't been assigned an id yet and\r
+        # is treated as a new record.  Note that this is a "shallow" clone:\r
+        # it copies the object's attributes only, not its associations.\r
+        # The extent of a "deep" clone is application-specific and is therefore\r
+        # left to the application to implement according to its need.\r
+        def clone\r
+          attrs = self.attributes_before_type_cast\r
+          self.class.primary_keys.each {|key| attrs.delete(key.to_s)}\r
+          self.class.new do |record|\r
+            record.send :instance_variable_set, '@attributes', attrs\r
+          end\r
+        end\r
+\r
+\r
+        private\r
+        # The xx_without_callbacks methods are overwritten as that is the end of the alias chain\r
+\r
+        # Creates a new record with values matching those of the instance attributes.\r
+        def create_without_callbacks\r
+          unless self.id\r
+            raise CompositeKeyError, "Composite keys do not generated ids from sequences, you must provide id values"\r
+          end\r
+          attributes_minus_pks = attributes_with_quotes(false)\r
+          quoted_pk_columns = self.class.primary_key.map { |col| connection.quote_column_name(col) }\r
+          cols = quoted_column_names(attributes_minus_pks) << quoted_pk_columns\r
+          vals = attributes_minus_pks.values << quoted_id\r
+          connection.insert(\r
+            "INSERT INTO #{self.class.quoted_table_name} " +\r
+            "(#{cols.join(', ')}) " +\r
+            "VALUES (#{vals.join(', ')})",\r
+            "#{self.class.name} Create",\r
+            self.class.primary_key,\r
+            self.id\r
+          )\r
+          @new_record = false\r
+          return true\r
+        end\r
+\r
+        # Updates the associated record with values matching those of the instance attributes.\r
+        def update_without_callbacks\r
+          where_clause_terms = [self.class.primary_key, quoted_id].transpose.map do |pair| \r
+            "(#{connection.quote_column_name(pair[0])} = #{pair[1]})"\r
+          end\r
+          where_clause = where_clause_terms.join(" AND ")\r
+          connection.update(\r
+            "UPDATE #{self.class.quoted_table_name} " +\r
+            "SET #{quoted_comma_pair_list(connection, attributes_with_quotes(false))} " +\r
+            "WHERE #{where_clause}",\r
+            "#{self.class.name} Update"\r
+          )\r
+          return true\r
+        end\r
+\r
+        # Deletes the record in the database and freezes this instance to reflect that no changes should\r
+        # be made (since they can't be persisted).\r
+        def destroy_without_callbacks\r
+          where_clause_terms = [self.class.primary_key, quoted_id].transpose.map do |pair| \r
+            "(#{connection.quote_column_name(pair[0])} = #{pair[1]})"\r
+          end\r
+          where_clause = where_clause_terms.join(" AND ")\r
+          unless new_record?\r
+            connection.delete(\r
+              "DELETE FROM #{self.class.quoted_table_name} " +\r
+              "WHERE #{where_clause}",\r
+              "#{self.class.name} Destroy"\r
+            )\r
+          end\r
+          freeze\r
+        end\r
+      end\r
+\r
+      module CompositeClassMethods\r
+        def primary_key; primary_keys; end\r
+        def primary_key=(keys); primary_keys = keys; end\r
+\r
+        def composite?\r
+          true\r
+        end\r
+\r
+        #ids_to_s([[1,2],[7,3]]) -> "(1,2),(7,3)"\r
+        #ids_to_s([[1,2],[7,3]], ',', ';') -> "1,2;7,3"\r
+        def ids_to_s(many_ids, id_sep = CompositePrimaryKeys::ID_SEP, list_sep = ',', left_bracket = '(', right_bracket = ')')\r
+          many_ids.map {|ids| "#{left_bracket}#{ids}#{right_bracket}"}.join(list_sep)\r
+        end\r
+        \r
+        # Creates WHERE condition from list of composited ids\r
+        #   User.update_all({:role => 'admin'}, :conditions => composite_where_clause([[1, 2], [2, 2]])) #=> UPDATE admins SET admin.role='admin' WHERE (admin.type=1 AND admin.type2=2) OR (admin.type=2 AND admin.type2=2)\r
+        #   User.find(:all, :conditions => composite_where_clause([[1, 2], [2, 2]])) #=> SELECT * FROM admins WHERE (admin.type=1 AND admin.type2=2) OR (admin.type=2 AND admin.type2=2)\r
+        def composite_where_clause(ids)\r
+          if ids.is_a?(String)\r
+            ids = [[ids]]\r
+          elsif not ids.first.is_a?(Array) # if single comp key passed, turn into an array of 1\r
+            ids = [ids.to_composite_ids]\r
+          end\r
+          \r
+          ids.map do |id_set|\r
+            [primary_keys, id_set].transpose.map do |key, id|\r
+              "#{table_name}.#{key.to_s}=#{sanitize(id)}"\r
+            end.join(" AND ")\r
+          end.join(") OR (")       \r
+        end\r
+\r
+        # Returns true if the given +ids+ represents the primary keys of a record in the database, false otherwise.\r
+        # Example:\r
+        #   Person.exists?(5,7)\r
+        def exists?(ids)\r
+          obj = find(ids) rescue false\r
+          !obj.nil? and obj.is_a?(self)\r
+        end\r
+\r
+        # Deletes the record with the given +ids+ without instantiating an object first, e.g. delete(1,2)\r
+        # If an array of ids is provided (e.g. delete([1,2], [3,4]), all of them\r
+        # are deleted.\r
+        def delete(*ids)\r
+          unless ids.is_a?(Array); raise "*ids must be an Array"; end\r
+          ids = [ids.to_composite_ids] if not ids.first.is_a?(Array)\r
+          where_clause = ids.map do |id_set|\r
+            [primary_keys, id_set].transpose.map do |key, id|\r
+              "#{quoted_table_name}.#{connection.quote_column_name(key.to_s)}=#{sanitize(id)}"\r
+            end.join(" AND ")\r
+          end.join(") OR (")\r
+          delete_all([ "(#{where_clause})" ])\r
+        end\r
+\r
+        # Destroys the record with the given +ids+ by instantiating the object and calling #destroy (all the callbacks are the triggered).\r
+        # If an array of ids is provided, all of them are destroyed.\r
+        def destroy(*ids)\r
+          unless ids.is_a?(Array); raise "*ids must be an Array"; end\r
+          if ids.first.is_a?(Array)\r
+            ids = ids.map{|compids| compids.to_composite_ids}\r
+          else\r
+            ids = ids.to_composite_ids\r
+          end\r
+          ids.first.is_a?(CompositeIds) ? ids.each { |id_set| find(id_set).destroy } : find(ids).destroy\r
+        end\r
+\r
+        # Returns an array of column objects for the table associated with this class.\r
+        # Each column that matches to one of the primary keys has its\r
+        # primary attribute set to true\r
+        def columns\r
+          unless @columns\r
+            @columns = connection.columns(table_name, "#{name} Columns")\r
+            @columns.each {|column| column.primary = primary_keys.include?(column.name.to_sym)}\r
+          end\r
+          @columns\r
+        end\r
+\r
+        ## DEACTIVATED METHODS ##\r
+        public\r
+        # Lazy-set the sequence name to the connection's default.  This method\r
+        # is only ever called once since set_sequence_name overrides it.\r
+        def sequence_name #:nodoc:\r
+          raise CompositeKeyError, CompositePrimaryKeys::ActiveRecord::Base::INVALID_FOR_COMPOSITE_KEYS\r
+        end\r
+\r
+        def reset_sequence_name #:nodoc:\r
+          raise CompositeKeyError, CompositePrimaryKeys::ActiveRecord::Base::INVALID_FOR_COMPOSITE_KEYS\r
+        end\r
+\r
+        def set_primary_key(value = nil, &block)\r
+          raise CompositeKeyError, CompositePrimaryKeys::ActiveRecord::Base::INVALID_FOR_COMPOSITE_KEYS\r
+        end\r
+\r
+        private\r
+        def find_one(id, options)\r
+          raise CompositeKeyError, CompositePrimaryKeys::ActiveRecord::Base::INVALID_FOR_COMPOSITE_KEYS\r
+        end\r
+\r
+        def find_some(ids, options)\r
+          raise CompositeKeyError, CompositePrimaryKeys::ActiveRecord::Base::INVALID_FOR_COMPOSITE_KEYS\r
+        end\r
+\r
+        def find_from_ids(ids, options)\r
+          ids = ids.first if ids.last == nil\r
+          conditions = " AND (#{sanitize_sql(options[:conditions])})" if options[:conditions]\r
+          # if ids is just a flat list, then its size must = primary_key.length (one id per primary key, in order)\r
+          # if ids is list of lists, then each inner list must follow rule above\r
+          if ids.first.is_a? String\r
+            # find '2,1' -> ids = ['2,1']\r
+            # find '2,1;7,3' -> ids = ['2,1;7,3']\r
+            ids = ids.first.split(ID_SET_SEP).map {|id_set| id_set.split(ID_SEP).to_composite_ids}\r
+            # find '2,1;7,3' -> ids = [['2','1'],['7','3']], inner [] are CompositeIds\r
+          end\r
+          ids = [ids.to_composite_ids] if not ids.first.kind_of?(Array)\r
+          ids.each do |id_set|\r
+            unless id_set.is_a?(Array)\r
+              raise "Ids must be in an Array, instead received: #{id_set.inspect}"\r
+            end\r
+            unless id_set.length == primary_keys.length\r
+              raise "#{id_set.inspect}: Incorrect number of primary keys for #{class_name}: #{primary_keys.inspect}"\r
+            end\r
+          end\r
+\r
+          # Let keys = [:a, :b]\r
+          # If ids = [[10, 50], [11, 51]], then :conditions => \r
+          #   "(#{quoted_table_name}.a, #{quoted_table_name}.b) IN ((10, 50), (11, 51))"\r
+\r
+          conditions = ids.map do |id_set|\r
+            [primary_keys, id_set].transpose.map do |key, id|\r
+                               col = columns_hash[key.to_s]\r
+                               val = quote_value(id, col)\r
+              "#{quoted_table_name}.#{connection.quote_column_name(key.to_s)}=#{val}"\r
+            end.join(" AND ")\r
+          end.join(") OR (")\r
+              \r
+          options.update :conditions => "(#{conditions})"\r
+\r
+          result = find_every(options)\r
+\r
+          if result.size == ids.size\r
+            ids.size == 1 ? result[0] : result\r
+          else\r
+            raise ::ActiveRecord::RecordNotFound, "Couldn't find all #{name.pluralize} with IDs (#{ids.inspect})#{conditions}"\r
+          end\r
+        end\r
+      end\r
+    end\r
+  end\r
+end\r
+\r
+\r
+module ActiveRecord\r
+  ID_SEP     = ','\r
+  ID_SET_SEP = ';'\r
+\r
+  class Base\r
+    # Allows +attr_name+ to be the list of primary_keys, and returns the id\r
+    # of the object\r
+    # e.g. @object[@object.class.primary_key] => [1,1]\r
+    def [](attr_name)\r
+      if attr_name.is_a?(String) and attr_name != attr_name.split(ID_SEP).first\r
+        attr_name = attr_name.split(ID_SEP)\r
+      end\r
+      attr_name.is_a?(Array) ?\r
+        attr_name.map {|name| read_attribute(name)} :\r
+        read_attribute(attr_name)\r
+    end\r
+\r
+    # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+.\r
+    # (Alias for the protected write_attribute method).\r
+    def []=(attr_name, value)\r
+      if attr_name.is_a?(String) and attr_name != attr_name.split(ID_SEP).first\r
+        attr_name = attr_name.split(ID_SEP)\r
+      end\r
+\r
+      if attr_name.is_a? Array\r
+        value = value.split(ID_SEP) if value.is_a? String\r
+        unless value.length == attr_name.length\r
+          raise "Number of attr_names and values do not match"\r
+        end\r
+        #breakpoint\r
+        [attr_name, value].transpose.map {|name,val| write_attribute(name.to_s, val)}\r
+      else\r
+        write_attribute(attr_name, value)\r
+      end\r
+    end\r
+  end\r
+end\r
diff --git a/vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/calculations.rb b/vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/calculations.rb
new file mode 100644 (file)
index 0000000..44280e1
--- /dev/null
@@ -0,0 +1,68 @@
+module CompositePrimaryKeys
+  module ActiveRecord
+    module Calculations
+      def self.append_features(base)
+        super
+        base.send(:extend, ClassMethods)
+      end
+
+      module ClassMethods
+        def construct_calculation_sql(operation, column_name, options) #:nodoc:
+          operation = operation.to_s.downcase
+          options = options.symbolize_keys
+
+          scope           = scope(:find)
+          merged_includes = merge_includes(scope ? scope[:include] : [], options[:include])
+          aggregate_alias = column_alias_for(operation, column_name)
+          use_workaround  = !connection.supports_count_distinct? && options[:distinct] && operation.to_s.downcase == 'count'
+          join_dependency = nil
+
+          if merged_includes.any? && operation.to_s.downcase == 'count'
+            options[:distinct] = true
+            use_workaround  = !connection.supports_count_distinct?
+            column_name = options[:select] || primary_key.map{ |part| "#{quoted_table_name}.#{connection.quote_column_name(part)}"}.join(',')
+          end
+
+          sql  = "SELECT #{operation}(#{'DISTINCT ' if options[:distinct]}#{column_name}) AS #{aggregate_alias}"
+
+          # A (slower) workaround if we're using a backend, like sqlite, that doesn't support COUNT DISTINCT.
+          sql = "SELECT COUNT(*) AS #{aggregate_alias}" if use_workaround
+
+          sql << ", #{connection.quote_column_name(options[:group_field])} AS #{options[:group_alias]}" if options[:group]
+          sql << " FROM (SELECT DISTINCT #{column_name}" if use_workaround
+          sql << " FROM #{quoted_table_name} "
+          if merged_includes.any?
+            join_dependency = ::ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, merged_includes, options[:joins])
+            sql << join_dependency.join_associations.collect{|join| join.association_join }.join
+          end
+          add_joins!(sql, options, scope)
+          add_conditions!(sql, options[:conditions], scope)
+          add_limited_ids_condition!(sql, options, join_dependency) if \
+            join_dependency &&
+            !using_limitable_reflections?(join_dependency.reflections) &&
+            ((scope && scope[:limit]) || options[:limit])
+
+          if options[:group]
+            group_key = connection.adapter_name == 'FrontBase' ?  :group_alias : :group_field
+            sql << " GROUP BY #{connection.quote_column_name(options[group_key])} "
+          end
+
+          if options[:group] && options[:having]
+            # FrontBase requires identifiers in the HAVING clause and chokes on function calls
+            if connection.adapter_name == 'FrontBase'
+              options[:having].downcase!
+              options[:having].gsub!(/#{operation}\s*\(\s*#{column_name}\s*\)/, aggregate_alias)
+            end
+
+            sql << " HAVING #{options[:having]} "
+          end
+
+          sql << " ORDER BY #{options[:order]} " if options[:order]
+          add_limit!(sql, options, scope)
+          sql << ') w1' if use_workaround # assign a dummy table name as required for postgresql
+          sql
+        end
+      end
+    end
+  end
+end
diff --git a/vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/composite_arrays.rb b/vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/composite_arrays.rb
new file mode 100644 (file)
index 0000000..030c416
--- /dev/null
@@ -0,0 +1,30 @@
+module CompositePrimaryKeys\r
+  ID_SEP     = ','\r
+  ID_SET_SEP = ';'\r
+\r
+  module ArrayExtension\r
+    def to_composite_keys\r
+      CompositeKeys.new(self)\r
+    end\r
+\r
+    def to_composite_ids\r
+      CompositeIds.new(self)\r
+    end\r
+  end\r
+\r
+  class CompositeArray < Array\r
+    def to_s\r
+      join(ID_SEP)\r
+    end\r
+  end\r
+\r
+  class CompositeKeys < CompositeArray\r
+\r
+  end\r
+\r
+  class CompositeIds < CompositeArray\r
+\r
+  end\r
+end\r
+\r
+Array.send(:include, CompositePrimaryKeys::ArrayExtension)\r
diff --git a/vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/connection_adapters/ibm_db_adapter.rb b/vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/connection_adapters/ibm_db_adapter.rb
new file mode 100644 (file)
index 0000000..1ab4717
--- /dev/null
@@ -0,0 +1,21 @@
+module ActiveRecord
+  module ConnectionAdapters
+    class IBM_DBAdapter < AbstractAdapter
+      
+      # This mightn't be in Core, but count(distinct x,y) doesn't work for me
+      def supports_count_distinct? #:nodoc:
+        false
+      end
+      
+      alias_method :quote_original, :quote
+      def quote(value, column = nil)
+        if value.kind_of?(String) && column && [:integer, :float].include?(column.type)
+              value = column.type == :integer ? value.to_i : value.to_f
+              value.to_s
+        else
+            quote_original(value, column)
+        end
+      end
+    end
+  end
+end
diff --git a/vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/connection_adapters/oracle_adapter.rb b/vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/connection_adapters/oracle_adapter.rb
new file mode 100644 (file)
index 0000000..af558fa
--- /dev/null
@@ -0,0 +1,15 @@
+module ActiveRecord
+  module ConnectionAdapters
+    class OracleAdapter < AbstractAdapter
+      
+      # This mightn't be in Core, but count(distinct x,y) doesn't work for me
+      def supports_count_distinct? #:nodoc:
+        false
+      end
+      
+      def concat(*columns)
+        "(#{columns.join('||')})"
+      end
+    end
+  end
+end
\ No newline at end of file
diff --git a/vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/connection_adapters/postgresql_adapter.rb b/vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/connection_adapters/postgresql_adapter.rb
new file mode 100644 (file)
index 0000000..65fce48
--- /dev/null
@@ -0,0 +1,53 @@
+module ActiveRecord
+  module ConnectionAdapters
+    class PostgreSQLAdapter < AbstractAdapter
+      
+      # This mightn't be in Core, but count(distinct x,y) doesn't work for me
+      def supports_count_distinct? #:nodoc:
+        false
+      end
+
+      def concat(*columns)
+        columns = columns.map { |c| "CAST(#{c} AS varchar)" }
+        "(#{columns.join('||')})"
+      end
+      
+      # Executes an INSERT query and returns the new record's ID
+      def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
+        # Extract the table from the insert sql. Yuck.
+        table = sql.split(" ", 4)[2].gsub('"', '')
+
+        # Try an insert with 'returning id' if available (PG >= 8.2)
+        if supports_insert_with_returning?
+          pk, sequence_name = *pk_and_sequence_for(table) unless pk
+          if pk
+            quoted_pk = if pk.is_a?(Array)
+                          pk.map { |col| quote_column_name(col) }.join(ID_SEP)
+                        else
+                          quote_column_name(pk)
+                        end
+            id = select_value("#{sql} RETURNING #{quoted_pk}")
+            clear_query_cache
+            return id
+          end
+        end
+
+        # Otherwise, insert then grab last_insert_id.
+        if insert_id = super
+          insert_id
+        else
+          # If neither pk nor sequence name is given, look them up.
+          unless pk || sequence_name
+            pk, sequence_name = *pk_and_sequence_for(table)
+          end
+
+          # If a pk is given, fallback to default sequence name.
+          # Don't fetch last insert id for a table without a pk.
+          if pk && sequence_name ||= default_sequence_name(table, pk)
+            last_insert_id(table, sequence_name)
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/connection_adapters/sqlite3_adapter.rb b/vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/connection_adapters/sqlite3_adapter.rb
new file mode 100644 (file)
index 0000000..0527577
--- /dev/null
@@ -0,0 +1,15 @@
+require 'active_record/connection_adapters/sqlite_adapter'
+
+module ActiveRecord
+  module ConnectionAdapters #:nodoc:
+    class SQLite3Adapter < SQLiteAdapter # :nodoc:
+      def supports_count_distinct? #:nodoc:
+        false
+      end
+      
+      def concat(*columns)
+        "(#{columns.join('||')})"
+      end
+    end
+  end
+end
\ No newline at end of file
diff --git a/vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/fixtures.rb b/vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/fixtures.rb
new file mode 100644 (file)
index 0000000..7dfaf08
--- /dev/null
@@ -0,0 +1,8 @@
+class Fixture #:nodoc:
+  def [](key)
+    if key.is_a? Array
+      return key.map { |a_key| self[a_key.to_s] }.to_composite_ids.to_s
+    end
+    @fixture[key]
+  end
+end
diff --git a/vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/migration.rb b/vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/migration.rb
new file mode 100644 (file)
index 0000000..2a50404
--- /dev/null
@@ -0,0 +1,20 @@
+ActiveRecord::ConnectionAdapters::ColumnDefinition.send(:alias_method, :to_s_without_composite_keys, :to_s)
+
+ActiveRecord::ConnectionAdapters::ColumnDefinition.class_eval <<-'EOF'
+  def to_s
+    if name.is_a? Array
+      "PRIMARY KEY (#{name.join(',')})"
+    else
+      to_s_without_composite_keys
+    end
+  end
+EOF
+
+ActiveRecord::ConnectionAdapters::TableDefinition.class_eval <<-'EOF'
+  def [](name)
+    @columns.find { |column|
+      !column.name.is_a?(Array) && column.name.to_s == name.to_s
+    }
+  end
+EOF
\ No newline at end of file
diff --git a/vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/reflection.rb b/vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/reflection.rb
new file mode 100644 (file)
index 0000000..309baf1
--- /dev/null
@@ -0,0 +1,19 @@
+module ActiveRecord\r
+  module Reflection\r
+    class AssociationReflection\r
+      def primary_key_name\r
+        return @primary_key_name if @primary_key_name\r
+        case\r
+          when macro == :belongs_to\r
+            @primary_key_name = options[:foreign_key] || class_name.foreign_key\r
+          when options[:as]\r
+            @primary_key_name = options[:foreign_key] || "#{options[:as]}_id"\r
+          else\r
+            @primary_key_name = options[:foreign_key] || active_record.name.foreign_key\r
+        end\r
+        @primary_key_name = @primary_key_name.to_composite_keys.to_s if @primary_key_name.is_a? Array\r
+        @primary_key_name\r
+      end\r
+    end\r
+  end\r
+end
\ No newline at end of file
diff --git a/vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/version.rb b/vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/version.rb
new file mode 100644 (file)
index 0000000..c6cbeeb
--- /dev/null
@@ -0,0 +1,8 @@
+module CompositePrimaryKeys\r
+  module VERSION #:nodoc:\r
+    MAJOR = 1\r
+    MINOR = 1\r
+    TINY  = 0\r
+    STRING = [MAJOR, MINOR, TINY].join('.')\r
+  end\r
+end\r
diff --git a/vendor/gems/composite_primary_keys-1.1.0/loader.rb b/vendor/gems/composite_primary_keys-1.1.0/loader.rb
new file mode 100644 (file)
index 0000000..052c47c
--- /dev/null
@@ -0,0 +1,24 @@
+# Load local config files in /local
+begin
+  local_file_supported = Dir[File.join(PROJECT_ROOT, 'local/*.sample')].map { |path| File.basename(path).sub(".sample","") }
+  local_file_supported.each do |file|
+    require "local/#{file}"
+  end
+rescue LoadError
+  puts <<-EOS
+  This Gem supports local developer extensions in local/ folder. 
+  Supported files:
+    #{local_file_supported.map { |f| "local/#{f}"}.join(', ')}
+
+  Setup default sample files:
+    rake local:setup
+
+  Current warning: #{$!}
+  
+  EOS
+end
+
+
+# Now load Rake tasks from /tasks
+rakefiles = Dir[File.join(File.dirname(__FILE__), "tasks/**/*.rake")]
+rakefiles.each { |rakefile| load File.expand_path(rakefile) }
diff --git a/vendor/gems/composite_primary_keys-1.1.0/local/database_connections.rb.sample b/vendor/gems/composite_primary_keys-1.1.0/local/database_connections.rb.sample
new file mode 100644 (file)
index 0000000..be67edd
--- /dev/null
@@ -0,0 +1,10 @@
+require 'yaml'
+
+ENV['cpk_adapters'] = {
+  "mysql" => {
+    :adapter  => "mysql",
+    :username => "root",
+    :password => "root",
+    # ...
+  }
+}.to_yaml
\ No newline at end of file
diff --git a/vendor/gems/composite_primary_keys-1.1.0/local/paths.rb.sample b/vendor/gems/composite_primary_keys-1.1.0/local/paths.rb.sample
new file mode 100644 (file)
index 0000000..65ba16f
--- /dev/null
@@ -0,0 +1,2 @@
+# location of folder containing activerecord, railties, etc folders for each Rails gem
+ENV['EDGE_RAILS_DIR'] ||= "/path/to/copy/of/edge/rails"
diff --git a/vendor/gems/composite_primary_keys-1.1.0/local/tasks.rb.sample b/vendor/gems/composite_primary_keys-1.1.0/local/tasks.rb.sample
new file mode 100644 (file)
index 0000000..29daf8d
--- /dev/null
@@ -0,0 +1,2 @@
+# This file loaded into Rakefile
+# Place any extra development tasks you want here
\ No newline at end of file
diff --git a/vendor/gems/composite_primary_keys-1.1.0/scripts/console.rb b/vendor/gems/composite_primary_keys-1.1.0/scripts/console.rb
new file mode 100755 (executable)
index 0000000..7053e92
--- /dev/null
@@ -0,0 +1,48 @@
+#!/usr/bin/env ruby
+
+#
+# if run as script, load the file as library while starting irb 
+#
+if __FILE__ == $0
+  irb = RUBY_PLATFORM =~ /(:?mswin|mingw)/ ? 'irb.bat' : 'irb'
+  ENV['ADAPTER'] = ARGV[0]
+  exec "#{irb} -f -r #{$0} --simple-prompt"
+end
+
+#
+# check if the given adapter is supported (default: mysql)
+#
+adapters = %w[mysql sqlite oracle oracle_enhanced postgresql ibm_db]
+adapter = ENV['ADAPTER'] || 'mysql'
+unless adapters.include? adapter
+  puts "Usage: #{__FILE__} <adapter>"
+  puts ''
+  puts 'Adapters: '
+  puts adapters.map{ |adapter| "    #{adapter}" }.join("\n")
+  exit 1
+end
+
+#
+# load all necessary libraries
+#
+require 'rubygems'
+require 'local/database_connections'
+
+$LOAD_PATH.unshift 'lib'
+
+begin
+  require 'local/paths'
+  $LOAD_PATH.unshift "#{ENV['EDGE_RAILS_DIR']}/activerecord/lib"  if ENV['EDGE_RAILS_DIR']
+  $LOAD_PATH.unshift "#{ENV['EDGE_RAILS_DIR']}/activesupport/lib" if ENV['EDGE_RAILS_DIR']
+rescue
+end
+
+require 'active_support'
+require 'active_record'
+
+require "test/connections/native_#{adapter}/connection"
+require 'composite_primary_keys'
+
+PROJECT_ROOT = File.join(File.dirname(__FILE__), '..')
+Dir[File.join(PROJECT_ROOT,'test/fixtures/*.rb')].each { |model| require model }
+
diff --git a/vendor/gems/composite_primary_keys-1.1.0/scripts/txt2html b/vendor/gems/composite_primary_keys-1.1.0/scripts/txt2html
new file mode 100644 (file)
index 0000000..d5ab2c6
--- /dev/null
@@ -0,0 +1,67 @@
+#!/usr/bin/env ruby
+
+require 'rubygems'
+require 'redcloth'
+require 'syntax/convertors/html'
+require 'erb'
+require File.dirname(__FILE__) + '/../lib/composite_primary_keys/version.rb'
+
+version  = CompositePrimaryKeys::VERSION::STRING
+download = 'http://rubyforge.org/projects/compositekeys'
+
+class Fixnum
+  def ordinal
+    # teens
+    return 'th' if (10..19).include?(self % 100)
+    # others
+    case self % 10
+    when 1: return 'st'
+    when 2: return 'nd'
+    when 3: return 'rd'
+    else    return 'th'
+    end
+  end
+end
+
+class Time
+  def pretty
+    return "#{mday}#{mday.ordinal} #{strftime('%B')} #{year}"
+  end
+end
+
+def convert_syntax(syntax, source)
+  return Syntax::Convertors::HTML.for_syntax(syntax).convert(source).gsub(%r!^<pre>|</pre>$!,'')
+end
+
+if ARGV.length >= 1
+  src, template = ARGV
+  template ||= File.dirname(__FILE__) + '/../website/template.rhtml'
+  
+else
+  puts("Usage: #{File.split($0).last} source.txt [template.rhtml] > output.html")
+  exit!
+end
+
+template = ERB.new(File.open(template).read)
+
+title = nil
+body = nil
+File.open(src) do |fsrc|
+  title_text = fsrc.readline
+  body_text = fsrc.read
+  syntax_items = []
+  body_text.gsub!(%r!<(pre|code)[^>]*?syntax=['"]([^'"]+)[^>]*>(.*?)</\1>!m){
+    ident = syntax_items.length
+    element, syntax, source = $1, $2, $3
+    syntax_items << "<#{element} class=\"syntax\">#{convert_syntax(syntax, source)}</#{element}>"
+    "syntax-temp-#{ident}"
+  }
+  title = RedCloth.new(title_text).to_html.gsub(%r!<.*?>!,'').strip
+  body = RedCloth.new(body_text).to_html
+  body.gsub!(%r!(?:<pre><code>)?syntax-temp-(\d+)(?:</code></pre>)?!){ syntax_items[$1.to_i] }
+end
+stat = File.stat(src)
+created = stat.ctime
+modified = stat.mtime
+
+$stdout << template.result(binding)
diff --git a/vendor/gems/composite_primary_keys-1.1.0/scripts/txt2js b/vendor/gems/composite_primary_keys-1.1.0/scripts/txt2js
new file mode 100644 (file)
index 0000000..4a287ca
--- /dev/null
@@ -0,0 +1,59 @@
+#!/usr/bin/env ruby
+
+require 'rubygems'
+require 'redcloth'
+require 'syntax/convertors/html'
+require 'erb'
+require 'active_support'
+require File.dirname(__FILE__) + '/../lib/composite_primary_keys/version.rb'
+
+version  = CompositePrimaryKeys::VERSION::STRING
+download = 'http://rubyforge.org/projects/compositekeys'
+
+class Fixnum
+  def ordinal
+    # teens
+    return 'th' if (10..19).include?(self % 100)
+    # others
+    case self % 10
+    when 1: return 'st'
+    when 2: return 'nd'
+    when 3: return 'rd'
+    else    return 'th'
+    end
+  end
+end
+
+class Time
+  def pretty
+    return "#{mday}#{mday.ordinal} #{strftime('%B')} #{year}"
+  end
+end
+
+def convert_syntax(syntax, source)
+  return Syntax::Convertors::HTML.for_syntax(syntax).convert(source).gsub(%r!^<pre>|</pre>$!,'')
+end
+
+if ARGV.length >= 1
+  src, template = ARGV
+  template ||= File.dirname(__FILE__) + '/../website/template.js'
+else
+  puts("Usage: #{File.split($0).last} source.txt [template.js] > output.html")
+  exit!
+end
+
+template = ERB.new(File.open(template).read)
+
+title = nil
+body = nil
+File.open(src) do |fsrc|
+  title_text = fsrc.readline
+  body_text = fsrc.read
+  title = RedCloth.new(title_text).to_html.gsub(%r!<.*?>!,'').strip
+  body = RedCloth.new(body_text)
+end
+stat = File.stat(src)
+created = stat.ctime
+modified = stat.mtime
+
+$stdout << template.result(binding)
diff --git a/vendor/gems/composite_primary_keys-1.1.0/tasks/activerecord_selection.rake b/vendor/gems/composite_primary_keys-1.1.0/tasks/activerecord_selection.rake
new file mode 100644 (file)
index 0000000..44ca4bb
--- /dev/null
@@ -0,0 +1,43 @@
+namespace :ar do
+  desc 'Pre-load edge rails ActiveRecord'
+  task :edge do
+    unless path = ENV['EDGE_RAILS_DIR'] || ENV['EDGE_RAILS']
+      puts <<-EOS
+
+Need to define env var EDGE_RAILS_DIR or EDGE_RAILS- root of edge rails on your machine.
+    i)  Get copy of Edge Rails - http://dev.rubyonrails.org
+    ii) Set EDGE_RAILS_DIR to this folder in local/paths.rb - see local/paths.rb.sample for example
+    or
+    a)  Set folder from environment or command line (rake ar:edge EDGE_RAILS_DIR=/path/to/rails)
+  
+      EOS
+      exit
+    end
+    
+    ENV['AR_LOAD_PATH'] = File.join(path, "activerecord/lib")
+  end
+  
+  desc 'Pre-load ActiveRecord using VERSION=X.Y.Z, instead of latest'
+  task :set do
+    unless version = ENV['VERSION']
+      puts <<-EOS
+Usage: rake ar:get_version VERSION=1.15.3
+    Specify the version number with VERSION=X.Y.Z; and make sure you have that activerecord gem version installed.
+    
+      EOS
+    end
+    version = nil if version == "" || version == []
+    begin
+      version ? gem('activerecord', version) : gem('activerecord')
+      require 'active_record'
+      ENV['AR_LOAD_PATH'] = $:.reverse.find { |path| /activerecord/ =~ path }
+    rescue LoadError
+      puts <<-EOS
+Missing: Cannot find activerecord #{version} installed.
+    Install: gem install activerecord -v #{version}
+    
+      EOS
+      exit
+    end
+  end
+end
\ No newline at end of file
diff --git a/vendor/gems/composite_primary_keys-1.1.0/tasks/databases.rake b/vendor/gems/composite_primary_keys-1.1.0/tasks/databases.rake
new file mode 100644 (file)
index 0000000..0d91517
--- /dev/null
@@ -0,0 +1,12 @@
+require 'active_record'
+
+# UNTESTED - firebird sqlserver sqlserver_odbc db2 sybase openbase
+for adapter in %w( mysql sqlite oracle oracle_enhanced postgresql ibm_db ) 
+  Rake::TestTask.new("test_#{adapter}") { |t|
+    t.libs << "test" << "test/connections/native_#{adapter}"
+    t.pattern = "test/test_*.rb"
+    t.verbose = true
+  }
+end
+
+SCHEMA_PATH = File.join(PROJECT_ROOT, *%w(test fixtures db_definitions))
diff --git a/vendor/gems/composite_primary_keys-1.1.0/tasks/databases/mysql.rake b/vendor/gems/composite_primary_keys-1.1.0/tasks/databases/mysql.rake
new file mode 100644 (file)
index 0000000..e05239e
--- /dev/null
@@ -0,0 +1,30 @@
+namespace :mysql do
+  desc 'Build the MySQL test databases'
+  task :build_databases => :load_connection do 
+    puts File.join(SCHEMA_PATH, 'mysql.sql')
+    options_str = ENV['cpk_adapter_options_str']
+    # creates something like "-u#{username} -p#{password} -S#{socket}"
+    sh %{ mysqladmin #{options_str} create "#{GEM_NAME}_unittest" }
+    sh %{ mysql #{options_str} "#{GEM_NAME}_unittest" < #{File.join(SCHEMA_PATH, 'mysql.sql')} }
+  end
+
+  desc 'Drop the MySQL test databases'
+  task :drop_databases => :load_connection do 
+    options_str = ENV['cpk_adapter_options_str']
+    sh %{ mysqladmin #{options_str} -f drop "#{GEM_NAME}_unittest" }
+  end
+
+  desc 'Rebuild the MySQL test databases'
+  task :rebuild_databases => [:drop_databases, :build_databases]
+  
+  task :load_connection do
+    require File.join(PROJECT_ROOT, %w[lib adapter_helper mysql])
+    spec = AdapterHelper::MySQL.load_connection_from_env
+    options = {}
+    options['u'] = spec[:username]  if spec[:username]
+    options['p'] = spec[:password]  if spec[:password]
+    options['S'] = spec[:sock]      if spec[:sock]
+    options_str = options.map { |key, value| "-#{key}#{value}" }.join(" ")
+    ENV['cpk_adapter_options_str'] = options_str
+  end
+end
diff --git a/vendor/gems/composite_primary_keys-1.1.0/tasks/databases/oracle.rake b/vendor/gems/composite_primary_keys-1.1.0/tasks/databases/oracle.rake
new file mode 100644 (file)
index 0000000..4861d01
--- /dev/null
@@ -0,0 +1,25 @@
+namespace :oracle do
+  desc 'Build the Oracle test databases'
+  task :build_databases => :load_connection do 
+    puts File.join(SCHEMA_PATH, 'oracle.sql')
+    options_str = ENV['cpk_adapter_options_str']
+    sh %( sqlplus #{options_str} < #{File.join(SCHEMA_PATH, 'oracle.sql')} )
+  end
+
+  desc 'Drop the Oracle test databases'
+  task :drop_databases => :load_connection do 
+    puts File.join(SCHEMA_PATH, 'oracle.drop.sql')
+    options_str = ENV['cpk_adapter_options_str']
+    sh %( sqlplus #{options_str} < #{File.join(SCHEMA_PATH, 'oracle.drop.sql')} )
+  end
+
+  desc 'Rebuild the Oracle test databases'
+  task :rebuild_databases => [:drop_databases, :build_databases]
+  
+  task :load_connection do
+    require File.join(PROJECT_ROOT, %w[lib adapter_helper oracle])
+    spec = AdapterHelper::Oracle.load_connection_from_env
+    ENV['cpk_adapter_options_str'] = "#{spec[:username]}/#{spec[:password]}@#{spec[:host]}"
+  end
+  
+end
diff --git a/vendor/gems/composite_primary_keys-1.1.0/tasks/databases/postgresql.rake b/vendor/gems/composite_primary_keys-1.1.0/tasks/databases/postgresql.rake
new file mode 100644 (file)
index 0000000..13b34e2
--- /dev/null
@@ -0,0 +1,26 @@
+namespace :postgresql do
+  desc 'Build the PostgreSQL test databases'
+  task :build_databases => :load_connection do 
+    sh %{ createdb "#{GEM_NAME}_unittest" }
+    sh %{ psql "#{GEM_NAME}_unittest" -f #{File.join(SCHEMA_PATH, 'postgresql.sql')} }
+  end
+
+  desc 'Drop the PostgreSQL test databases'
+  task :drop_databases => :load_connection do 
+    sh %{ dropdb "#{GEM_NAME}_unittest" }
+  end
+
+  desc 'Rebuild the PostgreSQL test databases'
+  task :rebuild_databases => [:drop_databases, :build_databases]
+
+  task :load_connection do
+    require File.join(PROJECT_ROOT, %w[lib adapter_helper postgresql])
+    spec = AdapterHelper::Postgresql.load_connection_from_env
+    options = {}
+    options['u'] = spec[:username]  if spec[:username]
+    options['p'] = spec[:password]  if spec[:password]
+    options_str = options.map { |key, value| "-#{key}#{value}" }.join(" ")
+    ENV['cpk_adapter_options_str'] = options_str
+  end
+end
+
diff --git a/vendor/gems/composite_primary_keys-1.1.0/tasks/databases/sqlite3.rake b/vendor/gems/composite_primary_keys-1.1.0/tasks/databases/sqlite3.rake
new file mode 100644 (file)
index 0000000..9a5579a
--- /dev/null
@@ -0,0 +1,28 @@
+namespace :sqlite3 do
+  desc 'Build the sqlite test databases'
+  task :build_databases => :load_connection do 
+    file = File.join(SCHEMA_PATH, 'sqlite.sql')
+    dbfile = File.join(PROJECT_ROOT, ENV['cpk_adapter_options_str'])
+    cmd = "mkdir -p #{File.dirname(dbfile)}"
+    puts cmd
+    sh %{ #{cmd} }
+    cmd = "sqlite3 #{dbfile} < #{file}"
+    puts cmd
+    sh %{ #{cmd} }
+  end
+
+  desc 'Drop the sqlite test databases'
+  task :drop_databases => :load_connection do 
+    dbfile = ENV['cpk_adapter_options_str']
+    sh %{ rm -f #{dbfile} }
+  end
+
+  desc 'Rebuild the sqlite test databases'
+  task :rebuild_databases => [:drop_databases, :build_databases]
+
+  task :load_connection do
+    require File.join(PROJECT_ROOT, %w[lib adapter_helper sqlite3])
+    spec = AdapterHelper::Sqlite3.load_connection_from_env
+    ENV['cpk_adapter_options_str'] = spec[:dbfile]
+  end
+end
diff --git a/vendor/gems/composite_primary_keys-1.1.0/tasks/deployment.rake b/vendor/gems/composite_primary_keys-1.1.0/tasks/deployment.rake
new file mode 100644 (file)
index 0000000..84f143b
--- /dev/null
@@ -0,0 +1,22 @@
+desc 'Release the website and new gem version'
+task :deploy => [:check_version, :website, :release] do
+  puts "Remember to create SVN tag:"
+  puts "svn copy svn+ssh://#{RUBYFORGE_USERNAME}@rubyforge.org/var/svn/#{PATH}/trunk " +
+    "svn+ssh://#{RUBYFORGE_USERNAME}@rubyforge.org/var/svn/#{PATH}/tags/REL-#{VERS} "
+  puts "Suggested comment:"
+  puts "Tagging release #{CHANGES}"
+end
+
+desc 'Runs tasks website_generate and install_gem as a local deployment of the gem'
+task :local_deploy => [:website_generate, :install_gem]
+
+task :check_version do
+  unless ENV['VERSION']
+    puts 'Must pass a VERSION=x.y.z release version'
+    exit
+  end
+  unless ENV['VERSION'] == VERS
+    puts "Please update your version.rb to match the release version, currently #{VERS}"
+    exit
+  end
+end
diff --git a/vendor/gems/composite_primary_keys-1.1.0/tasks/local_setup.rake b/vendor/gems/composite_primary_keys-1.1.0/tasks/local_setup.rake
new file mode 100644 (file)
index 0000000..1b8afa8
--- /dev/null
@@ -0,0 +1,13 @@
+namespace :local do
+  desc 'Copies over the same local files ready for editing'
+  task :setup do
+    sample_files = Dir[File.join(PROJECT_ROOT, "local/*.rb.sample")]
+    sample_files.each do |sample_file|
+      file = sample_file.sub(".sample","")
+      unless File.exists?(file)
+        puts "Copying #{sample_file} -> #{file}"
+        sh %{ cp #{sample_file} #{file} }
+      end
+    end
+  end
+end
\ No newline at end of file
diff --git a/vendor/gems/composite_primary_keys-1.1.0/tasks/website.rake b/vendor/gems/composite_primary_keys-1.1.0/tasks/website.rake
new file mode 100644 (file)
index 0000000..600f563
--- /dev/null
@@ -0,0 +1,18 @@
+desc 'Generate website files'
+task :website_generate do
+  sh %{ ruby scripts/txt2html website/index.txt > website/index.html }
+  sh %{ ruby scripts/txt2js website/version.txt > website/version.js }
+  sh %{ ruby scripts/txt2js website/version-raw.txt > website/version-raw.js }
+end
+
+desc 'Upload website files to rubyforge'
+task :website_upload do
+  config = YAML.load(File.read(File.expand_path("~/.rubyforge/user-config.yml")))
+  host = "#{config["username"]}@rubyforge.org"
+  remote_dir = "/var/www/gforge-projects/#{RUBYFORGE_PROJECT}/"
+  local_dir = 'website'
+  sh %{rsync -aCv #{local_dir}/ #{host}:#{remote_dir}}
+end
+
+desc 'Generate and upload website files'
+task :website => [:website_generate, :website_upload, :publish_docs]
diff --git a/vendor/gems/composite_primary_keys-1.1.0/test/README_tests.txt b/vendor/gems/composite_primary_keys-1.1.0/test/README_tests.txt
new file mode 100644 (file)
index 0000000..66fe21f
--- /dev/null
@@ -0,0 +1,67 @@
+= Composite Primary Keys - Testing Readme
+
+== Testing an adapter
+
+There are tests available for the following adapters:
+
+* ibmdb
+* mysql
+* oracle
+* postgresql
+* sqlite
+
+To run the tests for on of the adapters, follow these steps (using mysql in the example):
+
+* rake -T | grep mysql
+
+    rake mysql:build_databases         # Build the MySQL test databases
+    rake mysql:drop_databases          # Drop the MySQL test databases
+    rake mysql:rebuild_databases       # Rebuild the MySQL test databases
+    rake test_mysql                    # Run tests for test_mysql
+
+* rake mysql:build_databases
+* rake test_mysql
+
+== Testing against different ActiveRecord versions (or Edge Rails)
+
+ActiveRecord is a RubyGem within Rails, and is constantly being improved/changed on
+its repository (http://dev.rubyonrails.org). These changes may create errors for the CPK
+gem. So, we need a way to test CPK against Edge Rails, as well as officially released RubyGems.
+
+The default test (as above) uses the latest RubyGem in your cache.
+
+You can select an older RubyGem version by running the following:
+
+* rake ar:set VERSION=1.14.4 test_mysql
+
+== Edge Rails
+
+Before you can test CPK against Edge Rails, you must checkout a copy of edge rails somewhere (see http://dev.rubyonrails.org for for examples)
+
+* cd /path/to/gems
+* svn co http://svn.rubyonrails.org/rails/trunk rails
+
+Say the rails folder is /path/to/gems/rails
+
+Three ways to run CPK tests for Edge Rails:
+
+i)   Run:
+  
+        EDGE_RAILS_DIR=/path/to/gems/rails rake ar:edge test_mysql
+        
+ii)  In your .profile, set the environment variable EDGE_RAILS_DIR=/path/to/gems/rails, 
+     and once you reload your profile, run:  
+     
+        rake ar:edge test_mysql
+        
+iii) Store the path in local/paths.rb. Run:
+
+        cp local/paths.rb.sample local/paths.rb
+        # Now set ENV['EDGE_RAILS_DIR']=/path/to/gems/rails
+        rake ar:edge test_mysql
+
+These are all variations of the same theme:
+
+* Set the environment variable EDGE_RAILS_DIR to the path to Rails (which contains the activerecord/lib folder)
+* Run: rake ar:edge test_<adapter>
+  
diff --git a/vendor/gems/composite_primary_keys-1.1.0/test/abstract_unit.rb b/vendor/gems/composite_primary_keys-1.1.0/test/abstract_unit.rb
new file mode 100644 (file)
index 0000000..f33edfa
--- /dev/null
@@ -0,0 +1,94 @@
+$:.unshift(ENV['AR_LOAD_PATH']) if ENV['AR_LOAD_PATH']
+
+require 'test/unit'
+require 'hash_tricks'
+require 'rubygems'
+require 'active_record'
+require 'active_record/fixtures'
+begin
+  require 'connection'
+rescue MissingSourceFile => e
+  adapter = 'postgresql' #'sqlite'
+  require "#{File.dirname(__FILE__)}/connections/native_#{adapter}/connection"
+end
+require 'composite_primary_keys'
+
+QUOTED_TYPE = ActiveRecord::Base.connection.quote_column_name('type') unless Object.const_defined?(:QUOTED_TYPE)
+
+class Test::Unit::TestCase #:nodoc:
+  self.fixture_path = File.dirname(__FILE__) + "/fixtures/"
+  self.use_instantiated_fixtures = false
+  self.use_transactional_fixtures = true
+
+  def assert_date_from_db(expected, actual, message = nil)
+    # SQL Server doesn't have a separate column type just for dates, 
+    # so the time is in the string and incorrectly formatted
+    if current_adapter?(:SQLServerAdapter)
+      assert_equal expected.strftime("%Y/%m/%d 00:00:00"), actual.strftime("%Y/%m/%d 00:00:00")
+    elsif current_adapter?(:SybaseAdapter)
+      assert_equal expected.to_s, actual.to_date.to_s, message
+    else
+      assert_equal expected.to_s, actual.to_s, message
+    end
+  end
+
+  def assert_queries(num = 1)
+    ActiveRecord::Base.connection.class.class_eval do
+      self.query_count = 0
+      alias_method :execute, :execute_with_query_counting
+    end
+    yield
+  ensure
+    ActiveRecord::Base.connection.class.class_eval do
+      alias_method :execute, :execute_without_query_counting
+    end
+    assert_equal num, ActiveRecord::Base.connection.query_count, "#{ActiveRecord::Base.connection.query_count} instead of #{num} queries were executed."
+  end
+
+  def assert_no_queries(&block)
+    assert_queries(0, &block)
+  end
+  
+  cattr_accessor :classes
+protected
+  
+  def testing_with(&block)
+    classes.keys.each do |@key_test|
+      @klass_info = classes[@key_test]
+      @klass, @primary_keys = @klass_info[:class], @klass_info[:primary_keys]
+      order = @klass.primary_key.is_a?(String) ? @klass.primary_key : @klass.primary_key.join(',')
+      @first = @klass.find(:first, :order => order)
+      yield
+    end
+  end
+  
+  def first_id
+    ids = (1..@primary_keys.length).map {|num| 1}
+    composite? ? ids.to_composite_ids : ids.first
+  end
+  
+  def first_id_str
+    composite? ? first_id.join(CompositePrimaryKeys::ID_SEP) : first_id.to_s
+  end
+  
+  def composite?
+    @key_test != :single
+  end  
+end
+
+def current_adapter?(type)
+  ActiveRecord::ConnectionAdapters.const_defined?(type) &&
+    ActiveRecord::Base.connection.instance_of?(ActiveRecord::ConnectionAdapters.const_get(type))
+end
+
+ActiveRecord::Base.connection.class.class_eval do
+  cattr_accessor :query_count
+  alias_method :execute_without_query_counting, :execute
+  def execute_with_query_counting(sql, name = nil)
+    self.query_count += 1
+    execute_without_query_counting(sql, name)
+  end
+end
+
+#ActiveRecord::Base.logger = Logger.new(STDOUT)
+#ActiveRecord::Base.colorize_logging = false
diff --git a/vendor/gems/composite_primary_keys-1.1.0/test/connections/native_ibm_db/connection.rb b/vendor/gems/composite_primary_keys-1.1.0/test/connections/native_ibm_db/connection.rb
new file mode 100644 (file)
index 0000000..7a40f7c
--- /dev/null
@@ -0,0 +1,23 @@
+print "Using IBM2 \n"
+require 'logger'
+
+gem 'ibm_db'
+require 'IBM_DB'
+
+RAILS_CONNECTION_ADAPTERS = %w( mysql postgresql sqlite firebird sqlserver db2 oracle sybase openbase frontbase ibm_db )
+
+
+ActiveRecord::Base.logger = Logger.new("debug.log")
+
+db1 = 'composite_primary_keys_unittest'
+
+connection_options = {
+  :adapter  => "ibm_db",
+  :database => "ocdpdev",
+  :username => "db2inst1",
+  :password => "password",                      
+  :host => '192.168.2.21'
+}
+
+ActiveRecord::Base.configurations = { db1 => connection_options }
+ActiveRecord::Base.establish_connection(connection_options)
diff --git a/vendor/gems/composite_primary_keys-1.1.0/test/connections/native_mysql/connection.rb b/vendor/gems/composite_primary_keys-1.1.0/test/connections/native_mysql/connection.rb
new file mode 100644 (file)
index 0000000..12dbe4c
--- /dev/null
@@ -0,0 +1,13 @@
+print "Using native MySQL\n"
+require 'fileutils'
+require 'logger'
+require 'adapter_helper/mysql'
+
+log_path = File.expand_path(File.join(File.dirname(__FILE__), %w[.. .. .. log]))
+FileUtils.mkdir_p log_path
+puts "Logging to #{log_path}/debug.log"
+ActiveRecord::Base.logger = Logger.new("#{log_path}/debug.log")
+
+# Adapter config setup in locals/database_connections.rb
+connection_options = AdapterHelper::MySQL.load_connection_from_env
+ActiveRecord::Base.establish_connection(connection_options)
diff --git a/vendor/gems/composite_primary_keys-1.1.0/test/connections/native_oracle/connection.rb b/vendor/gems/composite_primary_keys-1.1.0/test/connections/native_oracle/connection.rb
new file mode 100644 (file)
index 0000000..383538f
--- /dev/null
@@ -0,0 +1,14 @@
+print "Using native Oracle\n"
+require 'fileutils'
+require 'logger'
+require 'adapter_helper/oracle'
+
+log_path = File.expand_path(File.join(File.dirname(__FILE__), %w[.. .. .. log]))
+FileUtils.mkdir_p log_path
+puts "Logging to #{log_path}/debug.log"
+ActiveRecord::Base.logger = Logger.new("#{log_path}/debug.log")
+
+# Adapter config setup in locals/database_connections.rb
+connection_options = AdapterHelper::Oracle.load_connection_from_env
+puts connection_options.inspect
+ActiveRecord::Base.establish_connection(connection_options)
diff --git a/vendor/gems/composite_primary_keys-1.1.0/test/connections/native_postgresql/connection.rb b/vendor/gems/composite_primary_keys-1.1.0/test/connections/native_postgresql/connection.rb
new file mode 100644 (file)
index 0000000..a2d93f9
--- /dev/null
@@ -0,0 +1,9 @@
+print "Using native Postgresql\n"
+require 'logger'
+require 'adapter_helper/postgresql'
+
+ActiveRecord::Base.logger = Logger.new("debug.log")
+
+# Adapter config setup in locals/database_connections.rb
+connection_options = AdapterHelper::Postgresql.load_connection_from_env
+ActiveRecord::Base.establish_connection(connection_options)
diff --git a/vendor/gems/composite_primary_keys-1.1.0/test/connections/native_sqlite/connection.rb b/vendor/gems/composite_primary_keys-1.1.0/test/connections/native_sqlite/connection.rb
new file mode 100644 (file)
index 0000000..7c6102e
--- /dev/null
@@ -0,0 +1,9 @@
+print "Using native Sqlite3\n"
+require 'logger'
+require 'adapter_helper/sqlite3'
+
+ActiveRecord::Base.logger = Logger.new("debug.log")
+
+# Adapter config setup in locals/database_connections.rb
+connection_options = AdapterHelper::Sqlite3.load_connection_from_env
+ActiveRecord::Base.establish_connection(connection_options)
diff --git a/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/article.rb b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/article.rb
new file mode 100644 (file)
index 0000000..7233f81
--- /dev/null
@@ -0,0 +1,5 @@
+class Article < ActiveRecord::Base\r
+  has_many :readings\r
+  has_many :users, :through => :readings\r
+end\r
+\r
diff --git a/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/articles.yml b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/articles.yml
new file mode 100644 (file)
index 0000000..e510604
--- /dev/null
@@ -0,0 +1,6 @@
+first:\r
+  id: 1\r
+  name: Article One\r
+second:\r
+  id: 2\r
+  name: Article Two
\ No newline at end of file
diff --git a/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/comment.rb b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/comment.rb
new file mode 100644 (file)
index 0000000..857bf70
--- /dev/null
@@ -0,0 +1,6 @@
+class Comment < ActiveRecord::Base
+  set_primary_keys :id
+  belongs_to :person, :polymorphic => true
+  belongs_to :hack
+end
+
diff --git a/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/comments.yml b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/comments.yml
new file mode 100644 (file)
index 0000000..7f14514
--- /dev/null
@@ -0,0 +1,16 @@
+comment1:
+  id: 1
+  person_id: 1
+  person_type: Employee
+  
+comment2:
+  id: 2
+  person_id: 1
+  person_type: User
+  hack_id: andrew
+  
+comment3:
+  id: 3
+  person_id: andrew
+  person_type: Hack
+  
\ No newline at end of file
diff --git a/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/db_definitions/db2-create-tables.sql b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/db_definitions/db2-create-tables.sql
new file mode 100644 (file)
index 0000000..edff930
--- /dev/null
@@ -0,0 +1,113 @@
+CREATE TABLE reference_types (
+  reference_type_id integer NOT NULL generated by default as identity (start with 100, increment by 1, no cache),  
+  type_label varchar(50) default NULL,  
+  abbreviation varchar(50) default NULL, 
+  description varchar(50) default NULL, 
+  PRIMARY KEY (reference_type_id)
+);
+
+CREATE TABLE reference_codes (
+  reference_type_id integer,
+  reference_code integer NOT NULL,
+  code_label varchar(50) default NULL,
+  abbreviation varchar(50) default NULL,
+  description varchar(50) default NULL,
+  PRIMARY KEY  (reference_type_id,reference_code)
+);
+
+CREATE TABLE products (
+  id integer NOT NULL,
+  name varchar(50) default NULL,
+  PRIMARY KEY  (id)
+);
+
+CREATE TABLE tariffs (
+  tariff_id integer NOT NULL,
+  start_date date NOT NULL,
+  amount integer default NULL,
+  PRIMARY KEY  (tariff_id,start_date)
+);
+
+CREATE TABLE product_tariffs (
+  product_id integer NOT NULL,
+  tariff_id integer NOT NULL,
+  tariff_start_date date NOT NULL,
+  PRIMARY KEY  (product_id,tariff_id,tariff_start_date)
+);
+
+CREATE TABLE suburbs (
+  city_id integer NOT NULL,
+  suburb_id integer NOT NULL,
+  name varchar(50) NOT NULL,
+  PRIMARY KEY  (city_id,suburb_id)
+);
+
+CREATE TABLE streets (
+  id integer NOT NULL ,
+  city_id integer NOT NULL,
+  suburb_id integer NOT NULL,
+  name varchar(50) NOT NULL,
+  PRIMARY KEY  (id)
+);
+
+CREATE TABLE users (
+  id integer NOT NULL ,
+  name varchar(50) NOT NULL,
+  PRIMARY KEY  (id)
+);
+
+CREATE TABLE articles (
+  id integer NOT NULL ,
+  name varchar(50) NOT NULL,
+  PRIMARY KEY  (id)
+);
+
+CREATE TABLE readings (
+  id integer NOT NULL ,
+  user_id integer NOT NULL,
+  article_id integer NOT NULL,
+  rating integer NOT NULL,
+  PRIMARY KEY  (id)
+);
+
+CREATE TABLE groups (
+  id integer NOT NULL ,
+  name varchar(50) NOT NULL,
+  PRIMARY KEY  (id)
+);               
+
+CREATE TABLE memberships (
+  user_id integer NOT NULL,
+  group_id integer NOT NULL,
+  PRIMARY KEY  (user_id,group_id)
+);
+
+CREATE TABLE membership_statuses (
+  id integer NOT NULL ,
+  user_id integer NOT NULL,
+  group_id integer NOT NULL,
+  status varchar(50) NOT NULL,
+  PRIMARY KEY (id)
+);
+
+create table kitchen_sinks (
+       id_1 integer not null,
+       id_2 integer not null,
+       a_date date,
+       a_string varchar(100),
+       primary key (id_1, id_2)
+);
+
+create table restaurants (
+       franchise_id integer not null,
+       store_id integer not null,
+       name varchar(100),
+       primary key (franchise_id, store_id)
+);
+
+create table restaurants_suburbs (
+       franchise_id integer not null,
+       store_id integer not null,
+       city_id integer not null,
+       suburb_id integer not null
+);
diff --git a/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/db_definitions/db2-drop-tables.sql b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/db_definitions/db2-drop-tables.sql
new file mode 100644 (file)
index 0000000..eb48433
--- /dev/null
@@ -0,0 +1,16 @@
+drop table MEMBERSHIPS;                                                                                                                    
+drop table REFERENCE_CODES;                                                                                                                 
+drop table TARIFFS;                                                                                                                         
+drop table ARTICLES;                                                                                                                        
+drop table GROUPS;                                                                                                                          
+drop table MEMBERSHIP_STATUSES;                                                                                                             
+drop table READINGS;                                                                                                                        
+drop table REFERENCE_TYPES;                                                                                                                 
+drop table STREETS;                                                                                                                         
+drop table PRODUCTS;                                                                                                                        
+drop table USERS;                                                                                                                           
+drop table SUBURBS;                                                                                                                         
+drop table PRODUCT_TARIFFS; 
+drop table KITCHEN_SINK;
+drop table RESTAURANTS;
+drop table RESTAURANTS_SUBURBS;
diff --git a/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/db_definitions/mysql.sql b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/db_definitions/mysql.sql
new file mode 100644 (file)
index 0000000..e83d3aa
--- /dev/null
@@ -0,0 +1,174 @@
+create table reference_types (
+    reference_type_id int(11) not null auto_increment,
+    type_label varchar(50) default null,
+    abbreviation varchar(50) default null,
+    description varchar(50) default null,
+    primary key (reference_type_id)
+) type=InnoDB;
+
+create table reference_codes (
+    reference_type_id int(11),
+    reference_code int(11) not null,
+    code_label varchar(50) default null,
+    abbreviation varchar(50) default null,
+    description varchar(50) default null,
+    primary key (reference_type_id, reference_code)
+) type=InnoDB;
+
+create table products (
+    id int(11) not null auto_increment,
+    name varchar(50) default null,
+    primary key (id)
+) type=InnoDB;
+
+create table tariffs (
+    tariff_id int(11) not null,
+    start_date date not null,
+    amount integer(11) default null,
+    primary key (tariff_id, start_date)
+) type=InnoDB;
+
+create table product_tariffs (
+    product_id int(11) not null,
+    tariff_id int(11) not null,
+    tariff_start_date date not null,
+    primary key (product_id, tariff_id, tariff_start_date)
+) type=InnoDB;
+
+create table suburbs (
+    city_id int(11) not null,
+    suburb_id int(11) not null,
+    name varchar(50) not null,
+    primary key (city_id, suburb_id)
+) type=InnoDB;
+
+create table streets (
+    id int(11) not null auto_increment,
+    city_id int(11) not null,
+    suburb_id int(11) not null,
+    name varchar(50) not null,
+    primary key (id)
+) type=InnoDB;
+
+create table users (
+    id int(11) not null auto_increment,
+    name varchar(50) not null,
+    primary key (id)
+) type=InnoDB;
+
+create table articles (
+    id int(11) not null auto_increment,
+    name varchar(50) not null,
+    primary key (id)
+) type=InnoDB;
+
+create table readings (
+    id int(11) not null auto_increment,
+    user_id int(11) not null,
+    article_id int(11) not null,
+    rating int(11) not null,
+    primary key (id)
+) type=InnoDB;
+
+create table groups (
+    id int(11) not null auto_increment,
+    name varchar(50) not null,
+    primary key (id)
+) type=InnoDB;
+
+create table memberships (
+    user_id int(11) not null,
+    group_id int(11) not null,
+    primary key  (user_id,group_id)
+) type=InnoDB;
+
+create table membership_statuses (
+    id int(11) not null auto_increment,
+    user_id int(11) not null,
+    group_id int(11) not null,
+    status varchar(50) not null,
+    primary key (id)
+) type=InnoDB;
+
+create table departments (
+    department_id int(11) not null,
+    location_id int(11) not null,
+    primary key (department_id, location_id)
+) type=InnoDB;
+
+create table employees (
+    id int(11) not null auto_increment,
+    department_id int(11) default null,
+    location_id int(11) default null,
+    primary key (id)
+) type=InnoDB;
+
+create table comments (
+    id int(11) not null auto_increment,
+    person_id varchar(100) default null,
+    person_type varchar(100) default null,
+    hack_id varchar(100) default null,
+    primary key (id)
+) type=InnoDB;
+
+create table hacks (
+    name varchar(50) not null,
+    primary key (name)
+) type=InnoDB;
+
+create table kitchen_sinks (
+    id_1 int(11) not null,
+    id_2 int(11) not null,
+    a_date date,
+    a_string varchar(100),
+    primary key (id_1, id_2)
+) type=InnoDB;
+
+create table restaurants (
+    franchise_id int(11) not null,
+    store_id int(11) not null,
+    name varchar(100),
+    primary key (franchise_id, store_id)
+) type=InnoDB;
+
+create table restaurants_suburbs (
+    franchise_id int(11) not null,
+    store_id int(11) not null,
+    city_id int(11) not null,
+    suburb_id int(11) not null
+) type=InnoDB;
+
+create table dorms (
+    id int(11) not null auto_increment,
+    primary key(id)
+) type=InnoDB;
+
+create table rooms (
+    dorm_id int(11) not null,
+    room_id int(11) not null,
+    primary key (dorm_id, room_id)
+) type=InnoDB;
+
+create table room_attributes (
+    id int(11) not null auto_increment,
+    name varchar(50),
+    primary key(id)
+) type=InnoDB;
+
+create table room_attribute_assignments (
+    dorm_id int(11) not null,
+    room_id int(11) not null,
+    room_attribute_id int(11) not null
+) type=InnoDB;
+
+create table students (
+    id int(11) not null auto_increment,
+    primary key(id)
+) type=InnoDB;
+
+create table room_assignments (
+    student_id int(11) not null,
+    dorm_id int(11) not null,
+    room_id int(11) not null
+) type=InnoDB;
+
diff --git a/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/db_definitions/oracle.drop.sql b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/db_definitions/oracle.drop.sql
new file mode 100644 (file)
index 0000000..d23e3a3
--- /dev/null
@@ -0,0 +1,39 @@
+drop table reference_types;
+drop sequence reference_types_seq;
+drop table reference_codes;
+drop table products;
+drop sequence products_seq;
+drop table tariffs;
+drop table product_tariffs;
+drop table suburbs;
+drop table streets;
+drop sequence streets_seq;
+drop table users;
+drop sequence users_seq;
+drop table articles;
+drop sequence articles_seq;
+drop table readings;
+drop sequence readings_seq;
+drop table groups;
+drop sequence groups_seq;
+drop table memberships;
+drop table membership_statuses;
+drop sequence membership_statuses_seq;
+drop table departments;
+drop table employees;
+drop sequence employees_seq;
+drop table comments;
+drop sequence comments_seq;
+drop table hacks;
+drop table kitchen_sinks;
+drop table restaurants;
+drop table restaurants_suburbs;
+drop table dorms;
+drop sequence dorms_seq;
+drop table rooms;
+drop table room_attributes;
+drop sequence room_attributes_seq;
+drop table room_attribute_assignments;
+drop table room_assignments;
+drop table students;
+drop sequence students_seq;
diff --git a/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/db_definitions/oracle.sql b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/db_definitions/oracle.sql
new file mode 100644 (file)
index 0000000..8db0ff2
--- /dev/null
@@ -0,0 +1,188 @@
+create sequence reference_types_seq start with 1000;
+
+create table reference_types (
+    reference_type_id number(11)   primary key,
+    type_label        varchar2(50) default null,
+    abbreviation      varchar2(50) default null,
+    description       varchar2(50) default null
+);
+
+create table reference_codes (
+    reference_type_id number(11),
+    reference_code    number(11),
+    code_label        varchar2(50) default null,
+    abbreviation      varchar2(50) default null,
+    description       varchar2(50) default null
+);
+
+create sequence products_seq start with 1000;
+
+create table products (
+    id   number(11)   primary key,
+    name varchar2(50) default null
+);
+
+create table tariffs (
+    tariff_id  number(11),
+    start_date date,
+    amount     number(11) default null,
+    constraint tariffs_pk primary key (tariff_id, start_date)
+);
+
+create table product_tariffs (
+    product_id        number(11),
+    tariff_id         number(11),
+    tariff_start_date date,
+    constraint product_tariffs_pk primary key (product_id, tariff_id, tariff_start_date)
+);
+
+create table suburbs (
+    city_id   number(11),
+    suburb_id number(11),
+    name      varchar2(50) not null,
+    constraint suburbs_pk primary key (city_id, suburb_id)
+);
+
+create sequence streets_seq start with 1000;
+
+create table streets (
+    id        number(11)   primary key,
+    city_id   number(11)   not null,
+    suburb_id number(11)   not null,
+    name      varchar2(50) not null
+);
+
+create sequence users_seq start with 1000;
+
+create table users (
+    id   number(11)   primary key,
+    name varchar2(50) not null
+);
+
+create sequence articles_seq start with 1000;
+
+create table articles (
+    id   number(11)   primary key,
+    name varchar2(50) not null
+);
+
+create sequence readings_seq start with 1000;
+
+create table readings (
+    id         number(11) primary key,
+    user_id    number(11) not null,
+    article_id number(11) not null,
+    rating     number(11) not null
+);
+
+create sequence groups_seq start with 1000;
+
+create table groups (
+    id   number(11)   primary key,
+    name varchar2(50) not null
+);
+
+create table memberships (
+    user_id  number(11) not null,
+    group_id number(11) not null,
+    constraint memberships_pk primary key (user_id, group_id)
+);
+
+create sequence membership_statuses_seq start with 1000;
+
+create table membership_statuses (
+    id       number(11)   primary key,
+    user_id  number(11)   not null,
+    group_id number(11)   not null,
+    status   varchar2(50) not null
+);
+
+create table departments (
+    department_id number(11) not null,
+    location_id   number(11) not null,
+    constraint departments_pk primary key (department_id, location_id)
+);
+
+create sequence employees_seq start with 1000;
+
+create table employees (
+    id            number(11) not null primary key,
+    department_id number(11) default null,
+    location_id   number(11) default null
+);
+
+create sequence comments_seq start with 1000;
+
+create table comments (
+    id          number(11)   not null primary key,
+    person_id   varchar(100) default null,
+    person_type varchar(100) default null,
+    hack_id     varchar(100) default null
+);
+
+create table hacks (
+    name varchar(50) not null primary key
+);
+
+create table kitchen_sinks (
+    id_1   number(11) not null,
+    id_2   number(11) not null,
+    a_date date,
+    a_string varchar(100),
+    constraint kitchen_sinks_pk primary key (id_1, id_2)
+);
+
+create table restaurants (
+    franchise_id number(11) not null,
+    store_id     number(11) not null,
+    name         varchar(100),
+    constraint restaurants_pk primary key (franchise_id, store_id)
+);
+
+create table restaurants_suburbs (
+    franchise_id number(11) not null,
+    store_id     number(11) not null,
+    city_id      number(11) not null,
+    suburb_id    number(11) not null
+);
+
+create sequence dorms_seq start with 1000;
+
+create table dorms (
+    id number(11) not null,
+    constraint dorms_pk primary key (id)
+);
+
+create table rooms (
+    dorm_id number(11) not null,
+    room_id number(11) not null,
+    constraint rooms_pk primary key (dorm_id, room_id)
+);
+
+create sequence room_attributes_seq start with 1000;
+
+create table room_attributes (
+    id   number(11) not null,
+    name varchar(50),
+    constraint room_attributes_pk primary key (id)
+);
+
+create table room_attribute_assignments (
+    dorm_id           number(11) not null,
+    room_id           number(11) not null,
+    room_attribute_id number(11) not null
+);
+
+create sequence students_seq start with 1000;
+
+create table students (
+    id number(11) not null,
+    constraint students_pk primary key (id)
+);
+
+create table room_assignments (
+    student_id number(11) not null,
+    dorm_id    number(11) not null,
+    room_id    number(11) not null
+);
+
diff --git a/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/db_definitions/postgresql.sql b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/db_definitions/postgresql.sql
new file mode 100644 (file)
index 0000000..c3ca788
--- /dev/null
@@ -0,0 +1,199 @@
+create sequence public.reference_types_seq start 1000;
+
+create table reference_types (
+    reference_type_id int         default nextval('public.reference_types_seq'),
+    type_label        varchar(50) default null,
+    abbreviation      varchar(50) default null,
+    description       varchar(50) default null,
+    primary key (reference_type_id)
+);
+
+create table reference_codes (
+    reference_type_id int,
+    reference_code    int         not null,
+    code_label        varchar(50) default null,
+    abbreviation      varchar(50) default null,
+    description       varchar(50) default null
+);
+
+create sequence public.products_seq start 1000;
+
+create table products (
+    id   int         not null default nextval('public.products_seq'),
+    name varchar(50) default null,
+    primary key (id)
+);
+
+create table tariffs (
+    tariff_id  int  not null,
+    start_date date not null,
+    amount     int  default null,
+    primary key (tariff_id, start_date)
+);
+
+create table product_tariffs (
+    product_id        int  not null,
+    tariff_id         int  not null,
+    tariff_start_date date not null,
+    primary key (product_id, tariff_id, tariff_start_date)
+);
+
+create table suburbs (
+    city_id   int         not null,
+    suburb_id int         not null,
+    name      varchar(50) not null,
+    primary key (city_id, suburb_id)
+);
+
+create sequence public.streets_seq start 1000;
+
+create table streets (
+    id        int         not null default nextval('public.streets_seq'),
+    city_id   int         not null,
+    suburb_id int         not null,
+    name      varchar(50) not null,
+    primary key (id)
+);
+
+create sequence public.users_seq start 1000;
+
+create table users (
+    id   int         not null default nextval('public.users_seq'),
+    name varchar(50) not null,
+    primary key (id)
+);
+
+create sequence public.articles_seq start 1000;
+
+create table articles (
+    id   int         not null default nextval('public.articles_seq'),
+    name varchar(50) not null,
+    primary key (id)
+);
+
+create sequence public.readings_seq start 1000;
+
+create table readings (
+    id         int not null default nextval('public.readings_seq'),
+    user_id    int not null,
+    article_id int not null,
+    rating     int not null,
+    primary key (id)
+);
+
+create sequence public.groups_seq start 1000;
+
+create table groups (
+    id   int         not null default nextval('public.groups_seq'),
+    name varchar(50) not null,
+    primary key (id)
+);
+
+create table memberships (
+    user_id  int not null,
+    group_id int not null,
+    primary key (user_id, group_id)
+);
+
+create sequence public.membership_statuses_seq start 1000;
+
+create table membership_statuses (
+    id       int         not null default nextval('public.membership_statuses_seq'),
+    user_id  int         not null,
+    group_id int         not null,
+    status   varchar(50) not null,
+    primary key (id)
+);
+
+create table departments (
+    department_id int not null,
+    location_id   int not null,
+    primary key (department_id, location_id)
+);
+
+create sequence public.employees_seq start 1000;
+
+create table employees (
+    id            int not null default nextval('public.employees_seq'),
+    department_id int default null,
+    location_id   int default null,
+    primary key (id)
+);
+
+create sequence public.comments_seq start 1000;
+
+create table comments (
+    id          int          not null default nextval('public.comments_seq'),
+    person_id   varchar(100) default null,
+    person_type varchar(100) default null,
+    hack_id     varchar(100) default null,
+    primary key (id)
+);
+
+create table hacks (
+    name varchar(50) not null,
+    primary key (name)
+);
+
+create table kitchen_sinks (
+    id_1   int not null,
+    id_2   int not null,
+    a_date date,
+    a_string varchar(100),
+    primary key (id_1, id_2)
+);
+
+create table restaurants (
+    franchise_id int not null,
+    store_id     int not null,
+    name         varchar(100),
+    primary key (franchise_id, store_id)
+);
+
+create table restaurants_suburbs (
+    franchise_id int not null,
+    store_id     int not null,
+    city_id      int not null,
+    suburb_id    int not null
+);
+
+create sequence public.dorms_seq start 1000;
+
+create table dorms (
+    id int not null default nextval('public.dorms_seq'),
+    primary key (id)
+);
+
+create table rooms (
+    dorm_id int not null,
+    room_id int not null,
+    primary key (dorm_id, room_id)
+);
+
+create sequence public.room_attributes_seq start 1000;
+
+create table room_attributes (
+    id   int not null default nextval('public.room_attributes_seq'),
+    name varchar(50),
+    primary key (id)
+);
+
+create table room_attribute_assignments (
+    dorm_id           int not null,
+    room_id           int not null,
+    room_attribute_id int not null
+);
+
+create sequence public.students_seq start 1000;
+
+create table students (
+    id int not null default nextval('public.students_seq'),
+    primary key (id)
+);
+
+create table room_assignments (
+    student_id int not null,
+    dorm_id    int not null,
+    room_id    int not null
+);
+
diff --git a/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/db_definitions/sqlite.sql b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/db_definitions/sqlite.sql
new file mode 100644 (file)
index 0000000..fd8c566
--- /dev/null
@@ -0,0 +1,160 @@
+create table reference_types (
+    reference_type_id integer primary key,
+    type_label varchar(50) default null,
+    abbreviation varchar(50) default null,
+    description varchar(50) default null
+);
+
+create table reference_codes (
+    reference_type_id int(11),
+    reference_code int(11) not null,
+    code_label varchar(50) default null,
+    abbreviation varchar(50) default null,
+    description varchar(50) default null,
+    primary key (reference_type_id, reference_code)
+);
+
+create table products (
+    id int(11) not null primary key,
+    name varchar(50) default null
+);
+
+create table tariffs (
+    tariff_id int(11) not null,
+    start_date date not null,
+    amount integer(11) default null,
+    primary key (tariff_id, start_date)
+);
+
+create table product_tariffs (
+    product_id int(11) not null,
+    tariff_id int(11) not null,
+    tariff_start_date date not null,
+    primary key (product_id, tariff_id, tariff_start_date)
+);
+
+create table suburbs (
+    city_id int(11) not null,
+    suburb_id int(11) not null,
+    name varchar(50) not null,
+    primary key (city_id, suburb_id)
+);
+
+create table streets (
+    id integer not null primary key autoincrement,
+    city_id int(11) not null,
+    suburb_id int(11) not null,
+    name varchar(50) not null
+);
+
+create table users (
+    id integer not null primary key autoincrement,
+    name varchar(50) not null
+);
+
+create table articles (
+    id integer not null primary key autoincrement,
+    name varchar(50) not null
+);
+
+create table readings (
+    id integer not null primary key autoincrement,
+    user_id int(11) not null,
+    article_id int(11) not null,
+    rating int(11) not null
+);
+
+create table groups (
+    id integer not null primary key autoincrement,
+    name varchar(50) not null
+);
+
+create table memberships (
+    user_id int not null,
+    group_id int not null,
+    primary key (user_id, group_id)
+);
+
+create table membership_statuses (
+    id integer not null primary key autoincrement,
+    user_id int not null,
+    group_id int not null,
+       status varchar(50) not null
+);
+
+create table departments (
+    department_id integer not null,
+    location_id integer not null,
+    primary key (department_id, location_id)
+);
+
+create table employees (
+    id integer not null primary key autoincrement,
+    department_id integer null,
+    location_id integer null
+);
+
+create table comments (
+       id integer not null primary key autoincrement,
+       person_id varchar(100) null,
+       person_type varchar(100) null,
+       hack_id varchar(100) null
+);
+
+create table hacks (
+    name varchar(50) not null primary key
+);
+
+create table kitchen_sinks (
+       id_1 integer not null,
+       id_2 integer not null,
+       a_date date,
+       a_string varchar(100),
+       primary key (id_1, id_2)
+);
+
+create table restaurants (
+       franchise_id integer not null,
+       store_id integer not null,
+       name varchar(100),
+       primary key (franchise_id, store_id)
+);
+
+create table restaurants_suburbs (
+       franchise_id integer not null,
+       store_id integer not null,
+       city_id integer not null,
+       suburb_id integer not null
+);
+
+create table dorms (
+       id integer not null primary key autoincrement
+);
+
+create table rooms (
+       dorm_id integer not null,
+       room_id integer not null,
+       primary key (dorm_id, room_id)
+);
+
+create table room_attributes (
+       id integer not null primary key autoincrement,
+       name varchar(50)
+);
+
+create table room_attribute_assignments (
+       dorm_id integer not null,
+       room_id integer not null,
+       room_attribute_id integer not null
+);
+
+create table students (
+       id integer not null primary key autoincrement
+);
+
+create table room_assignments (
+       student_id integer not null,
+       dorm_id integer not null,
+       room_id integer not null        
+);
+
diff --git a/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/department.rb b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/department.rb
new file mode 100644 (file)
index 0000000..a76eaf3
--- /dev/null
@@ -0,0 +1,5 @@
+class Department < ActiveRecord::Base
+  # set_primary_keys *keys - turns on composite key functionality
+  set_primary_keys :department_id, :location_id
+  has_many :employees, :foreign_key => [:department_id, :location_id]
+end
diff --git a/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/departments.yml b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/departments.yml
new file mode 100644 (file)
index 0000000..4213244
--- /dev/null
@@ -0,0 +1,3 @@
+department1-cpk:
+  department_id: 1
+  location_id: 1
diff --git a/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/employee.rb b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/employee.rb
new file mode 100644 (file)
index 0000000..2b47e09
--- /dev/null
@@ -0,0 +1,4 @@
+class Employee < ActiveRecord::Base
+       belongs_to :department, :foreign_key => [:department_id, :location_id]
+       has_many :comments, :as => :person
+end
diff --git a/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/employees.yml b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/employees.yml
new file mode 100644 (file)
index 0000000..c2efd83
--- /dev/null
@@ -0,0 +1,9 @@
+employee1:
+  id: 1
+  department_id: 1
+  location_id: 1
+employee2:
+  id: 2
+  department_id: 1
+  location_id: 1
+
diff --git a/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/group.rb b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/group.rb
new file mode 100644 (file)
index 0000000..889ee2f
--- /dev/null
@@ -0,0 +1,3 @@
+class Group < ActiveRecord::Base
+  has_many :memberships
+end
\ No newline at end of file
diff --git a/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/groups.yml b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/groups.yml
new file mode 100644 (file)
index 0000000..a15185e
--- /dev/null
@@ -0,0 +1,3 @@
+cpk:
+  id: 1
+  name: Composite Primary Keys
\ No newline at end of file
diff --git a/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/hack.rb b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/hack.rb
new file mode 100644 (file)
index 0000000..71d6cac
--- /dev/null
@@ -0,0 +1,6 @@
+class Hack < ActiveRecord::Base
+  set_primary_keys :name
+  has_many :comments, :as => :person
+  
+  has_one :first_comment, :as => :person, :class_name => "Comment"
+end
\ No newline at end of file
diff --git a/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/hacks.yml b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/hacks.yml
new file mode 100644 (file)
index 0000000..29f67b1
--- /dev/null
@@ -0,0 +1,2 @@
+andrew:
+  name: andrew
\ No newline at end of file
diff --git a/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/membership.rb b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/membership.rb
new file mode 100644 (file)
index 0000000..d5111e9
--- /dev/null
@@ -0,0 +1,7 @@
+class Membership < ActiveRecord::Base
+  # set_primary_keys *keys - turns on composite key functionality
+  set_primary_keys :user_id, :group_id
+  belongs_to :user
+       belongs_to :group
+       has_many :statuses, :class_name => 'MembershipStatus', :foreign_key => [:user_id, :group_id]
+end
\ No newline at end of file
diff --git a/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/membership_status.rb b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/membership_status.rb
new file mode 100644 (file)
index 0000000..54b687c
--- /dev/null
@@ -0,0 +1,3 @@
+class MembershipStatus < ActiveRecord::Base
+       belongs_to :membership, :foreign_key => [:user_id, :group_id]
+end
\ No newline at end of file
diff --git a/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/membership_statuses.yml b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/membership_statuses.yml
new file mode 100644 (file)
index 0000000..d3f3c30
--- /dev/null
@@ -0,0 +1,10 @@
+santiago-cpk:
+  id: 1
+  user_id: 1
+  group_id: 1
+  status: Active
+drnic-cpk:
+  id: 2
+  user_id: 2
+  group_id: 1
+  status: Owner
\ No newline at end of file
diff --git a/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/memberships.yml b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/memberships.yml
new file mode 100644 (file)
index 0000000..f6cdc84
--- /dev/null
@@ -0,0 +1,6 @@
+santiago-cpk:
+  user_id: 1
+  group_id: 1
+drnic-cpk:
+  user_id: 2
+  group_id: 1
\ No newline at end of file
diff --git a/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/product.rb b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/product.rb
new file mode 100644 (file)
index 0000000..5466dca
--- /dev/null
@@ -0,0 +1,7 @@
+class Product < ActiveRecord::Base\r
+       set_primary_keys :id  # redundant\r
+       has_many :product_tariffs, :foreign_key => :product_id\r
+       has_one :product_tariff, :foreign_key => :product_id\r
+\r
+       has_many :tariffs, :through => :product_tariffs, :foreign_key => [:tariff_id, :tariff_start_date]\r
+end\r
diff --git a/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/product_tariff.rb b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/product_tariff.rb
new file mode 100644 (file)
index 0000000..cbabee7
--- /dev/null
@@ -0,0 +1,5 @@
+class ProductTariff < ActiveRecord::Base\r
+       set_primary_keys :product_id, :tariff_id, :tariff_start_date\r
+       belongs_to :product, :foreign_key => :product_id\r
+       belongs_to :tariff,  :foreign_key => [:tariff_id, :tariff_start_date]\r
+end\r
diff --git a/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/product_tariffs.yml b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/product_tariffs.yml
new file mode 100644 (file)
index 0000000..27a464f
--- /dev/null
@@ -0,0 +1,12 @@
+first_flat:\r
+  product_id: 1\r
+  tariff_id: 1\r
+  tariff_start_date: <%= Date.today.to_s(:db) %>\r
+first_free:  \r
+  product_id: 1\r
+  tariff_id: 2\r
+  tariff_start_date: <%= Date.today.to_s(:db) %>\r
+second_free:\r
+  product_id: 2\r
+  tariff_id: 2\r
+  tariff_start_date: <%= Date.today.to_s(:db) %>\r
diff --git a/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/products.yml b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/products.yml
new file mode 100644 (file)
index 0000000..3c38a5b
--- /dev/null
@@ -0,0 +1,6 @@
+first_product:\r
+  id: 1\r
+  name: Product One\r
+second_product:\r
+  id: 2\r
+  name: Product Two
\ No newline at end of file
diff --git a/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/reading.rb b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/reading.rb
new file mode 100644 (file)
index 0000000..2e81970
--- /dev/null
@@ -0,0 +1,4 @@
+class Reading < ActiveRecord::Base\r
+  belongs_to :article\r
+  belongs_to :user\r
+end \r
diff --git a/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/readings.yml b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/readings.yml
new file mode 100644 (file)
index 0000000..e3afaa9
--- /dev/null
@@ -0,0 +1,10 @@
+santiago_first:\r
+  id: 1\r
+  user_id: 1\r
+  article_id: 1\r
+  rating: 4\r
+santiago_second:\r
+  id: 2\r
+  user_id: 1\r
+  article_id: 2\r
+  rating: 5
\ No newline at end of file
diff --git a/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/reference_code.rb b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/reference_code.rb
new file mode 100644 (file)
index 0000000..594d8d8
--- /dev/null
@@ -0,0 +1,7 @@
+class ReferenceCode < ActiveRecord::Base\r
+  set_primary_keys :reference_type_id, :reference_code\r
+  \r
+  belongs_to :reference_type, :foreign_key => "reference_type_id"\r
+  \r
+  validates_presence_of :reference_code, :code_label, :abbreviation\r
+end\r
diff --git a/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/reference_codes.yml b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/reference_codes.yml
new file mode 100644 (file)
index 0000000..3979381
--- /dev/null
@@ -0,0 +1,28 @@
+name_prefix_mr:\r
+  reference_type_id: 1\r
+  reference_code: 1\r
+  code_label: MR\r
+  abbreviation: Mr\r
+name_prefix_mrs:\r
+  reference_type_id: 1\r
+  reference_code: 2\r
+  code_label: MRS\r
+  abbreviation: Mrs\r
+name_prefix_ms:\r
+  reference_type_id: 1\r
+  reference_code: 3\r
+  code_label: MS\r
+  abbreviation: Ms\r
+  \r
+gender_male:\r
+  reference_type_id: 2\r
+  reference_code: 1\r
+  code_label: MALE\r
+  abbreviation: Male\r
+gender_female:\r
+  reference_type_id: 2\r
+  reference_code: 2\r
+  code_label: FEMALE\r
+  abbreviation: Female\r
+\r
+  
\ No newline at end of file
diff --git a/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/reference_type.rb b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/reference_type.rb
new file mode 100644 (file)
index 0000000..5b2b12b
--- /dev/null
@@ -0,0 +1,7 @@
+class ReferenceType < ActiveRecord::Base\r
+  set_primary_key :reference_type_id\r
+  has_many :reference_codes, :foreign_key => "reference_type_id"\r
+  \r
+  validates_presence_of :type_label, :abbreviation\r
+  validates_uniqueness_of :type_label\r
+end\r
diff --git a/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/reference_types.yml b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/reference_types.yml
new file mode 100644 (file)
index 0000000..0520ba9
--- /dev/null
@@ -0,0 +1,9 @@
+name_prefix:\r
+  reference_type_id: 1\r
+  type_label: NAME_PREFIX\r
+  abbreviation: Name Prefix\r
+\r
+gender:\r
+  reference_type_id: 2\r
+  type_label: GENDER\r
+  abbreviation: Gender\r
diff --git a/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/street.rb b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/street.rb
new file mode 100644 (file)
index 0000000..de92917
--- /dev/null
@@ -0,0 +1,3 @@
+class Street < ActiveRecord::Base
+  belongs_to :suburb,  :foreign_key => [:city_id, :suburb_id]
+end
\ No newline at end of file
diff --git a/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/streets.yml b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/streets.yml
new file mode 100644 (file)
index 0000000..38998c4
--- /dev/null
@@ -0,0 +1,15 @@
+first:
+  id: 1
+  city_id: 1
+  suburb_id: 1
+  name: First Street
+second1:
+  id: 2
+  city_id: 2
+  suburb_id: 1
+  name: First Street
+second2:
+  id: 3
+  city_id: 2
+  suburb_id: 1
+  name: Second Street
\ No newline at end of file
diff --git a/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/suburb.rb b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/suburb.rb
new file mode 100644 (file)
index 0000000..9304535
--- /dev/null
@@ -0,0 +1,6 @@
+class Suburb < ActiveRecord::Base\r
+  set_primary_keys :city_id, :suburb_id\r
+  has_many :streets,  :foreign_key => [:city_id, :suburb_id]\r
+  has_many :first_streets,  :foreign_key => [:city_id, :suburb_id], \r
+          :class_name => 'Street', :conditions => "streets.name = 'First Street'"\r
+end
\ No newline at end of file
diff --git a/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/suburbs.yml b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/suburbs.yml
new file mode 100644 (file)
index 0000000..efae0c0
--- /dev/null
@@ -0,0 +1,9 @@
+first:\r
+  city_id: 1\r
+  suburb_id: 1\r
+  name: First Suburb\r
+second:\r
+  city_id: 2\r
+  suburb_id: 1\r
+  name: Second Suburb\r
+  
\ No newline at end of file
diff --git a/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/tariff.rb b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/tariff.rb
new file mode 100644 (file)
index 0000000..d5cb07d
--- /dev/null
@@ -0,0 +1,6 @@
+class Tariff < ActiveRecord::Base\r
+       set_primary_keys [:tariff_id, :start_date]\r
+       has_many :product_tariffs, :foreign_key => [:tariff_id, :tariff_start_date]\r
+       has_one :product_tariff, :foreign_key => [:tariff_id, :tariff_start_date]\r
+       has_many :products, :through => :product_tariffs, :foreign_key => [:tariff_id, :tariff_start_date]\r
+end\r
diff --git a/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/tariffs.yml b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/tariffs.yml
new file mode 100644 (file)
index 0000000..7346fc5
--- /dev/null
@@ -0,0 +1,13 @@
+flat:\r
+  tariff_id: 1\r
+  start_date: <%= Date.today.to_s(:db) %>\r
+  amount: 50\r
+free:\r
+  tariff_id: 2\r
+  start_date: <%= Date.today.to_s(:db) %>\r
+  amount: 0\r
+flat_future:\r
+  tariff_id: 1\r
+  start_date: <%= Date.today.next.to_s(:db) %>\r
+  amount: 100\r
+  
\ No newline at end of file
diff --git a/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/user.rb b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/user.rb
new file mode 100644 (file)
index 0000000..a8487c4
--- /dev/null
@@ -0,0 +1,10 @@
+class User < ActiveRecord::Base\r
+  has_many :readings\r
+  has_many :articles, :through => :readings\r
+  has_many :comments, :as => :person\r
+  has_many :hacks, :through => :comments, :source => :hack\r
+  \r
+  def find_custom_articles\r
+    articles.find(:all, :conditions => ["name = ?", "Article One"])\r
+  end\r
+end\r
diff --git a/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/users.yml b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/users.yml
new file mode 100644 (file)
index 0000000..d33a38a
--- /dev/null
@@ -0,0 +1,6 @@
+santiago:\r
+  id: 1\r
+  name: Santiago\r
+drnic:\r
+  id: 2\r
+  name: Dr Nic
\ No newline at end of file
diff --git a/vendor/gems/composite_primary_keys-1.1.0/test/hash_tricks.rb b/vendor/gems/composite_primary_keys-1.1.0/test/hash_tricks.rb
new file mode 100644 (file)
index 0000000..b37bbbb
--- /dev/null
@@ -0,0 +1,34 @@
+# From:\r
+# http://www.bigbold.com/snippets/posts/show/2178\r
+# http://blog.caboo.se/articles/2006/06/11/stupid-hash-tricks\r
+# \r
+# An example utilisation of these methods in a controller is:\r
+# def some_action\r
+#    # some script kiddie also passed in :bee, which we don't want tampered with _here_.\r
+#    @model = Model.create(params.pass(:foo, :bar))\r
+#  end\r
+class Hash\r
+\r
+  # lets through the keys in the argument\r
+  # >> {:one => 1, :two => 2, :three => 3}.pass(:one)\r
+  # => {:one=>1}\r
+  def pass(*keys)\r
+    keys = keys.first if keys.first.is_a?(Array)\r
+    tmp = self.clone\r
+    tmp.delete_if {|k,v| ! keys.include?(k.to_sym) }\r
+    tmp.delete_if {|k,v| ! keys.include?(k.to_s) }\r
+    tmp\r
+  end\r
+\r
+  # blocks the keys in the arguments\r
+  # >> {:one => 1, :two => 2, :three => 3}.block(:one)\r
+  # => {:two=>2, :three=>3}\r
+  def block(*keys)\r
+    keys = keys.first if keys.first.is_a?(Array)\r
+    tmp = self.clone\r
+    tmp.delete_if {|k,v| keys.include?(k.to_sym) }\r
+    tmp.delete_if {|k,v| keys.include?(k.to_s) }\r
+    tmp\r
+  end\r
+\r
+end\r
diff --git a/vendor/gems/composite_primary_keys-1.1.0/test/plugins/pagination.rb b/vendor/gems/composite_primary_keys-1.1.0/test/plugins/pagination.rb
new file mode 100644 (file)
index 0000000..6a3e1a9
--- /dev/null
@@ -0,0 +1,405 @@
+module ActionController
+  # === Action Pack pagination for Active Record collections
+  #
+  # The Pagination module aids in the process of paging large collections of
+  # Active Record objects. It offers macro-style automatic fetching of your
+  # model for multiple views, or explicit fetching for single actions. And if
+  # the magic isn't flexible enough for your needs, you can create your own
+  # paginators with a minimal amount of code.
+  #
+  # The Pagination module can handle as much or as little as you wish. In the
+  # controller, have it automatically query your model for pagination; or,
+  # if you prefer, create Paginator objects yourself.
+  #
+  # Pagination is included automatically for all controllers.
+  #
+  # For help rendering pagination links, see 
+  # ActionView::Helpers::PaginationHelper.
+  #
+  # ==== Automatic pagination for every action in a controller
+  #
+  #   class PersonController < ApplicationController   
+  #     model :person
+  #
+  #     paginate :people, :order => 'last_name, first_name',
+  #              :per_page => 20
+  #     
+  #     # ...
+  #   end
+  #
+  # Each action in this controller now has access to a <tt>@people</tt>
+  # instance variable, which is an ordered collection of model objects for the
+  # current page (at most 20, sorted by last name and first name), and a 
+  # <tt>@person_pages</tt> Paginator instance. The current page is determined
+  # by the <tt>params[:page]</tt> variable.
+  #
+  # ==== Pagination for a single action
+  #
+  #   def list
+  #     @person_pages, @people =
+  #       paginate :people, :order => 'last_name, first_name'
+  #   end
+  #
+  # Like the previous example, but explicitly creates <tt>@person_pages</tt>
+  # and <tt>@people</tt> for a single action, and uses the default of 10 items
+  # per page.
+  #
+  # ==== Custom/"classic" pagination 
+  #
+  #   def list
+  #     @person_pages = Paginator.new self, Person.count, 10, params[:page]
+  #     @people = Person.find :all, :order => 'last_name, first_name', 
+  #                           :limit  =>  @person_pages.items_per_page,
+  #                           :offset =>  @person_pages.current.offset
+  #   end
+  # 
+  # Explicitly creates the paginator from the previous example and uses 
+  # Paginator#to_sql to retrieve <tt>@people</tt> from the model.
+  #
+  module Pagination
+    unless const_defined?(:OPTIONS)
+      # A hash holding options for controllers using macro-style pagination
+      OPTIONS = Hash.new
+  
+      # The default options for pagination
+      DEFAULT_OPTIONS = {
+        :class_name => nil,
+        :singular_name => nil,
+        :per_page   => 10,
+        :conditions => nil,
+        :order_by   => nil,
+        :order      => nil,
+        :join       => nil,
+        :joins      => nil,
+        :count      => nil,
+        :include    => nil,
+        :select     => nil,
+        :group      => nil,
+        :parameter  => 'page'
+      }
+    else
+      DEFAULT_OPTIONS[:group] = nil
+    end
+      
+    def self.included(base) #:nodoc:
+      super
+      base.extend(ClassMethods)
+    end
+  
+    def self.validate_options!(collection_id, options, in_action) #:nodoc:
+      options.merge!(DEFAULT_OPTIONS) {|key, old, new| old}
+
+      valid_options = DEFAULT_OPTIONS.keys
+      valid_options << :actions unless in_action
+    
+      unknown_option_keys = options.keys - valid_options
+      raise ActionController::ActionControllerError,
+            "Unknown options: #{unknown_option_keys.join(', ')}" unless
+              unknown_option_keys.empty?
+
+      options[:singular_name] ||= ActiveSupport::Inflector.singularize(collection_id.to_s)
+      options[:class_name]  ||= ActiveSupport::Inflector.camelize(options[:singular_name])
+    end
+
+    # Returns a paginator and a collection of Active Record model instances
+    # for the paginator's current page. This is designed to be used in a
+    # single action; to automatically paginate multiple actions, consider
+    # ClassMethods#paginate.
+    #
+    # +options+ are:
+    # <tt>:singular_name</tt>:: the singular name to use, if it can't be inferred by singularizing the collection name
+    # <tt>:class_name</tt>:: the class name to use, if it can't be inferred by
+    #                        camelizing the singular name
+    # <tt>:per_page</tt>::   the maximum number of items to include in a 
+    #                        single page. Defaults to 10
+    # <tt>:conditions</tt>:: optional conditions passed to Model.find(:all, *params) and
+    #                        Model.count
+    # <tt>:order</tt>::      optional order parameter passed to Model.find(:all, *params)
+    # <tt>:order_by</tt>::   (deprecated, used :order) optional order parameter passed to Model.find(:all, *params)
+    # <tt>:joins</tt>::      optional joins parameter passed to Model.find(:all, *params)
+    #                        and Model.count
+    # <tt>:join</tt>::       (deprecated, used :joins or :include) optional join parameter passed to Model.find(:all, *params)
+    #                        and Model.count
+    # <tt>:include</tt>::    optional eager loading parameter passed to Model.find(:all, *params)
+    #                        and Model.count
+    # <tt>:select</tt>::     :select parameter passed to Model.find(:all, *params)
+    #
+    # <tt>:count</tt>::      parameter passed as :select option to Model.count(*params)
+    #
+    # <tt>:group</tt>::     :group parameter passed to Model.find(:all, *params). It forces the use of DISTINCT instead of plain COUNT to come up with the total number of records
+    #
+    def paginate(collection_id, options={})
+      Pagination.validate_options!(collection_id, options, true)
+      paginator_and_collection_for(collection_id, options)
+    end
+
+    # These methods become class methods on any controller 
+    module ClassMethods
+      # Creates a +before_filter+ which automatically paginates an Active
+      # Record model for all actions in a controller (or certain actions if
+      # specified with the <tt>:actions</tt> option).
+      #
+      # +options+ are the same as PaginationHelper#paginate, with the addition 
+      # of:
+      # <tt>:actions</tt>:: an array of actions for which the pagination is
+      #                     active. Defaults to +nil+ (i.e., every action)
+      def paginate(collection_id, options={})
+        Pagination.validate_options!(collection_id, options, false)
+        module_eval do
+          before_filter :create_paginators_and_retrieve_collections
+          OPTIONS[self] ||= Hash.new
+          OPTIONS[self][collection_id] = options
+        end
+      end
+    end
+
+    def create_paginators_and_retrieve_collections #:nodoc:
+      Pagination::OPTIONS[self.class].each do |collection_id, options|
+        next unless options[:actions].include? action_name if
+          options[:actions]
+
+        paginator, collection = 
+          paginator_and_collection_for(collection_id, options)
+
+        paginator_name = "@#{options[:singular_name]}_pages"
+        self.instance_variable_set(paginator_name, paginator)
+
+        collection_name = "@#{collection_id.to_s}"
+        self.instance_variable_set(collection_name, collection)     
+      end
+    end
+  
+    # Returns the total number of items in the collection to be paginated for
+    # the +model+ and given +conditions+. Override this method to implement a
+    # custom counter.
+    def count_collection_for_pagination(model, options)
+      model.count(:conditions => options[:conditions],
+                  :joins => options[:join] || options[:joins],
+                  :include => options[:include],
+                  :select => (options[:group] ? "DISTINCT #{options[:group]}" : options[:count]))
+    end
+    
+    # Returns a collection of items for the given +model+ and +options[conditions]+,
+    # ordered by +options[order]+, for the current page in the given +paginator+.
+    # Override this method to implement a custom finder.
+    def find_collection_for_pagination(model, options, paginator)
+      model.find(:all, :conditions => options[:conditions],
+                 :order => options[:order_by] || options[:order],
+                 :joins => options[:join] || options[:joins], :include => options[:include],
+                 :select => options[:select], :limit => options[:per_page],
+                 :group => options[:group], :offset => paginator.current.offset)
+    end
+  
+    protected :create_paginators_and_retrieve_collections,
+              :count_collection_for_pagination,
+              :find_collection_for_pagination
+
+    def paginator_and_collection_for(collection_id, options) #:nodoc:
+      klass = options[:class_name].constantize
+      page  = params[options[:parameter]]
+      count = count_collection_for_pagination(klass, options)
+      paginator = Paginator.new(self, count, options[:per_page], page)
+      collection = find_collection_for_pagination(klass, options, paginator)
+    
+      return paginator, collection 
+    end
+      
+    private :paginator_and_collection_for
+
+    # A class representing a paginator for an Active Record collection.
+    class Paginator
+      include Enumerable
+
+      # Creates a new Paginator on the given +controller+ for a set of items
+      # of size +item_count+ and having +items_per_page+ items per page.
+      # Raises ArgumentError if items_per_page is out of bounds (i.e., less
+      # than or equal to zero). The page CGI parameter for links defaults to
+      # "page" and can be overridden with +page_parameter+.
+      def initialize(controller, item_count, items_per_page, current_page=1)
+        raise ArgumentError, 'must have at least one item per page' if
+          items_per_page <= 0
+
+        @controller = controller
+        @item_count = item_count || 0
+        @items_per_page = items_per_page
+        @pages = {}
+        
+        self.current_page = current_page
+      end
+      attr_reader :controller, :item_count, :items_per_page
+      
+      # Sets the current page number of this paginator. If +page+ is a Page
+      # object, its +number+ attribute is used as the value; if the page does 
+      # not belong to this Paginator, an ArgumentError is raised.
+      def current_page=(page)
+        if page.is_a? Page
+          raise ArgumentError, 'Page/Paginator mismatch' unless
+            page.paginator == self
+        end
+        page = page.to_i
+        @current_page_number = has_page_number?(page) ? page : 1
+      end
+
+      # Returns a Page object representing this paginator's current page.
+      def current_page
+        @current_page ||= self[@current_page_number]
+      end
+      alias current :current_page
+
+      # Returns a new Page representing the first page in this paginator.
+      def first_page
+        @first_page ||= self[1]
+      end
+      alias first :first_page
+
+      # Returns a new Page representing the last page in this paginator.
+      def last_page
+        @last_page ||= self[page_count] 
+      end
+      alias last :last_page
+
+      # Returns the number of pages in this paginator.
+      def page_count
+        @page_count ||= @item_count.zero? ? 1 :
+                          (q,r=@item_count.divmod(@items_per_page); r==0? q : q+1)
+      end
+
+      alias length :page_count
+
+      # Returns true if this paginator contains the page of index +number+.
+      def has_page_number?(number)
+        number >= 1 and number <= page_count
+      end
+
+      # Returns a new Page representing the page with the given index
+      # +number+.
+      def [](number)
+        @pages[number] ||= Page.new(self, number)
+      end
+
+      # Successively yields all the paginator's pages to the given block.
+      def each(&block)
+        page_count.times do |n|
+          yield self[n+1]
+        end
+      end
+
+      # A class representing a single page in a paginator.
+      class Page
+        include Comparable
+
+        # Creates a new Page for the given +paginator+ with the index
+        # +number+. If +number+ is not in the range of valid page numbers or
+        # is not a number at all, it defaults to 1.
+        def initialize(paginator, number)
+          @paginator = paginator
+          @number = number.to_i
+          @number = 1 unless @paginator.has_page_number? @number
+        end
+        attr_reader :paginator, :number
+        alias to_i :number
+
+        # Compares two Page objects and returns true when they represent the 
+        # same page (i.e., their paginators are the same and they have the
+        # same page number).
+        def ==(page)
+          return false if page.nil?
+          @paginator == page.paginator and 
+            @number == page.number
+        end
+
+        # Compares two Page objects and returns -1 if the left-hand page comes
+        # before the right-hand page, 0 if the pages are equal, and 1 if the
+        # left-hand page comes after the right-hand page. Raises ArgumentError
+        # if the pages do not belong to the same Paginator object.
+        def <=>(page)
+          raise ArgumentError unless @paginator == page.paginator
+          @number <=> page.number
+        end
+
+        # Returns the item offset for the first item in this page.
+        def offset
+          @paginator.items_per_page * (@number - 1)
+        end
+        
+        # Returns the number of the first item displayed.
+        def first_item
+          offset + 1
+        end
+        
+        # Returns the number of the last item displayed.
+        def last_item
+          [@paginator.items_per_page * @number, @paginator.item_count].min
+        end
+
+        # Returns true if this page is the first page in the paginator.
+        def first?
+          self == @paginator.first
+        end
+
+        # Returns true if this page is the last page in the paginator.
+        def last?
+          self == @paginator.last
+        end
+
+        # Returns a new Page object representing the page just before this
+        # page, or nil if this is the first page.
+        def previous
+          if first? then nil else @paginator[@number - 1] end
+        end
+
+        # Returns a new Page object representing the page just after this
+        # page, or nil if this is the last page.
+        def next
+          if last? then nil else @paginator[@number + 1] end
+        end
+
+        # Returns a new Window object for this page with the specified 
+        # +padding+.
+        def window(padding=2)
+          Window.new(self, padding)
+        end
+
+        # Returns the limit/offset array for this page.
+        def to_sql
+          [@paginator.items_per_page, offset]
+        end
+        
+        def to_param #:nodoc:
+          @number.to_s
+        end
+      end
+
+      # A class for representing ranges around a given page.
+      class Window
+        # Creates a new Window object for the given +page+ with the specified
+        # +padding+.
+        def initialize(page, padding=2)
+          @paginator = page.paginator
+          @page = page
+          self.padding = padding
+        end
+        attr_reader :paginator, :page
+
+        # Sets the window's padding (the number of pages on either side of the
+        # window page).
+        def padding=(padding)
+          @padding = padding < 0 ? 0 : padding
+          # Find the beginning and end pages of the window
+          @first = @paginator.has_page_number?(@page.number - @padding) ?
+            @paginator[@page.number - @padding] : @paginator.first
+          @last =  @paginator.has_page_number?(@page.number + @padding) ?
+            @paginator[@page.number + @padding] : @paginator.last
+        end
+        attr_reader :padding, :first, :last
+
+        # Returns an array of Page objects in the current window.
+        def pages
+          (@first.number..@last.number).to_a.collect! {|n| @paginator[n]}
+        end
+        alias to_a :pages
+      end
+    end
+
+  end
+end
diff --git a/vendor/gems/composite_primary_keys-1.1.0/test/plugins/pagination_helper.rb b/vendor/gems/composite_primary_keys-1.1.0/test/plugins/pagination_helper.rb
new file mode 100644 (file)
index 0000000..069d775
--- /dev/null
@@ -0,0 +1,135 @@
+module ActionView
+  module Helpers
+    # Provides methods for linking to ActionController::Pagination objects using a simple generator API.  You can optionally
+    # also build your links manually using ActionView::Helpers::AssetHelper#link_to like so:
+    #
+    # <%= link_to "Previous page", { :page => paginator.current.previous } if paginator.current.previous %>
+    # <%= link_to "Next page", { :page => paginator.current.next } if paginator.current.next %>
+    module PaginationHelper
+      unless const_defined?(:DEFAULT_OPTIONS)
+        DEFAULT_OPTIONS = {
+          :name => :page,
+          :window_size => 2,
+          :always_show_anchors => true,
+          :link_to_current_page => false,
+          :params => {}
+        }
+      end
+
+      # Creates a basic HTML link bar for the given +paginator+.  Links will be created
+      # for the next and/or previous page and for a number of other pages around the current
+      # pages position. The +html_options+ hash is passed to +link_to+ when the links are created.
+      #
+      # ==== Options
+      # <tt>:name</tt>::                 the routing name for this paginator
+      #                                  (defaults to +page+)
+      # <tt>:prefix</tt>::               prefix for pagination links
+      #                                  (i.e. Older Pages: 1 2 3 4)
+      # <tt>:suffix</tt>::               suffix for pagination links
+      #                                  (i.e. 1 2 3 4 <- Older Pages)
+      # <tt>:window_size</tt>::          the number of pages to show around 
+      #                                  the current page (defaults to <tt>2</tt>)
+      # <tt>:always_show_anchors</tt>::  whether or not the first and last
+      #                                  pages should always be shown
+      #                                  (defaults to +true+)
+      # <tt>:link_to_current_page</tt>:: whether or not the current page
+      #                                  should be linked to (defaults to
+      #                                  +false+)
+      # <tt>:params</tt>::               any additional routing parameters
+      #                                  for page URLs
+      #
+      # ==== Examples
+      #  # We'll assume we have a paginator setup in @person_pages...
+      #
+      #  pagination_links(@person_pages)
+      #  # => 1 <a href="/?page=2/">2</a> <a href="/?page=3/">3</a>  ... <a href="/?page=10/">10</a>
+      #
+      #  pagination_links(@person_pages, :link_to_current_page => true)
+      #  # => <a href="/?page=1/">1</a> <a href="/?page=2/">2</a> <a href="/?page=3/">3</a>  ... <a href="/?page=10/">10</a>
+      #
+      #  pagination_links(@person_pages, :always_show_anchors => false)
+      #  # => 1 <a href="/?page=2/">2</a> <a href="/?page=3/">3</a> 
+      #
+      #  pagination_links(@person_pages, :window_size => 1)
+      #  # => 1 <a href="/?page=2/">2</a>  ... <a href="/?page=10/">10</a>
+      #
+      #  pagination_links(@person_pages, :params => { :viewer => "flash" })
+      #  # => 1 <a href="/?page=2&amp;viewer=flash/">2</a> <a href="/?page=3&amp;viewer=flash/">3</a>  ... 
+      #  #    <a href="/?page=10&amp;viewer=flash/">10</a>
+      def pagination_links(paginator, options={}, html_options={})
+        name = options[:name] || DEFAULT_OPTIONS[:name]
+        params = (options[:params] || DEFAULT_OPTIONS[:params]).clone
+        
+        prefix = options[:prefix] || ''
+        suffix = options[:suffix] || ''
+
+        pagination_links_each(paginator, options, prefix, suffix) do |n|
+          params[name] = n
+          link_to(n.to_s, params, html_options)
+        end
+      end
+
+      # Iterate through the pages of a given +paginator+, invoking a
+      # block for each page number that needs to be rendered as a link.
+      # 
+      # ==== Options
+      # <tt>:window_size</tt>::          the number of pages to show around 
+      #                                  the current page (defaults to +2+)
+      # <tt>:always_show_anchors</tt>::  whether or not the first and last
+      #                                  pages should always be shown
+      #                                  (defaults to +true+)
+      # <tt>:link_to_current_page</tt>:: whether or not the current page
+      #                                  should be linked to (defaults to
+      #                                  +false+)
+      #
+      # ==== Example
+      #  # Turn paginated links into an Ajax call
+      #  pagination_links_each(paginator, page_options) do |link|
+      #    options = { :url => {:action => 'list'}, :update => 'results' }
+      #    html_options = { :href => url_for(:action => 'list') }
+      #
+      #    link_to_remote(link.to_s, options, html_options)
+      #  end
+      def pagination_links_each(paginator, options, prefix = nil, suffix = nil)
+        options = DEFAULT_OPTIONS.merge(options)
+        link_to_current_page = options[:link_to_current_page]
+        always_show_anchors = options[:always_show_anchors]
+
+        current_page = paginator.current_page
+        window_pages = current_page.window(options[:window_size]).pages
+        return if window_pages.length <= 1 unless link_to_current_page
+        
+        first, last = paginator.first, paginator.last
+        
+        html = ''
+
+        html << prefix if prefix
+
+        if always_show_anchors and not (wp_first = window_pages[0]).first?
+          html << yield(first.number)
+          html << ' ... ' if wp_first.number - first.number > 1
+          html << ' '
+        end
+          
+        window_pages.each do |page|
+          if current_page == page && !link_to_current_page
+            html << page.number.to_s
+          else
+            html << yield(page.number)
+          end
+          html << ' '
+        end
+        
+        if always_show_anchors and not (wp_last = window_pages[-1]).last? 
+          html << ' ... ' if last.number - wp_last.number > 1
+          html << yield(last.number)
+        end
+
+        html << suffix if suffix
+
+        html
+      end
+      
+    end # PaginationHelper
+  end # Helpers
+end # ActionView
diff --git a/vendor/gems/composite_primary_keys-1.1.0/test/test_associations.rb b/vendor/gems/composite_primary_keys-1.1.0/test/test_associations.rb
new file mode 100644 (file)
index 0000000..78302f8
--- /dev/null
@@ -0,0 +1,160 @@
+require 'abstract_unit'\r
+require 'fixtures/article'\r
+require 'fixtures/product'\r
+require 'fixtures/tariff'\r
+require 'fixtures/product_tariff'\r
+require 'fixtures/suburb'\r
+require 'fixtures/street'\r
+require 'fixtures/restaurant'\r
+require 'fixtures/dorm'\r
+require 'fixtures/room'\r
+require 'fixtures/room_attribute'\r
+require 'fixtures/room_attribute_assignment'\r
+require 'fixtures/student'\r
+require 'fixtures/room_assignment'\r
+require 'fixtures/user'\r
+require 'fixtures/reading'\r
+\r
+class TestAssociations < Test::Unit::TestCase\r
+  fixtures :articles, :products, :tariffs, :product_tariffs, :suburbs, :streets, :restaurants, :restaurants_suburbs,\r
+           :dorms, :rooms, :room_attributes, :room_attribute_assignments, :students, :room_assignments, :users, :readings\r
+  \r
+  def test_has_many_through_with_conditions_when_through_association_is_not_composite\r
+    user = User.find(:first)\r
+    assert_equal 1, user.articles.find(:all, :conditions => ["articles.name = ?", "Article One"]).size\r
+  end\r
+\r
+  def test_has_many_through_with_conditions_when_through_association_is_composite\r
+    room = Room.find(:first)\r
+    assert_equal 0, room.room_attributes.find(:all, :conditions => ["room_attributes.name != ?", "keg"]).size\r
+  end\r
+\r
+  def test_has_many_through_on_custom_finder_when_through_association_is_composite_finder_when_through_association_is_not_composite\r
+    user = User.find(:first)\r
+    assert_equal 1, user.find_custom_articles.size\r
+  end\r
+\r
+  def test_has_many_through_on_custom_finder_when_through_association_is_composite\r
+    room = Room.find(:first)\r
+    assert_equal 0, room.find_custom_room_attributes.size\r
+  end\r
+  \r
+  def test_count\r
+    assert_equal 2, Product.count(:include => :product_tariffs)\r
+    assert_equal 3, Tariff.count(:include => :product_tariffs)\r
+    assert_equal 2, Tariff.count(:group => :start_date).size\r
+  end\r
+  \r
+  def test_products\r
+    assert_not_nil products(:first_product).product_tariffs\r
+    assert_equal 2, products(:first_product).product_tariffs.length\r
+    assert_not_nil products(:first_product).tariffs\r
+    assert_equal 2, products(:first_product).tariffs.length\r
+    assert_not_nil products(:first_product).product_tariff\r
+  end\r
+  \r
+  def test_product_tariffs\r
+    assert_not_nil product_tariffs(:first_flat).product\r
+    assert_not_nil product_tariffs(:first_flat).tariff\r
+    assert_equal Product, product_tariffs(:first_flat).product.class\r
+    assert_equal Tariff, product_tariffs(:first_flat).tariff.class\r
+  end\r
+  \r
+  def test_tariffs\r
+    assert_not_nil tariffs(:flat).product_tariffs\r
+    assert_equal 1, tariffs(:flat).product_tariffs.length\r
+    assert_not_nil tariffs(:flat).products\r
+    assert_equal 1, tariffs(:flat).products.length\r
+    assert_not_nil tariffs(:flat).product_tariff\r
+  end\r
+  \r
+  # Its not generating the instances of associated classes from the rows\r
+  def test_find_includes_products\r
+    assert @products = Product.find(:all, :include => :product_tariffs)\r
+    assert_equal 2, @products.length\r
+    assert_not_nil @products.first.instance_variable_get('@product_tariffs'), '@product_tariffs not set; should be array'\r
+    assert_equal 3, @products.inject(0) {|sum, tariff| sum + tariff.instance_variable_get('@product_tariffs').length}, \r
+      "Incorrect number of product_tariffs returned"\r
+  end\r
+  \r
+  def test_find_includes_tariffs\r
+    assert @tariffs = Tariff.find(:all, :include => :product_tariffs)\r
+    assert_equal 3, @tariffs.length\r
+    assert_not_nil @tariffs.first.instance_variable_get('@product_tariffs'), '@product_tariffs not set; should be array'\r
+    assert_equal 3, @tariffs.inject(0) {|sum, tariff| sum + tariff.instance_variable_get('@product_tariffs').length}, \r
+      "Incorrect number of product_tariffs returnedturned"\r
+  end\r
+  \r
+  def test_find_includes_product\r
+    assert @product_tariffs = ProductTariff.find(:all, :include => :product)\r
+    assert_equal 3, @product_tariffs.length\r
+    assert_not_nil @product_tariffs.first.instance_variable_get('@product'), '@product not set'\r
+  end\r
+  \r
+  def test_find_includes_comp_belongs_to_tariff\r
+    assert @product_tariffs = ProductTariff.find(:all, :include => :tariff)\r
+    assert_equal 3, @product_tariffs.length\r
+    assert_not_nil @product_tariffs.first.instance_variable_get('@tariff'), '@tariff not set'\r
+  end\r
+  \r
+  def test_find_includes_extended\r
+    assert @products = Product.find(:all, :include => {:product_tariffs => :tariff})\r
+    assert_equal 3, @products.inject(0) {|sum, product| sum + product.instance_variable_get('@product_tariffs').length},\r
+      "Incorrect number of product_tariffs returned"\r
+    \r
+    assert @tariffs = Tariff.find(:all, :include => {:product_tariffs => :product})\r
+    assert_equal 3, @tariffs.inject(0) {|sum, tariff| sum + tariff.instance_variable_get('@product_tariffs').length}, \r
+      "Incorrect number of product_tariffs returned"\r
+  end\r
+  \r
+  def test_join_where_clause\r
+    @product = Product.find(:first, :include => :product_tariffs)\r
+    where_clause = @product.product_tariffs.composite_where_clause(\r
+      ['foo','bar'], [1,2]\r
+    )\r
+    assert_equal('(foo=1 AND bar=2)', where_clause)\r
+  end\r
+  \r
+  def test_has_many_through\r
+    @products = Product.find(:all, :include => :tariffs)\r
+    assert_equal 3, @products.inject(0) {|sum, product| sum + product.instance_variable_get('@tariffs').length},\r
+      "Incorrect number of tariffs returned"\r
+  end\r
+  \r
+  def test_has_many_through_when_not_pre_loaded\r
+       student = Student.find(:first)\r
+       rooms = student.rooms\r
+       assert_equal 1, rooms.size\r
+       assert_equal 1, rooms.first.dorm_id\r
+       assert_equal 1, rooms.first.room_id\r
+  end\r
+  \r
+  def test_has_many_through_when_through_association_is_composite\r
+    dorm = Dorm.find(:first)\r
+    assert_equal 1, dorm.rooms.length\r
+    assert_equal 1, dorm.rooms.first.room_attributes.length\r
+    assert_equal 'keg', dorm.rooms.first.room_attributes.first.name\r
+  end\r
+\r
+  def test_associations_with_conditions\r
+    @suburb = Suburb.find([2, 1])\r
+    assert_equal 2, @suburb.streets.size\r
+\r
+    @suburb = Suburb.find([2, 1])\r
+    assert_equal 1, @suburb.first_streets.size\r
+\r
+    @suburb = Suburb.find([2, 1], :include => :streets)\r
+    assert_equal 2, @suburb.streets.size\r
+\r
+    @suburb = Suburb.find([2, 1], :include => :first_streets)\r
+    assert_equal 1, @suburb.first_streets.size\r
+  end\r
+  \r
+  def test_has_and_belongs_to_many\r
+    @restaurant = Restaurant.find([1,1])\r
+    assert_equal 2, @restaurant.suburbs.size\r
+    \r
+    @restaurant = Restaurant.find([1,1], :include => :suburbs)\r
+    assert_equal 2, @restaurant.suburbs.size  \r
+  end\r
+end\r
diff --git a/vendor/gems/composite_primary_keys-1.1.0/test/test_attribute_methods.rb b/vendor/gems/composite_primary_keys-1.1.0/test/test_attribute_methods.rb
new file mode 100644 (file)
index 0000000..b020a64
--- /dev/null
@@ -0,0 +1,22 @@
+require 'abstract_unit'
+require 'fixtures/kitchen_sink'
+require 'fixtures/reference_type'
+
+class TestAttributeMethods < Test::Unit::TestCase
+  fixtures :kitchen_sinks, :reference_types
+  
+  def test_read_attribute_with_single_key
+    rt = ReferenceType.find(1)
+    assert_equal(1, rt.reference_type_id)
+    assert_equal('NAME_PREFIX', rt.type_label)
+    assert_equal('Name Prefix', rt.abbreviation)
+  end
+
+  def test_read_attribute_with_composite_keys
+    sink = KitchenSink.find(1,2)
+    assert_equal(1, sink.id_1)
+    assert_equal(2, sink.id_2)
+    assert_equal(Date.today, sink.a_date.to_date)
+    assert_equal('string', sink.a_string)
+  end
+end
diff --git a/vendor/gems/composite_primary_keys-1.1.0/test/test_attributes.rb b/vendor/gems/composite_primary_keys-1.1.0/test/test_attributes.rb
new file mode 100644 (file)
index 0000000..7504082
--- /dev/null
@@ -0,0 +1,84 @@
+require 'abstract_unit'
+require 'fixtures/reference_type'
+require 'fixtures/reference_code'
+require 'fixtures/product'
+require 'fixtures/tariff'
+require 'fixtures/product_tariff'
+
+class TestAttributes < Test::Unit::TestCase
+  fixtures :reference_types, :reference_codes, :products, :tariffs, :product_tariffs
+  
+  CLASSES = {
+    :single => {
+      :class => ReferenceType,
+      :primary_keys => :reference_type_id,
+    },
+    :dual   => { 
+      :class => ReferenceCode,
+      :primary_keys => [:reference_type_id, :reference_code],
+    },
+  }
+  
+  def setup
+    self.class.classes = CLASSES
+  end
+  
+  def test_brackets
+    testing_with do
+      @first.attributes.each_pair do |attr_name, value|
+        assert_equal value, @first[attr_name]
+      end
+    end
+  end
+    
+  def test_brackets_primary_key
+    testing_with do
+      assert_equal @first.id, @first[@primary_keys], "[] failing for #{@klass}"
+      assert_equal @first.id, @first[@first.class.primary_key]
+    end
+  end
+
+  def test_brackets_assignment
+    testing_with do
+      @first.attributes.each_pair do |attr_name, value|
+        @first[attr_name]= !value.nil? ? value * 2 : '1'
+        assert_equal !value.nil? ? value * 2 : '1', @first[attr_name]
+      end
+    end
+  end
+    
+  def test_brackets_foreign_key_assignment
+    @flat = Tariff.find(1, Date.today.to_s(:db))
+    @second_free = ProductTariff.find(2,2,Date.today.to_s(:db))
+    @second_free_fk = [:tariff_id, :tariff_start_date]
+    @second_free[key = @second_free_fk] = @flat.id
+      compare_indexes('@flat', @flat.class.primary_key, '@second_free', @second_free_fk)
+      assert_equal @flat.id, @second_free[key]
+    @second_free[key = @second_free_fk.to_composite_ids] = @flat.id
+      assert_equal @flat.id, @second_free[key]
+      compare_indexes('@flat', @flat.class.primary_key, '@second_free', @second_free_fk)
+    @second_free[key = @second_free_fk.to_composite_ids] = @flat.id.to_s
+      assert_equal @flat.id, @second_free[key]
+      compare_indexes('@flat', @flat.class.primary_key, '@second_free', @second_free_fk)
+    @second_free[key = @second_free_fk.to_composite_ids] = @flat.id.to_s
+      assert_equal @flat.id, @second_free[key]
+      compare_indexes('@flat', @flat.class.primary_key, '@second_free', @second_free_fk)
+    @second_free[key = @second_free_fk.to_composite_ids.to_s] = @flat.id
+      assert_equal @flat.id, @second_free[key]
+      compare_indexes('@flat', @flat.class.primary_key, '@second_free', @second_free_fk)
+    @second_free[key = @second_free_fk.to_composite_ids.to_s] = @flat.id.to_s
+      assert_equal @flat.id, @second_free[key]
+      compare_indexes('@flat', @flat.class.primary_key, '@second_free', @second_free_fk)
+  end
+private
+  def compare_indexes(obj_name1, indexes1, obj_name2, indexes2)
+    obj1, obj2 = eval "[#{obj_name1}, #{obj_name2}]"
+    indexes1.length.times do |key_index|
+      assert_equal obj1[indexes1[key_index].to_s], 
+                   obj2[indexes2[key_index].to_s],
+                   "#{obj_name1}[#{indexes1[key_index]}]=#{obj1[indexes1[key_index].to_s].inspect} != " +
+                   "#{obj_name2}[#{indexes2[key_index]}]=#{obj2[indexes2[key_index].to_s].inspect}; " +
+                   "#{obj_name2} = #{obj2.inspect}"
+    end
+  end
+end
\ No newline at end of file
diff --git a/vendor/gems/composite_primary_keys-1.1.0/test/test_clone.rb b/vendor/gems/composite_primary_keys-1.1.0/test/test_clone.rb
new file mode 100644 (file)
index 0000000..8229744
--- /dev/null
@@ -0,0 +1,34 @@
+require 'abstract_unit'\r
+require 'fixtures/reference_type'\r
+require 'fixtures/reference_code'\r
+\r
+class TestClone < Test::Unit::TestCase\r
+  fixtures :reference_types, :reference_codes\r
+  \r
+  CLASSES = {\r
+    :single => {\r
+      :class => ReferenceType,\r
+      :primary_keys => :reference_type_id,\r
+    },\r
+    :dual   => { \r
+      :class => ReferenceCode,\r
+      :primary_keys => [:reference_type_id, :reference_code],\r
+    },\r
+  }\r
+  \r
+  def setup\r
+    self.class.classes = CLASSES\r
+  end\r
+  \r
+  def test_truth\r
+    testing_with do\r
+      clone = @first.clone\r
+      assert_equal @first.attributes.block(@klass.primary_key), clone.attributes\r
+      if composite?\r
+        @klass.primary_key.each {|key| assert_nil clone[key], "Primary key '#{key}' should be nil"} \r
+      else\r
+        assert_nil clone[@klass.primary_key], "Sole primary key should be nil"\r
+      end\r
+    end\r
+  end\r
+end
\ No newline at end of file
diff --git a/vendor/gems/composite_primary_keys-1.1.0/test/test_composite_arrays.rb b/vendor/gems/composite_primary_keys-1.1.0/test/test_composite_arrays.rb
new file mode 100644 (file)
index 0000000..41e21f8
--- /dev/null
@@ -0,0 +1,51 @@
+require 'abstract_unit'
+require 'fixtures/reference_type'
+require 'fixtures/reference_code'
+
+class CompositeArraysTest < Test::Unit::TestCase
+
+  def test_new_primary_keys
+    keys = CompositePrimaryKeys::CompositeKeys.new
+    assert_not_nil keys
+    assert_equal '', keys.to_s
+    assert_equal '', "#{keys}"
+  end
+
+  def test_initialize_primary_keys
+    keys = CompositePrimaryKeys::CompositeKeys.new([1,2,3])
+    assert_not_nil keys
+    assert_equal '1,2,3', keys.to_s
+    assert_equal '1,2,3', "#{keys}"
+  end
+  
+  def test_to_composite_keys
+    keys = [1,2,3].to_composite_keys
+    assert_equal CompositePrimaryKeys::CompositeKeys, keys.class
+    assert_equal '1,2,3', keys.to_s
+  end
+
+  def test_new_ids
+    keys = CompositePrimaryKeys::CompositeIds.new
+    assert_not_nil keys
+    assert_equal '', keys.to_s
+    assert_equal '', "#{keys}"
+  end
+
+  def test_initialize_ids
+    keys = CompositePrimaryKeys::CompositeIds.new([1,2,3])
+    assert_not_nil keys
+    assert_equal '1,2,3', keys.to_s
+    assert_equal '1,2,3', "#{keys}"
+  end
+  
+  def test_to_composite_ids
+    keys = [1,2,3].to_composite_ids
+    assert_equal CompositePrimaryKeys::CompositeIds, keys.class
+    assert_equal '1,2,3', keys.to_s
+  end
+  
+  def test_flatten
+    keys = [CompositePrimaryKeys::CompositeIds.new([1,2,3]), CompositePrimaryKeys::CompositeIds.new([4,5,6])]
+    assert_equal 6, keys.flatten.size
+  end
+end
\ No newline at end of file
diff --git a/vendor/gems/composite_primary_keys-1.1.0/test/test_create.rb b/vendor/gems/composite_primary_keys-1.1.0/test/test_create.rb
new file mode 100644 (file)
index 0000000..dfbc773
--- /dev/null
@@ -0,0 +1,68 @@
+require 'abstract_unit'
+require 'fixtures/reference_type'
+require 'fixtures/reference_code'
+require 'fixtures/street'
+require 'fixtures/suburb'
+
+class TestCreate < Test::Unit::TestCase
+  fixtures :reference_types, :reference_codes, :streets, :suburbs
+  
+  CLASSES = {
+    :single => {
+      :class => ReferenceType,
+      :primary_keys => :reference_type_id,
+      :create => {:reference_type_id => 10, :type_label => 'NEW_TYPE', :abbreviation => 'New Type'}
+    },
+    :dual   => { 
+      :class => ReferenceCode,
+      :primary_keys => [:reference_type_id, :reference_code],
+      :create => {:reference_type_id => 1, :reference_code => 20, :code_label => 'NEW_CODE', :abbreviation => 'New Code'}
+    },
+  }
+  
+  def setup
+    self.class.classes = CLASSES
+  end
+  
+  def test_setup
+    testing_with do
+      assert_not_nil @klass_info[:create]
+    end
+  end
+  
+  def test_create
+    testing_with do
+      assert new_obj = @klass.create(@klass_info[:create])
+      assert !new_obj.new_record?
+    end
+  end
+  
+  def test_create_no_id
+    testing_with do
+      begin
+        @obj = @klass.create(@klass_info[:create].block(@klass.primary_key))
+        @successful = !composite?
+      rescue CompositePrimaryKeys::ActiveRecord::CompositeKeyError
+        @successful = false
+      rescue
+        flunk "Incorrect exception raised: #{$!}, #{$!.class}"
+      end
+      assert_equal composite?, !@successful, "Create should have failed for composites; #{@obj.inspect}"
+    end
+  end
+  
+  def test_create_on_association
+    suburb = Suburb.find(:first)
+    suburb.streets.create(:name => "my street")
+    street = Street.find_by_name('my street')
+    assert_equal(suburb.city_id, street.city_id)
+    assert_equal(suburb.suburb_id, street.suburb_id)
+  end
+  
+  def test_create_on_association_when_belongs_to_is_single_key
+    rt = ReferenceType.find(:first)
+    rt.reference_codes.create(:reference_code => 4321, :code_label => 'foo', :abbreviation => 'bar')
+    rc = ReferenceCode.find_by_reference_code(4321)
+    assert_equal(rc.reference_type_id, rt.reference_type_id)
+  end
+end
diff --git a/vendor/gems/composite_primary_keys-1.1.0/test/test_delete.rb b/vendor/gems/composite_primary_keys-1.1.0/test/test_delete.rb
new file mode 100644 (file)
index 0000000..cd79bbd
--- /dev/null
@@ -0,0 +1,96 @@
+require 'abstract_unit'\r
+require 'fixtures/reference_type'\r
+require 'fixtures/reference_code'\r
+require 'fixtures/department'\r
+require 'fixtures/employee'\r
+\r
+class TestDelete < Test::Unit::TestCase\r
+  fixtures :reference_types, :reference_codes, :departments, :employees\r
+  \r
+  CLASSES = {\r
+    :single => {\r
+      :class => ReferenceType,\r
+      :primary_keys => :reference_type_id,\r
+    },\r
+    :dual   => { \r
+      :class => ReferenceCode,\r
+      :primary_keys => [:reference_type_id, :reference_code],\r
+    },\r
+  }\r
+  \r
+  def setup\r
+    self.class.classes = CLASSES\r
+  end\r
+  \r
+  def test_destroy_one\r
+    testing_with do\r
+      #assert @first.destroy\r
+      assert true\r
+    end\r
+  end\r
+  \r
+  def test_destroy_one_via_class\r
+    testing_with do\r
+      assert @klass.destroy(*@first.id)\r
+    end\r
+  end\r
+  \r
+  def test_destroy_one_alone_via_class\r
+    testing_with do\r
+      assert @klass.destroy(@first.id)\r
+    end\r
+  end\r
+  \r
+  def test_delete_one\r
+    testing_with do\r
+      assert @klass.delete(*@first.id) if composite?\r
+    end\r
+  end\r
+  \r
+  def test_delete_one_alone\r
+    testing_with do\r
+      assert @klass.delete(@first.id)\r
+    end\r
+  end\r
+  \r
+  def test_delete_many\r
+    testing_with do\r
+      to_delete = @klass.find(:all)[0..1]\r
+      assert_equal 2, to_delete.length\r
+    end\r
+  end\r
+  \r
+  def test_delete_all\r
+    testing_with do\r
+      @klass.delete_all\r
+    end\r
+  end\r
+\r
+  def test_clear_association\r
+      department = Department.find(1,1)\r
+      assert_equal 2, department.employees.size, "Before clear employee count should be 2."\r
+      department.employees.clear\r
+      assert_equal 0, department.employees.size, "After clear employee count should be 0."\r
+      department.reload\r
+      assert_equal 0, department.employees.size, "After clear and a reload from DB employee count should be 0."\r
+  end\r
+\r
+  def test_delete_association\r
+      department = Department.find(1,1)\r
+      assert_equal 2, department.employees.size , "Before delete employee count should be 2."\r
+      first_employee = department.employees[0]\r
+      department.employees.delete(first_employee)\r
+      assert_equal 1, department.employees.size, "After delete employee count should be 1."\r
+      department.reload\r
+      assert_equal 1, department.employees.size, "After delete and a reload from DB employee count should be 1."\r
+  end\r
+\r
+  def test_delete_records_for_has_many_association_with_composite_primary_key\r
+      reference_type  = ReferenceType.find(1)\r
+      codes_to_delete = reference_type.reference_codes[0..1]\r
+      assert_equal 3, reference_type.reference_codes.size, "Before deleting records reference_code count should be 3."\r
+      reference_type.reference_codes.delete_records(codes_to_delete)\r
+      reference_type.reload\r
+      assert_equal 1, reference_type.reference_codes.size, "After deleting 2 records and a reload from DB reference_code count should be 1."\r
+  end\r
+end\r
diff --git a/vendor/gems/composite_primary_keys-1.1.0/test/test_dummy.rb b/vendor/gems/composite_primary_keys-1.1.0/test/test_dummy.rb
new file mode 100644 (file)
index 0000000..4438668
--- /dev/null
@@ -0,0 +1,28 @@
+require 'abstract_unit'\r
+require 'fixtures/reference_type'\r
+require 'fixtures/reference_code'\r
+\r
+class TestDummy < Test::Unit::TestCase\r
+  fixtures :reference_types, :reference_codes\r
+  \r
+  classes = {\r
+    :single => {\r
+      :class => ReferenceType,\r
+      :primary_keys => :reference_type_id,\r
+    },\r
+    :dual   => { \r
+      :class => ReferenceCode,\r
+      :primary_keys => [:reference_type_id, :reference_code],\r
+    },\r
+  }\r
+  \r
+  def setup\r
+    self.class.classes = classes\r
+  end\r
+  \r
+  def test_truth\r
+    testing_with do\r
+      assert true\r
+    end\r
+  end\r
+end
\ No newline at end of file
diff --git a/vendor/gems/composite_primary_keys-1.1.0/test/test_find.rb b/vendor/gems/composite_primary_keys-1.1.0/test/test_find.rb
new file mode 100644 (file)
index 0000000..a07d30a
--- /dev/null
@@ -0,0 +1,73 @@
+require 'abstract_unit'\r
+require 'fixtures/reference_type'\r
+require 'fixtures/reference_code'\r
+\r
+# Testing the find action on composite ActiveRecords with two primary keys\r
+class TestFind < Test::Unit::TestCase\r
+  fixtures :reference_types, :reference_codes\r
+  \r
+  CLASSES = {\r
+    :single => {\r
+      :class => ReferenceType,\r
+      :primary_keys => [:reference_type_id],\r
+    },\r
+    :dual   => { \r
+      :class => ReferenceCode,\r
+      :primary_keys => [:reference_type_id, :reference_code],\r
+    },\r
+    :dual_strs   => { \r
+      :class => ReferenceCode,\r
+      :primary_keys => ['reference_type_id', 'reference_code'],\r
+    },\r
+  }\r
+  \r
+  def setup\r
+    self.class.classes = CLASSES\r
+  end\r
+  \r
+  def test_find_first\r
+    testing_with do\r
+      obj = @klass.find(:first)\r
+      assert obj\r
+      assert_equal @klass, obj.class\r
+    end\r
+  end\r
+  \r
+  def test_find\r
+    testing_with do\r
+      found = @klass.find(*first_id) # e.g. find(1,1) or find 1,1\r
+      assert found\r
+      assert_equal @klass, found.class\r
+      assert_equal found, @klass.find(found.id)\r
+      assert_equal found, @klass.find(found.to_param)\r
+    end\r
+  end\r
+  \r
+  def test_find_composite_ids\r
+    testing_with do\r
+      found = @klass.find(first_id) # e.g. find([1,1].to_composite_ids)\r
+      assert found\r
+      assert_equal @klass, found.class\r
+      assert_equal found, @klass.find(found.id)\r
+      assert_equal found, @klass.find(found.to_param)\r
+    end\r
+  end\r
+  \r
+  def test_to_param\r
+    testing_with do\r
+      assert_equal first_id_str, @first.to_param.to_s\r
+    end\r
+  end\r
+  \r
+  def things_to_look_at\r
+    testing_with do\r
+      assert_equal found, @klass.find(found.id.to_s) # fails for 2+ keys\r
+    end\r
+  end\r
+  \r
+  def test_not_found\r
+    assert_raise(::ActiveRecord::RecordNotFound) do\r
+      ReferenceCode.send :find, '999,999'\r
+    end\r
+  end\r
+end
\ No newline at end of file
diff --git a/vendor/gems/composite_primary_keys-1.1.0/test/test_ids.rb b/vendor/gems/composite_primary_keys-1.1.0/test/test_ids.rb
new file mode 100644 (file)
index 0000000..9ba2d92
--- /dev/null
@@ -0,0 +1,97 @@
+require 'abstract_unit'\r
+require 'fixtures/reference_type'\r
+require 'fixtures/reference_code'\r
+\r
+class TestIds < Test::Unit::TestCase\r
+  fixtures :reference_types, :reference_codes\r
+  \r
+  CLASSES = {\r
+    :single => {\r
+      :class => ReferenceType,\r
+      :primary_keys => [:reference_type_id],\r
+    },\r
+    :dual   => { \r
+      :class => ReferenceCode,\r
+      :primary_keys => [:reference_type_id, :reference_code],\r
+    },\r
+    :dual_strs   => { \r
+      :class => ReferenceCode,\r
+      :primary_keys => ['reference_type_id', 'reference_code'],\r
+    },\r
+  }\r
+  \r
+  def setup\r
+    self.class.classes = CLASSES\r
+  end\r
+  \r
+  def test_id\r
+    testing_with do\r
+      assert_equal @first.id, @first.ids if composite?\r
+    end\r
+  end\r
+  \r
+  def test_id_to_s\r
+    testing_with do\r
+      assert_equal first_id_str, @first.id.to_s\r
+      assert_equal first_id_str, "#{@first.id}"\r
+    end\r
+  end\r
+  \r
+  def test_ids_to_s\r
+    testing_with do\r
+      order = @klass.primary_key.is_a?(String) ? @klass.primary_key : @klass.primary_key.join(',')\r
+      to_test = @klass.find(:all, :order => order)[0..1].map(&:id)\r
+      assert_equal '(1,1),(1,2)', @klass.ids_to_s(to_test) if @key_test == :dual\r
+      assert_equal '1,1;1,2', @klass.ids_to_s(to_test, ',', ';', '', '') if @key_test == :dual\r
+    end\r
+  end\r
+  \r
+  def test_composite_where_clause\r
+    testing_with do\r
+      where = 'reference_codes.reference_type_id=1 AND reference_codes.reference_code=2) OR (reference_codes.reference_type_id=2 AND reference_codes.reference_code=2'\r
+      assert_equal(where, @klass.composite_where_clause([[1, 2], [2, 2]])) if @key_test == :dual\r
+    end\r
+  end\r
+  \r
+  def test_set_ids_string\r
+    testing_with do\r
+      array = @primary_keys.collect {|key| 5}\r
+      expected = composite? ? array.to_composite_keys : array.first\r
+      @first.id = expected.to_s\r
+      assert_equal expected, @first.id\r
+    end\r
+  end\r
+  \r
+  def test_set_ids_array\r
+    testing_with do\r
+      array = @primary_keys.collect {|key| 5}\r
+      expected = composite? ? array.to_composite_keys : array.first\r
+      @first.id = expected\r
+      assert_equal expected, @first.id\r
+    end\r
+  end\r
+  \r
+  def test_set_ids_comp\r
+    testing_with do\r
+      array = @primary_keys.collect {|key| 5}\r
+      expected = composite? ? array.to_composite_keys : array.first\r
+      @first.id = expected\r
+      assert_equal expected, @first.id\r
+    end\r
+  end\r
+  \r
+  def test_primary_keys\r
+    testing_with do\r
+      if composite?\r
+        assert_not_nil @klass.primary_keys\r
+        assert_equal @primary_keys.map {|key| key.to_sym}, @klass.primary_keys\r
+        assert_equal @klass.primary_keys, @klass.primary_key\r
+      else\r
+        assert_not_nil @klass.primary_key\r
+        assert_equal @primary_keys, [@klass.primary_key.to_sym]\r
+      end\r
+      assert_equal @primary_keys.join(','), @klass.primary_key.to_s\r
+      # Need a :primary_keys should be Array with to_s overridden\r
+    end\r
+  end\r
+end
\ No newline at end of file
diff --git a/vendor/gems/composite_primary_keys-1.1.0/test/test_miscellaneous.rb b/vendor/gems/composite_primary_keys-1.1.0/test/test_miscellaneous.rb
new file mode 100644 (file)
index 0000000..25f6096
--- /dev/null
@@ -0,0 +1,39 @@
+require 'abstract_unit'\r
+require 'fixtures/reference_type'\r
+require 'fixtures/reference_code'\r
+\r
+class TestMiscellaneous < Test::Unit::TestCase\r
+  fixtures :reference_types, :reference_codes, :products\r
+  \r
+  CLASSES = {\r
+    :single => {\r
+      :class => ReferenceType,\r
+      :primary_keys => :reference_type_id,\r
+    },\r
+    :dual   => { \r
+      :class => ReferenceCode,\r
+      :primary_keys => [:reference_type_id, :reference_code],\r
+    },\r
+  }\r
+  \r
+  def setup\r
+    self.class.classes = CLASSES\r
+  end\r
+\r
+  def test_composite_class\r
+    testing_with do\r
+      assert_equal composite?, @klass.composite?\r
+    end\r
+  end\r
+\r
+  def test_composite_instance\r
+    testing_with do\r
+      assert_equal composite?, @first.composite?\r
+    end\r
+  end\r
+  \r
+  def test_count\r
+    assert_equal 2, Product.count\r
+  end\r
+  \r
+end
\ No newline at end of file
diff --git a/vendor/gems/composite_primary_keys-1.1.0/test/test_pagination.rb b/vendor/gems/composite_primary_keys-1.1.0/test/test_pagination.rb
new file mode 100644 (file)
index 0000000..fa19d95
--- /dev/null
@@ -0,0 +1,38 @@
+require 'abstract_unit'\r
+require 'fixtures/reference_type'\r
+require 'fixtures/reference_code'\r
+require 'plugins/pagination'\r
+\r
+class TestPagination < Test::Unit::TestCase\r
+  fixtures :reference_types, :reference_codes\r
+  \r
+  include ActionController::Pagination\r
+  DEFAULT_PAGE_SIZE = 2\r
+  \r
+  attr_accessor :params\r
+   \r
+  CLASSES = {\r
+    :single => {\r
+      :class => ReferenceType,\r
+      :primary_keys => :reference_type_id,\r
+      :table => :reference_types,\r
+    },\r
+    :dual   => { \r
+      :class => ReferenceCode,\r
+      :primary_keys => [:reference_type_id, :reference_code],\r
+      :table => :reference_codes,\r
+    },\r
+  }\r
+  \r
+  def setup\r
+    self.class.classes = CLASSES\r
+    @params = {}\r
+  end\r
+\r
+  def test_paginate_all\r
+    testing_with do\r
+      @object_pages, @objects = paginate @klass_info[:table], :per_page => DEFAULT_PAGE_SIZE\r
+      assert_equal 2, @objects.length, "Each page should have #{DEFAULT_PAGE_SIZE} items"\r
+    end\r
+  end\r
+end
\ No newline at end of file
diff --git a/vendor/gems/composite_primary_keys-1.1.0/test/test_polymorphic.rb b/vendor/gems/composite_primary_keys-1.1.0/test/test_polymorphic.rb
new file mode 100644 (file)
index 0000000..a632da9
--- /dev/null
@@ -0,0 +1,31 @@
+require 'abstract_unit'
+require 'fixtures/comment'
+require 'fixtures/user'
+require 'fixtures/employee'
+require 'fixtures/hack'
+
+class TestPolymorphic < Test::Unit::TestCase
+  fixtures :users, :employees, :comments, :hacks
+  
+  def test_polymorphic_has_many
+    comments = Hack.find('andrew').comments
+    assert_equal 'andrew', comments[0].person_id
+  end
+  
+  def test_polymorphic_has_one
+    first_comment = Hack.find('andrew').first_comment
+    assert_equal 'andrew', first_comment.person_id
+  end
+  
+  def test_has_many_through
+    user = users(:santiago)
+    article_names = user.articles.collect { |a| a.name }.sort
+    assert_equal ['Article One', 'Article Two'], article_names
+  end
+  
+  def test_polymorphic_has_many_through
+    user = users(:santiago)
+    assert_equal ['andrew'], user.hacks.collect { |a| a.name }.sort
+  end
+
+end
diff --git a/vendor/gems/composite_primary_keys-1.1.0/test/test_santiago.rb b/vendor/gems/composite_primary_keys-1.1.0/test/test_santiago.rb
new file mode 100644 (file)
index 0000000..4b5f433
--- /dev/null
@@ -0,0 +1,27 @@
+# Test cases devised by Santiago that broke the Composite Primary Keys\r
+# code at one point in time. But no more!!!\r
+\r
+require 'abstract_unit'\r
+require 'fixtures/user'\r
+require 'fixtures/article'\r
+require 'fixtures/reading'\r
+\r
+class TestSantiago < Test::Unit::TestCase\r
+  fixtures :suburbs, :streets, :users, :articles, :readings\r
+  \r
+  def test_normal_and_composite_associations\r
+    assert_not_nil @suburb = Suburb.find(1,1)\r
+    assert_equal 1, @suburb.streets.length\r
+    \r
+    assert_not_nil @street = Street.find(1)\r
+    assert_not_nil @street.suburb\r
+  end\r
+  \r
+  def test_single_keys\r
+    @santiago = User.find(1)\r
+    assert_not_nil @santiago.articles\r
+    assert_equal 2, @santiago.articles.length\r
+    assert_not_nil @santiago.readings\r
+    assert_equal 2, @santiago.readings.length\r
+  end\r
+end\r
diff --git a/vendor/gems/composite_primary_keys-1.1.0/test/test_tutorial_examle.rb b/vendor/gems/composite_primary_keys-1.1.0/test/test_tutorial_examle.rb
new file mode 100644 (file)
index 0000000..01f9ec6
--- /dev/null
@@ -0,0 +1,26 @@
+require 'abstract_unit'
+require 'fixtures/user'
+require 'fixtures/group'
+require 'fixtures/membership_status'
+require 'fixtures/membership'
+
+class TestTutorialExample < Test::Unit::TestCase
+  fixtures :users, :groups, :memberships, :membership_statuses
+  
+  def test_membership
+    assert(membership = Membership.find(1,1), "Cannot find a membership")
+    assert(membership.user)
+    assert(membership.group)
+  end
+  
+  def test_status
+    assert(membership = Membership.find(1,1), "Cannot find a membership")
+    assert(statuses = membership.statuses, "No has_many association to status")
+    assert_equal(membership, statuses.first.membership)
+  end
+  
+  def test_count
+    assert(membership = Membership.find(1,1), "Cannot find a membership")
+    assert_equal(1, membership.statuses.count)
+  end
+end
\ No newline at end of file
diff --git a/vendor/gems/composite_primary_keys-1.1.0/test/test_update.rb b/vendor/gems/composite_primary_keys-1.1.0/test/test_update.rb
new file mode 100644 (file)
index 0000000..d612c92
--- /dev/null
@@ -0,0 +1,40 @@
+require 'abstract_unit'\r
+require 'fixtures/reference_type'\r
+require 'fixtures/reference_code'\r
+\r
+class TestUpdate < Test::Unit::TestCase\r
+  fixtures :reference_types, :reference_codes\r
+  \r
+  CLASSES = {\r
+    :single => {\r
+      :class => ReferenceType,\r
+      :primary_keys => :reference_type_id,\r
+      :update => { :description => 'RT Desc' },\r
+    },\r
+    :dual   => { \r
+      :class => ReferenceCode,\r
+      :primary_keys => [:reference_type_id, :reference_code],\r
+      :update => { :description => 'RT Desc' },\r
+    },\r
+  }\r
+  \r
+  def setup\r
+    self.class.classes = CLASSES\r
+  end\r
+  \r
+  def test_setup\r
+    testing_with do\r
+      assert_not_nil @klass_info[:update]\r
+    end\r
+  end\r
+  \r
+  def test_update_attributes\r
+    testing_with do\r
+      assert @first.update_attributes(@klass_info[:update])\r
+      assert @first.reload\r
+      @klass_info[:update].each_pair do |attr_name, new_value|\r
+        assert_equal new_value, @first[attr_name], "Attribute #{attr_name} is incorrect"\r
+      end\r
+    end\r
+  end\r
+end
\ No newline at end of file
diff --git a/vendor/gems/composite_primary_keys-1.1.0/tmp/test.db b/vendor/gems/composite_primary_keys-1.1.0/tmp/test.db
new file mode 100644 (file)
index 0000000..923df5f
Binary files /dev/null and b/vendor/gems/composite_primary_keys-1.1.0/tmp/test.db differ
diff --git a/vendor/gems/composite_primary_keys-1.1.0/website/index.html b/vendor/gems/composite_primary_keys-1.1.0/website/index.html
new file mode 100644 (file)
index 0000000..27baec1
--- /dev/null
@@ -0,0 +1,199 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head>
+  <link rel="stylesheet" href="stylesheets/screen.css" type="text/css" media="screen" />
+  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+  <title>
+      Composite Primary Keys
+  </title>
+  <script src="javascripts/rounded_corners_lite.inc.js" type="text/javascript"></script>
+<style>
+
+</style>
+  <script type="text/javascript">
+    window.onload = function() {
+      settings = {
+          tl: { radius: 10 },
+          tr: { radius: 10 },
+          bl: { radius: 10 },
+          br: { radius: 10 },
+          antiAlias: true,
+          autoPad: true,
+          validTags: ["div"]
+      }
+      var versionBox = new curvyCorners(settings, document.getElementById("version"));
+      versionBox.applyCornersToAll();
+    }
+  </script>
+</head>
+<body>
+<div id="main">
+
+    <h1>Composite Primary Keys</h1>
+    <div id="version" class="clickable" onclick='document.location = "http://rubyforge.org/projects/compositekeys"; return false'>
+      Get Version
+      <a href="http://rubyforge.org/projects/compositekeys" class="numbers">1.1.0</a>
+    </div>
+    <h1>&amp;#x2192; Ruby on Rails</h1>
+<h1>&amp;#x2192; ActiveRecords</h1>
+<h2>What</h2>
+<p>Ruby on Rails does not support composite primary keys. This free software is an extension <br />
+to the database layer of Rails &#8211; <a href="http://wiki.rubyonrails.com/rails/pages/ActiveRecord">ActiveRecords</a> &#8211; to support composite primary keys as transparently as possible.</p>
+<p>Any Ruby script using ActiveRecords can use Composite Primary Keys with this library.</p>
+<h2>Installing</h2>
+<p><pre class="syntax"><span class="ident">sudo</span> <span class="ident">gem</span> <span class="ident">install</span> <span class="ident">composite_primary_keys</span></pre></p>
+<p>Rails: Add the following to the bottom of your <code>environment.rb</code> file</p>
+<p><pre class="syntax"><span class="ident">require</span> <span class="punct">'</span><span class="string">composite_primary_keys</span><span class="punct">'</span></pre></p>
+<p>Ruby scripts: Add the following to the top of your script</p>
+<p><pre class="syntax"><span class="ident">require</span> <span class="punct">'</span><span class="string">rubygems</span><span class="punct">'</span>
+<span class="ident">require</span> <span class="punct">'</span><span class="string">composite_primary_keys</span><span class="punct">'</span></pre></p>
+<h2>The basics</h2>
+<p>A model with composite primary keys would look like&#8230;</p>
+<p><pre class="syntax"><span class="keyword">class </span><span class="class">Membership</span> <span class="punct">&lt;</span> <span class="constant">ActiveRecord</span><span class="punct">::</span><span class="constant">Base</span>
+  <span class="comment"># set_primary_keys *keys - turns on composite key functionality</span>
+  <span class="ident">set_primary_keys</span> <span class="symbol">:user_id</span><span class="punct">,</span> <span class="symbol">:group_id</span>
+  <span class="ident">belongs_to</span> <span class="symbol">:user</span>
+  <span class="ident">belongs_to</span> <span class="symbol">:group</span>
+  <span class="ident">has_many</span> <span class="symbol">:statuses</span><span class="punct">,</span> <span class="symbol">:class_name</span> <span class="punct">=&gt;</span> <span class="punct">'</span><span class="string">MembershipStatus</span><span class="punct">',</span> <span class="symbol">:foreign_key</span> <span class="punct">=&gt;</span> <span class="punct">[</span><span class="symbol">:user_id</span><span class="punct">,</span> <span class="symbol">:group_id</span><span class="punct">]</span>
+<span class="keyword">end</span></pre></p>
+<p>A model associated with a composite key model would be defined like&#8230;</p>
+<p><pre class="syntax"><span class="keyword">class </span><span class="class">MembershipStatus</span> <span class="punct">&lt;</span> <span class="constant">ActiveRecord</span><span class="punct">::</span><span class="constant">Base</span>
+  <span class="ident">belongs_to</span> <span class="symbol">:membership</span><span class="punct">,</span> <span class="symbol">:foreign_key</span> <span class="punct">=&gt;</span> <span class="punct">[</span><span class="symbol">:user_id</span><span class="punct">,</span> <span class="symbol">:group_id</span><span class="punct">]</span>
+<span class="keyword">end</span></pre></p>
+<p>That is, associations can include composite keys too. Nice.</p>
+<h2>Demonstration of usage</h2>
+<p>Once you&#8217;ve created your models to specify composite primary keys (such as the Membership class) and associations (such as MembershipStatus#membership), you can uses them like any normal model with associations.</p>
+<p>But first, lets check out our primary keys.</p>
+<p><pre class="syntax"><span class="constant">MembershipStatus</span><span class="punct">.</span><span class="ident">primary_key</span> <span class="comment"># =&gt; &quot;id&quot;    # normal single key</span>
+<span class="constant">Membership</span><span class="punct">.</span><span class="ident">primary_key</span>  <span class="comment"># =&gt; [:user_id, :group_id] # composite keys</span>
+<span class="constant">Membership</span><span class="punct">.</span><span class="ident">primary_key</span><span class="punct">.</span><span class="ident">to_s</span> <span class="comment"># =&gt; &quot;user_id,group_id&quot;</span></pre></p>
+<p>Now we want to be able to find instances using the same syntax we always use for ActiveRecords&#8230;</p>
+<p><pre class="syntax"><span class="constant">MembershipStatus</span><span class="punct">.</span><span class="ident">find</span><span class="punct">(</span><span class="number">1</span><span class="punct">)</span>    <span class="comment"># single id returns single instance</span>
+<span class="punct">=&gt;</span> <span class="punct">&lt;</span><span class="constant">MembershipStatus</span><span class="punct">:</span><span class="number">0x392a8c8</span> <span class="attribute">@attributes</span><span class="punct">={&quot;</span><span class="string">id</span><span class="punct">&quot;=&gt;&quot;</span><span class="string">1</span><span class="punct">&quot;,</span> <span class="punct">&quot;</span><span class="string">status</span><span class="punct">&quot;=&gt;&quot;</span><span class="string">Active</span><span class="punct">&quot;}&gt;</span>
+<span class="constant">Membership</span><span class="punct">.</span><span class="ident">find</span><span class="punct">(</span><span class="number">1</span><span class="punct">,</span><span class="number">1</span><span class="punct">)</span>  <span class="comment"># composite ids returns single instance</span>
+<span class="punct">=&gt;</span> <span class="punct">&lt;</span><span class="constant">Membership</span><span class="punct">:</span><span class="number">0x39218b0</span> <span class="attribute">@attributes</span><span class="punct">={&quot;</span><span class="string">user_id</span><span class="punct">&quot;=&gt;&quot;</span><span class="string">1</span><span class="punct">&quot;,</span> <span class="punct">&quot;</span><span class="string">group_id</span><span class="punct">&quot;=&gt;&quot;</span><span class="string">1</span><span class="punct">&quot;}&gt;</span></pre></p>
+<p>Using <a href="http://www.rubyonrails.org">Ruby on Rails</a>? You&#8217;ll want to your url_for helpers<br />
+to convert composite keys into strings and back again&#8230;</p>
+<p><pre class="syntax"><span class="constant">Membership</span><span class="punct">.</span><span class="ident">find</span><span class="punct">(</span><span class="symbol">:first</span><span class="punct">).</span><span class="ident">to_param</span> <span class="comment"># =&gt; &quot;1,1&quot;</span></pre></p>
+<p>And then use the string id within your controller to find the object again</p>
+<p><pre class="syntax"><span class="ident">params</span><span class="punct">[</span><span class="symbol">:id</span><span class="punct">]</span> <span class="comment"># =&gt; '1,1'</span>
+<span class="constant">Membership</span><span class="punct">.</span><span class="ident">find</span><span class="punct">(</span><span class="ident">params</span><span class="punct">[</span><span class="symbol">:id</span><span class="punct">])</span>
+<span class="punct">=&gt;</span> <span class="punct">&lt;</span><span class="constant">Membership</span><span class="punct">:</span><span class="number">0x3904288</span> <span class="attribute">@attributes</span><span class="punct">={&quot;</span><span class="string">user_id</span><span class="punct">&quot;=&gt;&quot;</span><span class="string">1</span><span class="punct">&quot;,</span> <span class="punct">&quot;</span><span class="string">group_id</span><span class="punct">&quot;=&gt;&quot;</span><span class="string">1</span><span class="punct">&quot;}&gt;</span></pre></p>
+<p>That is, an ActiveRecord supporting composite keys behaves transparently<br />
+throughout your application. Just like a normal ActiveRecord.</p>
+<h2>Other tricks</h2>
+<h3>Pass a list of composite ids to the <code>#find</code> method</h3>
+<p><pre class="syntax"><span class="constant">Membership</span><span class="punct">.</span><span class="ident">find</span> <span class="punct">[</span><span class="number">1</span><span class="punct">,</span><span class="number">1</span><span class="punct">],</span> <span class="punct">[</span><span class="number">2</span><span class="punct">,</span><span class="number">1</span><span class="punct">]</span>
+<span class="punct">=&gt;</span> <span class="punct">[</span>
+  <span class="punct">&lt;</span><span class="constant">Membership</span><span class="punct">:</span><span class="number">0x394ade8</span> <span class="attribute">@attributes</span><span class="punct">={&quot;</span><span class="string">user_id</span><span class="punct">&quot;=&gt;&quot;</span><span class="string">1</span><span class="punct">&quot;,</span> <span class="punct">&quot;</span><span class="string">group_id</span><span class="punct">&quot;=&gt;&quot;</span><span class="string">1</span><span class="punct">&quot;}&gt;,</span> 
+  <span class="punct">&lt;</span><span class="constant">Membership</span><span class="punct">:</span><span class="number">0x394ada0</span> <span class="attribute">@attributes</span><span class="punct">={&quot;</span><span class="string">user_id</span><span class="punct">&quot;=&gt;&quot;</span><span class="string">2</span><span class="punct">&quot;,</span> <span class="punct">&quot;</span><span class="string">group_id</span><span class="punct">&quot;=&gt;&quot;</span><span class="string">1</span><span class="punct">&quot;}&gt;</span>
+<span class="punct">]</span></pre></p>
+<p>Perform <code>#count</code> operations</p>
+<p><pre class="syntax"><span class="constant">MembershipStatus</span><span class="punct">.</span><span class="ident">find</span><span class="punct">(</span><span class="symbol">:first</span><span class="punct">).</span><span class="ident">memberships</span><span class="punct">.</span><span class="ident">count</span> <span class="comment"># =&gt; 1</span></pre></p>
+<h3>Routes with Rails</h3>
+<p>From Pete Sumskas:</p>
+<blockquote>
+<p>I ran into one problem that I didn&#8217;t see mentioned on <a href="http://groups.google.com/group/compositekeys">this list</a> &#8211; <br />
+       and I   didn&#8217;t see any information about what I should do to address it in the<br />
+       documentation (might have missed it).</p>
+<p>The problem was that the urls being generated for a &#8216;show&#8217; action (for<br />
+       example) had a syntax like:<br />
+       <br />
+       <pre>/controller/show/123000,Bu70</pre></p>
+<p>for a two-field composite PK. The default routing would not match that,<br />
+       so after working out how to do the routing I added:<br />
+       <br />
+       <pre class="syntax"><span class="ident">map</span><span class="punct">.</span><span class="ident">connect</span> <span class="punct">'</span><span class="string">:controller/:action/:id</span><span class="punct">',</span> <span class="symbol">:id</span> <span class="punct">=&gt;</span> <span class="punct">/</span><span class="regex"><span class="escape">\w</span>+(,<span class="escape">\w</span>+)*</span><span class="punct">/</span></pre><br />
+       <br />
+       to my <code>route.rb</code> file.</p>
+</blockquote>
+<p><a name="dbs"></a></p>
+<h2>Which databases?</h2>
+<p>A suite of unit tests have been run on the following databases supported by ActiveRecord:</p>
+<table>
+       <tr>
+               <th>Database</th>
+               <th>Test Success</th>
+               <th>User feedback</th>
+       </tr>
+       <tr>
+               <td>mysql     </td>
+               <td><span class=success><span class="caps">YES</span></span></td>
+               <td><span class=success><span class="caps">YES</span></span> (<a href="mailto:compositekeys@googlegroups.com?subject=Mysql+is+working">Yes!</a> or <a href="mailto:compositekeys@googlegroups.com?subject=Mysql+is+failing">No&#8230;</a>)</td>
+       </tr>
+       <tr>
+               <td>sqlite3   </td>
+               <td><span class=success><span class="caps">YES</span></span></td>
+               <td><span class=success><span class="caps">YES</span></span> (<a href="mailto:compositekeys@googlegroups.com?subject=Sqlite3+is+working">Yes!</a> or <a href="mailto:compositekeys@googlegroups.com?subject=Sqlite3+is+failing">No&#8230;</a>)</td>
+       </tr>
+       <tr>
+               <td>postgresql</td>
+               <td><span class=success><span class="caps">YES</span></span></td>
+               <td><span class=success><span class="caps">YES</span></span> (<a href="mailto:compositekeys@googlegroups.com?subject=Postgresql+is+working">Yes!</a> or <a href="mailto:compositekeys@googlegroups.com?subject=Postgresql+is+failing">No&#8230;</a>)</td>
+       </tr>
+       <tr>
+               <td>oracle    </td>
+               <td><span class=success><span class="caps">YES</span></span></td>
+               <td><span class=success><span class="caps">YES</span></span> (<a href="mailto:compositekeys@googlegroups.com?subject=Oracle+is+working">Yes!</a> or <a href="mailto:compositekeys@googlegroups.com?subject=Oracle+is+failing">No&#8230;</a>)</td>
+       </tr>
+       <tr>
+               <td>sqlserver </td>
+               <td><span class=unknown>???</span> (<a href="mailto:compositekeys@googlegroups.com?subject=Help+with+SQLServer">I can help</a>)</td>
+               <td><span class=unknown>???</span> (<a href="mailto:compositekeys@googlegroups.com?subject=SQLServer+is+working">Yes!</a> or <a href="mailto:compositekeys@googlegroups.com?subject=SQLServer+is+failing">No&#8230;</a>)</td>
+       </tr>
+       <tr>
+               <td>db2       </td>
+               <td><span class=unknown>???</span> (<a href="mailto:compositekeys@googlegroups.com?subject=Help+with+DB2">I can help</a>)</td>
+               <td><span class=unknown>???</span> (<a href="mailto:compositekeys@googlegroups.com?subject=DB2+is+working">Yes!</a> or <a href="mailto:compositekeys@googlegroups.com?subject=DB2+is+failing">No&#8230;</a>)</td>
+       </tr>
+       <tr>
+               <td>firebird  </td>
+               <td><span class=unknown>???</span> (<a href="mailto:compositekeys@googlegroups.com?subject=Help+with+Firebird">I can help</a>)</td>
+               <td><span class=unknown>???</span> (<a href="mailto:compositekeys@googlegroups.com?subject=Firebird+is+working">Yes!</a> or <a href="mailto:compositekeys@googlegroups.com?subject=Firebird+is+failing">No&#8230;</a>)</td>
+       </tr>
+       <tr>
+               <td>sybase    </td>
+               <td><span class=unknown>???</span> (<a href="mailto:compositekeys@googlegroups.com?subject=Help+with+Sybase">I can help</a>)</td>
+               <td><span class=unknown>???</span> (<a href="mailto:compositekeys@googlegroups.com?subject=Sybase+is+working">Yes!</a> or <a href="mailto:compositekeys@googlegroups.com?subject=Sybase+is+failing">No&#8230;</a>)</td>
+       </tr>
+       <tr>
+               <td>openbase  </td>
+               <td><span class=unknown>???</span> (<a href="mailto:compositekeys@googlegroups.com?subject=Help+with+Openbase">I can help</a>)</td>
+               <td><span class=unknown>???</span> (<a href="mailto:compositekeys@googlegroups.com?subject=Openbase+is+working">Yes!</a> or <a href="mailto:compositekeys@googlegroups.com?subject=Openbase+is+failing">No&#8230;</a>)</td>
+       </tr>
+       <tr>
+               <td>frontbase </td>
+               <td><span class=unknown>???</span> (<a href="mailto:compositekeys@googlegroups.com?subject=Help+with+Frontbase">I can help</a>)</td>
+               <td><span class=unknown>???</span> (<a href="mailto:compositekeys@googlegroups.com?subject=Frontbase+is+working">Yes!</a> or <a href="mailto:compositekeys@googlegroups.com?subject=Frontbase+is+failing">No&#8230;</a>)</td>
+       </tr>
+</table>
+<h2>Dr Nic&#8217;s Blog</h2>
+<p><a href="http://www.drnicwilliams.com">http://www.drnicwilliams.com</a> &#8211; for future announcements and<br />
+other stories and things.</p>
+<h2>Forum</h2>
+<p><a href="http://groups.google.com/group/compositekeys">http://groups.google.com/group/compositekeys</a></p>
+<h2>How to submit patches</h2>
+<p>Read the <a href="http://drnicwilliams.com/2007/06/01/8-steps-for-fixing-other-peoples-code/">8 steps for fixing other people&#8217;s code</a> and for section <a href="http://drnicwilliams.com/2007/06/01/8-steps-for-fixing-other-peoples-code/#8b-google-groups">8b: Submit patch to Google Groups</a>, use the Google Group above.</p>
+<p>The source for this project is available via git. You can <a href="http://github.com/drnic/composite_primary_keys/tree/master">browse and/or fork the source</a>, or to clone the project locally:<br />
+  <br />
+<pre>git clone git://github.com/drnic/composite_primary_keys.git</pre></p>
+<h2>Licence</h2>
+<p>This code is free to use under the terms of the <span class="caps">MIT</span> licence.</p>
+<h2>Contact</h2>
+<p>Comments are welcome. Send an email to <a href="mailto:drnicwilliams@gmail.com">Dr Nic Williams</a>.</p>
+    <p class="coda">
+      <a href="mailto:drnicwilliams@gmail.com">Dr Nic</a>, 25th October 2008<br>
+      Theme extended from <a href="http://rb2js.rubyforge.org/">Paul Battley</a>
+    </p>
+</div>
+
+<script src="http://www.google-analytics.com/urchin.js" type="text/javascript">
+</script>
+<script type="text/javascript">
+_uacct = "UA-567811-2";
+urchinTracker();
+</script>
+
+</body>
+</html>
diff --git a/vendor/gems/composite_primary_keys-1.1.0/website/index.txt b/vendor/gems/composite_primary_keys-1.1.0/website/index.txt
new file mode 100644 (file)
index 0000000..fd66d97
--- /dev/null
@@ -0,0 +1,159 @@
+h1. Composite Primary Keys
+
+h1. &#x2192; Ruby on Rails
+
+h1. &#x2192; ActiveRecords
+
+h2. What
+
+Ruby on Rails does not support composite primary keys. This free software is an extension 
+to the database layer of Rails - "ActiveRecords":http://wiki.rubyonrails.com/rails/pages/ActiveRecord - to support composite primary keys as transparently as possible.
+
+Any Ruby script using ActiveRecords can use Composite Primary Keys with this library.
+
+h2. Installing
+
+<pre syntax="ruby">sudo gem install composite_primary_keys</pre>
+
+Rails: Add the following to the bottom of your <code>environment.rb</code> file
+
+<pre syntax="ruby">require 'composite_primary_keys'</pre>
+
+Ruby scripts: Add the following to the top of your script
+
+<pre syntax="ruby">require 'rubygems'
+require 'composite_primary_keys'</pre>
+
+h2. The basics
+
+A model with composite primary keys would look like...
+
+<pre syntax="ruby">class Membership < ActiveRecord::Base
+  # set_primary_keys *keys - turns on composite key functionality
+  set_primary_keys :user_id, :group_id
+  belongs_to :user
+  belongs_to :group
+  has_many :statuses, :class_name => 'MembershipStatus', :foreign_key => [:user_id, :group_id]
+end</pre>
+
+A model associated with a composite key model would be defined like...
+
+<pre syntax="ruby">class MembershipStatus < ActiveRecord::Base
+  belongs_to :membership, :foreign_key => [:user_id, :group_id]
+end</pre>
+
+That is, associations can include composite keys too. Nice.
+
+h2. Demonstration of usage
+
+Once you've created your models to specify composite primary keys (such as the Membership class) and associations (such as MembershipStatus#membership), you can uses them like any normal model with associations.
+
+But first, lets check out our primary keys.
+
+<pre syntax="ruby">MembershipStatus.primary_key # => "id"    # normal single key
+Membership.primary_key  # => [:user_id, :group_id] # composite keys
+Membership.primary_key.to_s # => "user_id,group_id"</pre>
+
+Now we want to be able to find instances using the same syntax we always use for ActiveRecords...
+
+<pre syntax="ruby">MembershipStatus.find(1)    # single id returns single instance
+=> <MembershipStatus:0x392a8c8 @attributes={"id"=>"1", "status"=>"Active"}>
+Membership.find(1,1)  # composite ids returns single instance
+=> <Membership:0x39218b0 @attributes={"user_id"=>"1", "group_id"=>"1"}></pre>
+
+Using "Ruby on Rails":http://www.rubyonrails.org? You'll want to your url_for helpers
+to convert composite keys into strings and back again...
+
+<pre syntax="ruby">Membership.find(:first).to_param # => "1,1"</pre>
+
+And then use the string id within your controller to find the object again
+
+<pre syntax="ruby">params[:id] # => '1,1'
+Membership.find(params[:id])
+=> <Membership:0x3904288 @attributes={"user_id"=>"1", "group_id"=>"1"}></pre>
+
+That is, an ActiveRecord supporting composite keys behaves transparently
+throughout your application. Just like a normal ActiveRecord.
+
+
+h2. Other tricks
+
+h3. Pass a list of composite ids to the <code>#find</code> method
+
+<pre syntax="ruby">Membership.find [1,1], [2,1]
+=> [
+  <Membership:0x394ade8 @attributes={"user_id"=>"1", "group_id"=>"1"}>, 
+  <Membership:0x394ada0 @attributes={"user_id"=>"2", "group_id"=>"1"}>
+]</pre>
+
+Perform <code>#count</code> operations
+
+<pre syntax="ruby">MembershipStatus.find(:first).memberships.count # => 1</pre>
+
+h3. Routes with Rails
+
+From Pete Sumskas:
+
+<blockquote>
+       I ran into one problem that I didn't see mentioned on "this list":http://groups.google.com/group/compositekeys - 
+       and I   didn't see any information about what I should do to address it in the
+       documentation (might have missed it).
+
+       The problem was that the urls being generated for a 'show' action (for
+       example) had a syntax like:
+       
+       <pre>/controller/show/123000,Bu70</pre>
+
+       for a two-field composite PK. The default routing would not match that,
+       so after working out how to do the routing I added:
+       
+       <pre syntax="ruby">map.connect ':controller/:action/:id', :id => /\w+(,\w+)*/</pre>
+       
+       to my <code>route.rb</code> file.
+       
+</blockquote>
+
+<a name="dbs"></a>
+
+h2. Which databases?
+
+
+A suite of unit tests have been run on the following databases supported by ActiveRecord:
+
+|_.Database|_.Test Success|_.User feedback|
+|mysql     |<span class=success>YES</span>|<span class=success>YES</span> ("Yes!":mailto:compositekeys@googlegroups.com?subject=Mysql+is+working or "No...":mailto:compositekeys@googlegroups.com?subject=Mysql+is+failing)|
+|sqlite3   |<span class=success>YES</span>|<span class=success>YES</span> ("Yes!":mailto:compositekeys@googlegroups.com?subject=Sqlite3+is+working or "No...":mailto:compositekeys@googlegroups.com?subject=Sqlite3+is+failing)|
+|postgresql|<span class=success>YES</span>|<span class=success>YES</span> ("Yes!":mailto:compositekeys@googlegroups.com?subject=Postgresql+is+working or "No...":mailto:compositekeys@googlegroups.com?subject=Postgresql+is+failing)|
+|oracle    |<span class=success>YES</span>|<span class=success>YES</span> ("Yes!":mailto:compositekeys@googlegroups.com?subject=Oracle+is+working or "No...":mailto:compositekeys@googlegroups.com?subject=Oracle+is+failing)|
+|sqlserver |<span class=unknown>???</span> ("I can help":mailto:compositekeys@googlegroups.com?subject=Help+with+SQLServer)|<span class=unknown>???</span> ("Yes!":mailto:compositekeys@googlegroups.com?subject=SQLServer+is+working or "No...":mailto:compositekeys@googlegroups.com?subject=SQLServer+is+failing)|
+|db2       |<span class=unknown>???</span> ("I can help":mailto:compositekeys@googlegroups.com?subject=Help+with+DB2)|<span class=unknown>???</span> ("Yes!":mailto:compositekeys@googlegroups.com?subject=DB2+is+working or "No...":mailto:compositekeys@googlegroups.com?subject=DB2+is+failing)|
+|firebird  |<span class=unknown>???</span> ("I can help":mailto:compositekeys@googlegroups.com?subject=Help+with+Firebird)|<span class=unknown>???</span> ("Yes!":mailto:compositekeys@googlegroups.com?subject=Firebird+is+working or "No...":mailto:compositekeys@googlegroups.com?subject=Firebird+is+failing)|
+|sybase    |<span class=unknown>???</span> ("I can help":mailto:compositekeys@googlegroups.com?subject=Help+with+Sybase)|<span class=unknown>???</span> ("Yes!":mailto:compositekeys@googlegroups.com?subject=Sybase+is+working or "No...":mailto:compositekeys@googlegroups.com?subject=Sybase+is+failing)|
+|openbase  |<span class=unknown>???</span> ("I can help":mailto:compositekeys@googlegroups.com?subject=Help+with+Openbase)|<span class=unknown>???</span> ("Yes!":mailto:compositekeys@googlegroups.com?subject=Openbase+is+working or "No...":mailto:compositekeys@googlegroups.com?subject=Openbase+is+failing)|
+|frontbase |<span class=unknown>???</span> ("I can help":mailto:compositekeys@googlegroups.com?subject=Help+with+Frontbase)|<span class=unknown>???</span> ("Yes!":mailto:compositekeys@googlegroups.com?subject=Frontbase+is+working or "No...":mailto:compositekeys@googlegroups.com?subject=Frontbase+is+failing)|
+
+h2. Dr Nic's Blog
+
+"http://www.drnicwilliams.com":http://www.drnicwilliams.com - for future announcements and
+other stories and things.
+
+h2. Forum
+
+"http://groups.google.com/group/compositekeys":http://groups.google.com/group/compositekeys
+
+h2. How to submit patches
+
+Read the "8 steps for fixing other people's code":http://drnicwilliams.com/2007/06/01/8-steps-for-fixing-other-peoples-code/ and for section "8b: Submit patch to Google Groups":http://drnicwilliams.com/2007/06/01/8-steps-for-fixing-other-peoples-code/#8b-google-groups, use the Google Group above.
+
+
+The source for this project is available via git. You can "browse and/or fork the source":http://github.com/drnic/composite_primary_keys/tree/master, or to clone the project locally:
+  
+<pre>git clone git://github.com/drnic/composite_primary_keys.git</pre>
+
+h2. Licence
+
+This code is free to use under the terms of the MIT licence. 
+
+h2. Contact
+
+Comments are welcome. Send an email to "Dr Nic Williams":mailto:drnicwilliams@gmail.com.
diff --git a/vendor/gems/composite_primary_keys-1.1.0/website/javascripts/rounded_corners_lite.inc.js b/vendor/gems/composite_primary_keys-1.1.0/website/javascripts/rounded_corners_lite.inc.js
new file mode 100644 (file)
index 0000000..afc3ea3
--- /dev/null
@@ -0,0 +1,285 @@
+
+ /****************************************************************
+  *                                                              *
+  *  curvyCorners                                                *
+  *  ------------                                                *
+  *                                                              *
+  *  This script generates rounded corners for your divs.        *
+  *                                                              *
+  *  Version 1.2.9                                               *
+  *  Copyright (c) 2006 Cameron Cooke                            *
+  *  By: Cameron Cooke and Tim Hutchison.                        *
+  *                                                              *
+  *                                                              *
+  *  Website: http://www.curvycorners.net                        *
+  *  Email:   info@totalinfinity.com                             *
+  *  Forum:   http://www.curvycorners.net/forum/                 *
+  *                                                              *
+  *                                                              *
+  *  This library is free software; you can redistribute         *
+  *  it and/or modify it under the terms of the GNU              *
+  *  Lesser General Public License as published by the           *
+  *  Free Software Foundation; either version 2.1 of the         *
+  *  License, or (at your option) any later version.             *
+  *                                                              *
+  *  This library is distributed in the hope that it will        *
+  *  be useful, but WITHOUT ANY WARRANTY; without even the       *
+  *  implied warranty of MERCHANTABILITY or FITNESS FOR A        *
+  *  PARTICULAR PURPOSE. See the GNU Lesser General Public       *
+  *  License for more details.                                   *
+  *                                                              *
+  *  You should have received a copy of the GNU Lesser           *
+  *  General Public License along with this library;             *
+  *  Inc., 59 Temple Place, Suite 330, Boston,                   *
+  *  MA 02111-1307 USA                                           *
+  *                                                              *
+  ****************************************************************/
+  
+var isIE = navigator.userAgent.toLowerCase().indexOf("msie") > -1; var isMoz = document.implementation && document.implementation.createDocument; var isSafari = ((navigator.userAgent.toLowerCase().indexOf('safari')!=-1)&&(navigator.userAgent.toLowerCase().indexOf('mac')!=-1))?true:false; function curvyCorners()
+{ if(typeof(arguments[0]) != "object") throw newCurvyError("First parameter of curvyCorners() must be an object."); if(typeof(arguments[1]) != "object" && typeof(arguments[1]) != "string") throw newCurvyError("Second parameter of curvyCorners() must be an object or a class name."); if(typeof(arguments[1]) == "string")
+{ var startIndex = 0; var boxCol = getElementsByClass(arguments[1]);}
+else
+{ var startIndex = 1; var boxCol = arguments;}
+var curvyCornersCol = new Array(); if(arguments[0].validTags)
+var validElements = arguments[0].validTags; else
+var validElements = ["div"]; for(var i = startIndex, j = boxCol.length; i < j; i++)
+{ var currentTag = boxCol[i].tagName.toLowerCase(); if(inArray(validElements, currentTag) !== false)
+{ curvyCornersCol[curvyCornersCol.length] = new curvyObject(arguments[0], boxCol[i]);}
+}
+this.objects = curvyCornersCol; this.applyCornersToAll = function()
+{ for(var x = 0, k = this.objects.length; x < k; x++)
+{ this.objects[x].applyCorners();}
+}
+}
+function curvyObject()
+{ this.box = arguments[1]; this.settings = arguments[0]; this.topContainer = null; this.bottomContainer = null; this.masterCorners = new Array(); this.contentDIV = null; var boxHeight = get_style(this.box, "height", "height"); var boxWidth = get_style(this.box, "width", "width"); var borderWidth = get_style(this.box, "borderTopWidth", "border-top-width"); var borderColour = get_style(this.box, "borderTopColor", "border-top-color"); var boxColour = get_style(this.box, "backgroundColor", "background-color"); var backgroundImage = get_style(this.box, "backgroundImage", "background-image"); var boxPosition = get_style(this.box, "position", "position"); var boxPadding = get_style(this.box, "paddingTop", "padding-top"); this.boxHeight = parseInt(((boxHeight != "" && boxHeight != "auto" && boxHeight.indexOf("%") == -1)? boxHeight.substring(0, boxHeight.indexOf("px")) : this.box.scrollHeight)); this.boxWidth = parseInt(((boxWidth != "" && boxWidth != "auto" && boxWidth.indexOf("%") == -1)? boxWidth.substring(0, boxWidth.indexOf("px")) : this.box.scrollWidth)); this.borderWidth = parseInt(((borderWidth != "" && borderWidth.indexOf("px") !== -1)? borderWidth.slice(0, borderWidth.indexOf("px")) : 0)); this.boxColour = format_colour(boxColour); this.boxPadding = parseInt(((boxPadding != "" && boxPadding.indexOf("px") !== -1)? boxPadding.slice(0, boxPadding.indexOf("px")) : 0)); this.borderColour = format_colour(borderColour); this.borderString = this.borderWidth + "px" + " solid " + this.borderColour; this.backgroundImage = ((backgroundImage != "none")? backgroundImage : ""); this.boxContent = this.box.innerHTML; if(boxPosition != "absolute") this.box.style.position = "relative"; this.box.style.padding = "0px"; if(isIE && boxWidth == "auto" && boxHeight == "auto") this.box.style.width = "100%"; if(this.settings.autoPad == true && this.boxPadding > 0)
+this.box.innerHTML = ""; this.applyCorners = function()
+{ for(var t = 0; t < 2; t++)
+{ switch(t)
+{ case 0:
+if(this.settings.tl || this.settings.tr)
+{ var newMainContainer = document.createElement("DIV"); newMainContainer.style.width = "100%"; newMainContainer.style.fontSize = "1px"; newMainContainer.style.overflow = "hidden"; newMainContainer.style.position = "absolute"; newMainContainer.style.paddingLeft = this.borderWidth + "px"; newMainContainer.style.paddingRight = this.borderWidth + "px"; var topMaxRadius = Math.max(this.settings.tl ? this.settings.tl.radius : 0, this.settings.tr ? this.settings.tr.radius : 0); newMainContainer.style.height = topMaxRadius + "px"; newMainContainer.style.top = 0 - topMaxRadius + "px"; newMainContainer.style.left = 0 - this.borderWidth + "px"; this.topContainer = this.box.appendChild(newMainContainer);}
+break; case 1:
+if(this.settings.bl || this.settings.br)
+{ var newMainContainer = document.createElement("DIV"); newMainContainer.style.width = "100%"; newMainContainer.style.fontSize = "1px"; newMainContainer.style.overflow = "hidden"; newMainContainer.style.position = "absolute"; newMainContainer.style.paddingLeft = this.borderWidth + "px"; newMainContainer.style.paddingRight = this.borderWidth + "px"; var botMaxRadius = Math.max(this.settings.bl ? this.settings.bl.radius : 0, this.settings.br ? this.settings.br.radius : 0); newMainContainer.style.height = botMaxRadius + "px"; newMainContainer.style.bottom = 0 - botMaxRadius + "px"; newMainContainer.style.left = 0 - this.borderWidth + "px"; this.bottomContainer = this.box.appendChild(newMainContainer);}
+break;}
+}
+if(this.topContainer) this.box.style.borderTopWidth = "0px"; if(this.bottomContainer) this.box.style.borderBottomWidth = "0px"; var corners = ["tr", "tl", "br", "bl"]; for(var i in corners)
+{ if(i > -1 < 4)
+{ var cc = corners[i]; if(!this.settings[cc])
+{ if(((cc == "tr" || cc == "tl") && this.topContainer != null) || ((cc == "br" || cc == "bl") && this.bottomContainer != null))
+{ var newCorner = document.createElement("DIV"); newCorner.style.position = "relative"; newCorner.style.fontSize = "1px"; newCorner.style.overflow = "hidden"; if(this.backgroundImage == "")
+newCorner.style.backgroundColor = this.boxColour; else
+newCorner.style.backgroundImage = this.backgroundImage; switch(cc)
+{ case "tl":
+newCorner.style.height = topMaxRadius - this.borderWidth + "px"; newCorner.style.marginRight = this.settings.tr.radius - (this.borderWidth*2) + "px"; newCorner.style.borderLeft = this.borderString; newCorner.style.borderTop = this.borderString; newCorner.style.left = -this.borderWidth + "px"; break; case "tr":
+newCorner.style.height = topMaxRadius - this.borderWidth + "px"; newCorner.style.marginLeft = this.settings.tl.radius - (this.borderWidth*2) + "px"; newCorner.style.borderRight = this.borderString; newCorner.style.borderTop = this.borderString; newCorner.style.backgroundPosition = "-" + (topMaxRadius + this.borderWidth) + "px 0px"; newCorner.style.left = this.borderWidth + "px"; break; case "bl":
+newCorner.style.height = botMaxRadius - this.borderWidth + "px"; newCorner.style.marginRight = this.settings.br.radius - (this.borderWidth*2) + "px"; newCorner.style.borderLeft = this.borderString; newCorner.style.borderBottom = this.borderString; newCorner.style.left = -this.borderWidth + "px"; newCorner.style.backgroundPosition = "-" + (this.borderWidth) + "px -" + (this.boxHeight + (botMaxRadius + this.borderWidth)) + "px"; break; case "br":
+newCorner.style.height = botMaxRadius - this.borderWidth + "px"; newCorner.style.marginLeft = this.settings.bl.radius - (this.borderWidth*2) + "px"; newCorner.style.borderRight = this.borderString; newCorner.style.borderBottom = this.borderString; newCorner.style.left = this.borderWidth + "px"
+newCorner.style.backgroundPosition = "-" + (botMaxRadius + this.borderWidth) + "px -" + (this.boxHeight + (botMaxRadius + this.borderWidth)) + "px"; break;}
+}
+}
+else
+{ if(this.masterCorners[this.settings[cc].radius])
+{ var newCorner = this.masterCorners[this.settings[cc].radius].cloneNode(true);}
+else
+{ var newCorner = document.createElement("DIV"); newCorner.style.height = this.settings[cc].radius + "px"; newCorner.style.width = this.settings[cc].radius + "px"; newCorner.style.position = "absolute"; newCorner.style.fontSize = "1px"; newCorner.style.overflow = "hidden"; var borderRadius = parseInt(this.settings[cc].radius - this.borderWidth); for(var intx = 0, j = this.settings[cc].radius; intx < j; intx++)
+{ if((intx +1) >= borderRadius)
+var y1 = -1; else
+var y1 = (Math.floor(Math.sqrt(Math.pow(borderRadius, 2) - Math.pow((intx+1), 2))) - 1); if(borderRadius != j)
+{ if((intx) >= borderRadius)
+var y2 = -1; else
+var y2 = Math.ceil(Math.sqrt(Math.pow(borderRadius,2) - Math.pow(intx, 2))); if((intx+1) >= j)
+var y3 = -1; else
+var y3 = (Math.floor(Math.sqrt(Math.pow(j ,2) - Math.pow((intx+1), 2))) - 1);}
+if((intx) >= j)
+var y4 = -1; else
+var y4 = Math.ceil(Math.sqrt(Math.pow(j ,2) - Math.pow(intx, 2))); if(y1 > -1) this.drawPixel(intx, 0, this.boxColour, 100, (y1+1), newCorner, -1, this.settings[cc].radius); if(borderRadius != j)
+{ for(var inty = (y1 + 1); inty < y2; inty++)
+{ if(this.settings.antiAlias)
+{ if(this.backgroundImage != "")
+{ var borderFract = (pixelFraction(intx, inty, borderRadius) * 100); if(borderFract < 30)
+{ this.drawPixel(intx, inty, this.borderColour, 100, 1, newCorner, 0, this.settings[cc].radius);}
+else
+{ this.drawPixel(intx, inty, this.borderColour, 100, 1, newCorner, -1, this.settings[cc].radius);}
+}
+else
+{ var pixelcolour = BlendColour(this.boxColour, this.borderColour, pixelFraction(intx, inty, borderRadius)); this.drawPixel(intx, inty, pixelcolour, 100, 1, newCorner, 0, this.settings[cc].radius, cc);}
+}
+}
+if(this.settings.antiAlias)
+{ if(y3 >= y2)
+{ if (y2 == -1) y2 = 0; this.drawPixel(intx, y2, this.borderColour, 100, (y3 - y2 + 1), newCorner, 0, 0);}
+}
+else
+{ if(y3 >= y1)
+{ this.drawPixel(intx, (y1 + 1), this.borderColour, 100, (y3 - y1), newCorner, 0, 0);}
+}
+var outsideColour = this.borderColour;}
+else
+{ var outsideColour = this.boxColour; var y3 = y1;}
+if(this.settings.antiAlias)
+{ for(var inty = (y3 + 1); inty < y4; inty++)
+{ this.drawPixel(intx, inty, outsideColour, (pixelFraction(intx, inty , j) * 100), 1, newCorner, ((this.borderWidth > 0)? 0 : -1), this.settings[cc].radius);}
+}
+}
+this.masterCorners[this.settings[cc].radius] = newCorner.cloneNode(true);}
+if(cc != "br")
+{ for(var t = 0, k = newCorner.childNodes.length; t < k; t++)
+{ var pixelBar = newCorner.childNodes[t]; var pixelBarTop = parseInt(pixelBar.style.top.substring(0, pixelBar.style.top.indexOf("px"))); var pixelBarLeft = parseInt(pixelBar.style.left.substring(0, pixelBar.style.left.indexOf("px"))); var pixelBarHeight = parseInt(pixelBar.style.height.substring(0, pixelBar.style.height.indexOf("px"))); if(cc == "tl" || cc == "bl"){ pixelBar.style.left = this.settings[cc].radius -pixelBarLeft -1 + "px";}
+if(cc == "tr" || cc == "tl"){ pixelBar.style.top = this.settings[cc].radius -pixelBarHeight -pixelBarTop + "px";}
+switch(cc)
+{ case "tr":
+pixelBar.style.backgroundPosition = "-" + Math.abs((this.boxWidth - this.settings[cc].radius + this.borderWidth) + pixelBarLeft) + "px -" + Math.abs(this.settings[cc].radius -pixelBarHeight -pixelBarTop - this.borderWidth) + "px"; break; case "tl":
+pixelBar.style.backgroundPosition = "-" + Math.abs((this.settings[cc].radius -pixelBarLeft -1) - this.borderWidth) + "px -" + Math.abs(this.settings[cc].radius -pixelBarHeight -pixelBarTop - this.borderWidth) + "px"; break; case "bl":
+pixelBar.style.backgroundPosition = "-" + Math.abs((this.settings[cc].radius -pixelBarLeft -1) - this.borderWidth) + "px -" + Math.abs((this.boxHeight + this.settings[cc].radius + pixelBarTop) -this.borderWidth) + "px"; break;}
+}
+}
+}
+if(newCorner)
+{ switch(cc)
+{ case "tl":
+if(newCorner.style.position == "absolute") newCorner.style.top = "0px"; if(newCorner.style.position == "absolute") newCorner.style.left = "0px"; if(this.topContainer) this.topContainer.appendChild(newCorner); break; case "tr":
+if(newCorner.style.position == "absolute") newCorner.style.top = "0px"; if(newCorner.style.position == "absolute") newCorner.style.right = "0px"; if(this.topContainer) this.topContainer.appendChild(newCorner); break; case "bl":
+if(newCorner.style.position == "absolute") newCorner.style.bottom = "0px"; if(newCorner.style.position == "absolute") newCorner.style.left = "0px"; if(this.bottomContainer) this.bottomContainer.appendChild(newCorner); break; case "br":
+if(newCorner.style.position == "absolute") newCorner.style.bottom = "0px"; if(newCorner.style.position == "absolute") newCorner.style.right = "0px"; if(this.bottomContainer) this.bottomContainer.appendChild(newCorner); break;}
+}
+}
+}
+var radiusDiff = new Array(); radiusDiff["t"] = Math.abs(this.settings.tl.radius - this.settings.tr.radius)
+radiusDiff["b"] = Math.abs(this.settings.bl.radius - this.settings.br.radius); for(z in radiusDiff)
+{ if(z == "t" || z == "b")
+{ if(radiusDiff[z])
+{ var smallerCornerType = ((this.settings[z + "l"].radius < this.settings[z + "r"].radius)? z +"l" : z +"r"); var newFiller = document.createElement("DIV"); newFiller.style.height = radiusDiff[z] + "px"; newFiller.style.width = this.settings[smallerCornerType].radius+ "px"
+newFiller.style.position = "absolute"; newFiller.style.fontSize = "1px"; newFiller.style.overflow = "hidden"; newFiller.style.backgroundColor = this.boxColour; switch(smallerCornerType)
+{ case "tl":
+newFiller.style.bottom = "0px"; newFiller.style.left = "0px"; newFiller.style.borderLeft = this.borderString; this.topContainer.appendChild(newFiller); break; case "tr":
+newFiller.style.bottom = "0px"; newFiller.style.right = "0px"; newFiller.style.borderRight = this.borderString; this.topContainer.appendChild(newFiller); break; case "bl":
+newFiller.style.top = "0px"; newFiller.style.left = "0px"; newFiller.style.borderLeft = this.borderString; this.bottomContainer.appendChild(newFiller); break; case "br":
+newFiller.style.top = "0px"; newFiller.style.right = "0px"; newFiller.style.borderRight = this.borderString; this.bottomContainer.appendChild(newFiller); break;}
+}
+var newFillerBar = document.createElement("DIV"); newFillerBar.style.position = "relative"; newFillerBar.style.fontSize = "1px"; newFillerBar.style.overflow = "hidden"; newFillerBar.style.backgroundColor = this.boxColour; newFillerBar.style.backgroundImage = this.backgroundImage; switch(z)
+{ case "t":
+if(this.topContainer)
+{ if(this.settings.tl.radius && this.settings.tr.radius)
+{ newFillerBar.style.height = topMaxRadius - this.borderWidth + "px"; newFillerBar.style.marginLeft = this.settings.tl.radius - this.borderWidth + "px"; newFillerBar.style.marginRight = this.settings.tr.radius - this.borderWidth + "px"; newFillerBar.style.borderTop = this.borderString; if(this.backgroundImage != "")
+newFillerBar.style.backgroundPosition = "-" + (topMaxRadius + this.borderWidth) + "px 0px"; this.topContainer.appendChild(newFillerBar);}
+this.box.style.backgroundPosition = "0px -" + (topMaxRadius - this.borderWidth) + "px";}
+break; case "b":
+if(this.bottomContainer)
+{ if(this.settings.bl.radius && this.settings.br.radius)
+{ newFillerBar.style.height = botMaxRadius - this.borderWidth + "px"; newFillerBar.style.marginLeft = this.settings.bl.radius - this.borderWidth + "px"; newFillerBar.style.marginRight = this.settings.br.radius - this.borderWidth + "px"; newFillerBar.style.borderBottom = this.borderString; if(this.backgroundImage != "")
+newFillerBar.style.backgroundPosition = "-" + (botMaxRadius + this.borderWidth) + "px -" + (this.boxHeight + (topMaxRadius + this.borderWidth)) + "px"; this.bottomContainer.appendChild(newFillerBar);}
+}
+break;}
+}
+}
+if(this.settings.autoPad == true && this.boxPadding > 0)
+{ var contentContainer = document.createElement("DIV"); contentContainer.style.position = "relative"; contentContainer.innerHTML = this.boxContent; contentContainer.className = "autoPadDiv"; var topPadding = Math.abs(topMaxRadius - this.boxPadding); var botPadding = Math.abs(botMaxRadius - this.boxPadding); if(topMaxRadius < this.boxPadding)
+contentContainer.style.paddingTop = topPadding + "px"; if(botMaxRadius < this.boxPadding)
+contentContainer.style.paddingBottom = botMaxRadius + "px"; contentContainer.style.paddingLeft = this.boxPadding + "px"; contentContainer.style.paddingRight = this.boxPadding + "px"; this.contentDIV = this.box.appendChild(contentContainer);}
+}
+this.drawPixel = function(intx, inty, colour, transAmount, height, newCorner, image, cornerRadius)
+{ var pixel = document.createElement("DIV"); pixel.style.height = height + "px"; pixel.style.width = "1px"; pixel.style.position = "absolute"; pixel.style.fontSize = "1px"; pixel.style.overflow = "hidden"; var topMaxRadius = Math.max(this.settings["tr"].radius, this.settings["tl"].radius); if(image == -1 && this.backgroundImage != "")
+{ pixel.style.backgroundImage = this.backgroundImage; pixel.style.backgroundPosition = "-" + (this.boxWidth - (cornerRadius - intx) + this.borderWidth) + "px -" + ((this.boxHeight + topMaxRadius + inty) -this.borderWidth) + "px";}
+else
+{ pixel.style.backgroundColor = colour;}
+if (transAmount != 100)
+setOpacity(pixel, transAmount); pixel.style.top = inty + "px"; pixel.style.left = intx + "px"; newCorner.appendChild(pixel);}
+}
+function insertAfter(parent, node, referenceNode)
+{ parent.insertBefore(node, referenceNode.nextSibling);}
+function BlendColour(Col1, Col2, Col1Fraction)
+{ var red1 = parseInt(Col1.substr(1,2),16); var green1 = parseInt(Col1.substr(3,2),16); var blue1 = parseInt(Col1.substr(5,2),16); var red2 = parseInt(Col2.substr(1,2),16); var green2 = parseInt(Col2.substr(3,2),16); var blue2 = parseInt(Col2.substr(5,2),16); if(Col1Fraction > 1 || Col1Fraction < 0) Col1Fraction = 1; var endRed = Math.round((red1 * Col1Fraction) + (red2 * (1 - Col1Fraction))); if(endRed > 255) endRed = 255; if(endRed < 0) endRed = 0; var endGreen = Math.round((green1 * Col1Fraction) + (green2 * (1 - Col1Fraction))); if(endGreen > 255) endGreen = 255; if(endGreen < 0) endGreen = 0; var endBlue = Math.round((blue1 * Col1Fraction) + (blue2 * (1 - Col1Fraction))); if(endBlue > 255) endBlue = 255; if(endBlue < 0) endBlue = 0; return "#" + IntToHex(endRed)+ IntToHex(endGreen)+ IntToHex(endBlue);}
+function IntToHex(strNum)
+{ base = strNum / 16; rem = strNum % 16; base = base - (rem / 16); baseS = MakeHex(base); remS = MakeHex(rem); return baseS + '' + remS;}
+function MakeHex(x)
+{ if((x >= 0) && (x <= 9))
+{ return x;}
+else
+{ switch(x)
+{ case 10: return "A"; case 11: return "B"; case 12: return "C"; case 13: return "D"; case 14: return "E"; case 15: return "F";}
+}
+}
+function pixelFraction(x, y, r)
+{ var pixelfraction = 0; var xvalues = new Array(1); var yvalues = new Array(1); var point = 0; var whatsides = ""; var intersect = Math.sqrt((Math.pow(r,2) - Math.pow(x,2))); if ((intersect >= y) && (intersect < (y+1)))
+{ whatsides = "Left"; xvalues[point] = 0; yvalues[point] = intersect - y; point = point + 1;}
+var intersect = Math.sqrt((Math.pow(r,2) - Math.pow(y+1,2))); if ((intersect >= x) && (intersect < (x+1)))
+{ whatsides = whatsides + "Top"; xvalues[point] = intersect - x; yvalues[point] = 1; point = point + 1;}
+var intersect = Math.sqrt((Math.pow(r,2) - Math.pow(x+1,2))); if ((intersect >= y) && (intersect < (y+1)))
+{ whatsides = whatsides + "Right"; xvalues[point] = 1; yvalues[point] = intersect - y; point = point + 1;}
+var intersect = Math.sqrt((Math.pow(r,2) - Math.pow(y,2))); if ((intersect >= x) && (intersect < (x+1)))
+{ whatsides = whatsides + "Bottom"; xvalues[point] = intersect - x; yvalues[point] = 0;}
+switch (whatsides)
+{ case "LeftRight":
+pixelfraction = Math.min(yvalues[0],yvalues[1]) + ((Math.max(yvalues[0],yvalues[1]) - Math.min(yvalues[0],yvalues[1]))/2); break; case "TopRight":
+pixelfraction = 1-(((1-xvalues[0])*(1-yvalues[1]))/2); break; case "TopBottom":
+pixelfraction = Math.min(xvalues[0],xvalues[1]) + ((Math.max(xvalues[0],xvalues[1]) - Math.min(xvalues[0],xvalues[1]))/2); break; case "LeftBottom":
+pixelfraction = (yvalues[0]*xvalues[1])/2; break; default:
+pixelfraction = 1;}
+return pixelfraction;}
+function rgb2Hex(rgbColour)
+{ try{ var rgbArray = rgb2Array(rgbColour); var red = parseInt(rgbArray[0]); var green = parseInt(rgbArray[1]); var blue = parseInt(rgbArray[2]); var hexColour = "#" + IntToHex(red) + IntToHex(green) + IntToHex(blue);}
+catch(e){ alert("There was an error converting the RGB value to Hexadecimal in function rgb2Hex");}
+return hexColour;}
+function rgb2Array(rgbColour)
+{ var rgbValues = rgbColour.substring(4, rgbColour.indexOf(")")); var rgbArray = rgbValues.split(", "); return rgbArray;}
+function setOpacity(obj, opacity)
+{ opacity = (opacity == 100)?99.999:opacity; if(isSafari && obj.tagName != "IFRAME")
+{ var rgbArray = rgb2Array(obj.style.backgroundColor); var red = parseInt(rgbArray[0]); var green = parseInt(rgbArray[1]); var blue = parseInt(rgbArray[2]); obj.style.backgroundColor = "rgba(" + red + ", " + green + ", " + blue + ", " + opacity/100 + ")";}
+else if(typeof(obj.style.opacity) != "undefined")
+{ obj.style.opacity = opacity/100;}
+else if(typeof(obj.style.MozOpacity) != "undefined")
+{ obj.style.MozOpacity = opacity/100;}
+else if(typeof(obj.style.filter) != "undefined")
+{ obj.style.filter = "alpha(opacity:" + opacity + ")";}
+else if(typeof(obj.style.KHTMLOpacity) != "undefined")
+{ obj.style.KHTMLOpacity = opacity/100;}
+}
+function inArray(array, value)
+{ for(var i = 0; i < array.length; i++){ if (array[i] === value) return i;}
+return false;}
+function inArrayKey(array, value)
+{ for(key in array){ if(key === value) return true;}
+return false;}
+function addEvent(elm, evType, fn, useCapture) { if (elm.addEventListener) { elm.addEventListener(evType, fn, useCapture); return true;}
+else if (elm.attachEvent) { var r = elm.attachEvent('on' + evType, fn); return r;}
+else { elm['on' + evType] = fn;}
+}
+function removeEvent(obj, evType, fn, useCapture){ if (obj.removeEventListener){ obj.removeEventListener(evType, fn, useCapture); return true;} else if (obj.detachEvent){ var r = obj.detachEvent("on"+evType, fn); return r;} else { alert("Handler could not be removed");}
+}
+function format_colour(colour)
+{ var returnColour = "#ffffff"; if(colour != "" && colour != "transparent")
+{ if(colour.substr(0, 3) == "rgb")
+{ returnColour = rgb2Hex(colour);}
+else if(colour.length == 4)
+{ returnColour = "#" + colour.substring(1, 2) + colour.substring(1, 2) + colour.substring(2, 3) + colour.substring(2, 3) + colour.substring(3, 4) + colour.substring(3, 4);}
+else
+{ returnColour = colour;}
+}
+return returnColour;}
+function get_style(obj, property, propertyNS)
+{ try
+{ if(obj.currentStyle)
+{ var returnVal = eval("obj.currentStyle." + property);}
+else
+{ if(isSafari && obj.style.display == "none")
+{ obj.style.display = ""; var wasHidden = true;}
+var returnVal = document.defaultView.getComputedStyle(obj, '').getPropertyValue(propertyNS); if(isSafari && wasHidden)
+{ obj.style.display = "none";}
+}
+}
+catch(e)
+{ }
+return returnVal;}
+function getElementsByClass(searchClass, node, tag)
+{ var classElements = new Array(); if(node == null)
+node = document; if(tag == null)
+tag = '*'; var els = node.getElementsByTagName(tag); var elsLen = els.length; var pattern = new RegExp("(^|\s)"+searchClass+"(\s|$)"); for (i = 0, j = 0; i < elsLen; i++)
+{ if(pattern.test(els[i].className))
+{ classElements[j] = els[i]; j++;}
+}
+return classElements;}
+function newCurvyError(errorMessage)
+{ return new Error("curvyCorners Error:\n" + errorMessage)
+}
diff --git a/vendor/gems/composite_primary_keys-1.1.0/website/stylesheets/screen.css b/vendor/gems/composite_primary_keys-1.1.0/website/stylesheets/screen.css
new file mode 100644 (file)
index 0000000..3f2d8f9
--- /dev/null
@@ -0,0 +1,126 @@
+body {\r
+  background-color: #2F30EE;\r
+  font-family: "Georgia", sans-serif;\r
+  font-size: 16px;\r
+  line-height: 1.6em;\r
+  padding: 1.6em 0 0 0;\r
+  color: #eee;\r
+}\r
+h1, h2, h3, h4, h5, h6 {\r
+  color: #FFEDFA;\r
+}\r
+h1 { \r
+  font-family: sans-serif;\r
+  font-weight: normal;\r
+  font-size: 4em;\r
+  line-height: 0.8em;\r
+  letter-spacing: -0.1ex;\r
+       margin: 5px;\r
+}\r
+li {\r
+  padding: 0;\r
+  margin: 0;\r
+  list-style-type: square;\r
+}\r
+a {\r
+  color: #99f;\r
+  font-weight: normal;\r
+  text-decoration: underline;\r
+}\r
+blockquote {\r
+  font-size: 90%;\r
+  font-style: italic;\r
+  border-left: 1px solid #eee;\r
+  padding-left: 1em;\r
+}\r
+.caps {\r
+  font-size: 80%;\r
+}\r
+\r
+#main {\r
+  width: 45em;\r
+  padding: 0;\r
+  margin: 0 auto;\r
+}\r
+.coda {\r
+  text-align: right;\r
+  color: #77f;\r
+  font-size: smaller;\r
+}\r
+\r
+table {\r
+  font-size: 90%;\r
+  line-height: 1.4em;\r
+  color: #ff8;\r
+  background-color: #111;\r
+  padding: 2px 10px 2px 10px;\r
+       border-style: dashed;\r
+}\r
+\r
+th {\r
+       color: #fff;\r
+}\r
+\r
+td {\r
+  padding: 2px 10px 2px 10px;\r
+}\r
+\r
+.success {\r
+       color: #0CC52B;\r
+}\r
+\r
+.failed {\r
+       color: #E90A1B;\r
+}\r
+\r
+.unknown {\r
+       color: #995000;\r
+}\r
+pre, code {\r
+  font-family: monospace;\r
+  font-size: 90%;\r
+  line-height: 1.4em;\r
+  color: #ff8;\r
+  background-color: #111;\r
+  padding: 2px 10px 2px 10px;\r
+}\r
+.comment { color: #aaa; font-style: italic; }\r
+.keyword { color: #eff; font-weight: bold; }\r
+.punct { color: #eee; font-weight: bold; }\r
+.symbol { color: #0bb; }\r
+.string { color: #6b4; }\r
+.ident { color: #ff8; }\r
+.constant { color: #66f; }\r
+.regex { color: #ec6; }\r
+.number { color: #F99; }\r
+.expr { color: #227; }\r
+\r
+#version {\r
+  float: right;\r
+  text-align: right;\r
+  font-family: sans-serif;\r
+  font-weight: normal;\r
+  background-color: #ff8;\r
+  color: #66f;\r
+  padding: 15px 20px 10px 20px;\r
+  margin: 0 auto;\r
+       margin-top: 15px;\r
+  border: 3px solid #66f;\r
+}\r
+\r
+#version .numbers {\r
+  display: block;\r
+  font-size: 4em;\r
+  line-height: 0.8em;\r
+  letter-spacing: -0.1ex;\r
+}\r
+\r
+#version a {\r
+  text-decoration: none;\r
+}\r
+\r
+.clickable {\r
+       cursor: pointer; \r
+       cursor: hand;\r
+}\r
+\r
diff --git a/vendor/gems/composite_primary_keys-1.1.0/website/template.js b/vendor/gems/composite_primary_keys-1.1.0/website/template.js
new file mode 100644 (file)
index 0000000..fbaf5a5
--- /dev/null
@@ -0,0 +1,3 @@
+// <%= title %>
+var version = <%= version.to_json %>;
+<%= body %>
diff --git a/vendor/gems/composite_primary_keys-1.1.0/website/template.rhtml b/vendor/gems/composite_primary_keys-1.1.0/website/template.rhtml
new file mode 100644 (file)
index 0000000..3e2c531
--- /dev/null
@@ -0,0 +1,53 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head>
+  <link rel="stylesheet" href="stylesheets/screen.css" type="text/css" media="screen" />
+  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+  <title>
+      <%= title %>
+  </title>
+  <script src="javascripts/rounded_corners_lite.inc.js" type="text/javascript"></script>
+<style>
+
+</style>
+  <script type="text/javascript">
+    window.onload = function() {
+      settings = {
+          tl: { radius: 10 },
+          tr: { radius: 10 },
+          bl: { radius: 10 },
+          br: { radius: 10 },
+          antiAlias: true,
+          autoPad: true,
+          validTags: ["div"]
+      }
+      var versionBox = new curvyCorners(settings, document.getElementById("version"));
+      versionBox.applyCornersToAll();
+    }
+  </script>
+</head>
+<body>
+<div id="main">
+
+    <h1><%= title %></h1>
+    <div id="version" class="clickable" onclick='document.location = "<%= download %>"; return false'>
+      Get Version
+      <a href="<%= download %>" class="numbers"><%= version %></a>
+    </div>
+    <%= body %>
+    <p class="coda">
+      <a href="mailto:drnicwilliams@gmail.com">Dr Nic</a>, <%= modified.pretty %><br>
+      Theme extended from <a href="http://rb2js.rubyforge.org/">Paul Battley</a>
+    </p>
+</div>
+
+<script src="http://www.google-analytics.com/urchin.js" type="text/javascript">
+</script>
+<script type="text/javascript">
+_uacct = "UA-567811-2";
+urchinTracker();
+</script>
+
+</body>
+</html>
diff --git a/vendor/gems/composite_primary_keys-1.1.0/website/version-raw.js b/vendor/gems/composite_primary_keys-1.1.0/website/version-raw.js
new file mode 100644 (file)
index 0000000..9d2ac78
--- /dev/null
@@ -0,0 +1,3 @@
+// Announcement JS file
+var version = "1.1.0";
+MagicAnnouncement.show('compositekeys', version);
diff --git a/vendor/gems/composite_primary_keys-1.1.0/website/version-raw.txt b/vendor/gems/composite_primary_keys-1.1.0/website/version-raw.txt
new file mode 100644 (file)
index 0000000..74ca3ac
--- /dev/null
@@ -0,0 +1,2 @@
+h1. Announcement JS file\r
+MagicAnnouncement.show('compositekeys', version);
\ No newline at end of file
diff --git a/vendor/gems/composite_primary_keys-1.1.0/website/version.js b/vendor/gems/composite_primary_keys-1.1.0/website/version.js
new file mode 100644 (file)
index 0000000..56921a9
--- /dev/null
@@ -0,0 +1,4 @@
+// Version JS file
+var version = "1.1.0";
+\r
+document.write(" - " + version);
diff --git a/vendor/gems/composite_primary_keys-1.1.0/website/version.txt b/vendor/gems/composite_primary_keys-1.1.0/website/version.txt
new file mode 100644 (file)
index 0000000..d0ac6a7
--- /dev/null
@@ -0,0 +1,3 @@
+h1. Version JS file\r
+\r
+document.write(" - " + version);
\ No newline at end of file
index b6e9cf4bc5378d03a7bbf81fd6f8d264acefb681..6a3e1a97b0f661cb63937fc8bbcc6b0b6c533a38 100644 (file)
@@ -97,8 +97,8 @@ module ActionController
             "Unknown options: #{unknown_option_keys.join(', ')}" unless
               unknown_option_keys.empty?
 
-      options[:singular_name] ||= Inflector.singularize(collection_id.to_s)
-      options[:class_name]  ||= Inflector.camelize(options[:singular_name])
+      options[:singular_name] ||= ActiveSupport::Inflector.singularize(collection_id.to_s)
+      options[:class_name]  ||= ActiveSupport::Inflector.camelize(options[:singular_name])
     end
 
     # Returns a paginator and a collection of Active Record model instances
diff --git a/vendor/plugins/deadlock_retry/README b/vendor/plugins/deadlock_retry/README
new file mode 100644 (file)
index 0000000..b5937ce
--- /dev/null
@@ -0,0 +1,10 @@
+Deadlock Retry
+==============
+
+Deadlock retry allows the database adapter (currently only tested with the
+MySQLAdapter) to retry transactions that fall into deadlock. It will retry
+such transactions three times before finally failing.
+
+This capability is automatically added to ActiveRecord. No code changes or otherwise are required.
+
+Copyright (c) 2005 Jamis Buck, released under the MIT license
\ No newline at end of file
diff --git a/vendor/plugins/deadlock_retry/Rakefile b/vendor/plugins/deadlock_retry/Rakefile
new file mode 100644 (file)
index 0000000..8063a6e
--- /dev/null
@@ -0,0 +1,10 @@
+require 'rake'
+require 'rake/testtask'
+
+desc "Default task"
+task :default => [ :test ]
+
+Rake::TestTask.new do |t|
+  t.test_files = Dir["test/**/*_test.rb"]
+  t.verbose = true
+end
diff --git a/vendor/plugins/deadlock_retry/init.rb b/vendor/plugins/deadlock_retry/init.rb
new file mode 100644 (file)
index 0000000..e090f68
--- /dev/null
@@ -0,0 +1,2 @@
+require 'deadlock_retry'
+ActiveRecord::Base.send :include, DeadlockRetry
diff --git a/vendor/plugins/deadlock_retry/lib/deadlock_retry.rb b/vendor/plugins/deadlock_retry/lib/deadlock_retry.rb
new file mode 100644 (file)
index 0000000..413cb82
--- /dev/null
@@ -0,0 +1,58 @@
+# Copyright (c) 2005 Jamis Buck
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+module DeadlockRetry
+  def self.append_features(base)
+    super
+    base.extend(ClassMethods)
+    base.class_eval do
+      class <<self
+        alias_method :transaction_without_deadlock_handling, :transaction
+        alias_method :transaction, :transaction_with_deadlock_handling
+      end
+    end
+  end
+
+  module ClassMethods
+    DEADLOCK_ERROR_MESSAGES = [
+      "Deadlock found when trying to get lock",
+      "Lock wait timeout exceeded"
+    ]
+
+    MAXIMUM_RETRIES_ON_DEADLOCK = 3
+
+    def transaction_with_deadlock_handling(*objects, &block)
+      retry_count = 0
+
+      begin
+        transaction_without_deadlock_handling(*objects, &block)
+      rescue ActiveRecord::StatementInvalid => error
+        if DEADLOCK_ERROR_MESSAGES.any? { |msg| error.message =~ /#{Regexp.escape(msg)}/ }
+          raise if retry_count >= MAXIMUM_RETRIES_ON_DEADLOCK
+          retry_count += 1
+          logger.info "Deadlock detected on retry #{retry_count}, restarting transaction"
+          retry
+        else
+          raise
+        end
+      end
+    end
+  end
+end
diff --git a/vendor/plugins/deadlock_retry/test/deadlock_retry_test.rb b/vendor/plugins/deadlock_retry/test/deadlock_retry_test.rb
new file mode 100644 (file)
index 0000000..db0f619
--- /dev/null
@@ -0,0 +1,65 @@
+begin
+  require 'active_record'
+rescue LoadError
+  if ENV['ACTIVERECORD_PATH'].nil?
+    abort <<MSG
+Please set the ACTIVERECORD_PATH environment variable to the directory
+containing the active_record.rb file.
+MSG
+  else
+    $LOAD_PATH.unshift << ENV['ACTIVERECORD_PATH']
+    begin
+      require 'active_record'
+    rescue LoadError
+      abort "ActiveRecord could not be found."
+    end
+  end
+end
+
+require 'test/unit'
+require "#{File.dirname(__FILE__)}/../lib/deadlock_retry"
+
+class MockModel
+  def self.transaction(*objects, &block)
+    block.call
+  end
+
+  def self.logger
+    @logger ||= Logger.new(nil)
+  end
+
+  include DeadlockRetry
+end
+
+class DeadlockRetryTest < Test::Unit::TestCase
+  DEADLOCK_ERROR = "MySQL::Error: Deadlock found when trying to get lock"
+  TIMEOUT_ERROR = "MySQL::Error: Lock wait timeout exceeded"
+
+  def test_no_errors
+    assert_equal :success, MockModel.transaction { :success }
+  end
+
+  def test_no_errors_with_deadlock
+    errors = [ DEADLOCK_ERROR ] * 3
+    assert_equal :success, MockModel.transaction { raise ActiveRecord::StatementInvalid, errors.shift unless errors.empty?; :success }
+    assert errors.empty?
+  end
+
+  def test_no_errors_with_lock_timeout
+    errors = [ TIMEOUT_ERROR ] * 3
+    assert_equal :success, MockModel.transaction { raise ActiveRecord::StatementInvalid, errors.shift unless errors.empty?; :success }
+    assert errors.empty?
+  end
+
+  def test_error_if_limit_exceeded
+    assert_raise(ActiveRecord::StatementInvalid) do
+      MockModel.transaction { raise ActiveRecord::StatementInvalid, DEADLOCK_ERROR }
+    end
+  end
+
+  def test_error_if_unrecognized_error
+    assert_raise(ActiveRecord::StatementInvalid) do
+      MockModel.transaction { raise ActiveRecord::StatementInvalid, "Something else" }
+    end
+  end
+end
diff --git a/vendor/plugins/file_column/CHANGELOG b/vendor/plugins/file_column/CHANGELOG
new file mode 100644 (file)
index 0000000..bb4e5c6
--- /dev/null
@@ -0,0 +1,69 @@
+*svn*
+    * allow for directories in file_column dirs as well
+    * use subdirs for versions instead of fiddling with filename
+    * url_for_image_column_helper for dynamic resizing of images from views
+    * new "crop" feature [Sean Treadway]
+    * url_for_file_column helper: do not require model objects to be stored in
+      instance variables
+    * allow more fined-grained control over :store_dir via callback
+      methods [Gerret Apelt]
+    * allow assignment of regular file objects
+    * validation of file format and file size [Kyle Maxwell]
+    * validation of image dimensions [Lee O'Mara]
+    * file permissions can be set via :permissions option
+    * fixed bug that prevents deleting of file via assigning nil if
+      column is declared as NON NULL on some databases
+    * don't expand absolute paths. This is necessary for file_column to work
+      when your rails app is deployed into a sub-directory via a symbolic link
+    * url_for_*_column will no longer return absolute URLs! Instead, although the
+      generated URL starts with a slash, it will be relative to your application's
+      root URL. This is so, because rails' image_tag helper will automatically
+      convert it to an absolute URL. If you need an absolute URL (e.g., to pass
+      it to link_to) use url_for_file_column's :absolute => true option.
+    * added support for file_column enabled unit tests [Manuel Holtgrewe]
+    * support for custom transformation of images [Frederik Fix]
+    * allow setting of image attributes (e.g., quality) [Frederik Fix]
+    * :magick columns can optionally ignore non-images (i.e., do not try to
+       resize them)
+
+0.3.1
+    * make object with file_columns serializable
+    * use normal require for RMagick, so that it works with gem
+      and custom install as well
+
+0.3
+    * fixed bug where empty file uploads were not recognized with some browsers
+    * fixed bug on windows when "file" utility is not present
+    * added option to disable automatic file extension correction
+    * Only allow one attribute per call to file_column, so that options only
+      apply to one argument
+    * try to detect when people forget to set the form encoding to
+      'multipart/form-data'
+    * converted to rails plugin
+    * easy integration with RMagick
+
+0.2
+    * complete rewrite using state pattern
+    * fixed sanitize filename [Michael Raidel]
+    * fixed bug when no file was uploaded [Michael Raidel]
+    * try to fix filename extensions [Michael Raidel]
+    * Feed absolute paths through File.expand_path to make them as simple as possible
+    * Make file_column_field helper work with auto-ids (e.g., "event[]")
+
+0.1.3
+    * test cases with more than 1 file_column
+    * fixed bug when file_column was called with several arguments
+    * treat empty ("") file_columns as nil
+    * support for binary files on windows
+
+0.1.2
+    * better rails integration, so that you do not have to include the modules yourself. You
+      just have to "require 'rails_file_column'" in your "config/environment.rb"
+    * Rakefile for testing and packaging
+
+0.1.1 (2005-08-11)
+    * fixed nasty bug in url_for_file_column that made it unusable on Apache
+    * prepared for public release
+    
+0.1 (2005-08-10)
+    * initial release
diff --git a/vendor/plugins/file_column/README b/vendor/plugins/file_column/README
new file mode 100644 (file)
index 0000000..07a6e96
--- /dev/null
@@ -0,0 +1,54 @@
+FEATURES
+========
+
+Let's assume an model class named Entry, where we want to define the "image" column
+as a "file_upload" column.
+
+class Entry < ActiveRecord::Base
+  file_column :image
+end
+
+* every entry can have one uploaded file, the filename will be stored in the "image" column
+
+* files will be stored in "public/entry/image/<entry.id>/filename.ext"
+
+* Newly uploaded files will be stored in "public/entry/tmp/<random>/filename.ext" so that
+  they can be reused in form redisplays (due to validation etc.)
+
+* in a view, "<%= file_column_field 'entry', 'image' %> will create a file upload field as well
+  as a hidden field to recover files uploaded before in a case of a form redisplay
+
+* in a view, "<%= url_for_file_column 'entry', 'image' %> will create an URL to access the
+  uploaded file. Note that you need an Entry object in the instance variable @entry for this
+  to work.
+
+* easy integration with RMagick to resize images and/or create thumb-nails.
+
+USAGE
+=====
+
+Just drop the whole directory into your application's "vendor/plugins" directory. Starting
+with version 1.0rc of rails, it will be automatically picked for you by rails plugin
+mechanism.
+
+DOCUMENTATION
+=============
+
+Please look at the rdoc-generated documentation in the "doc" directory.
+
+RUNNING UNITTESTS
+=================
+
+There are extensive unittests in the "test" directory. Currently, only MySQL is supported, but
+you should be able to easily fix this by looking at "connection.rb". You have to create a
+database for the tests and put the connection information into "connection.rb". The schema
+for MySQL can be found in "test/fixtures/mysql.sql".
+
+You can run the tests by starting the "*_test.rb" in the directory "test"
+
+BUGS & FEEDBACK
+===============
+
+Bug reports (as well as patches) and feedback are very welcome. Please send it to
+sebastian.kanthak@muehlheim.de
+
diff --git a/vendor/plugins/file_column/Rakefile b/vendor/plugins/file_column/Rakefile
new file mode 100644 (file)
index 0000000..0a24682
--- /dev/null
@@ -0,0 +1,36 @@
+task :default => [:test]
+
+PKG_NAME = "file-column"
+PKG_VERSION = "0.3.1"
+
+PKG_DIR = "release/#{PKG_NAME}-#{PKG_VERSION}"
+
+task :clean do
+  rm_rf "release"
+end
+
+task :setup_directories do
+  mkpath "release"
+end
+
+
+task :checkout_release => :setup_directories do
+  rm_rf PKG_DIR
+  revision = ENV["REVISION"] || "HEAD"
+  sh "svn export -r #{revision} . #{PKG_DIR}"
+end
+
+task :release_docs => :checkout_release do
+  sh "cd #{PKG_DIR}; rdoc lib"
+end
+
+task :package => [:checkout_release, :release_docs] do
+  sh "cd release; tar czf #{PKG_NAME}-#{PKG_VERSION}.tar.gz #{PKG_NAME}-#{PKG_VERSION}"
+end
+
+task :test do
+  sh "cd test; ruby file_column_test.rb"
+  sh "cd test; ruby file_column_helper_test.rb"
+  sh "cd test; ruby magick_test.rb"
+  sh "cd test; ruby magick_view_only_test.rb"
+end
diff --git a/vendor/plugins/file_column/TODO b/vendor/plugins/file_column/TODO
new file mode 100644 (file)
index 0000000..d46e9fa
--- /dev/null
@@ -0,0 +1,6 @@
+* document configuration options better
+* support setting of permissions
+* validation methods for file format/size
+* delete stale files from tmp directories
+
+* ensure valid URLs are created even when deployed at sub-path (compute_public_url?)
diff --git a/vendor/plugins/file_column/init.rb b/vendor/plugins/file_column/init.rb
new file mode 100644 (file)
index 0000000..d31ef1b
--- /dev/null
@@ -0,0 +1,13 @@
+# plugin init file for rails
+# this file will be picked up by rails automatically and
+# add the file_column extensions to rails
+
+require 'file_column'
+require 'file_compat'
+require 'file_column_helper'
+require 'validations'
+require 'test_case'
+
+ActiveRecord::Base.send(:include, FileColumn)
+ActionView::Base.send(:include, FileColumnHelper)
+ActiveRecord::Base.send(:include, FileColumn::Validations)
\ No newline at end of file
diff --git a/vendor/plugins/file_column/lib/file_column.rb b/vendor/plugins/file_column/lib/file_column.rb
new file mode 100644 (file)
index 0000000..791a5be
--- /dev/null
@@ -0,0 +1,720 @@
+require 'fileutils'
+require 'tempfile'
+require 'magick_file_column'
+
+module FileColumn # :nodoc:
+  def self.append_features(base)
+    super
+    base.extend(ClassMethods)
+  end
+
+  def self.create_state(instance,attr)
+    filename = instance[attr]
+    if filename.nil? or filename.empty?
+      NoUploadedFile.new(instance,attr)
+    else
+      PermanentUploadedFile.new(instance,attr)
+    end
+  end
+
+  def self.init_options(defaults, model, attr)
+    options = defaults.dup
+    options[:store_dir] ||= File.join(options[:root_path], model, attr)
+    unless options[:store_dir].is_a?(Symbol)
+      options[:tmp_base_dir] ||= File.join(options[:store_dir], "tmp")
+    end
+    options[:base_url] ||= options[:web_root] + File.join(model, attr)
+
+    [:store_dir, :tmp_base_dir].each do |dir_sym|
+      if options[dir_sym].is_a?(String) and !File.exists?(options[dir_sym])
+        FileUtils.mkpath(options[dir_sym])
+      end
+    end
+
+    options
+  end
+
+  class BaseUploadedFile # :nodoc:
+
+    def initialize(instance,attr)
+      @instance, @attr = instance, attr
+      @options_method = "#{attr}_options".to_sym
+    end
+
+
+    def assign(file)
+      if file.is_a? File
+        # this did not come in via a CGI request. However,
+        # assigning files directly may be useful, so we
+        # make just this file object similar enough to an uploaded
+        # file that we can handle it. 
+        file.extend FileColumn::FileCompat
+      end
+
+      if file.nil?
+        delete
+      else
+        if file.size == 0
+          # user did not submit a file, so we
+          # can simply ignore this
+          self
+        else
+          if file.is_a?(String)
+            # if file is a non-empty string it is most probably
+            # the filename and the user forgot to set the encoding
+            # to multipart/form-data. Since we would raise an exception
+            # because of the missing "original_filename" method anyways,
+            # we raise a more meaningful exception rightaway.
+            raise TypeError.new("Do not know how to handle a string with value '#{file}' that was passed to a file_column. Check if the form's encoding has been set to 'multipart/form-data'.")
+          end
+          upload(file)
+        end
+      end
+    end
+
+    def just_uploaded?
+      @just_uploaded
+    end
+
+    def on_save(&blk)
+      @on_save ||= []
+      @on_save << Proc.new
+    end
+    
+    # the following methods are overriden by sub-classes if needed
+
+    def temp_path
+      nil
+    end
+
+    def absolute_dir
+      if absolute_path then File.dirname(absolute_path) else nil end
+    end
+
+    def relative_dir
+      if relative_path then File.dirname(relative_path) else nil end
+    end
+
+    def after_save
+      @on_save.each { |blk| blk.call } if @on_save
+      self
+    end
+
+    def after_destroy
+    end
+
+    def options
+      @instance.send(@options_method)
+    end
+
+    private
+    
+    def store_dir
+      if options[:store_dir].is_a? Symbol
+        raise ArgumentError.new("'#{options[:store_dir]}' is not an instance method of class #{@instance.class.name}") unless @instance.respond_to?(options[:store_dir])
+
+        dir = File.join(options[:root_path], @instance.send(options[:store_dir]))
+        FileUtils.mkpath(dir) unless File.exists?(dir)
+        dir
+      else 
+        options[:store_dir]
+      end
+    end
+
+    def tmp_base_dir
+      if options[:tmp_base_dir]
+        options[:tmp_base_dir] 
+      else
+        dir = File.join(store_dir, "tmp")
+        FileUtils.mkpath(dir) unless File.exists?(dir)
+        dir
+      end
+    end
+
+    def clone_as(klass)
+      klass.new(@instance, @attr)
+    end
+
+  end
+    
+
+  class NoUploadedFile < BaseUploadedFile # :nodoc:
+    def delete
+      # we do not have a file so deleting is easy
+      self
+    end
+
+    def upload(file)
+      # replace ourselves with a TempUploadedFile
+      temp = clone_as TempUploadedFile
+      temp.store_upload(file)
+      temp
+    end
+
+    def absolute_path(subdir=nil)
+      nil
+    end
+
+
+    def relative_path(subdir=nil)
+      nil
+    end
+
+    def assign_temp(temp_path)
+      return self if temp_path.nil? or temp_path.empty?
+      temp = clone_as TempUploadedFile
+      temp.parse_temp_path temp_path
+      temp
+    end
+  end
+
+  class RealUploadedFile < BaseUploadedFile # :nodoc:
+    def absolute_path(subdir=nil)
+      if subdir
+        File.join(@dir, subdir, @filename)
+      else
+        File.join(@dir, @filename)
+      end
+    end
+
+    def relative_path(subdir=nil)
+      if subdir
+        File.join(relative_path_prefix, subdir, @filename)
+      else
+        File.join(relative_path_prefix, @filename)
+      end
+    end
+
+    private
+
+    # regular expressions to try for identifying extensions
+    EXT_REGEXPS = [ 
+      /^(.+)\.([^.]+\.[^.]+)$/, # matches "something.tar.gz"
+      /^(.+)\.([^.]+)$/ # matches "something.jpg"
+    ]
+
+    def split_extension(filename,fallback=nil)
+      EXT_REGEXPS.each do |regexp|
+        if filename =~ regexp
+          base,ext = $1, $2
+          return [base, ext] if options[:extensions].include?(ext.downcase)
+        end
+      end
+      if fallback and filename =~ EXT_REGEXPS.last
+        return [$1, $2]
+      end
+      [filename, ""]
+    end
+    
+  end
+
+  class TempUploadedFile < RealUploadedFile # :nodoc:
+
+    def store_upload(file)
+      @tmp_dir = FileColumn.generate_temp_name
+      @dir = File.join(tmp_base_dir, @tmp_dir)      
+      FileUtils.mkdir(@dir)
+      
+      @filename = FileColumn::sanitize_filename(file.original_filename)
+      local_file_path = File.join(tmp_base_dir,@tmp_dir,@filename)
+      
+      # stored uploaded file into local_file_path
+      # If it was a Tempfile object, the temporary file will be
+      # cleaned up automatically, so we do not have to care for this
+      if file.respond_to?(:local_path) and file.local_path and File.exists?(file.local_path)
+        FileUtils.copy_file(file.local_path, local_file_path)
+      elsif file.respond_to?(:read)
+        File.open(local_file_path, "wb") { |f| f.write(file.read) }
+      else
+        raise ArgumentError.new("Do not know how to handle #{file.inspect}")
+      end
+      File.chmod(options[:permissions], local_file_path)
+      
+      if options[:fix_file_extensions]
+        # try to determine correct file extension and fix
+        # if necessary
+        content_type = get_content_type((file.content_type.chomp if file.content_type))
+        if content_type and options[:mime_extensions][content_type]
+          @filename = correct_extension(@filename,options[:mime_extensions][content_type])
+        end
+
+        new_local_file_path = File.join(tmp_base_dir,@tmp_dir,@filename)
+        File.rename(local_file_path, new_local_file_path) unless new_local_file_path == local_file_path
+        local_file_path = new_local_file_path
+      end
+      
+      @instance[@attr] = @filename
+      @just_uploaded = true
+    end
+
+
+    # tries to identify and strip the extension of filename
+    # if an regular expresion from EXT_REGEXPS matches and the
+    # downcased extension is a known extension (in options[:extensions])
+    # we'll strip this extension
+    def strip_extension(filename)
+      split_extension(filename).first
+    end
+
+    def correct_extension(filename, ext)
+      strip_extension(filename) << ".#{ext}"
+    end
+    
+    def parse_temp_path(temp_path, instance_options=nil)
+      raise ArgumentError.new("invalid format of '#{temp_path}'") unless temp_path =~ %r{^((\d+\.)+\d+)/([^/].+)$}
+      @tmp_dir, @filename = $1, FileColumn.sanitize_filename($3)
+      @dir = File.join(tmp_base_dir, @tmp_dir)
+
+      @instance[@attr] = @filename unless instance_options == :ignore_instance
+    end
+    
+    def upload(file)
+      # store new file
+      temp = clone_as TempUploadedFile
+      temp.store_upload(file)
+      
+      # delete old copy
+      delete_files
+
+      # and return new TempUploadedFile object
+      temp
+    end
+
+    def delete
+      delete_files
+      @instance[@attr] = ""
+      clone_as NoUploadedFile
+    end
+
+    def assign_temp(temp_path)
+      return self if temp_path.nil? or temp_path.empty?
+      # we can ignore this since we've already received a newly uploaded file
+
+      # however, we delete the old temporary files
+      temp = clone_as TempUploadedFile
+      temp.parse_temp_path(temp_path, :ignore_instance)
+      temp.delete_files
+
+      self
+    end
+
+    def temp_path
+      File.join(@tmp_dir, @filename)
+    end
+
+    def after_save
+      super
+
+      # we have a newly uploaded image, move it to the correct location
+      file = clone_as PermanentUploadedFile
+      file.move_from(File.join(tmp_base_dir, @tmp_dir), @just_uploaded)
+
+      # delete temporary files
+      delete_files
+
+      # replace with the new PermanentUploadedFile object
+      file
+    end
+
+    def delete_files
+      FileUtils.rm_rf(File.join(tmp_base_dir, @tmp_dir))
+    end
+
+    def get_content_type(fallback=nil)
+      if options[:file_exec]
+        begin
+          content_type = `#{options[:file_exec]} -bi "#{File.join(@dir,@filename)}"`.chomp
+          content_type = fallback unless $?.success?
+          content_type.gsub!(/;.+$/,"") if content_type
+          content_type
+        rescue
+          fallback
+        end
+      else
+        fallback
+      end
+    end
+
+    private
+
+    def relative_path_prefix
+      File.join("tmp", @tmp_dir)
+    end
+  end
+
+  
+  class PermanentUploadedFile < RealUploadedFile # :nodoc:
+    def initialize(*args)
+      super *args
+      @dir = File.join(store_dir, relative_path_prefix)
+      @filename = @instance[@attr]
+      @filename = nil if @filename.empty?
+    end
+
+    def move_from(local_dir, just_uploaded)
+      # remove old permament dir first
+      # this creates a short moment, where neither the old nor
+      # the new files exist but we can't do much about this as
+      # filesystems aren't transactional.
+      FileUtils.rm_rf @dir
+
+      FileUtils.mv local_dir, @dir
+
+      @just_uploaded = just_uploaded
+    end
+
+    def upload(file)
+      temp = clone_as TempUploadedFile
+      temp.store_upload(file)
+      temp
+    end
+
+    def delete
+      file = clone_as NoUploadedFile
+      @instance[@attr] = ""
+      file.on_save { delete_files }
+      file
+    end
+
+    def assign_temp(temp_path)
+      return nil if temp_path.nil? or temp_path.empty?
+
+      temp = clone_as TempUploadedFile
+      temp.parse_temp_path(temp_path)
+      temp
+    end
+
+    def after_destroy
+      delete_files
+    end
+
+    def delete_files
+      FileUtils.rm_rf @dir
+    end
+
+    private
+    
+    def relative_path_prefix
+      raise RuntimeError.new("Trying to access file_column, but primary key got lost.") if @instance.id.to_s.empty?
+      @instance.id.to_s
+    end
+  end
+    
+  # The FileColumn module allows you to easily handle file uploads. You can designate
+  # one or more columns of your model's table as "file columns" like this:
+  #
+  #   class Entry < ActiveRecord::Base
+  #
+  #     file_column :image
+  #   end
+  #
+  # Now, by default, an uploaded file "test.png" for an entry object with primary key 42 will
+  # be stored in in "public/entry/image/42/test.png". The filename "test.png" will be stored
+  # in the record's "image" column. The "entries" table should have a +VARCHAR+ column
+  # named "image".
+  #
+  # The methods of this module are automatically included into <tt>ActiveRecord::Base</tt>
+  # as class methods, so that you can use them in your models.
+  #
+  # == Generated Methods
+  #
+  # After calling "<tt>file_column :image</tt>" as in the example above, a number of instance methods
+  # will automatically be generated, all prefixed by "image":
+  #
+  # * <tt>Entry#image=(uploaded_file)</tt>: this will handle a newly uploaded file
+  #   (see below). Note that
+  #   you can simply call your upload field "entry[image]" in your view (or use the
+  #   helper).
+  # * <tt>Entry#image(subdir=nil)</tt>: This will return an absolute path (as a
+  #   string) to the currently uploaded file
+  #   or nil if no file has been uploaded
+  # * <tt>Entry#image_relative_path(subdir=nil)</tt>: This will return a path relative to
+  #   this file column's base directory
+  #   as a string or nil if no file has been uploaded. This would be "42/test.png" in the example.
+  # * <tt>Entry#image_just_uploaded?</tt>: Returns true if a new file has been uploaded to this instance.
+  #   You can use this in your code to perform certain actions (e. g., validation,
+  #   custom post-processing) only on newly uploaded files.
+  #
+  # You can access the raw value of the "image" column (which will contain the filename) via the
+  # <tt>ActiveRecord::Base#attributes</tt> or <tt>ActiveRecord::Base#[]</tt> methods like this:
+  #
+  #   entry['image']    # e.g."test.png"
+  #
+  # == Storage of uploaded files
+  #
+  # For a model class +Entry+ and a column +image+, all files will be stored under
+  # "public/entry/image". A sub-directory named after the primary key of the object will
+  # be created, so that files can be stored using their real filename. For example, a file
+  # "test.png" stored in an Entry object with id 42 will be stored in
+  #
+  #   public/entry/image/42/test.png
+  #
+  # Files will be moved to this location in an +after_save+ callback. They will be stored in
+  # a temporary location previously as explained in the next section.
+  #
+  # By default, files will be created with unix permissions of <tt>0644</tt> (i. e., owner has
+  # read/write access, group and others only have read access). You can customize
+  # this by passing the desired mode as a <tt>:permissions</tt> options. The value
+  # you give here is passed directly to <tt>File::chmod</tt>, so on Unix you should
+  # give some octal value like 0644, for example.
+  #
+  # == Handling of form redisplay
+  #
+  # Suppose you have a form for creating a new object where the user can upload an image. The form may
+  # have to be re-displayed because of validation errors. The uploaded file has to be stored somewhere so
+  # that the user does not have to upload it again. FileColumn will store these in a temporary directory
+  # (called "tmp" and located under the column's base directory by default) so that it can be moved to
+  # the final location if the object is successfully created. If the form is never completed, though, you
+  # can easily remove all the images in this "tmp" directory once per day or so.
+  #
+  # So in the example above, the image "test.png" would first be stored in 
+  # "public/entry/image/tmp/<some_random_key>/test.png" and be moved to
+  # "public/entry/image/<primary_key>/test.png".
+  #
+  # This temporary location of newly uploaded files has another advantage when updating objects. If the
+  # update fails for some reasons (e.g. due to validations), the existing image will not be overwritten, so
+  # it has a kind of "transactional behaviour".
+  #
+  # == Additional Files and Directories
+  #
+  # FileColumn allows you to keep more than one file in a directory and will move/delete
+  # all the files and directories it finds in a model object's directory when necessary.
+  #
+  # As a convenience you can access files stored in sub-directories via the +subdir+
+  # parameter if they have the same filename.
+  #
+  # Suppose your uploaded file is named "vancouver.jpg" and you want to create a
+  # thumb-nail and store it in the "thumb" directory. If you call
+  # <tt>image("thumb")</tt>, you
+  # will receive an absolute path for the file "thumb/vancouver.jpg" in the same
+  # directory "vancouver.jpg" is stored. Look at the documentation of FileColumn::Magick
+  # for more examples and how to create these thumb-nails automatically.
+  #
+  # == File Extensions
+  #
+  # FileColumn will try to fix the file extension of uploaded files, so that
+  # the files are served with the correct mime-type by your web-server. Most
+  # web-servers are setting the mime-type based on the file's extension. You
+  # can disable this behaviour by passing the <tt>:fix_file_extensions</tt> option
+  # with a value of +nil+ to +file_column+.
+  #
+  # In order to set the correct extension, FileColumn tries to determine
+  # the files mime-type first. It then uses the +MIME_EXTENSIONS+ hash to
+  # choose the corresponding file extension. You can override this hash
+  # by passing in a <tt>:mime_extensions</tt> option to +file_column+.
+  #
+  # The mime-type of the uploaded file is determined with the following steps:
+  #
+  # 1. Run the external "file" utility. You can specify the full path to
+  #    the executable in the <tt>:file_exec</tt> option or set this option
+  #    to +nil+ to disable this step
+  #
+  # 2. If the file utility couldn't determine the mime-type or the utility was not
+  #    present, the content-type provided by the user's browser is used
+  #    as a fallback.
+  #
+  # == Custom Storage Directories
+  #
+  # FileColumn's storage location is determined in the following way. All
+  # files are saved below the so-called "root_path" directory, which defaults to
+  # "RAILS_ROOT/public". For every file_column, you can set a separte "store_dir"
+  # option. It defaults to "model_name/attribute_name".
+  # 
+  # Files will always be stored in sub-directories of the store_dir path. The
+  # subdirectory is named after the instance's +id+ attribute for a saved model,
+  # or "tmp/<randomkey>" for unsaved models.
+  #
+  # You can specify a custom root_path by setting the <tt>:root_path</tt> option.
+  # 
+  # You can specify a custom storage_dir by setting the <tt>:storage_dir</tt> option.
+  #
+  # For setting a static storage_dir that doesn't change with respect to a particular
+  # instance, you assign <tt>:storage_dir</tt> a String representing a directory
+  # as an absolute path.
+  #
+  # If you need more fine-grained control over the storage directory, you
+  # can use the name of a callback-method as a symbol for the
+  # <tt>:store_dir</tt> option. This method has to be defined as an
+  # instance method in your model. It will be called without any arguments
+  # whenever the storage directory for an uploaded file is needed. It should return
+  # a String representing a directory relativeo to root_path.
+  #
+  # Uploaded files for unsaved models objects will be stored in a temporary
+  # directory. By default this directory will be a "tmp" directory in
+  # your <tt>:store_dir</tt>. You can override this via the
+  # <tt>:tmp_base_dir</tt> option.
+  module ClassMethods
+
+    # default mapping of mime-types to file extensions. FileColumn will try to
+    # rename a file to the correct extension if it detects a known mime-type
+    MIME_EXTENSIONS = {
+      "image/gif" => "gif",
+      "image/jpeg" => "jpg",
+      "image/pjpeg" => "jpg",
+      "image/x-png" => "png",
+      "image/jpg" => "jpg",
+      "image/png" => "png",
+      "application/x-shockwave-flash" => "swf",
+      "application/pdf" => "pdf",
+      "application/pgp-signature" => "sig",
+      "application/futuresplash" => "spl",
+      "application/msword" => "doc",
+      "application/postscript" => "ps",
+      "application/x-bittorrent" => "torrent",
+      "application/x-dvi" => "dvi",
+      "application/x-gzip" => "gz",
+      "application/x-ns-proxy-autoconfig" => "pac",
+      "application/x-shockwave-flash" => "swf",
+      "application/x-tgz" => "tar.gz",
+      "application/x-tar" => "tar",
+      "application/zip" => "zip",
+      "audio/mpeg" => "mp3",
+      "audio/x-mpegurl" => "m3u",
+      "audio/x-ms-wma" => "wma",
+      "audio/x-ms-wax" => "wax",
+      "audio/x-wav" => "wav",
+      "image/x-xbitmap" => "xbm",             
+      "image/x-xpixmap" => "xpm",             
+      "image/x-xwindowdump" => "xwd",             
+      "text/css" => "css",             
+      "text/html" => "html",                          
+      "text/javascript" => "js",
+      "text/plain" => "txt",
+      "text/xml" => "xml",
+      "video/mpeg" => "mpeg",
+      "video/quicktime" => "mov",
+      "video/x-msvideo" => "avi",
+      "video/x-ms-asf" => "asf",
+      "video/x-ms-wmv" => "wmv"
+    }
+
+    EXTENSIONS = Set.new MIME_EXTENSIONS.values
+    EXTENSIONS.merge %w(jpeg)
+
+    # default options. You can override these with +file_column+'s +options+ parameter
+    DEFAULT_OPTIONS = {
+      :root_path => File.join(RAILS_ROOT, "public"),
+      :web_root => "",
+      :mime_extensions => MIME_EXTENSIONS,
+      :extensions => EXTENSIONS,
+      :fix_file_extensions => true,
+      :permissions => 0644,
+
+      # path to the unix "file" executbale for
+      # guessing the content-type of files
+      :file_exec => "file" 
+    }
+    
+    # handle the +attr+ attribute as a "file-upload" column, generating additional methods as explained
+    # above. You should pass the attribute's name as a symbol, like this:
+    #
+    #   file_column :image
+    #
+    # You can pass in an options hash that overrides the options
+    # in +DEFAULT_OPTIONS+.
+    def file_column(attr, options={})
+      options = DEFAULT_OPTIONS.merge(options) if options
+      
+      my_options = FileColumn::init_options(options, 
+        ActiveSupport::Inflector.underscore(self.name).to_s,
+                                            attr.to_s)
+      
+      state_attr = "@#{attr}_state".to_sym
+      state_method = "#{attr}_state".to_sym
+      
+      define_method state_method do
+        result = instance_variable_get state_attr
+        if result.nil?
+          result = FileColumn::create_state(self, attr.to_s)
+          instance_variable_set state_attr, result
+        end
+        result
+      end
+      
+      private state_method
+      
+      define_method attr do |*args|
+        send(state_method).absolute_path *args
+      end
+      
+      define_method "#{attr}_relative_path" do |*args|
+        send(state_method).relative_path *args
+      end
+
+      define_method "#{attr}_dir" do
+        send(state_method).absolute_dir
+      end
+
+      define_method "#{attr}_relative_dir" do
+        send(state_method).relative_dir
+      end
+
+      define_method "#{attr}=" do |file|
+        state = send(state_method).assign(file)
+        instance_variable_set state_attr, state
+        if state.options[:after_upload] and state.just_uploaded?
+          state.options[:after_upload].each do |sym|
+            self.send sym
+          end
+        end
+      end
+      
+      define_method "#{attr}_temp" do
+        send(state_method).temp_path
+      end
+      
+      define_method "#{attr}_temp=" do |temp_path|
+        instance_variable_set state_attr, send(state_method).assign_temp(temp_path)
+      end
+      
+      after_save_method = "#{attr}_after_save".to_sym
+      
+      define_method after_save_method do
+        instance_variable_set state_attr, send(state_method).after_save
+      end
+      
+      after_save after_save_method
+      
+      after_destroy_method = "#{attr}_after_destroy".to_sym
+      
+      define_method after_destroy_method do
+        send(state_method).after_destroy
+      end
+      after_destroy after_destroy_method
+      
+      define_method "#{attr}_just_uploaded?" do
+        send(state_method).just_uploaded?
+      end
+
+      # this creates a closure keeping a reference to my_options
+      # right now that's the only way we store the options. We
+      # might use a class attribute as well
+      define_method "#{attr}_options" do
+        my_options
+      end
+
+      private after_save_method, after_destroy_method
+
+      FileColumn::MagickExtension::file_column(self, attr, my_options) if options[:magick]
+    end
+    
+  end
+  
+  private
+  
+  def self.generate_temp_name
+    now = Time.now
+    "#{now.to_i}.#{now.usec}.#{Process.pid}"
+  end
+  
+  def self.sanitize_filename(filename)
+    filename = File.basename(filename.gsub("\\", "/")) # work-around for IE
+    filename.gsub!(/[^a-zA-Z0-9\.\-\+_]/,"_")
+    filename = "_#{filename}" if filename =~ /^\.+$/
+    filename = "unnamed" if filename.size == 0
+    filename
+  end
+  
+end
+
+
diff --git a/vendor/plugins/file_column/lib/file_column_helper.rb b/vendor/plugins/file_column/lib/file_column_helper.rb
new file mode 100644 (file)
index 0000000..f4ebe38
--- /dev/null
@@ -0,0 +1,150 @@
+# This module contains helper methods for displaying and uploading files
+# for attributes created by +FileColumn+'s +file_column+ method. It will be
+# automatically included into ActionView::Base, thereby making this module's
+# methods available in all your views.
+module FileColumnHelper
+  
+  # Use this helper to create an upload field for a file_column attribute. This will generate
+  # an additional hidden field to keep uploaded files during form-redisplays. For example,
+  # when called with
+  #
+  #   <%= file_column_field("entry", "image") %>
+  #
+  # the following HTML will be generated (assuming the form is redisplayed and something has
+  # already been uploaded):
+  #
+  #   <input type="hidden" name="entry[image_temp]" value="..." />
+  #   <input type="file" name="entry[image]" />
+  #
+  # You can use the +option+ argument to pass additional options to the file-field tag.
+  #
+  # Be sure to set the enclosing form's encoding to 'multipart/form-data', by
+  # using something like this:
+  #
+  #    <%= form_tag {:action => "create", ...}, :multipart => true %>
+  def file_column_field(object, method, options={})
+    result = ActionView::Helpers::InstanceTag.new(object.dup, method.to_s+"_temp", self).to_input_field_tag("hidden", {})
+    result << ActionView::Helpers::InstanceTag.new(object.dup, method, self).to_input_field_tag("file", options)
+  end
+  
+  # Creates an URL where an uploaded file can be accessed. When called for an Entry object with
+  # id 42 (stored in <tt>@entry</tt>) like this
+  #
+  #   <%= url_for_file_column(@entry, "image")
+  #
+  # the following URL will be produced, assuming the file "test.png" has been stored in
+  # the "image"-column of an Entry object stored in <tt>@entry</tt>:
+  #
+  #  /entry/image/42/test.png
+  #
+  # This will produce a valid URL even for temporary uploaded files, e.g. files where the object
+  # they are belonging to has not been saved in the database yet.
+  #
+  # The URL produces, although starting with a slash, will be relative
+  # to your app's root. If you pass it to one rails' +image_tag+
+  # helper, rails will properly convert it to an absolute
+  # URL. However, this will not be the case, if you create a link with
+  # the +link_to+ helper. In this case, you can pass <tt>:absolute =>
+  # true</tt> to +options+, which will make sure, the generated URL is
+  # absolute on your server.  Examples:
+  #
+  #    <%= image_tag url_for_file_column(@entry, "image") %>
+  #    <%= link_to "Download", url_for_file_column(@entry, "image", :absolute => true) %>
+  #
+  # If there is currently no uploaded file stored in the object's column this method will
+  # return +nil+.
+  def url_for_file_column(object, method, options=nil)
+    case object
+    when String, Symbol
+      object = instance_variable_get("@#{object.to_s}")
+    end
+
+    # parse options
+    subdir = nil
+    absolute = false
+    if options
+      case options
+      when Hash
+        subdir = options[:subdir]
+        absolute = options[:absolute]
+      when String, Symbol
+        subdir = options
+      end
+    end
+    
+    relative_path = object.send("#{method}_relative_path", subdir)
+    return nil unless relative_path
+
+    url = ""
+    url << request.relative_url_root.to_s if absolute
+    url << "/"
+    url << object.send("#{method}_options")[:base_url] << "/"
+    url << relative_path
+  end
+
+  # Same as +url_for_file_colum+ but allows you to access different versions
+  # of the image that have been processed by RMagick.
+  #
+  # If your +options+ parameter is non-nil this will
+  # access a different version of an image that will be produced by
+  # RMagick. You can use the following types for +options+:
+  #
+  # * a <tt>:symbol</tt> will select a version defined in the model
+  #   via FileColumn::Magick's <tt>:versions</tt> feature.
+  # * a <tt>geometry_string</tt> will dynamically create an
+  #   image resized as specified by <tt>geometry_string</tt>. The image will
+  #   be stored so that it does not have to be recomputed the next time the
+  #   same version string is used.
+  # * <tt>some_hash</tt> will dynamically create an image
+  #   that is created according to the options in <tt>some_hash</tt>. This
+  #   accepts exactly the same options as Magick's version feature.
+  #
+  # The version produced by RMagick will be stored in a special sub-directory.
+  # The directory's name will be derived from the options you specified
+  # (via a hash function) but if you want
+  # to set it yourself, you can use the <tt>:name => name</tt> option.
+  #
+  # Examples:
+  #
+  #    <%= url_for_image_column @entry, "image", "640x480" %>
+  #
+  # will produce an URL like this
+  #
+  #    /entry/image/42/bdn19n/filename.jpg
+  #    # "640x480".hash.abs.to_s(36) == "bdn19n"
+  #
+  # and
+  #
+  #    <%= url_for_image_column @entry, "image", 
+  #       :size => "50x50", :crop => "1:1", :name => "thumb" %>
+  #
+  # will produce something like this:
+  #
+  #    /entry/image/42/thumb/filename.jpg
+  #
+  # Hint: If you are using the same geometry string / options hash multiple times, you should
+  # define it in a helper to stay with DRY. Another option is to define it in the model via
+  # FileColumn::Magick's <tt>:versions</tt> feature and then refer to it via a symbol.
+  #
+  # The URL produced by this method is relative to your application's root URL,
+  # although it will start with a slash.
+  # If you pass this URL to rails' +image_tag+ helper, it will be converted to an
+  # absolute URL automatically.
+  # If there is currently no image uploaded, or there is a problem while loading
+  # the image this method will return +nil+.
+  def url_for_image_column(object, method, options=nil)
+    case object
+    when String, Symbol
+      object = instance_variable_get("@#{object.to_s}")
+    end
+    subdir = nil
+    if options
+      subdir = object.send("#{method}_state").create_magick_version_if_needed(options)
+    end
+    if subdir.nil?
+      nil
+    else
+      url_for_file_column(object, method, subdir)
+    end
+  end
+end
diff --git a/vendor/plugins/file_column/lib/file_compat.rb b/vendor/plugins/file_column/lib/file_compat.rb
new file mode 100644 (file)
index 0000000..f284410
--- /dev/null
@@ -0,0 +1,28 @@
+module FileColumn
+
+  # This bit of code allows you to pass regular old files to
+  # file_column.  file_column depends on a few extra methods that the
+  # CGI uploaded file class adds.  We will add the equivalent methods
+  # to file objects if necessary by extending them with this module. This
+  # avoids opening up the standard File class which might result in
+  # naming conflicts.
+
+  module FileCompat # :nodoc:
+    def original_filename
+      File.basename(path)
+    end
+    
+    def size
+      File.size(path)
+    end
+    
+    def local_path
+      path
+    end
+    
+    def content_type
+      nil
+    end
+  end
+end
+
diff --git a/vendor/plugins/file_column/lib/magick_file_column.rb b/vendor/plugins/file_column/lib/magick_file_column.rb
new file mode 100644 (file)
index 0000000..c4dc06f
--- /dev/null
@@ -0,0 +1,260 @@
+module FileColumn # :nodoc:
+  
+  class BaseUploadedFile # :nodoc:
+    def transform_with_magick
+      if needs_transform?
+        begin
+          img = ::Magick::Image::read(absolute_path).first
+        rescue ::Magick::ImageMagickError
+          if options[:magick][:image_required]
+            @magick_errors ||= []
+            @magick_errors << "invalid image"
+          end
+          return
+        end
+        
+        if options[:magick][:versions]
+          options[:magick][:versions].each_pair do |version, version_options|
+            next if version_options[:lazy]
+            dirname = version_options[:name]
+            FileUtils.mkdir File.join(@dir, dirname)
+            transform_image(img, version_options, absolute_path(dirname))
+          end
+        end
+        if options[:magick][:size] or options[:magick][:crop] or options[:magick][:transformation] or options[:magick][:attributes]
+          transform_image(img, options[:magick], absolute_path)
+        end
+
+        GC.start
+      end
+    end
+
+    def create_magick_version_if_needed(version)
+      # RMagick might not have been loaded so far.
+      # We do not want to require it on every call of this method
+      # as this might be fairly expensive, so we just try if ::Magick
+      # exists and require it if not.
+      begin 
+        ::Magick 
+      rescue NameError
+        require 'RMagick'
+      end
+
+      if version.is_a?(Symbol)
+        version_options = options[:magick][:versions][version]
+      else
+        version_options = MagickExtension::process_options(version)
+      end
+
+      unless File.exists?(absolute_path(version_options[:name]))
+        begin
+          img = ::Magick::Image::read(absolute_path).first
+        rescue ::Magick::ImageMagickError
+          # we might be called directly from the view here
+          # so we just return nil if we cannot load the image
+          return nil
+        end
+        dirname = version_options[:name]
+        FileUtils.mkdir File.join(@dir, dirname)
+        transform_image(img, version_options, absolute_path(dirname))
+      end
+
+      version_options[:name]
+    end
+
+    attr_reader :magick_errors
+    
+    def has_magick_errors?
+      @magick_errors and !@magick_errors.empty?
+    end
+
+    private
+    
+    def needs_transform?
+      options[:magick] and just_uploaded? and 
+        (options[:magick][:size] or options[:magick][:versions] or options[:magick][:transformation] or options[:magick][:attributes])
+    end
+
+    def transform_image(img, img_options, dest_path)
+      begin
+        if img_options[:transformation]
+          if img_options[:transformation].is_a?(Symbol)
+            img = @instance.send(img_options[:transformation], img)
+          else
+            img = img_options[:transformation].call(img)
+          end
+        end
+        if img_options[:crop]
+          dx, dy = img_options[:crop].split(':').map { |x| x.to_f }
+          w, h = (img.rows * dx / dy), (img.columns * dy / dx)
+          img = img.crop(::Magick::CenterGravity, [img.columns, w].min, 
+                         [img.rows, h].min, true)
+        end
+
+        if img_options[:size]
+          img = img.change_geometry(img_options[:size]) do |c, r, i|
+            i.resize(c, r)
+          end
+        end
+      ensure
+        img.write(dest_path) do
+          if img_options[:attributes]
+            img_options[:attributes].each_pair do |property, value| 
+              self.send "#{property}=", value
+            end
+          end
+        end
+        File.chmod options[:permissions], dest_path
+      end
+    end
+  end
+
+  # If you are using file_column to upload images, you can
+  # directly process the images with RMagick,
+  # a ruby extension
+  # for accessing the popular imagemagick libraries. You can find
+  # more information about RMagick at http://rmagick.rubyforge.org.
+  #
+  # You can control what to do by adding a <tt>:magick</tt> option
+  # to your options hash. All operations are performed immediately
+  # after a new file is assigned to the file_column attribute (i.e.,
+  # when a new file has been uploaded).
+  #
+  # == Resizing images
+  #
+  # To resize the uploaded image according to an imagemagick geometry
+  # string, just use the <tt>:size</tt> option:
+  #
+  #    file_column :image, :magick => {:size => "800x600>"}
+  #
+  # If the uploaded file cannot be loaded by RMagick, file_column will
+  # signal a validation error for the corresponding attribute. If you
+  # want to allow non-image files to be uploaded in a column that uses
+  # the <tt>:magick</tt> option, you can set the <tt>:image_required</tt>
+  # attribute to +false+:
+  #
+  #    file_column :image, :magick => {:size => "800x600>",
+  #                                    :image_required => false }
+  #
+  # == Multiple versions
+  #
+  # You can also create additional versions of your image, for example
+  # thumb-nails, like this:
+  #    file_column :image, :magick => {:versions => {
+  #         :thumb => {:size => "50x50"},
+  #         :medium => {:size => "640x480>"}
+  #       }
+  #
+  # These versions will be stored in separate sub-directories, named like the
+  # symbol you used to identify the version. So in the previous example, the
+  # image versions will be stored in "thumb", "screen" and "widescreen"
+  # directories, resp. 
+  # A name different from the symbol can be set via the <tt>:name</tt> option.
+  #
+  # These versions can be accessed via FileColumnHelper's +url_for_image_column+
+  # method like this:
+  #
+  #    <%= url_for_image_column "entry", "image", :thumb %>
+  #
+  # == Cropping images
+  #
+  # If you wish to crop your images with a size ratio before scaling
+  # them according to your version geometry, you can use the :crop directive.
+  #    file_column :image, :magick => {:versions => {
+  #         :square => {:crop => "1:1", :size => "50x50", :name => "thumb"},
+  #         :screen => {:crop => "4:3", :size => "640x480>"},
+  #         :widescreen => {:crop => "16:9", :size => "640x360!"},
+  #       }
+  #    }
+  #
+  # == Custom attributes
+  #
+  # To change some of the image properties like compression level before they
+  # are saved you can set the <tt>:attributes</tt> option.
+  # For a list of available attributes go to http://www.simplesystems.org/RMagick/doc/info.html
+  # 
+  #     file_column :image, :magick => { :attributes => { :quality => 30 } }
+  # 
+  # == Custom transformations
+  #
+  # To perform custom transformations on uploaded images, you can pass a
+  # callback to file_column:
+  #    file_column :image, :magick => 
+  #       Proc.new { |image| image.quantize(256, Magick::GRAYColorspace) }
+  #
+  # The callback you give, receives one argument, which is an instance
+  # of Magick::Image, the RMagick image class. It should return a transformed
+  # image. Instead of passing a <tt>Proc</tt> object, you can also give a
+  # <tt>Symbol</tt>, the name of an instance method of your model.
+  #
+  # Custom transformations can be combined via the standard :size and :crop
+  # features, by using the :transformation option:
+  #   file_column :image, :magick => {
+  #      :transformation => Proc.new { |image| ... },
+  #      :size => "640x480"
+  #    }
+  #
+  # In this case, the standard resizing operations will be performed after the
+  # custom transformation.
+  #
+  # Of course, custom transformations can be used in versions, as well.
+  #
+  # <b>Note:</b> You'll need the
+  # RMagick extension being installed  in order to use file_column's
+  # imagemagick integration.
+  module MagickExtension
+
+    def self.file_column(klass, attr, options) # :nodoc:
+      require 'RMagick'
+      options[:magick] = process_options(options[:magick],false) if options[:magick]
+      if options[:magick][:versions]
+        options[:magick][:versions].each_pair do |name, value|
+          options[:magick][:versions][name] = process_options(value, name.to_s)
+        end
+      end
+      state_method = "#{attr}_state".to_sym
+      after_assign_method = "#{attr}_magick_after_assign".to_sym
+      
+      klass.send(:define_method, after_assign_method) do
+        self.send(state_method).transform_with_magick
+      end
+      
+      options[:after_upload] ||= []
+      options[:after_upload] << after_assign_method
+      
+      klass.validate do |record|
+        state = record.send(state_method)
+        if state.has_magick_errors?
+          state.magick_errors.each do |error|
+            record.errors.add attr, error
+          end
+        end
+      end
+    end
+
+    
+    def self.process_options(options,create_name=true)
+      case options
+      when String then options = {:size => options}
+      when Proc, Symbol then options = {:transformation => options }
+      end
+      if options[:geometry]
+        options[:size] = options.delete(:geometry)
+      end
+      options[:image_required] = true unless options.key?(:image_required)
+      if options[:name].nil? and create_name
+        if create_name == true
+          hash = 0
+          for key in [:size, :crop]
+            hash = hash ^ options[key].hash if options[key]
+          end
+          options[:name] = hash.abs.to_s(36)
+        else
+          options[:name] = create_name
+        end
+      end
+      options
+    end
+
+  end
+end
diff --git a/vendor/plugins/file_column/lib/rails_file_column.rb b/vendor/plugins/file_column/lib/rails_file_column.rb
new file mode 100644 (file)
index 0000000..af8c95a
--- /dev/null
@@ -0,0 +1,19 @@
+# require this file from your "config/environment.rb" (after rails has been loaded)
+# to integrate the file_column extension into rails.
+
+require 'file_column'
+require 'file_column_helper'
+
+
+module ActiveRecord # :nodoc:
+  class Base # :nodoc:
+    # make file_column method available in all active record decendants
+    include FileColumn
+  end
+end
+
+module ActionView # :nodoc:
+  class Base # :nodoc:
+    include FileColumnHelper
+  end
+end
diff --git a/vendor/plugins/file_column/lib/test_case.rb b/vendor/plugins/file_column/lib/test_case.rb
new file mode 100644 (file)
index 0000000..1416a1e
--- /dev/null
@@ -0,0 +1,124 @@
+require 'test/unit'
+
+# Add the methods +upload+, the <tt>setup_file_fixtures</tt> and
+# <tt>teardown_file_fixtures</tt> to the class Test::Unit::TestCase.
+class Test::Unit::TestCase
+  # Returns a +Tempfile+ object as it would have been generated on file upload.
+  # Use this method to create the parameters when emulating form posts with 
+  # file fields.
+  #
+  # === Example:
+  #
+  #    def test_file_column_post
+  #      entry = { :title => 'foo', :file => upload('/tmp/foo.txt')}
+  #      post :upload, :entry => entry
+  #  
+  #      # ...
+  #    end
+  #
+  # === Parameters
+  #
+  # * <tt>path</tt> The path to the file to upload.
+  # * <tt>content_type</tt> The MIME type of the file. If it is <tt>:guess</tt>,
+  #   the method will try to guess it.
+  def upload(path, content_type=:guess, type=:tempfile)
+    if content_type == :guess
+      case path
+      when /\.jpg$/ then content_type = "image/jpeg"
+      when /\.png$/ then content_type = "image/png"
+      else content_type = nil
+      end
+    end
+    uploaded_file(path, content_type, File.basename(path), type)
+  end
+  
+  # Copies the fixture files from "RAILS_ROOT/test/fixtures/file_column" into
+  # the temporary storage directory used for testing
+  # ("RAILS_ROOT/test/tmp/file_column"). Call this method in your
+  # <tt>setup</tt> methods to get the file fixtures (images, for example) into
+  # the directory used by file_column in testing.
+  #
+  # Note that the files and directories in the "fixtures/file_column" directory 
+  # must have the same structure as you would expect in your "/public" directory
+  # after uploading with FileColumn.
+  #
+  # For example, the directory structure could look like this:
+  #
+  #   test/fixtures/file_column/
+  #   `-- container
+  #       |-- first_image
+  #       |   |-- 1
+  #       |   |   `-- image1.jpg
+  #       |   `-- tmp
+  #       `-- second_image
+  #           |-- 1
+  #           |   `-- image2.jpg
+  #           `-- tmp
+  #
+  # Your fixture file for this one "container" class fixture could look like this:
+  #
+  #   first:
+  #     id:           1
+  #     first_image:  image1.jpg
+  #     second_image: image1.jpg
+  #
+  # A usage example:
+  #
+  #  def setup
+  #    setup_fixture_files
+  #
+  #    # ...
+  #  end
+  def setup_fixture_files
+    tmp_path = File.join(RAILS_ROOT, "test", "tmp", "file_column")
+    file_fixtures = Dir.glob File.join(RAILS_ROOT, "test", "fixtures", "file_column", "*")
+    
+    FileUtils.mkdir_p tmp_path unless File.exists?(tmp_path)
+    FileUtils.cp_r file_fixtures, tmp_path
+  end
+  
+  # Removes the directory "RAILS_ROOT/test/tmp/file_column/" so the files
+  # copied on test startup are removed. Call this in your unit test's +teardown+
+  # method.
+  #
+  # A usage example:
+  #
+  #  def teardown
+  #    teardown_fixture_files
+  #
+  #    # ...
+  #  end
+  def teardown_fixture_files
+    FileUtils.rm_rf File.join(RAILS_ROOT, "test", "tmp", "file_column")
+  end
+  
+  private
+  
+  def uploaded_file(path, content_type, filename, type=:tempfile) # :nodoc:
+    if type == :tempfile
+      t = Tempfile.new(File.basename(filename))
+      FileUtils.copy_file(path, t.path)
+    else
+      if path
+        t = StringIO.new(IO.read(path))
+      else
+        t = StringIO.new
+      end
+    end
+    (class << t; self; end).class_eval do
+      alias local_path path if type == :tempfile
+      define_method(:local_path) { "" } if type == :stringio
+      define_method(:original_filename) {filename}
+      define_method(:content_type) {content_type}
+    end
+    return t
+  end
+end
+
+# If we are running in the "test" environment, we overwrite the default 
+# settings for FileColumn so that files are not uploaded into "/public/"
+# in tests but rather into the directory "/test/tmp/file_column".
+if RAILS_ENV == "test"
+  FileColumn::ClassMethods::DEFAULT_OPTIONS[:root_path] =
+    File.join(RAILS_ROOT, "test", "tmp", "file_column")
+end
diff --git a/vendor/plugins/file_column/lib/validations.rb b/vendor/plugins/file_column/lib/validations.rb
new file mode 100644 (file)
index 0000000..5b961eb
--- /dev/null
@@ -0,0 +1,112 @@
+module FileColumn
+  module Validations #:nodoc:
+    
+    def self.append_features(base)
+      super
+      base.extend(ClassMethods)
+    end
+
+    # This module contains methods to create validations of uploaded files. All methods
+    # in this module will be included as class methods into <tt>ActiveRecord::Base</tt>
+    # so that you can use them in your models like this:
+    #
+    #    class Entry < ActiveRecord::Base
+    #      file_column :image
+    #      validates_filesize_of :image, :in => 0..1.megabyte
+    #    end
+    module ClassMethods
+      EXT_REGEXP = /\.([A-z0-9]+)$/
+    
+      # This validates the file type of one or more file_columns.  A list of file columns
+      # should be given followed by an options hash.
+      #
+      # Required options:
+      # * <tt>:in</tt> => list of extensions or mime types. If mime types are used they
+      #   will be mapped into an extension via FileColumn::ClassMethods::MIME_EXTENSIONS.
+      #
+      # Examples:
+      #     validates_file_format_of :field, :in => ["gif", "png", "jpg"]
+      #     validates_file_format_of :field, :in => ["image/jpeg"]
+      def validates_file_format_of(*attrs)
+      
+        options = attrs.pop if attrs.last.is_a?Hash
+        raise ArgumentError, "Please include the :in option." if !options || !options[:in]
+        options[:in] = [options[:in]] if options[:in].is_a?String
+        raise ArgumentError, "Invalid value for option :in" unless options[:in].is_a?Array
+      
+        validates_each(attrs, options) do |record, attr, value|
+          unless value.blank?
+            mime_extensions = record.send("#{attr}_options")[:mime_extensions]
+            extensions = options[:in].map{|o| mime_extensions[o] || o }
+            record.errors.add attr, "is not a valid format." unless extensions.include?(value.scan(EXT_REGEXP).flatten.first)
+          end
+        end
+      
+      end
+    
+      # This validates the file size of one or more file_columns.  A list of file columns
+      # should be given followed by an options hash.
+      #
+      # Required options:
+      # * <tt>:in</tt> => A size range.  Note that you can use ActiveSupport's
+      #   numeric extensions for kilobytes, etc.
+      #
+      # Examples:
+      #    validates_filesize_of :field, :in => 0..100.megabytes
+      #    validates_filesize_of :field, :in => 15.kilobytes..1.megabyte
+      def validates_filesize_of(*attrs)  
+      
+        options = attrs.pop if attrs.last.is_a?Hash
+        raise ArgumentError, "Please include the :in option." if !options || !options[:in]
+        raise ArgumentError, "Invalid value for option :in" unless options[:in].is_a?Range
+      
+        validates_each(attrs, options) do |record, attr, value|
+          unless value.blank?
+            size = File.size(value)
+            record.errors.add attr, "is smaller than the allowed size range." if size < options[:in].first
+            record.errors.add attr, "is larger than the allowed size range." if size > options[:in].last
+          end
+        end
+      
+      end 
+
+      IMAGE_SIZE_REGEXP = /^(\d+)x(\d+)$/
+
+      # Validates the image size of one or more file_columns.  A list of file columns
+      # should be given followed by an options hash. The validation will pass
+      # if both image dimensions (rows and columns) are at least as big as
+      # given in the <tt>:min</tt> option.
+      #
+      # Required options:
+      # * <tt>:min</tt> => minimum image dimension string, in the format NNxNN
+      #   (columns x rows).
+      #
+      # Example:
+      #    validates_image_size :field, :min => "1200x1800"
+      #
+      # This validation requires RMagick to be installed on your system
+      # to check the image's size.
+      def validates_image_size(*attrs)      
+        options = attrs.pop if attrs.last.is_a?Hash
+        raise ArgumentError, "Please include a :min option." if !options || !options[:min]
+        minimums = options[:min].scan(IMAGE_SIZE_REGEXP).first.collect{|n| n.to_i} rescue []
+        raise ArgumentError, "Invalid value for option :min (should be 'XXxYY')" unless minimums.size == 2
+
+        require 'RMagick'
+
+        validates_each(attrs, options) do |record, attr, value|
+          unless value.blank?
+            begin
+              img = ::Magick::Image::read(value).first
+              record.errors.add('image', "is too small, must be at least #{minimums[0]}x#{minimums[1]}") if ( img.rows < minimums[1] || img.columns < minimums[0] )
+            rescue ::Magick::ImageMagickError
+              record.errors.add('image', "invalid image")
+            end
+            img = nil
+            GC.start
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/vendor/plugins/file_column/test/abstract_unit.rb b/vendor/plugins/file_column/test/abstract_unit.rb
new file mode 100644 (file)
index 0000000..22bc53b
--- /dev/null
@@ -0,0 +1,63 @@
+require 'test/unit'
+require 'rubygems'
+require 'active_support'
+require 'active_record'
+require 'action_view'
+require File.dirname(__FILE__) + '/connection'
+require 'stringio'
+
+RAILS_ROOT = File.dirname(__FILE__)
+RAILS_ENV = ""
+
+$: << "../lib"
+
+require 'file_column'
+require 'file_compat'
+require 'validations'
+require 'test_case'
+
+# do not use the file executable normally in our tests as
+# it may not be present on the machine we are running on
+FileColumn::ClassMethods::DEFAULT_OPTIONS = 
+  FileColumn::ClassMethods::DEFAULT_OPTIONS.merge({:file_exec => nil})
+
+class ActiveRecord::Base
+    include FileColumn
+    include FileColumn::Validations
+end
+
+
+class RequestMock
+  attr_accessor :relative_url_root
+
+  def initialize
+    @relative_url_root = ""
+  end
+end
+
+class Test::Unit::TestCase
+
+  def assert_equal_paths(expected_path, path)
+    assert_equal normalize_path(expected_path), normalize_path(path)
+  end
+
+
+  private
+  
+  def normalize_path(path)
+    Pathname.new(path).realpath
+  end
+
+  def clear_validations
+    [:validate, :validate_on_create, :validate_on_update].each do |attr|
+        Entry.write_inheritable_attribute attr, []
+        Movie.write_inheritable_attribute attr, []
+      end
+  end
+
+  def file_path(filename)
+    File.expand_path("#{File.dirname(__FILE__)}/fixtures/#{filename}")
+  end
+
+  alias_method :f, :file_path
+end
diff --git a/vendor/plugins/file_column/test/connection.rb b/vendor/plugins/file_column/test/connection.rb
new file mode 100644 (file)
index 0000000..a2f28ba
--- /dev/null
@@ -0,0 +1,17 @@
+print "Using native MySQL\n"
+require 'logger'
+
+ActiveRecord::Base.logger = Logger.new("debug.log")
+
+db = 'file_column_test'
+
+ActiveRecord::Base.establish_connection(
+  :adapter  => "mysql",
+  :host     => "localhost",
+  :username => "rails",
+  :password => "",
+  :database => db,
+  :socket => "/var/run/mysqld/mysqld.sock"
+)
+
+load File.dirname(__FILE__) + "/fixtures/schema.rb"
diff --git a/vendor/plugins/file_column/test/file_column_helper_test.rb b/vendor/plugins/file_column/test/file_column_helper_test.rb
new file mode 100644 (file)
index 0000000..ffb2c43
--- /dev/null
@@ -0,0 +1,97 @@
+require File.dirname(__FILE__) + '/abstract_unit'
+require File.dirname(__FILE__) + '/fixtures/entry'
+
+class UrlForFileColumnTest < Test::Unit::TestCase
+  include FileColumnHelper
+
+  def setup
+    Entry.file_column :image
+    @request = RequestMock.new
+  end
+
+  def test_url_for_file_column_with_temp_entry
+    @e = Entry.new(:image => upload(f("skanthak.png")))
+    url = url_for_file_column("e", "image")
+    assert_match %r{^/entry/image/tmp/\d+(\.\d+)+/skanthak.png$}, url
+  end
+
+  def test_url_for_file_column_with_saved_entry
+    @e = Entry.new(:image => upload(f("skanthak.png")))
+    assert @e.save
+
+    url = url_for_file_column("e", "image")
+    assert_equal "/entry/image/#{@e.id}/skanthak.png", url
+  end
+
+  def test_url_for_file_column_works_with_symbol
+    @e = Entry.new(:image => upload(f("skanthak.png")))
+    assert @e.save
+
+    url = url_for_file_column(:e, :image)
+    assert_equal "/entry/image/#{@e.id}/skanthak.png", url
+  end
+  
+  def test_url_for_file_column_works_with_object
+    e = Entry.new(:image => upload(f("skanthak.png")))
+    assert e.save
+
+    url = url_for_file_column(e, "image")
+    assert_equal "/entry/image/#{e.id}/skanthak.png", url
+  end
+
+  def test_url_for_file_column_should_return_nil_on_no_uploaded_file
+    e = Entry.new
+    assert_nil url_for_file_column(e, "image")
+  end
+
+  def test_url_for_file_column_without_extension
+    e = Entry.new
+    e.image = uploaded_file(file_path("kerb.jpg"), "something/unknown", "local_filename")
+    assert e.save
+    assert_equal "/entry/image/#{e.id}/local_filename", url_for_file_column(e, "image")
+  end
+end
+
+class UrlForFileColumnTest < Test::Unit::TestCase
+  include FileColumnHelper
+  include ActionView::Helpers::AssetTagHelper
+  include ActionView::Helpers::TagHelper
+  include ActionView::Helpers::UrlHelper
+
+  def setup
+    Entry.file_column :image
+
+    # mock up some request data structures for AssetTagHelper
+    @request = RequestMock.new
+    @request.relative_url_root = "/foo/bar"
+    @controller = self
+  end
+
+  def request
+    @request
+  end
+
+  IMAGE_URL = %r{^/foo/bar/entry/image/.+/skanthak.png$}
+  def test_with_image_tag
+    e = Entry.new(:image => upload(f("skanthak.png")))
+    html = image_tag url_for_file_column(e, "image")
+    url = html.scan(/src=\"(.+)\"/).first.first
+
+    assert_match IMAGE_URL, url
+  end
+
+  def test_with_link_to_tag
+    e = Entry.new(:image => upload(f("skanthak.png")))
+    html = link_to "Download", url_for_file_column(e, "image", :absolute => true)
+    url = html.scan(/href=\"(.+)\"/).first.first
+    
+    assert_match IMAGE_URL, url
+  end
+
+  def test_relative_url_root_not_modified
+    e = Entry.new(:image => upload(f("skanthak.png")))
+    url_for_file_column(e, "image", :absolute => true)
+    
+    assert_equal "/foo/bar", @request.relative_url_root
+  end
+end
diff --git a/vendor/plugins/file_column/test/file_column_test.rb b/vendor/plugins/file_column/test/file_column_test.rb
new file mode 100755 (executable)
index 0000000..452b781
--- /dev/null
@@ -0,0 +1,650 @@
+require File.dirname(__FILE__) + '/abstract_unit'
+
+require File.dirname(__FILE__) + '/fixtures/entry'
+
+class Movie < ActiveRecord::Base
+end
+
+
+class FileColumnTest < Test::Unit::TestCase
+  
+  def setup
+    # we define the file_columns here so that we can change
+    # settings easily in a single test
+
+    Entry.file_column :image
+    Entry.file_column :file
+    Movie.file_column :movie
+
+    clear_validations
+  end
+  
+  def teardown
+    FileUtils.rm_rf File.dirname(__FILE__)+"/public/entry/"
+    FileUtils.rm_rf File.dirname(__FILE__)+"/public/movie/"
+    FileUtils.rm_rf File.dirname(__FILE__)+"/public/my_store_dir/"
+  end
+  
+  def test_column_write_method
+    assert Entry.new.respond_to?("image=")
+  end
+  
+  def test_column_read_method
+    assert Entry.new.respond_to?("image")
+  end
+  
+  def test_sanitize_filename
+    assert_equal "test.jpg", FileColumn::sanitize_filename("test.jpg")
+    assert FileColumn::sanitize_filename("../../very_tricky/foo.bar") !~ /[\\\/]/, "slashes not removed"
+    assert_equal "__foo", FileColumn::sanitize_filename('`*foo')
+    assert_equal "foo.txt", FileColumn::sanitize_filename('c:\temp\foo.txt')
+    assert_equal "_.", FileColumn::sanitize_filename(".")
+  end
+  
+  def test_default_options
+    e = Entry.new
+    assert_match %r{/public/entry/image}, e.image_options[:store_dir]
+    assert_match %r{/public/entry/image/tmp}, e.image_options[:tmp_base_dir]
+  end
+  
+  def test_assign_without_save_with_tempfile
+    do_test_assign_without_save(:tempfile)
+  end
+  
+  def test_assign_without_save_with_stringio
+    do_test_assign_without_save(:stringio)
+  end
+  
+  def do_test_assign_without_save(upload_type)
+    e = Entry.new
+    e.image = uploaded_file(file_path("skanthak.png"), "image/png", "skanthak.png", upload_type)
+    assert e.image.is_a?(String), "#{e.image.inspect} is not a String"
+    assert File.exists?(e.image)
+    assert FileUtils.identical?(e.image, file_path("skanthak.png"))
+  end
+  
+  def test_filename_preserved
+    e = Entry.new
+    e.image = uploaded_file(file_path("kerb.jpg"), "image/jpeg", "local_filename.jpg")
+    assert_equal "local_filename.jpg", File.basename(e.image)
+  end
+  
+  def test_filename_stored_in_attribute
+    e = Entry.new("image" => uploaded_file(file_path("kerb.jpg"), "image/jpeg", "kerb.jpg"))
+    assert_equal "kerb.jpg", e["image"]
+  end
+  
+  def test_extension_added
+    e = Entry.new
+    e.image = uploaded_file(file_path("kerb.jpg"), "image/jpeg", "local_filename")
+    assert_equal "local_filename.jpg", File.basename(e.image)
+    assert_equal "local_filename.jpg", e["image"]
+  end
+
+  def test_no_extension_without_content_type
+    e = Entry.new
+    e.image = uploaded_file(file_path("kerb.jpg"), "something/unknown", "local_filename")
+    assert_equal "local_filename", File.basename(e.image)
+    assert_equal "local_filename", e["image"]
+  end
+
+  def test_extension_unknown_type
+    e = Entry.new
+    e.image = uploaded_file(file_path("kerb.jpg"), "not/known", "local_filename")
+    assert_equal "local_filename", File.basename(e.image)
+    assert_equal "local_filename", e["image"]
+  end
+
+  def test_extension_unknown_type_with_extension
+    e = Entry.new
+    e.image = uploaded_file(file_path("kerb.jpg"), "not/known", "local_filename.abc")
+    assert_equal "local_filename.abc", File.basename(e.image)
+    assert_equal "local_filename.abc", e["image"]
+  end
+
+  def test_extension_corrected
+    e = Entry.new
+    e.image = uploaded_file(file_path("kerb.jpg"), "image/jpeg", "local_filename.jpeg")
+    assert_equal "local_filename.jpg", File.basename(e.image)
+    assert_equal "local_filename.jpg", e["image"]
+  end
+
+  def test_double_extension
+    e = Entry.new
+    e.image = uploaded_file(file_path("kerb.jpg"), "application/x-tgz", "local_filename.tar.gz")
+    assert_equal "local_filename.tar.gz", File.basename(e.image)
+    assert_equal "local_filename.tar.gz", e["image"]
+  end
+
+  FILE_UTILITY = "/usr/bin/file"
+
+  def test_get_content_type_with_file
+    Entry.file_column :image, :file_exec => FILE_UTILITY
+
+    # run this test only if the machine we are running on
+    # has the file utility installed
+    if File.executable?(FILE_UTILITY)
+      e = Entry.new
+      file = FileColumn::TempUploadedFile.new(e, "image")
+      file.instance_variable_set :@dir, File.dirname(file_path("kerb.jpg"))
+      file.instance_variable_set :@filename, File.basename(file_path("kerb.jpg"))
+      
+      assert_equal "image/jpeg", file.get_content_type
+    else
+      puts "Warning: Skipping test_get_content_type_with_file test as '#{options[:file_exec]}' does not exist"
+    end
+  end
+
+  def test_fix_extension_with_file
+    Entry.file_column :image, :file_exec => FILE_UTILITY
+
+    # run this test only if the machine we are running on
+    # has the file utility installed
+    if File.executable?(FILE_UTILITY)
+      e = Entry.new(:image => uploaded_file(file_path("skanthak.png"), "", "skanthak.jpg"))
+      
+      assert_equal "skanthak.png", File.basename(e.image)
+    else
+      puts "Warning: Skipping test_fix_extension_with_file test as '#{options[:file_exec]}' does not exist"
+    end
+  end
+
+  def test_do_not_fix_file_extensions
+    Entry.file_column :image, :fix_file_extensions => false
+
+    e = Entry.new(:image => uploaded_file(file_path("kerb.jpg"), "image/jpeg", "kerb"))
+
+    assert_equal "kerb", File.basename(e.image)
+  end
+
+  def test_correct_extension
+    e = Entry.new
+    file = FileColumn::TempUploadedFile.new(e, "image")
+    
+    assert_equal "filename.jpg", file.correct_extension("filename.jpeg","jpg")
+    assert_equal "filename.tar.gz", file.correct_extension("filename.jpg","tar.gz")
+    assert_equal "filename.jpg", file.correct_extension("filename.tar.gz","jpg")
+    assert_equal "Protokoll_01.09.2005.doc", file.correct_extension("Protokoll_01.09.2005","doc")
+    assert_equal "strange.filenames.exist.jpg", file.correct_extension("strange.filenames.exist","jpg")
+    assert_equal "another.strange.one.jpg", file.correct_extension("another.strange.one.png","jpg")
+  end
+
+  def test_assign_with_save
+    e = Entry.new
+    e.image = uploaded_file(file_path("kerb.jpg"), "image/jpeg", "kerb.jpg")
+    tmp_file_path = e.image
+    assert e.save
+    assert File.exists?(e.image)
+    assert FileUtils.identical?(e.image, file_path("kerb.jpg"))
+    assert_equal "#{e.id}/kerb.jpg", e.image_relative_path
+    assert !File.exists?(tmp_file_path), "temporary file '#{tmp_file_path}' not removed"
+    assert !File.exists?(File.dirname(tmp_file_path)), "temporary directory '#{File.dirname(tmp_file_path)}' not removed"
+    
+    local_path = e.image
+    e = Entry.find(e.id)
+    assert_equal local_path, e.image
+  end
+
+  def test_dir_methods
+    e = Entry.new
+    e.image = uploaded_file(file_path("kerb.jpg"), "image/jpeg", "kerb.jpg")
+    e.save
+    
+    assert_equal_paths File.join(RAILS_ROOT, "public", "entry", "image", e.id.to_s), e.image_dir
+    assert_equal File.join(e.id.to_s), e.image_relative_dir
+  end
+
+  def test_store_dir_callback
+    Entry.file_column :image, {:store_dir => :my_store_dir}
+    e = Entry.new
+
+    e.image = uploaded_file(file_path("kerb.jpg"), "image/jpeg", "kerb.jpg")    
+    assert e.save
+    
+    assert_equal_paths File.join(RAILS_ROOT, "public", "my_store_dir", e.id), e.image_dir   
+  end
+
+  def test_tmp_dir_with_store_dir_callback
+    Entry.file_column :image, {:store_dir => :my_store_dir}
+    e = Entry.new
+    e.image = upload(f("kerb.jpg"))
+    
+    assert_equal File.expand_path(File.join(RAILS_ROOT, "public", "my_store_dir", "tmp")), File.expand_path(File.join(e.image_dir,".."))
+  end
+
+  def test_invalid_store_dir_callback
+    Entry.file_column :image, {:store_dir => :my_store_dir_doesnt_exit}    
+    e = Entry.new
+    assert_raise(ArgumentError) {
+      e.image = uploaded_file(file_path("kerb.jpg"), "image/jpeg", "kerb.jpg")
+      e.save
+    }
+  end
+
+  def test_subdir_parameter
+    e = Entry.new
+    assert_nil e.image("thumb")
+    assert_nil e.image_relative_path("thumb")
+    assert_nil e.image(nil)
+
+    e.image = uploaded_file(file_path("kerb.jpg"), "image/jpeg", "kerb.jpg")
+    
+    assert_equal "kerb.jpg", File.basename(e.image("thumb"))
+    assert_equal "kerb.jpg", File.basename(e.image_relative_path("thumb"))
+
+    assert_equal File.join(e.image_dir,"thumb","kerb.jpg"), e.image("thumb")
+    assert_match %r{/thumb/kerb\.jpg$}, e.image_relative_path("thumb") 
+
+    assert_equal e.image, e.image(nil)
+    assert_equal e.image_relative_path, e.image_relative_path(nil)
+  end
+
+  def test_cleanup_after_destroy
+    e = Entry.new("image" => uploaded_file(file_path("kerb.jpg"), "image/jpeg", "kerb.jpg"))
+    assert e.save
+    local_path = e.image
+    assert File.exists?(local_path)
+    assert e.destroy
+    assert !File.exists?(local_path), "'#{local_path}' still exists although entry was destroyed"
+    assert !File.exists?(File.dirname(local_path))
+  end
+  
+  def test_keep_tmp_image
+    e = Entry.new("image" => uploaded_file(file_path("kerb.jpg"), "image/jpeg", "kerb.jpg"))
+    e.validation_should_fail = true
+    assert !e.save, "e should not save due to validation errors"
+    assert File.exists?(local_path = e.image)
+    image_temp = e.image_temp
+    e = Entry.new("image_temp" => image_temp)
+    assert_equal local_path, e.image
+    assert e.save
+    assert FileUtils.identical?(e.image, file_path("kerb.jpg"))
+  end
+  
+  def test_keep_tmp_image_with_existing_image
+    e = Entry.new("image" =>uploaded_file(file_path("kerb.jpg"), "image/jpeg", "kerb.jpg"))
+    assert e.save
+    assert File.exists?(local_path = e.image)
+    e = Entry.find(e.id)
+    e.image = uploaded_file(file_path("skanthak.png"), "image/png", "skanthak.png")
+    e.validation_should_fail = true
+    assert !e.save
+    temp_path = e.image_temp
+    e = Entry.find(e.id)
+    e.image_temp = temp_path
+    assert e.save
+    
+    assert FileUtils.identical?(e.image, file_path("skanthak.png"))
+    assert !File.exists?(local_path), "old image has not been deleted"
+  end
+  
+  def test_replace_tmp_image_temp_first
+    do_test_replace_tmp_image([:image_temp, :image])
+  end
+  
+  def test_replace_tmp_image_temp_last
+    do_test_replace_tmp_image([:image, :image_temp])
+  end
+  
+  def do_test_replace_tmp_image(order)
+    e = Entry.new("image" => uploaded_file(file_path("kerb.jpg"), "image/jpeg", "kerb.jpg"))
+    e.validation_should_fail = true
+    assert !e.save
+    image_temp = e.image_temp
+    temp_path = e.image
+    new_img = uploaded_file(file_path("skanthak.png"), "image/png", "skanthak.png")
+    e = Entry.new
+    for method in order
+      case method
+      when :image_temp then e.image_temp = image_temp
+      when :image then e.image = new_img
+      end
+    end
+    assert e.save
+    assert FileUtils.identical?(e.image, file_path("skanthak.png")), "'#{e.image}' is not the expected 'skanthak.png'"
+    assert !File.exists?(temp_path), "temporary file '#{temp_path}' is not cleaned up"
+    assert !File.exists?(File.dirname(temp_path)), "temporary directory not cleaned up"
+    assert e.image_just_uploaded?
+  end
+  
+  def test_replace_image_on_saved_object
+    e = Entry.new("image" => uploaded_file(file_path("kerb.jpg"), "image/jpeg", "kerb.jpg"))
+    assert e.save
+    old_file = e.image
+    e = Entry.find(e.id)
+    e.image = uploaded_file(file_path("skanthak.png"), "image/png", "skanthak.png")
+    assert e.save
+    assert FileUtils.identical?(file_path("skanthak.png"), e.image)
+    assert old_file != e.image
+    assert !File.exists?(old_file), "'#{old_file}' has not been cleaned up"
+  end
+  
+  def test_edit_without_touching_image
+    e = Entry.new("image" => uploaded_file(file_path("kerb.jpg"), "image/jpeg", "kerb.jpg"))
+    assert e.save
+    e = Entry.find(e.id)
+    assert e.save
+    assert FileUtils.identical?(file_path("kerb.jpg"), e.image)
+  end
+  
+  def test_save_without_image
+    e = Entry.new
+    assert e.save
+    e.reload
+    assert_nil e.image
+  end
+  
+  def test_delete_saved_image
+    e = Entry.new("image" => uploaded_file(file_path("kerb.jpg"), "image/jpeg", "kerb.jpg"))
+    assert e.save
+    local_path = e.image
+    e.image = nil
+    assert_nil e.image
+    assert File.exists?(local_path), "file '#{local_path}' should not be deleted until transaction is saved"
+    assert e.save
+    assert_nil e.image
+    assert !File.exists?(local_path)
+    e.reload
+    assert e["image"].blank?
+    e = Entry.find(e.id)
+    assert_nil e.image
+  end
+  
+  def test_delete_tmp_image
+    e = Entry.new("image" => uploaded_file(file_path("kerb.jpg"), "image/jpeg", "kerb.jpg"))
+    local_path = e.image
+    e.image = nil
+    assert_nil e.image
+    assert e["image"].blank?
+    assert !File.exists?(local_path)
+  end
+  
+  def test_delete_nonexistant_image
+    e = Entry.new
+    e.image = nil
+    assert e.save
+    assert_nil e.image
+  end
+
+  def test_delete_image_on_non_null_column
+    e = Entry.new("file" => upload(f("skanthak.png")))
+    assert e.save
+
+    local_path = e.file
+    assert File.exists?(local_path)
+    e.file = nil
+    assert e.save
+    assert !File.exists?(local_path)
+  end
+
+  def test_ie_filename
+    e = Entry.new("image" => uploaded_file(file_path("kerb.jpg"), "image/jpeg", 'c:\images\kerb.jpg'))
+    assert e.image_relative_path =~ /^tmp\/[\d\.]+\/kerb\.jpg$/, "relative path '#{e.image_relative_path}' was not as expected"
+    assert File.exists?(e.image)
+  end
+  
+  def test_just_uploaded?
+    e = Entry.new("image" => uploaded_file(file_path("kerb.jpg"), "image/jpeg", 'c:\images\kerb.jpg'))
+    assert e.image_just_uploaded?
+    assert e.save
+    assert e.image_just_uploaded?
+    
+    e = Entry.new("image" => uploaded_file(file_path("kerb.jpg"), "image/jpeg", 'kerb.jpg'))
+    temp_path = e.image_temp
+    e = Entry.new("image_temp" => temp_path)
+    assert !e.image_just_uploaded?
+    assert e.save
+    assert !e.image_just_uploaded?
+  end
+  
+  def test_empty_tmp
+    e = Entry.new
+    e.image_temp = ""
+    assert_nil e.image
+  end
+  
+  def test_empty_tmp_with_image
+    e = Entry.new
+    e.image_temp = ""
+    e.image = uploaded_file(file_path("kerb.jpg"), "image/jpeg", 'c:\images\kerb.jpg')
+    local_path = e.image
+    assert File.exists?(local_path)
+    e.image_temp = ""
+    assert local_path, e.image
+  end
+  
+  def test_empty_filename
+    e = Entry.new
+    assert_equal "", e["file"]
+    assert_nil e.file
+    assert_nil e["image"]
+    assert_nil e.image
+  end
+  
+  def test_with_two_file_columns
+    e = Entry.new
+    e.image = uploaded_file(file_path("kerb.jpg"), "image/jpeg", "kerb.jpg")
+    e.file = uploaded_file(file_path("skanthak.png"), "image/png", "skanthak.png")
+    assert e.save
+    assert_match %{/entry/image/}, e.image
+    assert_match %{/entry/file/}, e.file
+    assert FileUtils.identical?(e.image, file_path("kerb.jpg"))
+    assert FileUtils.identical?(e.file, file_path("skanthak.png"))
+  end
+  
+  def test_with_two_models
+    e = Entry.new(:image => uploaded_file(file_path("kerb.jpg"), "image/jpeg", "kerb.jpg"))
+    m = Movie.new(:movie => uploaded_file(file_path("skanthak.png"), "image/png", "skanthak.png"))
+    assert e.save
+    assert m.save
+    assert_match %{/entry/image/}, e.image
+    assert_match %{/movie/movie/}, m.movie
+    assert FileUtils.identical?(e.image, file_path("kerb.jpg"))
+    assert FileUtils.identical?(m.movie, file_path("skanthak.png"))
+  end
+
+  def test_no_file_uploaded
+    e = Entry.new
+    assert_nothing_raised { e.image =
+        uploaded_file(nil, "application/octet-stream", "", :stringio) }
+    assert_equal nil, e.image
+  end
+
+  # when safari submits a form where no file has been
+  # selected, it does not transmit a content-type and
+  # the result is an empty string ""
+  def test_no_file_uploaded_with_safari
+    e = Entry.new
+    assert_nothing_raised { e.image = "" }
+    assert_equal nil, e.image
+  end
+
+  def test_detect_wrong_encoding
+    e = Entry.new
+    assert_raise(TypeError) { e.image ="img42.jpg" }
+  end
+
+  def test_serializable_before_save
+    e = Entry.new
+    e.image = uploaded_file(file_path("skanthak.png"), "image/png", "skanthak.png")
+    assert_nothing_raised { 
+      flash = Marshal.dump(e) 
+      e = Marshal.load(flash)
+    }
+    assert File.exists?(e.image)
+  end
+
+  def test_should_call_after_upload_on_new_upload
+    Entry.file_column :image, :after_upload => [:after_assign]
+    e = Entry.new
+    e.image = upload(f("skanthak.png"))
+    assert e.after_assign_called?
+  end
+
+  def test_should_call_user_after_save_on_save
+    e = Entry.new(:image => upload(f("skanthak.png")))
+    assert e.save
+    
+    assert_kind_of FileColumn::PermanentUploadedFile, e.send(:image_state)
+    assert e.after_save_called?
+  end
+
+
+  def test_assign_standard_files
+    e = Entry.new
+    e.image = File.new(file_path('skanthak.png'))
+    
+    assert_equal 'skanthak.png', File.basename(e.image)
+    assert FileUtils.identical?(file_path('skanthak.png'), e.image)
+    
+    assert e.save
+  end
+
+
+  def test_validates_filesize
+    Entry.validates_filesize_of :image, :in => 50.kilobytes..100.kilobytes
+
+    e = Entry.new(:image => upload(f("kerb.jpg")))
+    assert e.save
+
+    e.image = upload(f("skanthak.png"))
+    assert !e.save
+    assert e.errors.invalid?("image")
+  end
+
+  def test_validates_file_format_simple
+    e = Entry.new(:image => upload(f("skanthak.png")))
+    assert e.save
+    
+    Entry.validates_file_format_of :image, :in => ["jpg"]
+
+    e.image = upload(f("kerb.jpg"))
+    assert e.save
+
+    e.image = upload(f("mysql.sql"))
+    assert !e.save
+    assert e.errors.invalid?("image")
+    
+  end
+
+  def test_validates_image_size
+    Entry.validates_image_size :image, :min => "640x480"
+    
+    e = Entry.new(:image => upload(f("kerb.jpg")))
+    assert e.save
+
+    e = Entry.new(:image => upload(f("skanthak.png")))
+    assert !e.save
+    assert e.errors.invalid?("image")
+  end
+
+  def do_permission_test(uploaded_file, permissions=0641)
+    Entry.file_column :image, :permissions => permissions
+    
+    e = Entry.new(:image => uploaded_file)
+    assert e.save
+
+    assert_equal permissions, (File.stat(e.image).mode & 0777)
+  end
+
+  def test_permissions_with_small_file
+    do_permission_test upload(f("skanthak.png"), :guess, :stringio)
+  end
+
+  def test_permission_with_big_file
+    do_permission_test upload(f("kerb.jpg"))
+  end
+
+  def test_permission_that_overrides_umask
+    do_permission_test upload(f("skanthak.png"), :guess, :stringio), 0666
+    do_permission_test upload(f("kerb.jpg")), 0666
+  end
+
+  def test_access_with_empty_id
+    # an empty id might happen after a clone or through some other
+    # strange event. Since we would create a path that contains nothing
+    # where the id would have been, we should fail fast with an exception
+    # in this case
+    
+    e = Entry.new(:image => upload(f("skanthak.png")))
+    assert e.save
+    id = e.id
+
+    e = Entry.find(id)
+    
+    e["id"] = ""
+    assert_raise(RuntimeError) { e.image }
+    
+    e = Entry.find(id)
+    e["id"] = nil
+    assert_raise(RuntimeError) { e.image }
+  end
+end
+
+# Tests for moving temp dir to permanent dir
+class FileColumnMoveTest < Test::Unit::TestCase
+  
+  def setup
+    # we define the file_columns here so that we can change
+    # settings easily in a single test
+
+    Entry.file_column :image
+    
+  end
+  
+  def teardown
+    FileUtils.rm_rf File.dirname(__FILE__)+"/public/entry/"
+  end
+
+  def test_should_move_additional_files_from_tmp
+    e = Entry.new
+    e.image = uploaded_file(file_path("skanthak.png"), "image/png", "skanthak.png")
+    FileUtils.cp file_path("kerb.jpg"), File.dirname(e.image)
+    assert e.save
+    dir = File.dirname(e.image)
+    assert File.exists?(File.join(dir, "skanthak.png"))
+    assert File.exists?(File.join(dir, "kerb.jpg"))
+  end
+
+  def test_should_move_direcotries_on_save
+    e = Entry.new(:image => upload(f("skanthak.png")))
+    
+    FileUtils.mkdir( e.image_dir+"/foo" )
+    FileUtils.cp file_path("kerb.jpg"), e.image_dir+"/foo/kerb.jpg"
+    
+    assert e.save
+
+    assert File.exists?(e.image)
+    assert File.exists?(File.dirname(e.image)+"/foo/kerb.jpg")
+  end
+
+  def test_should_overwrite_dirs_with_files_on_reupload
+    e = Entry.new(:image => upload(f("skanthak.png")))
+
+    FileUtils.mkdir( e.image_dir+"/kerb.jpg")
+    FileUtils.cp file_path("kerb.jpg"), e.image_dir+"/kerb.jpg/"
+    assert e.save
+
+    e.image = upload(f("kerb.jpg"))
+    assert e.save
+
+    assert File.file?(e.image_dir+"/kerb.jpg")
+  end
+
+  def test_should_overwrite_files_with_dirs_on_reupload
+    e = Entry.new(:image => upload(f("skanthak.png")))
+
+    assert e.save
+    assert File.file?(e.image_dir+"/skanthak.png")
+
+    e.image = upload(f("kerb.jpg"))
+    FileUtils.mkdir(e.image_dir+"/skanthak.png")
+    
+    assert e.save
+    assert File.file?(e.image_dir+"/kerb.jpg")
+    assert !File.file?(e.image_dir+"/skanthak.png")
+    assert File.directory?(e.image_dir+"/skanthak.png")
+  end
+
+end
+
diff --git a/vendor/plugins/file_column/test/fixtures/entry.rb b/vendor/plugins/file_column/test/fixtures/entry.rb
new file mode 100644 (file)
index 0000000..b9f7c95
--- /dev/null
@@ -0,0 +1,32 @@
+class Entry < ActiveRecord::Base
+  attr_accessor :validation_should_fail
+
+  def validate
+    errors.add("image","some stupid error") if @validation_should_fail
+  end
+  
+  def after_assign
+    @after_assign_called = true
+  end
+  
+  def after_assign_called?
+    @after_assign_called
+  end
+  
+  def after_save
+    @after_save_called = true
+  end
+
+  def after_save_called?
+    @after_save_called
+  end
+
+  def my_store_dir
+    # not really dynamic but at least it could be...
+    "my_store_dir"
+  end
+
+  def load_image_with_rmagick(path)
+    Magick::Image::read(path).first
+  end
+end
diff --git a/vendor/plugins/file_column/test/fixtures/invalid-image.jpg b/vendor/plugins/file_column/test/fixtures/invalid-image.jpg
new file mode 100644 (file)
index 0000000..bd4933b
--- /dev/null
@@ -0,0 +1 @@
+this is certainly not a JPEG image
diff --git a/vendor/plugins/file_column/test/fixtures/kerb.jpg b/vendor/plugins/file_column/test/fixtures/kerb.jpg
new file mode 100644 (file)
index 0000000..083138e
Binary files /dev/null and b/vendor/plugins/file_column/test/fixtures/kerb.jpg differ
diff --git a/vendor/plugins/file_column/test/fixtures/mysql.sql b/vendor/plugins/file_column/test/fixtures/mysql.sql
new file mode 100644 (file)
index 0000000..55143f2
--- /dev/null
@@ -0,0 +1,25 @@
+-- MySQL dump 9.11
+--
+-- Host: localhost    Database: file_column_test
+-- ------------------------------------------------------
+-- Server version      4.0.24
+
+--
+-- Table structure for table `entries`
+--
+
+DROP TABLE IF EXISTS entries;
+CREATE TABLE entries (
+  id int(11) NOT NULL auto_increment,
+  image varchar(200) default NULL,
+  file varchar(200) NOT NULL,
+  PRIMARY KEY  (id)
+) TYPE=MyISAM;
+
+DROP TABLE IF EXISTS movies;
+CREATE TABLE movies (
+  id int(11) NOT NULL auto_increment,
+  movie varchar(200) default NULL,
+  PRIMARY KEY  (id)
+) TYPE=MyISAM;
+
diff --git a/vendor/plugins/file_column/test/fixtures/schema.rb b/vendor/plugins/file_column/test/fixtures/schema.rb
new file mode 100644 (file)
index 0000000..49b5ddb
--- /dev/null
@@ -0,0 +1,10 @@
+ActiveRecord::Schema.define do
+  create_table :entries, :force => true do |t|
+    t.column :image, :string, :null => true
+    t.column :file, :string, :null => false
+  end
+  
+  create_table :movies, :force => true do |t|
+    t.column :movie, :string
+  end
+end
diff --git a/vendor/plugins/file_column/test/fixtures/skanthak.png b/vendor/plugins/file_column/test/fixtures/skanthak.png
new file mode 100644 (file)
index 0000000..7415eb6
Binary files /dev/null and b/vendor/plugins/file_column/test/fixtures/skanthak.png differ
diff --git a/vendor/plugins/file_column/test/magick_test.rb b/vendor/plugins/file_column/test/magick_test.rb
new file mode 100644 (file)
index 0000000..0362719
--- /dev/null
@@ -0,0 +1,380 @@
+require File.dirname(__FILE__) + '/abstract_unit'
+require 'RMagick'
+require File.dirname(__FILE__) + '/fixtures/entry'
+
+
+class AbstractRMagickTest < Test::Unit::TestCase
+  def teardown
+    FileUtils.rm_rf File.dirname(__FILE__)+"/public/entry/"
+  end
+
+  def test_truth
+    assert true
+  end
+
+  private
+
+  def read_image(path)
+    Magick::Image::read(path).first
+  end
+
+  def assert_max_image_size(img, s)
+    assert img.columns <= s, "img has #{img.columns} columns, expected: #{s}"
+    assert img.rows <= s, "img has #{img.rows} rows, expected: #{s}"
+    assert_equal s, [img.columns, img.rows].max
+  end
+end
+
+class RMagickSimpleTest < AbstractRMagickTest
+  def setup
+    Entry.file_column :image, :magick => { :geometry => "100x100" }
+  end
+
+  def test_simple_resize_without_save
+    e = Entry.new
+    e.image = upload(f("kerb.jpg"))
+    
+    img = read_image(e.image)
+    assert_max_image_size img, 100
+  end
+
+  def test_simple_resize_with_save
+    e = Entry.new
+    e.image = upload(f("kerb.jpg"))
+    assert e.save
+    e.reload
+    
+    img = read_image(e.image)
+    assert_max_image_size img, 100
+  end
+
+  def test_resize_on_saved_image
+    Entry.file_column :image, :magick => { :geometry => "100x100" }
+    
+    e = Entry.new
+    e.image = upload(f("skanthak.png"))
+    assert e.save
+    e.reload
+    old_path = e.image
+    
+    e.image = upload(f("kerb.jpg"))
+    assert e.save
+    assert "kerb.jpg", File.basename(e.image)
+    assert !File.exists?(old_path), "old image '#{old_path}' still exists"
+
+    img = read_image(e.image)
+    assert_max_image_size img, 100
+  end
+
+  def test_invalid_image
+    e = Entry.new
+    assert_nothing_raised { e.image = upload(f("invalid-image.jpg")) }
+    assert !e.valid?
+  end
+
+  def test_serializable
+    e = Entry.new
+    e.image = upload(f("skanthak.png"))
+    assert_nothing_raised {
+      flash = Marshal.dump(e)
+      e = Marshal.load(flash)
+    }
+    assert File.exists?(e.image)
+  end
+
+  def test_imagemagick_still_usable
+    e = Entry.new
+    assert_nothing_raised {
+      img = e.load_image_with_rmagick(file_path("skanthak.png"))
+      assert img.kind_of?(Magick::Image)
+    }
+  end
+end
+
+class RMagickRequiresImageTest < AbstractRMagickTest
+  def setup
+    Entry.file_column :image, :magick => { 
+      :size => "100x100>",
+      :image_required => false,
+      :versions => {
+        :thumb => "80x80>",
+        :large => {:size => "200x200>", :lazy => true}
+      }
+    }
+  end
+
+  def test_image_required_with_image
+    e = Entry.new(:image => upload(f("skanthak.png")))
+    assert_max_image_size read_image(e.image), 100
+    assert e.valid?
+  end
+
+  def test_image_required_with_invalid_image
+    e = Entry.new(:image => upload(f("invalid-image.jpg")))
+    assert e.valid?, "did not ignore invalid image"
+    assert FileUtils.identical?(e.image, f("invalid-image.jpg")), "uploaded file has not been left alone"
+  end
+
+  def test_versions_with_invalid_image
+    e = Entry.new(:image => upload(f("invalid-image.jpg")))
+    assert e.valid?
+
+    image_state = e.send(:image_state)
+    assert_nil image_state.create_magick_version_if_needed(:thumb)
+    assert_nil image_state.create_magick_version_if_needed(:large)
+    assert_nil image_state.create_magick_version_if_needed("300x300>")
+  end
+end
+
+class RMagickCustomAttributesTest < AbstractRMagickTest
+  def assert_image_property(img, property, value, text = nil)
+    assert File.exists?(img), "the image does not exist"
+    assert_equal value, read_image(img).send(property), text
+  end
+
+  def test_simple_attributes
+    Entry.file_column :image, :magick => { :attributes => { :quality => 20 } }
+    e = Entry.new("image" => upload(f("kerb.jpg")))
+    assert_image_property e.image, :quality, 20, "the quality was not set"
+  end
+
+  def test_version_attributes
+    Entry.file_column :image, :magick => {
+      :versions => {
+        :thumb => { :attributes => { :quality => 20 } }
+      }
+    }
+    e = Entry.new("image" => upload(f("kerb.jpg")))
+    assert_image_property e.image("thumb"), :quality, 20, "the quality was not set"
+  end
+  
+  def test_lazy_attributes
+    Entry.file_column :image, :magick => {
+      :versions => {
+        :thumb => { :attributes => { :quality => 20 }, :lazy => true }
+      }
+    }
+    e = Entry.new("image" => upload(f("kerb.jpg")))
+    e.send(:image_state).create_magick_version_if_needed(:thumb)
+    assert_image_property e.image("thumb"), :quality, 20, "the quality was not set"
+  end
+end
+
+class RMagickVersionsTest < AbstractRMagickTest
+  def setup
+    Entry.file_column :image, :magick => {:geometry => "200x200",
+      :versions => {
+        :thumb => "50x50",
+        :medium => {:geometry => "100x100", :name => "100_100"},
+        :large => {:geometry => "150x150", :lazy => true}
+      }
+    }
+  end
+
+
+  def test_should_create_thumb
+    e = Entry.new("image" => upload(f("skanthak.png")))
+    
+    assert File.exists?(e.image("thumb")), "thumb-nail not created"
+    
+    assert_max_image_size read_image(e.image("thumb")), 50
+  end
+
+  def test_version_name_can_be_different_from_key
+    e = Entry.new("image" => upload(f("skanthak.png")))
+    
+    assert File.exists?(e.image("100_100"))
+    assert !File.exists?(e.image("medium"))
+  end
+
+  def test_should_not_create_lazy_versions
+    e = Entry.new("image" => upload(f("skanthak.png")))
+    assert !File.exists?(e.image("large")), "lazy versions should not be created unless needed"
+  end
+
+  def test_should_create_lazy_version_on_demand
+    e = Entry.new("image" => upload(f("skanthak.png")))
+    
+    e.send(:image_state).create_magick_version_if_needed(:large)
+    
+    assert File.exists?(e.image("large")), "lazy version should be created on demand"
+    
+    assert_max_image_size read_image(e.image("large")), 150
+  end
+
+  def test_generated_name_should_not_change
+    e = Entry.new("image" => upload(f("skanthak.png")))
+    
+    name1 = e.send(:image_state).create_magick_version_if_needed("50x50")
+    name2 = e.send(:image_state).create_magick_version_if_needed("50x50")
+    name3 = e.send(:image_state).create_magick_version_if_needed(:geometry => "50x50")
+    assert_equal name1, name2, "hash value has changed"
+    assert_equal name1, name3, "hash value has changed"
+  end
+
+  def test_should_create_version_with_string
+    e = Entry.new("image" => upload(f("skanthak.png")))
+    
+    name = e.send(:image_state).create_magick_version_if_needed("32x32")
+    
+    assert File.exists?(e.image(name))
+
+    assert_max_image_size read_image(e.image(name)), 32
+  end
+
+  def test_should_create_safe_auto_id
+    e = Entry.new("image" => upload(f("skanthak.png")))
+
+    name = e.send(:image_state).create_magick_version_if_needed("32x32")
+
+    assert_match /^[a-zA-Z0-9]+$/, name
+  end
+end
+
+class RMagickCroppingTest < AbstractRMagickTest
+  def setup
+    Entry.file_column :image, :magick => {:geometry => "200x200",
+      :versions => {
+        :thumb => {:crop => "1:1", :geometry => "50x50"}
+      }
+    }
+  end
+  
+  def test_should_crop_image_on_upload
+    e = Entry.new("image" => upload(f("skanthak.png")))
+    
+    img = read_image(e.image("thumb"))
+    
+    assert_equal 50, img.rows 
+    assert_equal 50, img.columns
+  end
+    
+end
+
+class UrlForImageColumnTest < AbstractRMagickTest
+  include FileColumnHelper
+
+  def setup
+    Entry.file_column :image, :magick => {
+      :versions => {:thumb => "50x50"} 
+    }
+    @request = RequestMock.new
+  end
+    
+  def test_should_use_version_on_symbol_option
+    e = Entry.new(:image => upload(f("skanthak.png")))
+    
+    url = url_for_image_column(e, "image", :thumb)
+    assert_match %r{^/entry/image/tmp/.+/thumb/skanthak.png$}, url
+  end
+
+  def test_should_use_string_as_size
+    e = Entry.new(:image => upload(f("skanthak.png")))
+
+    url = url_for_image_column(e, "image", "50x50")
+    
+    assert_match %r{^/entry/image/tmp/.+/.+/skanthak.png$}, url
+    
+    url =~ /\/([^\/]+)\/skanthak.png$/
+    dirname = $1
+    
+    assert_max_image_size read_image(e.image(dirname)), 50
+  end
+
+  def test_should_accept_version_hash
+    e = Entry.new(:image => upload(f("skanthak.png")))
+
+    url = url_for_image_column(e, "image", :size => "50x50", :crop => "1:1", :name => "small")
+
+    assert_match %r{^/entry/image/tmp/.+/small/skanthak.png$}, url
+
+    img = read_image(e.image("small"))
+    assert_equal 50, img.rows
+    assert_equal 50, img.columns
+  end
+end
+
+class RMagickPermissionsTest < AbstractRMagickTest
+  def setup
+    Entry.file_column :image, :magick => {:geometry => "200x200",
+      :versions => {
+        :thumb => {:crop => "1:1", :geometry => "50x50"}
+      }
+    }, :permissions => 0616
+  end
+  
+  def check_permissions(e)
+    assert_equal 0616, (File.stat(e.image).mode & 0777)
+    assert_equal 0616, (File.stat(e.image("thumb")).mode & 0777)
+  end
+
+  def test_permissions_with_rmagick
+    e = Entry.new(:image => upload(f("skanthak.png")))
+    
+    check_permissions e
+
+    assert e.save
+
+    check_permissions e
+  end
+end
+
+class Entry 
+  def transform_grey(img)
+    img.quantize(256, Magick::GRAYColorspace)
+  end
+end
+
+class RMagickTransformationTest < AbstractRMagickTest
+  def assert_transformed(image)
+    assert File.exists?(image), "the image does not exist"
+    assert 256 > read_image(image).number_colors, "the number of colors was not changed"
+  end
+  
+  def test_simple_transformation
+    Entry.file_column :image, :magick => { :transformation => Proc.new { |image| image.quantize(256, Magick::GRAYColorspace) } }
+    e = Entry.new("image" => upload(f("skanthak.png")))
+    assert_transformed(e.image)
+  end
+  
+  def test_simple_version_transformation
+    Entry.file_column :image, :magick => {
+      :versions => { :thumb => Proc.new { |image| image.quantize(256, Magick::GRAYColorspace) } }
+    }
+    e = Entry.new("image" => upload(f("skanthak.png")))
+    assert_transformed(e.image("thumb"))
+  end
+  
+  def test_complex_version_transformation
+    Entry.file_column :image, :magick => {
+      :versions => {
+        :thumb => { :transformation => Proc.new { |image| image.quantize(256, Magick::GRAYColorspace) } }
+      }
+    }
+    e = Entry.new("image" => upload(f("skanthak.png")))
+    assert_transformed(e.image("thumb"))
+  end
+  
+  def test_lazy_transformation
+    Entry.file_column :image, :magick => {
+      :versions => {
+        :thumb => { :transformation => Proc.new { |image| image.quantize(256, Magick::GRAYColorspace) }, :lazy => true }
+      }
+    }
+    e = Entry.new("image" => upload(f("skanthak.png")))
+    e.send(:image_state).create_magick_version_if_needed(:thumb)
+    assert_transformed(e.image("thumb"))
+  end
+
+  def test_simple_callback_transformation
+    Entry.file_column :image, :magick => :transform_grey
+    e = Entry.new(:image => upload(f("skanthak.png")))
+    assert_transformed(e.image)
+  end
+
+  def test_complex_callback_transformation
+    Entry.file_column :image, :magick => { :transformation => :transform_grey }
+    e = Entry.new(:image => upload(f("skanthak.png")))
+    assert_transformed(e.image)
+  end
+end
diff --git a/vendor/plugins/file_column/test/magick_view_only_test.rb b/vendor/plugins/file_column/test/magick_view_only_test.rb
new file mode 100644 (file)
index 0000000..a7daa61
--- /dev/null
@@ -0,0 +1,21 @@
+require File.dirname(__FILE__) + '/abstract_unit'
+require File.dirname(__FILE__) + '/fixtures/entry'
+
+class RMagickViewOnlyTest < Test::Unit::TestCase
+  include FileColumnHelper
+
+  def setup
+    Entry.file_column :image
+    @request = RequestMock.new
+  end
+
+  def teardown
+    FileUtils.rm_rf File.dirname(__FILE__)+"/public/entry/"
+  end
+
+  def test_url_for_image_column_without_model_versions
+    e = Entry.new(:image => upload(f("skanthak.png")))
+    
+    assert_nothing_raised { url_for_image_column e, "image", "50x50" }
+  end
+end
diff --git a/vendor/plugins/sql_session_store/LICENSE b/vendor/plugins/sql_session_store/LICENSE
new file mode 100644 (file)
index 0000000..5cb5c7b
--- /dev/null
@@ -0,0 +1,20 @@
+Copyright (c) 2006-2008 Dr.-Ing. Stefan Kaes
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/vendor/plugins/sql_session_store/README b/vendor/plugins/sql_session_store/README
new file mode 100755 (executable)
index 0000000..07b0833
--- /dev/null
@@ -0,0 +1,60 @@
+== SqlSessionStore
+
+See http://railsexpress.de/blog/articles/2005/12/19/roll-your-own-sql-session-store
+
+Only Mysql, Postgres and Oracle are currently supported (others work,
+but you won't see much performance improvement).
+
+== Step 1
+
+If you have generated your sessions table using rake db:sessions:create, go to Step 2
+
+If you're using an old version of sql_session_store, run
+    script/generate sql_session_store DB
+where DB is mysql, postgresql or oracle
+
+Then run
+    rake migrate
+or
+    rake db:migrate
+for edge rails.
+
+== Step 2
+
+Add the code below after the initializer config section:
+
+    ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS.
+      update(:database_manager => SqlSessionStore)
+
+Finally, depending on your database type, add
+
+    SqlSessionStore.session_class = MysqlSession
+or
+
+    SqlSessionStore.session_class = PostgresqlSession
+or
+    SqlSessionStore.session_class = OracleSession
+
+after the initializer section in environment.rb
+
+== Step 3 (optional)
+
+If you want to use a database separate from your default one to store
+your sessions, specify a configuration in your database.yml file (say
+sessions), and establish the connection on SqlSession in
+environment.rb:
+
+   SqlSession.establish_connection :sessions
+
+
+== IMPORTANT NOTES
+
+1. The class name SQLSessionStore has changed to SqlSessionStore to
+   let Rails work its autoload magic.
+
+2. You will need the binary drivers for Mysql or Postgresql.
+   These have been verified to work:
+
+   * ruby-postgres (0.7.1.2005.12.21) with postgreql 8.1
+   * ruby-mysql 2.7.1 with Mysql 4.1
+   * ruby-mysql 2.7.2 with Mysql 5.0
diff --git a/vendor/plugins/sql_session_store/Rakefile b/vendor/plugins/sql_session_store/Rakefile
new file mode 100755 (executable)
index 0000000..0145def
--- /dev/null
@@ -0,0 +1,22 @@
+require 'rake'
+require 'rake/testtask'
+require 'rake/rdoctask'
+
+desc 'Default: run unit tests.'
+task :default => :test
+
+desc 'Test the sql_session_store plugin.'
+Rake::TestTask.new(:test) do |t|
+  t.libs << 'lib'
+  t.pattern = 'test/**/*_test.rb'
+  t.verbose = true
+end
+
+desc 'Generate documentation for the sql_session_store plugin.'
+Rake::RDocTask.new(:rdoc) do |rdoc|
+  rdoc.rdoc_dir = 'rdoc'
+  rdoc.title    = 'SqlSessionStore'
+  rdoc.options << '--line-numbers' << '--inline-source'
+  rdoc.rdoc_files.include('README')
+  rdoc.rdoc_files.include('lib/**/*.rb')
+end
diff --git a/vendor/plugins/sql_session_store/generators/sql_session_store/USAGE b/vendor/plugins/sql_session_store/generators/sql_session_store/USAGE
new file mode 100755 (executable)
index 0000000..1e3f58a
--- /dev/null
@@ -0,0 +1,17 @@
+Description:
+    The sql_session_store generator creates a migration for use with
+    the sql session store.  It takes one argument: the database
+    type. Only mysql and postgreql are currently supported.
+
+Example:
+    ./script/generate sql_session_store mysql
+
+    This will create the following migration:
+
+      db/migrate/XXX_add_sql_session.rb
+
+    Use
+
+    ./script/generate sql_session_store postgreql
+
+    to get a migration for postgres.
diff --git a/vendor/plugins/sql_session_store/generators/sql_session_store/sql_session_store_generator.rb b/vendor/plugins/sql_session_store/generators/sql_session_store/sql_session_store_generator.rb
new file mode 100755 (executable)
index 0000000..6af6bd0
--- /dev/null
@@ -0,0 +1,25 @@
+class SqlSessionStoreGenerator < Rails::Generator::NamedBase
+  def initialize(runtime_args, runtime_options = {})
+    runtime_args.insert(0, 'add_sql_session')
+    if runtime_args.include?('postgresql')
+      @_database = 'postgresql'
+    elsif runtime_args.include?('mysql')
+      @_database = 'mysql'
+    elsif runtime_args.include?('oracle')
+      @_database = 'oracle'
+    else
+      puts "error: database type not given.\nvalid arguments are: mysql or postgresql"
+      exit
+    end
+    super
+  end
+
+  def manifest
+    record do |m|
+      m.migration_template("migration.rb", 'db/migrate',
+                           :assigns => { :migration_name => "SqlSessionStoreSetup", :database => @_database },
+                           :migration_file_name => "sql_session_store_setup"
+                           )
+    end
+  end
+end
diff --git a/vendor/plugins/sql_session_store/generators/sql_session_store/templates/migration.rb b/vendor/plugins/sql_session_store/generators/sql_session_store/templates/migration.rb
new file mode 100755 (executable)
index 0000000..5126500
--- /dev/null
@@ -0,0 +1,38 @@
+class <%= migration_name %> < ActiveRecord::Migration
+
+  class Session < ActiveRecord::Base; end
+
+  def self.up
+    c = ActiveRecord::Base.connection
+    if c.tables.include?('sessions')
+      if (columns = Session.column_names).include?('sessid')
+        rename_column :sessions, :sessid, :session_id
+      else
+        add_column :sessions, :session_id, :string unless columns.include?('session_id')
+        add_column :sessions, :data, :text unless columns.include?('data')
+        if columns.include?('created_on')
+          rename_column :sessions, :created_on, :created_at
+        else
+          add_column :sessions, :created_at, :timestamp unless columns.include?('created_at')
+        end
+        if columns.include?('updated_on')
+          rename_column :sessions, :updated_on, :updated_at
+        else
+          add_column :sessions, :updated_at, :timestamp unless columns.include?('updated_at')
+        end
+      end
+    else
+      create_table :sessions, :options => '<%= database == "mysql" ? "ENGINE=MyISAM" : "" %>' do |t|
+        t.column :session_id, :string
+        t.column :data,       :text
+        t.column :created_at, :timestamp
+        t.column :updated_at, :timestamp
+      end
+      add_index :sessions, :session_id, :name => 'session_id_idx'
+    end
+  end
+
+  def self.down
+    raise IrreversibleMigration
+  end
+end
diff --git a/vendor/plugins/sql_session_store/init.rb b/vendor/plugins/sql_session_store/init.rb
new file mode 100755 (executable)
index 0000000..956151e
--- /dev/null
@@ -0,0 +1 @@
+require 'sql_session_store'\r
diff --git a/vendor/plugins/sql_session_store/install.rb b/vendor/plugins/sql_session_store/install.rb
new file mode 100755 (executable)
index 0000000..f40549d
--- /dev/null
@@ -0,0 +1,2 @@
+# Install hook code here
+puts IO.read(File.join(File.dirname(__FILE__), 'README'))
diff --git a/vendor/plugins/sql_session_store/lib/mysql_session.rb b/vendor/plugins/sql_session_store/lib/mysql_session.rb
new file mode 100755 (executable)
index 0000000..8c86384
--- /dev/null
@@ -0,0 +1,132 @@
+require 'mysql'
+
+# allow access to the real Mysql connection
+class ActiveRecord::ConnectionAdapters::MysqlAdapter
+  attr_reader :connection
+end
+
+# MysqlSession is a down to the bare metal session store
+# implementation to be used with +SQLSessionStore+. It is much faster
+# than the default ActiveRecord implementation.
+#
+# The implementation assumes that the table column names are 'id',
+# 'data', 'created_at' and 'updated_at'. If you want use other names,
+# you will need to change the SQL statments in the code.
+
+class MysqlSession
+
+  # if you need Rails components, and you have a pages which create
+  # new sessions, and embed components insides this pages that need
+  # session access, then you *must* set +eager_session_creation+ to
+  # true (as of Rails 1.0).
+  cattr_accessor :eager_session_creation
+  @@eager_session_creation = false
+
+  attr_accessor :id, :session_id, :data
+
+  def initialize(session_id, data)
+    @session_id = session_id
+    @data = data
+    @id = nil
+  end
+
+  class << self
+
+    # retrieve the session table connection and get the 'raw' Mysql connection from it
+    def session_connection
+      SqlSession.connection.connection
+    end
+
+    # try to find a session with a given +session_id+. returns nil if
+    # no such session exists. note that we don't retrieve
+    # +created_at+ and +updated_at+ as they are not accessed anywhyere
+    # outside this class
+    def find_session(session_id)
+      connection = session_connection
+      connection.query_with_result = true
+      session_id = Mysql::quote(session_id)
+      result = connection.query("SELECT id, data FROM sessions WHERE `session_id`='#{session_id}' LIMIT 1")
+      my_session = nil
+      # each is used below, as other methods barf on my 64bit linux machine
+      # I suspect this to be a bug in mysql-ruby
+      result.each do |row|
+        my_session = new(session_id, row[1])
+        my_session.id = row[0]
+      end
+      result.free
+      my_session
+    end
+
+    # create a new session with given +session_id+ and +data+
+    # and save it immediately to the database
+    def create_session(session_id, data)
+      session_id = Mysql::quote(session_id)
+      new_session = new(session_id, data)
+      if @@eager_session_creation
+        connection = session_connection
+        connection.query("INSERT INTO sessions (`created_at`, `updated_at`, `session_id`, `data`) VALUES (NOW(), NOW(), '#{session_id}', '#{Mysql::quote(data)}')")
+        new_session.id = connection.insert_id
+      end
+      new_session
+    end
+
+    # delete all sessions meeting a given +condition+. it is the
+    # caller's responsibility to pass a valid sql condition
+    def delete_all(condition=nil)
+      if condition
+        session_connection.query("DELETE FROM sessions WHERE #{condition}")
+      else
+        session_connection.query("DELETE FROM sessions")
+      end
+    end
+
+  end # class methods
+
+  # update session with given +data+.
+  # unlike the default implementation using ActiveRecord, updating of
+  # column `updated_at` will be done by the datbase itself
+  def update_session(data)
+    connection = self.class.session_connection
+    if @id
+      # if @id is not nil, this is a session already stored in the database
+      # update the relevant field using @id as key
+      connection.query("UPDATE sessions SET `updated_at`=NOW(), `data`='#{Mysql::quote(data)}' WHERE id=#{@id}")
+    else
+      # if @id is nil, we need to create a new session in the database
+      # and set @id to the primary key of the inserted record
+      connection.query("INSERT INTO sessions (`created_at`, `updated_at`, `session_id`, `data`) VALUES (NOW(), NOW(), '#{@session_id}', '#{Mysql::quote(data)}')")
+      @id = connection.insert_id
+    end
+  end
+
+  # destroy the current session
+  def destroy
+    self.class.delete_all("session_id='#{session_id}'")
+  end
+
+end
+
+__END__
+
+# This software is released under the MIT license
+#
+# Copyright (c) 2005-2008 Stefan Kaes
+
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/vendor/plugins/sql_session_store/lib/oracle_session.rb b/vendor/plugins/sql_session_store/lib/oracle_session.rb
new file mode 100755 (executable)
index 0000000..0b82f63
--- /dev/null
@@ -0,0 +1,143 @@
+require 'oci8'
+
+# allow access to the real Oracle connection
+class ActiveRecord::ConnectionAdapters::OracleAdapter
+  attr_reader :connection
+end
+
+# OracleSession is a down to the bare metal session store
+# implementation to be used with +SQLSessionStore+. It is much faster
+# than the default ActiveRecord implementation.
+#
+# The implementation assumes that the table column names are 'id',
+# 'session_id', 'data', 'created_at' and 'updated_at'. If you want use
+# other names, you will need to change the SQL statments in the code.
+#
+# This table layout is compatible with ActiveRecordStore.
+
+class OracleSession
+
+  # if you need Rails components, and you have a pages which create
+  # new sessions, and embed components insides these pages that need
+  # session access, then you *must* set +eager_session_creation+ to
+  # true (as of Rails 1.0). Not needed for Rails 1.1 and up.
+  cattr_accessor :eager_session_creation
+  @@eager_session_creation = false
+
+  attr_accessor :id, :session_id, :data
+
+  def initialize(session_id, data)
+    @session_id = session_id
+    @data = data
+    @id = nil
+  end
+
+  class << self
+
+    # retrieve the session table connection and get the 'raw' Oracle connection from it
+    def session_connection
+      SqlSession.connection.connection
+    end
+
+    # try to find a session with a given +session_id+. returns nil if
+    # no such session exists. note that we don't retrieve
+    # +created_at+ and +updated_at+ as they are not accessed anywhyere
+    # outside this class.
+    def find_session(session_id)
+      new_session = nil
+      connection = session_connection
+      result = connection.exec("SELECT id, data FROM sessions WHERE session_id = :a and rownum=1", session_id)
+
+      # Make sure to save the @id if we find an existing session
+      while row = result.fetch
+        new_session = new(session_id,row[1].read)
+        new_session.id = row[0]
+      end
+      result.close
+      new_session
+    end
+
+    # create a new session with given +session_id+ and +data+
+    # and save it immediately to the database
+    def create_session(session_id, data)
+      new_session = new(session_id, data)
+      if @@eager_session_creation
+        connection = session_connection
+        connection.exec("INSERT INTO sessions (id, created_at, updated_at, session_id, data)"+
+                        " VALUES (sessions_seq.nextval, SYSDATE, SYSDATE, :a, :b)",
+                         session_id, data)
+        result = connection.exec("SELECT sessions_seq.currval FROM dual")
+        row = result.fetch
+        new_session.id = row[0].to_i
+      end
+      new_session
+    end
+
+    # delete all sessions meeting a given +condition+. it is the
+    # caller's responsibility to pass a valid sql condition
+    def delete_all(condition=nil)
+      if condition
+        session_connection.exec("DELETE FROM sessions WHERE #{condition}")
+      else
+        session_connection.exec("DELETE FROM sessions")
+      end
+    end
+
+  end # class methods
+
+  # update session with given +data+.
+  # unlike the default implementation using ActiveRecord, updating of
+  # column `updated_at` will be done by the database itself
+  def update_session(data)
+    connection = self.class.session_connection
+    if @id
+      # if @id is not nil, this is a session already stored in the database
+      # update the relevant field using @id as key
+      connection.exec("UPDATE sessions SET updated_at = SYSDATE, data = :a WHERE id = :b",
+                       data, @id)
+    else
+      # if @id is nil, we need to create a new session in the database
+      # and set @id to the primary key of the inserted record
+      connection.exec("INSERT INTO sessions (id, created_at, updated_at, session_id, data)"+
+                      " VALUES (sessions_seq.nextval, SYSDATE, SYSDATE, :a, :b)",
+                       @session_id, data)
+      result = connection.exec("SELECT sessions_seq.currval FROM dual")
+      row = result.fetch
+      @id = row[0].to_i
+    end
+  end
+
+  # destroy the current session
+  def destroy
+    self.class.delete_all("session_id='#{session_id}'")
+  end
+
+end
+
+__END__
+
+# This software is released under the MIT license
+#
+# Copyright (c) 2006-2008 Stefan Kaes
+# Copyright (c) 2006-2008 Tiago Macedo
+# Copyright (c) 2007-2008 Nate Wiger
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
diff --git a/vendor/plugins/sql_session_store/lib/postgresql_session.rb b/vendor/plugins/sql_session_store/lib/postgresql_session.rb
new file mode 100755 (executable)
index 0000000..d922913
--- /dev/null
@@ -0,0 +1,136 @@
+require 'postgres'
+
+# allow access to the real Mysql connection
+class ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
+  attr_reader :connection
+end
+
+# PostgresqlSession is a down to the bare metal session store
+# implementation to be used with +SQLSessionStore+. It is much faster
+# than the default ActiveRecord implementation.
+#
+# The implementation assumes that the table column names are 'id',
+# 'session_id', 'data', 'created_at' and 'updated_at'. If you want use
+# other names, you will need to change the SQL statments in the code.
+#
+# This table layout is compatible with ActiveRecordStore.
+
+class PostgresqlSession
+
+  # if you need Rails components, and you have a pages which create
+  # new sessions, and embed components insides these pages that need
+  # session access, then you *must* set +eager_session_creation+ to
+  # true (as of Rails 1.0). Not needed for Rails 1.1 and up.
+  cattr_accessor :eager_session_creation
+  @@eager_session_creation = false
+
+  attr_accessor :id, :session_id, :data
+
+  def initialize(session_id, data)
+    @session_id = session_id
+    @data = data
+    @id = nil
+  end
+
+  class << self
+
+    # retrieve the session table connection and get the 'raw' Postgresql connection from it
+    def session_connection
+      SqlSession.connection.connection
+    end
+
+    # try to find a session with a given +session_id+. returns nil if
+    # no such session exists. note that we don't retrieve
+    # +created_at+ and +updated_at+ as they are not accessed anywhyere
+    # outside this class.
+    def find_session(session_id)
+      connection = session_connection
+      # postgres adds string delimiters when quoting, so strip them off
+      session_id = PGconn::quote(session_id)[1..-2]
+      result = connection.query("SELECT id, data FROM sessions WHERE session_id='#{session_id}' LIMIT 1")
+      my_session = nil
+      # each is used below, as other methods barf on my 64bit linux machine
+      # I suspect this to be a bug in mysql-ruby
+      result.each do |row|
+        my_session = new(session_id, row[1])
+        my_session.id = row[0]
+      end
+      result.clear
+      my_session
+    end
+
+    # create a new session with given +session_id+ and +data+
+    # and save it immediately to the database
+    def create_session(session_id, data)
+      # postgres adds string delimiters when quoting, so strip them off
+      session_id = PGconn::quote(session_id)[1..-2]
+      new_session = new(session_id, data)
+      if @@eager_session_creation
+        connection = session_connection
+        connection.query("INSERT INTO sessions (\"created_at\", \"updated_at\", \"session_id\", \"data\") VALUES (NOW(), NOW(), '#{session_id}', #{PGconn::quote(data)})")
+        new_session.id = connection.lastval
+      end
+      new_session
+    end
+
+    # delete all sessions meeting a given +condition+. it is the
+    # caller's responsibility to pass a valid sql condition
+    def delete_all(condition=nil)
+      if condition
+        session_connection.query("DELETE FROM sessions WHERE #{condition}")
+      else
+        session_connection.query("DELETE FROM sessions")
+      end
+    end
+
+  end # class methods
+
+  # update session with given +data+.
+  # unlike the default implementation using ActiveRecord, updating of
+  # column `updated_at` will be done by the database itself
+  def update_session(data)
+    connection = self.class.session_connection
+    if @id
+      # if @id is not nil, this is a session already stored in the database
+      # update the relevant field using @id as key
+      connection.query("UPDATE sessions SET \"updated_at\"=NOW(), \"data\"=#{PGconn::quote(data)} WHERE id=#{@id}")
+    else
+      # if @id is nil, we need to create a new session in the database
+      # and set @id to the primary key of the inserted record
+      connection.query("INSERT INTO sessions (\"created_at\",  \"updated_at\", \"session_id\", \"data\") VALUES (NOW(), NOW(), '#{@session_id}', #{PGconn::quote(data)})")
+      @id = connection.lastval rescue connection.query("select lastval()").first[0]
+    end
+  end
+
+  # destroy the current session
+  def destroy
+    self.class.delete_all("session_id=#{PGconn.quote(session_id)}")
+  end
+
+end
+
+__END__
+
+# This software is released under the MIT license
+#
+# Copyright (c) 2006-2008 Stefan Kaes
+
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
diff --git a/vendor/plugins/sql_session_store/lib/sql_session.rb b/vendor/plugins/sql_session_store/lib/sql_session.rb
new file mode 100644 (file)
index 0000000..19d2ad5
--- /dev/null
@@ -0,0 +1,27 @@
+# An ActiveRecord class which corresponds to the database table\r
+# +sessions+. Functions +find_session+, +create_session+,\r
+# +update_session+ and +destroy+ constitute the interface to class\r
+# +SqlSessionStore+.\r
+\r
+class SqlSession < ActiveRecord::Base\r
+  # this class should not be reloaded\r
+  def self.reloadable?\r
+    false\r
+  end\r
+\r
+  # retrieve session data for a given +session_id+ from the database,\r
+  # return nil if no such session exists\r
+  def self.find_session(session_id)\r
+    find :first, :conditions => "session_id='#{session_id}'"\r
+  end\r
+\r
+  # create a new session with given +session_id+ and +data+\r
+  def self.create_session(session_id, data)\r
+    new(:session_id => session_id, :data => data)\r
+  end\r
+\r
+  # update session data and store it in the database\r
+  def update_session(data)\r
+    update_attribute('data', data)\r
+  end\r
+end\r
diff --git a/vendor/plugins/sql_session_store/lib/sql_session_store.rb b/vendor/plugins/sql_session_store/lib/sql_session_store.rb
new file mode 100755 (executable)
index 0000000..8b0ff15
--- /dev/null
@@ -0,0 +1,116 @@
+require 'active_record'
+require 'cgi'
+require 'cgi/session'
+begin
+  require 'base64'
+rescue LoadError
+end
+
+# +SqlSessionStore+ is a stripped down, optimized for speed version of
+# class +ActiveRecordStore+.
+
+class SqlSessionStore
+
+  # The class to be used for creating, retrieving and updating sessions.
+  # Defaults to SqlSessionStore::Session, which is derived from +ActiveRecord::Base+.
+  #
+  # In order to achieve acceptable performance you should implement
+  # your own session class, similar to the one provided for Myqsl.
+  #
+  # Only functions +find_session+, +create_session+,
+  # +update_session+ and +destroy+ are required. See file +mysql_session.rb+.
+
+  cattr_accessor :session_class
+  @@session_class = SqlSession
+
+  # Create a new SqlSessionStore instance.
+  #
+  # +session+ is the session for which this instance is being created.
+  #
+  # +option+ is currently ignored as no options are recognized.
+
+  def initialize(session, option=nil)
+    if @session = @@session_class.find_session(session.session_id)
+      @data = unmarshalize(@session.data)
+    else
+      @session = @@session_class.create_session(session.session_id, marshalize({}))
+      @data = {}
+    end
+  end
+
+  # Update the database and disassociate the session object
+  def close
+    if @session
+      @session.update_session(marshalize(@data))
+      @session = nil
+    end
+  end
+
+  # Delete the current session, disassociate and destroy session object
+  def delete
+    if @session
+      @session.destroy
+      @session = nil
+    end
+  end
+
+  # Restore session data from the session object
+  def restore
+    if @session
+      @data = unmarshalize(@session.data)
+    end
+  end
+
+  # Save session data in the session object
+  def update
+    if @session
+      @session.update_session(marshalize(@data))
+    end
+  end
+
+  private
+  if defined?(Base64)
+    def unmarshalize(data)
+      Marshal.load(Base64.decode64(data))
+    end
+
+    def marshalize(data)
+      Base64.encode64(Marshal.dump(data))
+    end
+  else
+    def unmarshalize(data)
+      Marshal.load(data.unpack("m").first)
+    end
+
+    def marshalize(data)
+      [Marshal.dump(data)].pack("m")
+    end
+  end
+
+end
+
+__END__
+
+# This software is released under the MIT license
+#
+# Copyright (c) 2005-2008 Stefan Kaes
+
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
diff --git a/vendor/plugins/sql_session_store/lib/sqlite_session.rb b/vendor/plugins/sql_session_store/lib/sqlite_session.rb
new file mode 100755 (executable)
index 0000000..822b232
--- /dev/null
@@ -0,0 +1,133 @@
+require 'sqlite3'
+
+# allow access to the real Sqlite connection
+#class ActiveRecord::ConnectionAdapters::SQLiteAdapter
+#  attr_reader :connection
+#end
+
+# SqliteSession is a down to the bare metal session store
+# implementation to be used with +SQLSessionStore+. It is much faster
+# than the default ActiveRecord implementation.
+#
+# The implementation assumes that the table column names are 'id',
+# 'data', 'created_at' and 'updated_at'. If you want use other names,
+# you will need to change the SQL statments in the code.
+
+class SqliteSession
+
+  # if you need Rails components, and you have a pages which create
+  # new sessions, and embed components insides this pages that need
+  # session access, then you *must* set +eager_session_creation+ to
+  # true (as of Rails 1.0).
+  cattr_accessor :eager_session_creation
+  @@eager_session_creation = false
+
+  attr_accessor :id, :session_id, :data
+
+  def initialize(session_id, data)
+    @session_id = session_id
+    @data = data
+    @id = nil
+  end
+
+  class << self
+
+    # retrieve the session table connection and get the 'raw' Sqlite connection from it
+    def session_connection
+      SqlSession.connection.instance_variable_get(:@connection)
+    end
+
+    # try to find a session with a given +session_id+. returns nil if
+    # no such session exists. note that we don't retrieve
+    # +created_at+ and +updated_at+ as they are not accessed anywhyere
+    # outside this class
+    def find_session(session_id)
+      connection = session_connection
+      session_id = SQLite3::Database.quote(session_id)
+      result = connection.execute("SELECT id, data FROM sessions WHERE `session_id`='#{session_id}' LIMIT 1")
+      my_session = nil
+      # each is used below, as other methods barf on my 64bit linux machine
+      # I suspect this to be a bug in sqlite-ruby
+      result.each do |row|
+        my_session = new(session_id, row[1])
+        my_session.id = row[0]
+      end
+#      result.free
+      my_session
+    end
+
+    # create a new session with given +session_id+ and +data+
+    # and save it immediately to the database
+    def create_session(session_id, data)
+      session_id = SQLite3::Database.quote(session_id)
+      new_session = new(session_id, data)
+      if @@eager_session_creation
+        connection = session_connection
+        connection.execute("INSERT INTO sessions ('id', `created_at`, `updated_at`, `session_id`, `data`) VALUES (NULL, datetime('now'), datetime('now'), '#{session_id}', '#{SQLite3::Database.quote(data)}')")
+        new_session.id = connection.last_insert_row_id()
+      end
+      new_session
+    end
+
+    # delete all sessions meeting a given +condition+. it is the
+    # caller's responsibility to pass a valid sql condition
+    def delete_all(condition=nil)
+      if condition
+        session_connection.execute("DELETE FROM sessions WHERE #{condition}")
+      else
+        session_connection.execute("DELETE FROM sessions")
+      end
+    end
+
+  end # class methods
+
+  # update session with given +data+.
+  # unlike the default implementation using ActiveRecord, updating of
+  # column `updated_at` will be done by the database itself
+  def update_session(data)
+    connection = SqlSession.connection.instance_variable_get(:@connection) #self.class.session_connection
+    if @id
+      # if @id is not nil, this is a session already stored in the database
+      # update the relevant field using @id as key
+      connection.execute("UPDATE sessions SET `updated_at`=datetime('now'), `data`='#{SQLite3::Database.quote(data)}' WHERE id=#{@id}")
+    else
+      # if @id is nil, we need to create a new session in the database
+      # and set @id to the primary key of the inserted record
+      connection.execute("INSERT INTO sessions ('id', `created_at`, `updated_at`, `session_id`, `data`) VALUES (NULL, datetime('now'), datetime('now'), '#{@session_id}', '#{SQLite3::Database.quote(data)}')")
+      @id = connection.last_insert_row_id()
+    end
+  end
+
+  # destroy the current session
+  def destroy
+    connection = SqlSession.connection.instance_variable_get(:@connection)
+    connection.execute("delete from sessions where session_id='#{session_id}'")
+  end
+
+end
+
+__END__
+
+# This software is released under the MIT license
+#
+# Copyright (c) 2005-2008 Stefan Kaes
+# Copyright (c) 2006-2008 Ted X Toth
+
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.