From e5ff2959bc756a97d10b130d1ad6d0376eb57d83 Mon Sep 17 00:00:00 2001 From: Tom Hughes Date: Mon, 7 Apr 2008 23:09:56 +0000 Subject: [PATCH 001/381] Make a copy of the rails code for the node tag splitting project, and remove the associated migrations from the other branches. From f7d2bf9e3903b309c79290db1b75f9a0e9b15b0a Mon Sep 17 00:00:00 2001 From: Tom Hughes Date: Mon, 7 Apr 2008 23:11:25 +0000 Subject: [PATCH 002/381] Remove export controller from branches where it isn't needed yet. --- app/controllers/export_controller.rb | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 app/controllers/export_controller.rb diff --git a/app/controllers/export_controller.rb b/app/controllers/export_controller.rb deleted file mode 100644 index f1c6286ce..000000000 --- a/app/controllers/export_controller.rb +++ /dev/null @@ -1,8 +0,0 @@ -class ExportController < ApplicationController - def start - render :update do |page| - page.replace_html :sidebar_content, :partial => 'start' - page.call "openSidebar" - end - end -end From cf21eae743566090dbdd42198245d6d5183d4fdb Mon Sep 17 00:00:00 2001 From: Nick Black Date: Sat, 3 May 2008 14:07:07 +0000 Subject: [PATCH 003/381] adding node tag models --- app/models/node_tag.rb | 5 +++++ app/models/old_node_tag.rb | 7 +++++++ 2 files changed, 12 insertions(+) create mode 100644 app/models/node_tag.rb create mode 100644 app/models/old_node_tag.rb diff --git a/app/models/node_tag.rb b/app/models/node_tag.rb new file mode 100644 index 000000000..9795ff493 --- /dev/null +++ b/app/models/node_tag.rb @@ -0,0 +1,5 @@ +class NodeTag < ActiveRecord::Base + set_table_name 'current_node_tags' + + belongs_to :node, :foreign_key => 'id' +end diff --git a/app/models/old_node_tag.rb b/app/models/old_node_tag.rb new file mode 100644 index 000000000..26a6c92b4 --- /dev/null +++ b/app/models/old_node_tag.rb @@ -0,0 +1,7 @@ +class OldNodeTag < ActiveRecord::Base + belongs_to :user + + set_table_name 'node_tags' + + +end From 4db3faa68cba0d7d570d11f9f8cac6d1f85afc70 Mon Sep 17 00:00:00 2001 From: Gabriel Ebner Date: Sat, 3 May 2008 14:32:30 +0000 Subject: [PATCH 004/381] split_node_tags: New migration (in C). --- config/database.yml | 6 +- db/migrate/012_add_user_preference_id.rb | 2 +- db/migrate/012_create_node_tags.rb | 18 -- db/migrate/013_create_old_node_tags.rb | 17 -- .../013_populate_node_tags_and_remove.rb | 62 +++++ ...013_populate_node_tags_and_remove_helper.c | 234 ++++++++++++++++++ .../014_populate_node_tags_and_remove.rb | 13 - db/migrate/015_create_temp_old_nodes.rb | 24 -- 8 files changed, 300 insertions(+), 76 deletions(-) delete mode 100644 db/migrate/012_create_node_tags.rb delete mode 100644 db/migrate/013_create_old_node_tags.rb create mode 100644 db/migrate/013_populate_node_tags_and_remove.rb create mode 100644 db/migrate/013_populate_node_tags_and_remove_helper.c delete mode 100644 db/migrate/014_populate_node_tags_and_remove.rb delete mode 100644 db/migrate/015_create_temp_old_nodes.rb diff --git a/config/database.yml b/config/database.yml index b884f3b93..363782953 100644 --- a/config/database.yml +++ b/config/database.yml @@ -12,9 +12,9 @@ # http://dev.mysql.com/doc/refman/5.0/en/old-client.html development: adapter: mysql - database: openstreetmap - username: openstreetmap - password: openstreetmap + database: osm + username: osm + password: osm host: localhost # Warning: The database defined as 'test' will be erased and diff --git a/db/migrate/012_add_user_preference_id.rb b/db/migrate/012_add_user_preference_id.rb index 9dee2378a..17b677605 100644 --- a/db/migrate/012_add_user_preference_id.rb +++ b/db/migrate/012_add_user_preference_id.rb @@ -1,6 +1,6 @@ class AddUserPreferenceId < ActiveRecord::Migration def self.up - add_column "user_preferences", "id", :bigint, :limit => 64, :null => false, :options => "AUTO_INCREMENT" + add_column "user_preferences", "id", :bigint, :limit => 64, :null => false add_index "user_preferences", ["id"], :name => "user_preferences_id_idx" end diff --git a/db/migrate/012_create_node_tags.rb b/db/migrate/012_create_node_tags.rb deleted file mode 100644 index 316602d69..000000000 --- a/db/migrate/012_create_node_tags.rb +++ /dev/null @@ -1,18 +0,0 @@ -class CreateNodeTags < ActiveRecord::Migration - def self.up - create_table "current_node_tags", myisam_table do |t| - t.column "id", :bigint, :limit => 64, :null => false - t.column "sequence_id", :bigint, :limit => 11, :null => false - t.column "k", :string, :default => "", :null => false - t.column "v", :string, :default => "", :null => false - end - - add_primary_key "current_node_tags", ["id", "sequence_id"] - - execute "CREATE FULLTEXT INDEX `current_node_tags_v_idx` ON `current_node_tags` (`v`)" - end - - def self.down - drop_table :current_node_tags - end -end diff --git a/db/migrate/013_create_old_node_tags.rb b/db/migrate/013_create_old_node_tags.rb deleted file mode 100644 index aeb5abd4c..000000000 --- a/db/migrate/013_create_old_node_tags.rb +++ /dev/null @@ -1,17 +0,0 @@ -class CreateOldNodeTags < ActiveRecord::Migration - def self.up - create_table "node_tags", myisam_table do |t| - t.column "id", :bigint, :limit => 64, :default => 0, :null => false - t.column "version", :bigint, :limit => 20, :null => false - t.column "sequence_id", :bigint, :limit => 11, :null => false - t.column "k", :string, :null => false - t.column "v", :string, :null => false - end - - add_primary_key "node_tags", ["id", "version", "sequence_id"] - end - - def self.down - drop_table :node_tags - end -end diff --git a/db/migrate/013_populate_node_tags_and_remove.rb b/db/migrate/013_populate_node_tags_and_remove.rb new file mode 100644 index 000000000..29a91c70b --- /dev/null +++ b/db/migrate/013_populate_node_tags_and_remove.rb @@ -0,0 +1,62 @@ +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, "013_populate_node_tags_and_remove.#{$$}." + + cmd = "db/migrate/013_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/013_populate_node_tags_and_remove_helper.c b/db/migrate/013_populate_node_tags_and_remove_helper.c new file mode 100644 index 000000000..b1868ef24 --- /dev/null +++ b/db/migrate/013_populate_node_tags_and_remove_helper.c @@ -0,0 +1,234 @@ +#include +#include +#include +#include +#include + +static void exit_mysql_err(MYSQL *mysql) { + const char *err = mysql_error(mysql); + if (err) { + fprintf(stderr, "013_populate_node_tags_and_remove_helper: MySQL error: %s\n", err); + } else { + fprintf(stderr, "013_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; + + while (*i) { + if (*i == '\\') { + i++; + switch (*i++) { + case 's': *o++ = ';'; break; + case 'e': *o++ = '='; break; + case '\\': *o++ = '\\'; 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; + uint32_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: 013_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 = malloc(sizeof(uint32_t) * d->version_size); + + prefix_len = strlen(argv[7]); + tempfn = (char *) malloc(prefix_len + 16); + strcpy(tempfn, argv[7]); + + strcpy(tempfn + prefix_len, "current_nodes"); + open_file(¤t_nodes, tempfn); + + strcpy(tempfn + prefix_len, "current_node_tags"); + open_file(¤t_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/014_populate_node_tags_and_remove.rb b/db/migrate/014_populate_node_tags_and_remove.rb deleted file mode 100644 index 7583c6618..000000000 --- a/db/migrate/014_populate_node_tags_and_remove.rb +++ /dev/null @@ -1,13 +0,0 @@ -class PopulateNodeTagsAndRemove < ActiveRecord::Migration - def self.up - #rake import - #commented out to stop people from breaking their db -# remove_column :nodes, :tags -# remove_column :current_nodes, :tags - end - - def self.down -# add_column :nodes, "tags", :text, :default => "", :null => false -# add_column :current_nodes, "tags", :text, :default => "", :null => false - end -end diff --git a/db/migrate/015_create_temp_old_nodes.rb b/db/migrate/015_create_temp_old_nodes.rb deleted file mode 100644 index 60edb8406..000000000 --- a/db/migrate/015_create_temp_old_nodes.rb +++ /dev/null @@ -1,24 +0,0 @@ -class CreateTempOldNodes < ActiveRecord::Migration - def self.up - create_table "temp_nodes", myisam_table do |t| - t.column "id", :bigint, :limit => 64, :null => false - t.column "version", :bigint, :limit => 20, :null => false - t.column "latitude", :double, :null => false - t.column "longitude", :double, :null => false - t.column "user_id", :bigint, :limit => 20, :null => false - t.column "visible", :boolean, :null => false - t.column "timestamp", :datetime, :null => false - t.column "tile", :integer, :null => false - end - - add_primary_key "temp_nodes", ["id", "version"] - add_index "temp_nodes", ["timestamp"], :name => "temp_nodes_timestamp_idx" - add_index "temp_nodes", ["tile"], :name => "temp_nodes_tile_idx" - - change_column "temp_nodes", "version", :bigint, :limit => 20, :null => false, :options => "AUTO_INCREMENT" - end - - def self.down - drop_table :temp_nodes - end -end From b214158d993e60eb60a1c0b2c7fae85da61df311 Mon Sep 17 00:00:00 2001 From: Nick Black Date: Sat, 3 May 2008 14:32:36 +0000 Subject: [PATCH 005/381] adds a temporary old_node object for use during migration --- app/models/temp_old_node.rb | 50 +++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 app/models/temp_old_node.rb diff --git a/app/models/temp_old_node.rb b/app/models/temp_old_node.rb new file mode 100644 index 000000000..a024eaac7 --- /dev/null +++ b/app/models/temp_old_node.rb @@ -0,0 +1,50 @@ +class TempOldNode < ActiveRecord::Base + set_table_name 'temp_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 + + def validate_position + errors.add_to_base("Node is not in the world") unless in_world? + end + + def in_world? + return true + end + + def self.from_node(node) + old_node = OldNode.new + old_node.latitude = node.latitude + old_node.longitude = node.longitude + old_node.visible = node.visible + old_node.tags = node.tags + old_node.timestamp = node.timestamp + old_node.user_id = node.user_id + old_node.id = node.id + return old_node + 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? + + Tags.split(self.tags) do |k,v| + el2 = XML::Node.new('tag') + el2['k'] = k.to_s + el2['v'] = v.to_s + el1 << el2 + end + + el1['visible'] = self.visible.to_s + el1['timestamp'] = self.timestamp.xmlschema + return el1 + end + +end From 2660ce1868fa8f27d8285e4afe55a7ce05e8d212 Mon Sep 17 00:00:00 2001 From: Nick Black Date: Sat, 3 May 2008 16:01:55 +0000 Subject: [PATCH 006/381] node models for normalised node tags - pre merge --- app/models/node.rb.pre-merge | 195 +++++++++++++++++++++++++++++++ app/models/old_node.rb.pre-merge | 88 ++++++++++++++ 2 files changed, 283 insertions(+) create mode 100644 app/models/node.rb.pre-merge create mode 100644 app/models/old_node.rb.pre-merge diff --git a/app/models/node.rb.pre-merge b/app/models/node.rb.pre-merge new file mode 100644 index 000000000..5a8e03ef1 --- /dev/null +++ b/app/models/node.rb.pre-merge @@ -0,0 +1,195 @@ +class Node < GeoRecord + require 'xml/libxml' + + 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 + + has_many :old_nodes, :foreign_key => :id + has_many :way_nodes + has_many :node_tags, :foreign_key => :id + belongs_to :user + + 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 + + # + # Search for nodes matching tags within bounding_box + # + # Also adheres to limitations such as within max_number_of_nodes + # + def self.search(bounding_box, tags = {}) + min_lon, min_lat, max_lon, max_lat = *bounding_box + # @fixme a bit of a hack to search for only visible nodes + # couldn't think of another to add to tags condition + #conditions_hash = tags.merge({ 'visible' => 1 }) + + # using named placeholders http://www.robbyonrails.com/articles/2005/10/21/using-named-placeholders-in-ruby + #keys = [] + #values = {} + + #conditions_hash.each do |key,value| + # keys << "#{key} = :#{key}" + # values[key.to_sym] = value + #end + #conditions = keys.join(' AND ') + + find_by_area(min_lat, min_lon, max_lat, max_lon, + :conditions => 'visible = 1', + :limit => APP_CONFIG['max_number_of_nodes']+1) + end + + # Read in xml as text and return it's Node object representation + def self.from_xml(xml, create=false) + begin + 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 nil unless node.in_world? + + unless create + if pt['id'] != '0' + node.id = pt['id'].to_i + end + end + + node.visible = pt['visible'] and pt['visible'] == 'true' + + if create + node.timestamp = Time.now + else + if pt['timestamp'] + node.timestamp = Time.parse(pt['timestamp']) + end + end + + tags = [] + + pt.find('tag').each do |tag| + node.add_tag_key_val(tag['k'],tag['v']) + end + + end + rescue + node = nil + end + + return node + end + + def save_with_history! + t = Time.now + + Node.transaction do + # apply timestamp to the new node + self.timestamp = t + self.save! + end + + # Create a NodeTag + NodeTag.transaction do + tags = self.tags + + NodeTag.delete_all(['id = ?', self.id]) + + sequence_id = 1 + tags.each do |k,v| + tag = NodeTag.new + tag.k = k + tag.v = v + tag.id = self.id + tag.sequence_id = sequence_id + tag.save! + sequence_id += 1 + end + end + # Create an OldNode + old_node = OldNode.from_node(self) + old_node.timestamp = t + old_node.save_with_dependencies! + + 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 'node' + el1['id'] = self.id.to_s + el1['lat'] = self.lat.to_s + el1['lon'] = self.lon.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? + + 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['visible'] = self.visible.to_s + el1['timestamp'] = self.timestamp.xmlschema + return el1 + end + + def tags_as_hash + hash = {} + Tags.split(self.tags) do |k,v| + hash[k] = v + end + hash + 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 + @tags[k] = v + end + + + +end diff --git a/app/models/old_node.rb.pre-merge b/app/models/old_node.rb.pre-merge new file mode 100644 index 000000000..4f10e65a5 --- /dev/null +++ b/app/models/old_node.rb.pre-merge @@ -0,0 +1,88 @@ +class OldNode < GeoRecord + set_table_name '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 + + 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 + old_node.longitude = node.longitude + old_node.visible = node.visible + old_node.tags = node.tags + old_node.timestamp = node.timestamp + old_node.user_id = node.user_id + old_node.id = node.id + return old_node + 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? + + 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['visible'] = self.visible.to_s + el1['timestamp'] = self.timestamp.xmlschema + return el1 + end + + 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 = ?', self.id, self.timestamp]).instance_variable_get('@attributes')) + + sequence_id = 1 + self.tags.each do |k,v| + tag = OldNodeTag.new + tag.k = k + tag.v = v + tag.id = self.id + tag.version = self.version + tag.sequence_id = sequence_id + tag.save! + sequence_id += 1 + end + end + + 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 + + def tags=(t) + @tags = t + end + +end From 1a6230fb25f44491156dd20b07a4bfd8a86cca56 Mon Sep 17 00:00:00 2001 From: Nick Black Date: Sat, 3 May 2008 16:06:43 +0000 Subject: [PATCH 007/381] merged node and old node models for normalised node tags --- app/models/node.rb | 74 +++++++++++++++++++++++++++++++++--------- app/models/old_node.rb | 39 +++++++++++++++++++++- 2 files changed, 97 insertions(+), 16 deletions(-) diff --git a/app/models/node.rb b/app/models/node.rb index cc646b768..5a8e03ef1 100644 --- a/app/models/node.rb +++ b/app/models/node.rb @@ -1,5 +1,3 @@ -# The node model represents a current existing node, that is, the latest version. Use OldNode for historical nodes. - class Node < GeoRecord require 'xml/libxml' @@ -10,16 +8,21 @@ class Node < GeoRecord validates_numericality_of :latitude, :longitude validate :validate_position - has_many :ways, :through => :way_nodes has_many :old_nodes, :foreign_key => :id has_many :way_nodes + has_many :node_tags, :foreign_key => :id belongs_to :user - # 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? 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 + # # Search for nodes matching tags within bounding_box # @@ -80,10 +83,9 @@ class Node < GeoRecord tags = [] pt.find('tag').each do |tag| - tags << [tag['k'],tag['v']] + node.add_tag_key_val(tag['k'],tag['v']) end - node.tags = Tags.join(tags) end rescue node = nil @@ -92,24 +94,45 @@ class Node < GeoRecord return node end - # Save this node with the appropriate OldNode object to represent it's history. def save_with_history! - Node.transaction do - self.timestamp = Time.now + t = Time.now + + Node.transaction do + # apply timestamp to the new node + self.timestamp = t self.save! - old_node = OldNode.from_node(self) - old_node.save! end + + # Create a NodeTag + NodeTag.transaction do + tags = self.tags + + NodeTag.delete_all(['id = ?', self.id]) + + sequence_id = 1 + tags.each do |k,v| + tag = NodeTag.new + tag.k = k + tag.v = v + tag.id = self.id + tag.sequence_id = sequence_id + tag.save! + sequence_id += 1 + end + end + # Create an OldNode + old_node = OldNode.from_node(self) + old_node.timestamp = t + old_node.save_with_dependencies! + end - # Turn this Node in to a complete OSM XML object with wrapper 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 wrapper. def to_xml_node(user_display_name_cache = nil) el1 = XML::Node.new 'node' el1['id'] = self.id.to_s @@ -128,7 +151,7 @@ class Node < GeoRecord el1['user'] = user_display_name_cache[self.user_id] unless user_display_name_cache[self.user_id].nil? - 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 @@ -140,7 +163,6 @@ class Node < GeoRecord 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| @@ -148,4 +170,26 @@ class Node < GeoRecord end hash 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 + @tags[k] = v + end + + + end diff --git a/app/models/old_node.rb b/app/models/old_node.rb index 2c3e93b20..4f10e65a5 100644 --- a/app/models/old_node.rb +++ b/app/models/old_node.rb @@ -37,7 +37,7 @@ class OldNode < GeoRecord el1['lon'] = self.lon.to_s el1['user'] = self.user.display_name if self.user.data_public? - 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,4 +48,41 @@ class OldNode < GeoRecord el1['timestamp'] = self.timestamp.xmlschema return el1 end + + 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 = ?', self.id, self.timestamp]).instance_variable_get('@attributes')) + + sequence_id = 1 + self.tags.each do |k,v| + tag = OldNodeTag.new + tag.k = k + tag.v = v + tag.id = self.id + tag.version = self.version + tag.sequence_id = sequence_id + tag.save! + sequence_id += 1 + end + end + + 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 + + def tags=(t) + @tags = t + end + end From 6ae23bed90f249ed943cc43f1bad0e40852f04bb Mon Sep 17 00:00:00 2001 From: Gabriel Ebner Date: Sat, 3 May 2008 16:27:49 +0000 Subject: [PATCH 008/381] split_node_tags: - Use InnoDB. - Put version column on the current_* tables. - Use transactions (untested). --- app/controllers/node_controller.rb | 1 + app/controllers/relation_controller.rb | 1 + app/controllers/way_controller.rb | 1 + app/models/node.rb | 34 ++++++++++---------------- app/models/old_node.rb | 4 +-- app/models/old_relation.rb | 1 + app/models/old_way.rb | 1 + app/models/relation.rb | 5 +--- app/models/way.rb | 17 ++++--------- db/migrate/014_move_to_innodb.rb | 29 ++++++++++++++++++++++ 10 files changed, 54 insertions(+), 40 deletions(-) create mode 100644 db/migrate/014_move_to_innodb.rb diff --git a/app/controllers/node_controller.rb b/app/controllers/node_controller.rb index edc3675e5..9f8f4a38b 100644 --- a/app/controllers/node_controller.rb +++ b/app/controllers/node_controller.rb @@ -15,6 +15,7 @@ class NodeController < ApplicationController node = Node.from_xml(request.raw_post, true) if node + node.version = 0 node.user_id = @user.id node.visible = true node.save_with_history! diff --git a/app/controllers/relation_controller.rb b/app/controllers/relation_controller.rb index 2b1ba6c75..f4e938176 100644 --- a/app/controllers/relation_controller.rb +++ b/app/controllers/relation_controller.rb @@ -15,6 +15,7 @@ class RelationController < ApplicationController if !relation.preconditions_ok? render :text => "", :status => :precondition_failed else + relation.version = 0 relation.user_id = @user.id relation.save_with_history! diff --git a/app/controllers/way_controller.rb b/app/controllers/way_controller.rb index 3b6491cf0..a7f74e50c 100644 --- a/app/controllers/way_controller.rb +++ b/app/controllers/way_controller.rb @@ -15,6 +15,7 @@ class WayController < ApplicationController if !way.preconditions_ok? render :text => "", :status => :precondition_failed else + way.version = 0 way.user_id = @user.id way.save_with_history! diff --git a/app/models/node.rb b/app/models/node.rb index 5a8e03ef1..17521428e 100644 --- a/app/models/node.rb +++ b/app/models/node.rb @@ -96,35 +96,27 @@ class Node < GeoRecord def save_with_history! t = Time.now - - Node.transaction do - # apply timestamp to the new node + Node.transaction do + self.version += 1 self.timestamp = t self.save! - end - # Create a NodeTag - NodeTag.transaction do + # Create a NodeTag tags = self.tags - NodeTag.delete_all(['id = ?', self.id]) - - sequence_id = 1 tags.each do |k,v| - tag = NodeTag.new - tag.k = k - tag.v = v - tag.id = self.id - tag.sequence_id = sequence_id - tag.save! - sequence_id += 1 + tag = NodeTag.new + tag.k = k + tag.v = v + tag.id = self.id + tag.save! end - end - # Create an OldNode - old_node = OldNode.from_node(self) - old_node.timestamp = t - old_node.save_with_dependencies! + # Create an OldNode + old_node = OldNode.from_node(self) + old_node.timestamp = t + old_node.save_with_dependencies! + end end def to_xml diff --git a/app/models/old_node.rb b/app/models/old_node.rb index 4f10e65a5..2f960d886 100644 --- a/app/models/old_node.rb +++ b/app/models/old_node.rb @@ -27,6 +27,7 @@ class OldNode < GeoRecord old_node.timestamp = node.timestamp old_node.user_id = node.user_id old_node.id = node.id + old_node.version = node.version return old_node end @@ -57,16 +58,13 @@ class OldNode < GeoRecord #ok from here @attributes.update(OldNode.find(:first, :conditions => ['id = ? AND timestamp = ?', self.id, self.timestamp]).instance_variable_get('@attributes')) - sequence_id = 1 self.tags.each do |k,v| tag = OldNodeTag.new tag.k = k tag.v = v tag.id = self.id tag.version = self.version - tag.sequence_id = sequence_id tag.save! - sequence_id += 1 end end diff --git a/app/models/old_relation.rb b/app/models/old_relation.rb index 6da7814c2..076c03eec 100644 --- a/app/models/old_relation.rb +++ b/app/models/old_relation.rb @@ -9,6 +9,7 @@ class OldRelation < ActiveRecord::Base old_relation.user_id = relation.user_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 diff --git a/app/models/old_way.rb b/app/models/old_way.rb index a2b165e42..cdc0c4717 100644 --- a/app/models/old_way.rb +++ b/app/models/old_way.rb @@ -9,6 +9,7 @@ class OldWay < ActiveRecord::Base old_way.user_id = way.user_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 diff --git a/app/models/relation.rb b/app/models/relation.rb index 61344bdfb..3a9c0d9d5 100644 --- a/app/models/relation.rb +++ b/app/models/relation.rb @@ -167,13 +167,12 @@ class Relation < ActiveRecord::Base def save_with_history! Relation.transaction do t = Time.now + self.version += 1 self.timestamp = t self.save! tags = self.tags - RelationTag.delete_all(['id = ?', self.id]) - tags.each do |k,v| tag = RelationTag.new tag.k = k @@ -183,9 +182,7 @@ class Relation < ActiveRecord::Base end members = self.members - RelationMember.delete_all(['id = ?', self.id]) - members.each do |n| mem = RelationMember.new mem.id = self.id diff --git a/app/models/way.rb b/app/models/way.rb index f1dc76eb4..56c0717a7 100644 --- a/app/models/way.rb +++ b/app/models/way.rb @@ -158,15 +158,12 @@ class Way < ActiveRecord::Base t = Time.now Way.transaction do + self.version += 1 self.timestamp = t self.save! - end - WayTag.transaction do tags = self.tags - WayTag.delete_all(['id = ?', self.id]) - tags.each do |k,v| tag = WayTag.new tag.k = k @@ -174,13 +171,9 @@ class Way < ActiveRecord::Base tag.id = self.id tag.save! end - end - WayNode.transaction do nds = self.nds - WayNode.delete_all(['id = ?', self.id]) - sequence = 1 nds.each do |n| nd = WayNode.new @@ -189,11 +182,11 @@ class Way < ActiveRecord::Base nd.save! sequence += 1 end - end - old_way = OldWay.from_way(self) - old_way.timestamp = t - old_way.save_with_dependencies! + old_way = OldWay.from_way(self) + old_way.timestamp = t + old_way.save_with_dependencies! + end end def preconditions_ok? diff --git a/db/migrate/014_move_to_innodb.rb b/db/migrate/014_move_to_innodb.rb new file mode 100644 index 000000000..38c741cca --- /dev/null +++ b/db/migrate/014_move_to_innodb.rb @@ -0,0 +1,29 @@ +class MoveToInnodb < ActiveRecord::Migration + @@conv_tables = ['nodes', 'ways', 'way_tags', 'way_nodes', + 'current_way_nodes', 'relation_members', 'relations', + 'relation_tags', 'current_relation_tags'] + + @@ver_tbl = ['nodes', 'ways', 'relations'] + + def self.up + execute 'DROP INDEX current_way_tags_v_idx ON current_way_tags' + execute 'DROP INDEX current_relation_tags_v_idx ON current_relation_tags' + + @@ver_tbl.each { |tbl| + change_column tbl, "version", :bigint, :limit => 20, :null => false + } + + @@conv_tables.each { |tbl| + execute "ALTER TABLE #{tbl} ENGINE = InnoDB" + } + + @@ver_tbl.each { |tbl| + add_column "current_#{tbl}", "version", :bigint, :limit => 20, :null => false + execute "UPDATE current_#{tbl} SET version = " + + "(SELECT max(version)+1 FROM #{tbl} WHERE #{tbl}.id = current_#{tbl}.id)" + } + end + + def self.down + end +end From de90f11f53bba00665ce3526e58857bc95769576 Mon Sep 17 00:00:00 2001 From: Gabriel Ebner Date: Sat, 3 May 2008 16:28:50 +0000 Subject: [PATCH 009/381] split_node_tags: Remove the left-over pre-merge files. --- app/models/node.rb.pre-merge | 195 ------------------------------- app/models/old_node.rb.pre-merge | 88 -------------- 2 files changed, 283 deletions(-) delete mode 100644 app/models/node.rb.pre-merge delete mode 100644 app/models/old_node.rb.pre-merge diff --git a/app/models/node.rb.pre-merge b/app/models/node.rb.pre-merge deleted file mode 100644 index 5a8e03ef1..000000000 --- a/app/models/node.rb.pre-merge +++ /dev/null @@ -1,195 +0,0 @@ -class Node < GeoRecord - require 'xml/libxml' - - 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 - - has_many :old_nodes, :foreign_key => :id - has_many :way_nodes - has_many :node_tags, :foreign_key => :id - belongs_to :user - - 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 - - # - # Search for nodes matching tags within bounding_box - # - # Also adheres to limitations such as within max_number_of_nodes - # - def self.search(bounding_box, tags = {}) - min_lon, min_lat, max_lon, max_lat = *bounding_box - # @fixme a bit of a hack to search for only visible nodes - # couldn't think of another to add to tags condition - #conditions_hash = tags.merge({ 'visible' => 1 }) - - # using named placeholders http://www.robbyonrails.com/articles/2005/10/21/using-named-placeholders-in-ruby - #keys = [] - #values = {} - - #conditions_hash.each do |key,value| - # keys << "#{key} = :#{key}" - # values[key.to_sym] = value - #end - #conditions = keys.join(' AND ') - - find_by_area(min_lat, min_lon, max_lat, max_lon, - :conditions => 'visible = 1', - :limit => APP_CONFIG['max_number_of_nodes']+1) - end - - # Read in xml as text and return it's Node object representation - def self.from_xml(xml, create=false) - begin - 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 nil unless node.in_world? - - unless create - if pt['id'] != '0' - node.id = pt['id'].to_i - end - end - - node.visible = pt['visible'] and pt['visible'] == 'true' - - if create - node.timestamp = Time.now - else - if pt['timestamp'] - node.timestamp = Time.parse(pt['timestamp']) - end - end - - tags = [] - - pt.find('tag').each do |tag| - node.add_tag_key_val(tag['k'],tag['v']) - end - - end - rescue - node = nil - end - - return node - end - - def save_with_history! - t = Time.now - - Node.transaction do - # apply timestamp to the new node - self.timestamp = t - self.save! - end - - # Create a NodeTag - NodeTag.transaction do - tags = self.tags - - NodeTag.delete_all(['id = ?', self.id]) - - sequence_id = 1 - tags.each do |k,v| - tag = NodeTag.new - tag.k = k - tag.v = v - tag.id = self.id - tag.sequence_id = sequence_id - tag.save! - sequence_id += 1 - end - end - # Create an OldNode - old_node = OldNode.from_node(self) - old_node.timestamp = t - old_node.save_with_dependencies! - - 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 'node' - el1['id'] = self.id.to_s - el1['lat'] = self.lat.to_s - el1['lon'] = self.lon.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? - - 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['visible'] = self.visible.to_s - el1['timestamp'] = self.timestamp.xmlschema - return el1 - end - - def tags_as_hash - hash = {} - Tags.split(self.tags) do |k,v| - hash[k] = v - end - hash - 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 - @tags[k] = v - end - - - -end diff --git a/app/models/old_node.rb.pre-merge b/app/models/old_node.rb.pre-merge deleted file mode 100644 index 4f10e65a5..000000000 --- a/app/models/old_node.rb.pre-merge +++ /dev/null @@ -1,88 +0,0 @@ -class OldNode < GeoRecord - set_table_name '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 - - 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 - old_node.longitude = node.longitude - old_node.visible = node.visible - old_node.tags = node.tags - old_node.timestamp = node.timestamp - old_node.user_id = node.user_id - old_node.id = node.id - return old_node - 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? - - 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['visible'] = self.visible.to_s - el1['timestamp'] = self.timestamp.xmlschema - return el1 - end - - 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 = ?', self.id, self.timestamp]).instance_variable_get('@attributes')) - - sequence_id = 1 - self.tags.each do |k,v| - tag = OldNodeTag.new - tag.k = k - tag.v = v - tag.id = self.id - tag.version = self.version - tag.sequence_id = sequence_id - tag.save! - sequence_id += 1 - end - end - - 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 - - def tags=(t) - @tags = t - end - -end From 71e390bf3153dbe886b385c0d1195db26333ed8b Mon Sep 17 00:00:00 2001 From: Gabriel Ebner Date: Sat, 3 May 2008 16:40:51 +0000 Subject: [PATCH 010/381] split_node_tags: Forgot to migrate current_way_tags to InnoDB. --- db/migrate/014_move_to_innodb.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/db/migrate/014_move_to_innodb.rb b/db/migrate/014_move_to_innodb.rb index 38c741cca..57065a594 100644 --- a/db/migrate/014_move_to_innodb.rb +++ b/db/migrate/014_move_to_innodb.rb @@ -1,7 +1,7 @@ class MoveToInnodb < ActiveRecord::Migration @@conv_tables = ['nodes', 'ways', 'way_tags', 'way_nodes', - 'current_way_nodes', 'relation_members', 'relations', - 'relation_tags', 'current_relation_tags'] + 'current_way_nodes', 'current_way_tags', 'relation_members', + 'relations', 'relation_tags', 'current_relation_tags'] @@ver_tbl = ['nodes', 'ways', 'relations'] @@ -25,5 +25,6 @@ class MoveToInnodb < ActiveRecord::Migration end def self.down + raise IrreversibleMigration.new end end From ba01b66242148eeabea00d687a82654e02858ef4 Mon Sep 17 00:00:00 2001 From: Gabriel Ebner Date: Sun, 4 May 2008 08:25:29 +0000 Subject: [PATCH 011/381] split_node_tags: Implement key constraints. --- db/migrate/015_key_constraints.rb | 41 +++++++++++++++++++++++++++++++ lib/migrate.rb | 10 ++++++++ 2 files changed, 51 insertions(+) create mode 100644 db/migrate/015_key_constraints.rb diff --git a/db/migrate/015_key_constraints.rb b/db/migrate/015_key_constraints.rb new file mode 100644 index 000000000..6a070b2da --- /dev/null +++ b/db/migrate/015_key_constraints.rb @@ -0,0 +1,41 @@ +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] + + # 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/lib/migrate.rb b/lib/migrate.rb index 1d32d175d..26e95a496 100644 --- a/lib/migrate.rb +++ b/lib/migrate.rb @@ -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) From 6c31dbaaec8dcd86682acf404be5f80fc8c0a352 Mon Sep 17 00:00:00 2001 From: Gabriel Ebner Date: Sun, 4 May 2008 09:04:40 +0000 Subject: [PATCH 012/381] split_node_tags: Remove old unused files. --- app/models/temp_old_node.rb | 50 ------------------------------- lib/tasks/populate_node_tags.rake | 42 -------------------------- 2 files changed, 92 deletions(-) delete mode 100644 app/models/temp_old_node.rb delete mode 100644 lib/tasks/populate_node_tags.rake diff --git a/app/models/temp_old_node.rb b/app/models/temp_old_node.rb deleted file mode 100644 index a024eaac7..000000000 --- a/app/models/temp_old_node.rb +++ /dev/null @@ -1,50 +0,0 @@ -class TempOldNode < ActiveRecord::Base - set_table_name 'temp_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 - - def validate_position - errors.add_to_base("Node is not in the world") unless in_world? - end - - def in_world? - return true - end - - def self.from_node(node) - old_node = OldNode.new - old_node.latitude = node.latitude - old_node.longitude = node.longitude - old_node.visible = node.visible - old_node.tags = node.tags - old_node.timestamp = node.timestamp - old_node.user_id = node.user_id - old_node.id = node.id - return old_node - 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? - - Tags.split(self.tags) do |k,v| - el2 = XML::Node.new('tag') - el2['k'] = k.to_s - el2['v'] = v.to_s - el1 << el2 - end - - el1['visible'] = self.visible.to_s - el1['timestamp'] = self.timestamp.xmlschema - return el1 - end - -end diff --git a/lib/tasks/populate_node_tags.rake b/lib/tasks/populate_node_tags.rake deleted file mode 100644 index 86747cfe4..000000000 --- a/lib/tasks/populate_node_tags.rake +++ /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 From 9f177a7a1ad0c626d708c040eb65542d723bf8e8 Mon Sep 17 00:00:00 2001 From: Gabriel Ebner Date: Sun, 4 May 2008 09:14:41 +0000 Subject: [PATCH 013/381] Rename split_node_tags branch to api06. From bcbf417796c9da00397237d6e3808859d5d3adca Mon Sep 17 00:00:00 2001 From: Gabriel Ebner Date: Sun, 4 May 2008 10:01:23 +0000 Subject: [PATCH 014/381] api06: Return version ids in XML responses. --- app/models/node.rb | 1 + app/models/old_node.rb | 1 + app/models/old_relation.rb | 1 + app/models/old_way.rb | 1 + app/models/relation.rb | 1 + app/models/way.rb | 1 + 6 files changed, 6 insertions(+) diff --git a/app/models/node.rb b/app/models/node.rb index 17521428e..5e5e7a0b2 100644 --- a/app/models/node.rb +++ b/app/models/node.rb @@ -152,6 +152,7 @@ class Node < GeoRecord el1['visible'] = self.visible.to_s el1['timestamp'] = self.timestamp.xmlschema + el1['version'] = self.version.to_s return el1 end diff --git a/app/models/old_node.rb b/app/models/old_node.rb index 2f960d886..247a69923 100644 --- a/app/models/old_node.rb +++ b/app/models/old_node.rb @@ -47,6 +47,7 @@ class OldNode < GeoRecord el1['visible'] = self.visible.to_s el1['timestamp'] = self.timestamp.xmlschema + el1['version'] = self.version.to_s return el1 end diff --git a/app/models/old_relation.rb b/app/models/old_relation.rb index 076c03eec..03d5aebff 100644 --- a/app/models/old_relation.rb +++ b/app/models/old_relation.rb @@ -92,6 +92,7 @@ class OldRelation < ActiveRecord::Base el1['visible'] = self.visible.to_s el1['timestamp'] = self.timestamp.xmlschema el1['user'] = self.user.display_name if self.user.data_public? + el1['version'] = self.version.to_s self.old_members.each do |member| e = XML::Node.new 'member' diff --git a/app/models/old_way.rb b/app/models/old_way.rb index cdc0c4717..136e64793 100644 --- a/app/models/old_way.rb +++ b/app/models/old_way.rb @@ -95,6 +95,7 @@ class OldWay < ActiveRecord::Base el1['visible'] = self.visible.to_s el1['timestamp'] = self.timestamp.xmlschema el1['user'] = self.user.display_name if self.user.data_public? + el1['version'] = self.version.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' diff --git a/app/models/relation.rb b/app/models/relation.rb index 3a9c0d9d5..ec8c92c92 100644 --- a/app/models/relation.rb +++ b/app/models/relation.rb @@ -58,6 +58,7 @@ 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 user_display_name_cache = {} if user_display_name_cache.nil? diff --git a/app/models/way.rb b/app/models/way.rb index 56c0717a7..104c550a9 100644 --- a/app/models/way.rb +++ b/app/models/way.rb @@ -70,6 +70,7 @@ 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 user_display_name_cache = {} if user_display_name_cache.nil? From 9274a6010a03bbccdeb471cea5e0be319bb735c7 Mon Sep 17 00:00:00 2001 From: Gabriel Ebner Date: Sun, 4 May 2008 10:04:07 +0000 Subject: [PATCH 015/381] api06: Fix the version-numbers-on-current-tables migration: We want the rows in the current table to have the same version number as the latest one in the history table (the latest version is kept in both the current and the history tables.) --- db/migrate/014_move_to_innodb.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/migrate/014_move_to_innodb.rb b/db/migrate/014_move_to_innodb.rb index 57065a594..da6442dab 100644 --- a/db/migrate/014_move_to_innodb.rb +++ b/db/migrate/014_move_to_innodb.rb @@ -20,7 +20,7 @@ class MoveToInnodb < ActiveRecord::Migration @@ver_tbl.each { |tbl| add_column "current_#{tbl}", "version", :bigint, :limit => 20, :null => false execute "UPDATE current_#{tbl} SET version = " + - "(SELECT max(version)+1 FROM #{tbl} WHERE #{tbl}.id = current_#{tbl}.id)" + "(SELECT max(version) FROM #{tbl} WHERE #{tbl}.id = current_#{tbl}.id)" } end From 921518274f3f4b24280023553749adc987ab448a Mon Sep 17 00:00:00 2001 From: Tom Hughes Date: Sun, 4 May 2008 10:35:59 +0000 Subject: [PATCH 016/381] When adding new primary keys, drop the old indexes which are no longer needed as they are a prefix of the new primary key. --- db/migrate/015_key_constraints.rb | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/db/migrate/015_key_constraints.rb b/db/migrate/015_key_constraints.rb index 6a070b2da..40f98be02 100644 --- a/db/migrate/015_key_constraints.rb +++ b/db/migrate/015_key_constraints.rb @@ -11,6 +11,15 @@ class KeyConstraints < ActiveRecord::Migration 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 From 8c5638d6968be3286414f04b8dee2261ada84e76 Mon Sep 17 00:00:00 2001 From: Frederik Ramm Date: Sun, 4 May 2008 10:41:42 +0000 Subject: [PATCH 017/381] initial changeset support --- app/controllers/changeset_controller.rb | 26 ++++++++ app/controllers/changeset_tag_controller.rb | 9 +++ app/models/changeset.rb | 73 +++++++++++++++++++++ app/models/changeset_tag.rb | 5 ++ config/database.yml | 14 ++-- config/environment.rb | 4 +- config/routes.rb | 2 + db/migrate/013_add_changesets.rb | 32 +++++++++ 8 files changed, 156 insertions(+), 9 deletions(-) create mode 100644 app/controllers/changeset_controller.rb create mode 100644 app/controllers/changeset_tag_controller.rb create mode 100644 app/models/changeset.rb create mode 100644 app/models/changeset_tag.rb create mode 100644 db/migrate/013_add_changesets.rb diff --git a/app/controllers/changeset_controller.rb b/app/controllers/changeset_controller.rb new file mode 100644 index 000000000..1041c7469 --- /dev/null +++ b/app/controllers/changeset_controller.rb @@ -0,0 +1,26 @@ +# The ChangesetController is the RESTful interface to Changeset objects + +class ChangesetController < ApplicationController + require 'xml/libxml' + + before_filter :authorize, :only => [:create, :update, :delete] + before_filter :check_write_availability, :only => [:create, :update, :delete] + before_filter :check_read_availability, :except => [:create, :update, :delete] + + # 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 +end diff --git a/app/controllers/changeset_tag_controller.rb b/app/controllers/changeset_tag_controller.rb new file mode 100644 index 000000000..3e8db3fc2 --- /dev/null +++ b/app/controllers/changeset_tag_controller.rb @@ -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 diff --git a/app/models/changeset.rb b/app/models/changeset.rb new file mode 100644 index 000000000..85621dce2 --- /dev/null +++ b/app/models/changeset.rb @@ -0,0 +1,73 @@ +class Changeset < ActiveRecord::Base + require 'xml/libxml' + + belongs_to :user + + has_many :changeset_tags, :foreign_key => 'id' + + 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 + end + + pt.find('tag').each do |tag| + cs.add_tag_keyval(tag['k'], tag['v']) + end + end + rescue Exception => ex + print "noes "+ ex.to_s + "\n" + cs = nil + end + + return cs + 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 + + Changeset.transaction do + # fixme update modified_at time? + self.save! + end + + ChangesetTag.transaction do + 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 +end diff --git a/app/models/changeset_tag.rb b/app/models/changeset_tag.rb new file mode 100644 index 000000000..6298fbe77 --- /dev/null +++ b/app/models/changeset_tag.rb @@ -0,0 +1,5 @@ +class ChangesetTag < ActiveRecord::Base + + belongs_to :changeset, :foreign_key => 'id' + +end diff --git a/config/database.yml b/config/database.yml index b884f3b93..c0b5dd98c 100644 --- a/config/database.yml +++ b/config/database.yml @@ -12,9 +12,9 @@ # http://dev.mysql.com/doc/refman/5.0/en/old-client.html development: adapter: mysql - database: openstreetmap - username: openstreetmap - password: openstreetmap + database: osm + username: osm + password: osm host: localhost # Warning: The database defined as 'test' will be erased and @@ -29,8 +29,8 @@ test: production: adapter: mysql - database: openstreetmap - username: openstreetmap - password: openstreetmap - host: db.openstreetmap.org + database: osm + username: osm + password: osm + host: localhost diff --git a/config/environment.rb b/config/environment.rb index 02d99f20c..45c9ed4d6 100644 --- a/config/environment.rb +++ b/config/environment.rb @@ -6,13 +6,13 @@ ENV['RAILS_ENV'] ||= 'production' # Specifies gem version of Rails to use when vendor/rails is not present # DO NOT BUMP THIS TO 2.0.2 AS THE LIVE SERVERS CAN'T RUN THAT -RAILS_GEM_VERSION = '2.0.1' unless defined? RAILS_GEM_VERSION +RAILS_GEM_VERSION = '2.0.2' unless defined? RAILS_GEM_VERSION # Set the server URL SERVER_URL = ENV['OSM_SERVER_URL'] || 'www.openstreetmap.org' # 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 to :readonly to put the API in read-only mode or :offline to # take it completely offline diff --git a/config/routes.rb b/config/routes.rb index 2ac48cc29..3b7af885d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,6 +1,8 @@ ActionController::Routing::Routes.draw do |map| # API + map.connect "api/#{API_VERSION}/changeset/create", :controller => 'changeset', :action => 'create' + 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+/ diff --git a/db/migrate/013_add_changesets.rb b/db/migrate/013_add_changesets.rb new file mode 100644 index 000000000..40455ec68 --- /dev/null +++ b/db/migrate/013_add_changesets.rb @@ -0,0 +1,32 @@ +class AddChangesets < ActiveRecord::Migration + def self.up + create_table "changesets", innodb_table do |t| + t.column "id", :bigint, :limit => 20, :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 + + add_primary_key "changesets", ["id"] + # FIXME add indexes? + + change_column "changesets", "id", :bigint, :limit => 20, :null => false, :options => "AUTO_INCREMENT" + + 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" + end + + def self.down + drop_table "changesets" + drop_table "changeset_tags" + end +end From adcaeeb90b023dcea17e4ba7a85cf32ffa39961f Mon Sep 17 00:00:00 2001 From: Gabriel Ebner Date: Sun, 4 May 2008 10:47:39 +0000 Subject: [PATCH 018/381] api06: Move add-changeset migr. to no. 16. --- db/migrate/{013_add_changesets.rb => 016_add_changesets.rb} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename db/migrate/{013_add_changesets.rb => 016_add_changesets.rb} (100%) diff --git a/db/migrate/013_add_changesets.rb b/db/migrate/016_add_changesets.rb similarity index 100% rename from db/migrate/013_add_changesets.rb rename to db/migrate/016_add_changesets.rb From 0ef056ffbcaa0ec5042478f53f1e5eab4403027d Mon Sep 17 00:00:00 2001 From: Tom Hughes Date: Sun, 4 May 2008 11:32:49 +0000 Subject: [PATCH 019/381] Stick with rails 2.0.1... --- config/environment.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/environment.rb b/config/environment.rb index 45c9ed4d6..8aaab7302 100644 --- a/config/environment.rb +++ b/config/environment.rb @@ -6,7 +6,7 @@ ENV['RAILS_ENV'] ||= 'production' # Specifies gem version of Rails to use when vendor/rails is not present # DO NOT BUMP THIS TO 2.0.2 AS THE LIVE SERVERS CAN'T RUN THAT -RAILS_GEM_VERSION = '2.0.2' unless defined? RAILS_GEM_VERSION +RAILS_GEM_VERSION = '2.0.1' unless defined? RAILS_GEM_VERSION # Set the server URL SERVER_URL = ENV['OSM_SERVER_URL'] || 'www.openstreetmap.org' From ead1817e11853165a0f7388e250dc96536daea00 Mon Sep 17 00:00:00 2001 From: Tom Hughes Date: Sun, 4 May 2008 11:33:09 +0000 Subject: [PATCH 020/381] Add timestamp indexes to current_ways and current_relations. --- db/migrate/012_add_timestamp_indexes.rb | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 db/migrate/012_add_timestamp_indexes.rb diff --git a/db/migrate/012_add_timestamp_indexes.rb b/db/migrate/012_add_timestamp_indexes.rb new file mode 100644 index 000000000..c6b3bc7c2 --- /dev/null +++ b/db/migrate/012_add_timestamp_indexes.rb @@ -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 From 7b2c9cd774043783e5ba387dcc90aed9fbeb76c7 Mon Sep 17 00:00:00 2001 From: Frederik Ramm Date: Sun, 4 May 2008 11:43:04 +0000 Subject: [PATCH 021/381] buffer overrun fix --- db/migrate/013_populate_node_tags_and_remove_helper.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/migrate/013_populate_node_tags_and_remove_helper.c b/db/migrate/013_populate_node_tags_and_remove_helper.c index b1868ef24..8150e4da8 100644 --- a/db/migrate/013_populate_node_tags_and_remove_helper.c +++ b/db/migrate/013_populate_node_tags_and_remove_helper.c @@ -201,7 +201,7 @@ int main(int argc, char **argv) { d->version = malloc(sizeof(uint32_t) * d->version_size); prefix_len = strlen(argv[7]); - tempfn = (char *) malloc(prefix_len + 16); + tempfn = (char *) malloc(prefix_len + 32); strcpy(tempfn, argv[7]); strcpy(tempfn + prefix_len, "current_nodes"); From e0e5423fb9faf5500626ce7ec2f8f823c4b484f1 Mon Sep 17 00:00:00 2001 From: Gabriel Ebner Date: Sun, 4 May 2008 11:44:36 +0000 Subject: [PATCH 022/381] api06: 013_populate_node_tags_and_remove_helper: Initialize memory. --- db/migrate/013_populate_node_tags_and_remove_helper.c | 1 + 1 file changed, 1 insertion(+) diff --git a/db/migrate/013_populate_node_tags_and_remove_helper.c b/db/migrate/013_populate_node_tags_and_remove_helper.c index 8150e4da8..fd000343f 100644 --- a/db/migrate/013_populate_node_tags_and_remove_helper.c +++ b/db/migrate/013_populate_node_tags_and_remove_helper.c @@ -199,6 +199,7 @@ int main(int argc, char **argv) { d->version_size = 1 + select_size(d->mysql, "SELECT max(id) FROM current_nodes"); d->version = malloc(sizeof(uint32_t) * d->version_size); + memset(d->version, 0, sizeof(uint32_t) * d->version_size); prefix_len = strlen(argv[7]); tempfn = (char *) malloc(prefix_len + 32); From 2b596387d2d0ede41a77d36c17195b1a85a6e7b0 Mon Sep 17 00:00:00 2001 From: Gabriel Ebner Date: Sun, 4 May 2008 11:50:12 +0000 Subject: [PATCH 023/381] api06: Fix bounds check. --- db/migrate/013_populate_node_tags_and_remove_helper.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/migrate/013_populate_node_tags_and_remove_helper.c b/db/migrate/013_populate_node_tags_and_remove_helper.c index fd000343f..26a8f71df 100644 --- a/db/migrate/013_populate_node_tags_and_remove_helper.c +++ b/db/migrate/013_populate_node_tags_and_remove_helper.c @@ -105,7 +105,7 @@ static void proc_nodes(struct data *d, const char *tbl, FILE *out, FILE *out_tag unsigned long id = strtoul(row[0], NULL, 10); uint32_t version; - if (id > d->version_size) { + if (id >= d->version_size) { fprintf(stderr, "preallocated nodes size exceeded"); abort(); } From db52fe692315470d22e525ce37da32fd207abbb3 Mon Sep 17 00:00:00 2001 From: Gabriel Ebner Date: Sun, 4 May 2008 12:03:05 +0000 Subject: [PATCH 024/381] api06: Use 2-byte ints and check malloc return value. --- db/migrate/013_populate_node_tags_and_remove_helper.c | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/db/migrate/013_populate_node_tags_and_remove_helper.c b/db/migrate/013_populate_node_tags_and_remove_helper.c index 26a8f71df..e0e8bb3f7 100644 --- a/db/migrate/013_populate_node_tags_and_remove_helper.c +++ b/db/migrate/013_populate_node_tags_and_remove_helper.c @@ -85,7 +85,7 @@ static int read_node_tags(char **tags, char **k, char **v) { struct data { MYSQL *mysql; size_t version_size; - uint32_t *version; + uint16_t *version; }; static void proc_nodes(struct data *d, const char *tbl, FILE *out, FILE *out_tags, int hist) { @@ -198,8 +198,13 @@ int main(int argc, char **argv) { d->mysql = connect_to_mysql(argv); d->version_size = 1 + select_size(d->mysql, "SELECT max(id) FROM current_nodes"); - d->version = malloc(sizeof(uint32_t) * d->version_size); - memset(d->version, 0, sizeof(uint32_t) * d->version_size); + 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); From 0499559f79926c3a6f1339e93b012637df1c292c Mon Sep 17 00:00:00 2001 From: Gabriel Ebner Date: Sun, 4 May 2008 13:51:45 +0000 Subject: [PATCH 025/381] api06: Fix tag parsing in the node-tags-normalization helper, as suggested by Jon Burgess. --- db/migrate/013_populate_node_tags_and_remove_helper.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/db/migrate/013_populate_node_tags_and_remove_helper.c b/db/migrate/013_populate_node_tags_and_remove_helper.c index e0e8bb3f7..5a0fbb6cd 100644 --- a/db/migrate/013_populate_node_tags_and_remove_helper.c +++ b/db/migrate/013_populate_node_tags_and_remove_helper.c @@ -45,15 +45,16 @@ static void write_csv_col(FILE *f, const char *str, char end) { } static void unescape(char *str) { - char *i = str, *o = str; + char *i = str, *o = str, tmp; while (*i) { if (*i == '\\') { i++; - switch (*i++) { + switch (tmp = *i++) { case 's': *o++ = ';'; break; case 'e': *o++ = '='; break; case '\\': *o++ = '\\'; break; + default: *o++ = tmp; break; } } else { *o++ = *i++; From 3d6e1c67fb2291c6933f37bac3b9a3f518e8487a Mon Sep 17 00:00:00 2001 From: Gabriel Ebner Date: Sun, 4 May 2008 13:52:43 +0000 Subject: [PATCH 026/381] api06: Preliminary support for diff uploading. This will not return anything useful and the code will need some heavy refactoring. --- app/controllers/changeset_controller.rb | 94 ++++++++++++++++++++++++- app/controllers/node_controller.rb | 56 +++++++++------ app/controllers/relation_controller.rb | 35 +++++---- app/controllers/way_controller.rb | 24 +++++-- app/models/node.rb | 63 +++++++++-------- app/models/relation.rb | 50 +++++++------ app/models/way.rb | 50 +++++++------ config/routes.rb | 1 + 8 files changed, 256 insertions(+), 117 deletions(-) diff --git a/app/controllers/changeset_controller.rb b/app/controllers/changeset_controller.rb index 1041c7469..19d1591a0 100644 --- a/app/controllers/changeset_controller.rb +++ b/app/controllers/changeset_controller.rb @@ -3,9 +3,10 @@ class ChangesetController < ApplicationController require 'xml/libxml' - before_filter :authorize, :only => [:create, :update, :delete] - before_filter :check_write_availability, :only => [:create, :update, :delete] - before_filter :check_read_availability, :except => [:create, :update, :delete] + before_filter :authorize, :only => [:create, :update, :delete, :upload] + before_filter :check_write_availability, :only => [:create, :update, :delete, :upload] + before_filter :check_read_availability, :except => [:create, :update, :delete, :upload] + after_filter :compress_output # Create a changeset from XML. def create @@ -23,4 +24,91 @@ class ChangesetController < ApplicationController render :nothing => true, :status => :method_not_allowed end end + + def create_prim(ids, prim, nd) + prim.version = 0 + prim.user_id = @user.id + prim.visible = true + prim.save_with_history! + + ids[nd['id'].to_i] = prim.id + end + + def fix_way(w, node_ids) + w.nds.each { |nd| + new_id = node_ids[nd.node_id] + nd.node_id = new_id unless new_id.nil? + } + end + + def fix_rel(r, ids) + r.members.each { |memb| + new_id = ids[memb.member_type][memb.member_id] + nd.member_id = new_id unless new_id.nil? + } + end + + def upload + if not request.put? + render :nothing => true, :status => :method_not_allowed + return + end + + # FIXME: this should really be done without loading the whole XML file + # into memory. + p = XML::Parser.new + p.string = request.raw_post + doc = p.parse + + node_ids, way_ids, rel_ids = {}, {}, {} + ids = {"node"=>node_ids, "way"=>way_ids, "relation"=>rel_ids} + + Changeset.transaction do + doc.find('//osm/create/node').each do |nd| + create_prim node_ids, Node.from_xml_node(nd, true), nd + end + doc.find('//osm/create/way').each do |nd| + way = Way.from_xml_node(nd, true) + raise OSM::APIPreconditionFailedError.new if !way.preconditions_ok? + create_prim way_ids, fix_way(way, node_ids), nd + end + doc.find('//osm/create/relation').each do |nd| + relation = Relation.from_xml_node(nd, true) + raise OSM::APIPreconditionFailedError.new if !way.preconditions_ok? + create_prim relation_ids, fix_rel(relation, ids), nd + end + + doc.find('//osm/modify/node').each do |nd| + unless NodeController.update_internal nil, Node.from_xml_node(nd) + raise OSM::APIPreconditionFailedError.new + end + end + doc.find('//osm/modify/way').each do |nd| + unless WayController.update_internal nil, fix_way(Way.from_xml_node(nd), node_ids) + raise OSM::APIPreconditionFailedError.new + end + end + doc.find('//osm/modify/relation').each do |nd| + unless RelationController.update_internal nil, fix_rel(Relation.from_xml_node(nd), ids) + raise OSM::APIPreconditionFailedError.new + end + end + + doc.find('//osm/delete/node').each do |nd| + unless NodeController.delete_internal nil, Node.from_xml_node(n) + raise OSM::APIPreconditionFailedError.new + end + end + doc.find('//osm/delete/way').each do |nd| + Way.from_xml_node(nd).delete_with_relations_and_history(@user) + end + doc.find('//osm/delete/relation').each do |nd| + unless RelationController.delete_internal nil, fix_rel(Relation.from_xml_node(nd), ids) + raise OSM::APIPreconditionFailedError.new + end + end + end + + render :text => "Ok, Fine. Upload worked without errors.\n", :status => 200 + end end diff --git a/app/controllers/node_controller.rb b/app/controllers/node_controller.rb index 9f8f4a38b..ae5245016 100644 --- a/app/controllers/node_controller.rb +++ b/app/controllers/node_controller.rb @@ -51,13 +51,7 @@ 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! - + update_internal node, new_node render :nothing => true else render :nothing => true, :status => :bad_request @@ -67,32 +61,54 @@ class NodeController < ApplicationController end end + def update_internal(node, new_node) + node = Node.find(new_node.id) if node.nil? + + 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! + + return true + end + # Delete a node. Doesn't actually delete it, but retains its history in a wiki-like way. # FIXME remove all the fricking SQL 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 + res = delete_internal(node) + unless res + render :text => "", :status => :precondition_failed else - render :text => "", :status => :gone + render :text => "", :status => res end rescue ActiveRecord::RecordNotFound render :nothing => true, :status => :not_found end end + def delete_internal(node) + 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 ]) + return false + 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]]) + return false + else + node.user_id = @user.id + node.visible = 0 + node.save_with_history! + + return :ok + end + else + return :gone + end + end + # WTF does this do? def nodes ids = params['nodes'].split(',').collect { |n| n.to_i } diff --git a/app/controllers/relation_controller.rb b/app/controllers/relation_controller.rb index f4e938176..894ab40e8 100644 --- a/app/controllers/relation_controller.rb +++ b/app/controllers/relation_controller.rb @@ -77,20 +77,11 @@ class RelationController < ApplicationController 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 + res = delete_internal(node) + unless res + render :text => "", :status => :precondition_failed else - render :text => "", :status => :gone + render :text => "", :status => res end rescue ActiveRecord::RecordNotFound render :nothing => true, :status => :not_found @@ -99,6 +90,24 @@ class RelationController < ApplicationController end end + def delete_internal(relation) + 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]]) + return false + else + relation.user_id = @user.id + relation.tags = [] + relation.members = [] + relation.visible = false + relation.save_with_history! + + return :ok + end + else + return :gone + end + end + # ----------------------------------------------------------------- # full # diff --git a/app/controllers/way_controller.rb b/app/controllers/way_controller.rb index a7f74e50c..fd01008fa 100644 --- a/app/controllers/way_controller.rb +++ b/app/controllers/way_controller.rb @@ -51,15 +51,9 @@ 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? + unless update_internal(way, new_way) 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 else @@ -70,6 +64,22 @@ class WayController < ApplicationController end end + def update_internal way, new_way + way = Way.find(new_way.id) if way.nil? + + if !new_way.preconditions_ok? + return false + else + way.user_id = @user.id + way.tags = new_way.tags + way.nds = new_way.nds + way.visible = true + way.save_with_history! + + return true + end + end + # This is the API call to delete a way def delete begin diff --git a/app/models/node.rb b/app/models/node.rb index 5e5e7a0b2..872c5c922 100644 --- a/app/models/node.rb +++ b/app/models/node.rb @@ -55,40 +55,43 @@ class Node < GeoRecord 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 nil unless node.in_world? - - unless create - if pt['id'] != '0' - node.id = pt['id'].to_i - end - end - - node.visible = pt['visible'] and pt['visible'] == 'true' - - if create - node.timestamp = Time.now - else - if pt['timestamp'] - node.timestamp = Time.parse(pt['timestamp']) - end - end - - tags = [] - - pt.find('tag').each do |tag| - node.add_tag_key_val(tag['k'],tag['v']) - end - + return Node.from_xml_node(pt, create) end rescue - node = nil + return nil + end + end + + def self.from_xml_node(pt, create=false) + node = Node.new + + node.lat = pt['lat'].to_f + node.lon = pt['lon'].to_f + + return nil unless node.in_world? + + unless create + if pt['id'] != '0' + node.id = pt['id'].to_i + end + end + + node.visible = pt['visible'] and pt['visible'] == 'true' + + if create + node.timestamp = Time.now + else + if pt['timestamp'] + node.timestamp = Time.parse(pt['timestamp']) + end + end + + tags = [] + + pt.find('tag').each do |tag| + node.add_tag_key_val(tag['k'],tag['v']) end return node diff --git a/app/models/relation.rb b/app/models/relation.rb index ec8c92c92..2c155e9e1 100644 --- a/app/models/relation.rb +++ b/app/models/relation.rb @@ -16,29 +16,8 @@ class Relation < ActiveRecord::Base 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 - - if create - relation.timestamp = Time.now - relation.visible = true - else - if pt['timestamp'] - relation.timestamp = Time.parse(pt['timestamp']) - end - end - - pt.find('tag').each do |tag| - relation.add_tag_keyval(tag['k'], tag['v']) - end - - pt.find('member').each do |member| - relation.add_member(member['type'], member['ref'], member['role']) - end + relation = Relation.from_xml_node pt, create end rescue relation = nil @@ -47,6 +26,33 @@ class Relation < ActiveRecord::Base return relation end + def self.from_xml_node(pt, create=false) + relation = Relation.new + + if !create and pt['id'] != '0' + relation.id = pt['id'].to_i + end + + if create + relation.timestamp = Time.now + relation.visible = true + else + if pt['timestamp'] + relation.timestamp = Time.parse(pt['timestamp']) + end + end + + pt.find('tag').each do |tag| + relation.add_tag_keyval(tag['k'], tag['v']) + end + + pt.find('member').each do |member| + relation.add_member(member['type'], member['ref'], member['role']) + end + + return relation + end + def to_xml doc = OSM::API.new.get_xml_doc doc.root << to_xml_node() diff --git a/app/models/way.rb b/app/models/way.rb index 104c550a9..0c927c11c 100644 --- a/app/models/way.rb +++ b/app/models/way.rb @@ -17,29 +17,8 @@ class Way < ActiveRecord::Base 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 - - 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 + way = Way.from_xml_node pt, create end rescue way = nil @@ -48,6 +27,33 @@ class Way < ActiveRecord::Base return way end + def self.from_xml_node(pt, create=false) + way = Way.new + + 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 + + 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 + end + # Find a way given it's ID, and in a single SQL call also grab its nodes # diff --git a/config/routes.rb b/config/routes.rb index e4d68f87a..64293b34d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -2,6 +2,7 @@ ActionController::Routing::Routes.draw do |map| # API map.connect "api/#{API_VERSION}/changeset/create", :controller => 'changeset', :action => 'create' + map.connect "api/#{API_VERSION}/changeset/upload", :controller => 'changeset', :action => 'upload' 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+/ From 1bbc523e0f9e00ab99e647731e6da758b9f258ad Mon Sep 17 00:00:00 2001 From: Gabriel Ebner Date: Sun, 4 May 2008 14:37:12 +0000 Subject: [PATCH 027/381] api06: Fix diff uploading (creation works fine now) --- app/controllers/changeset_controller.rb | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/app/controllers/changeset_controller.rb b/app/controllers/changeset_controller.rb index 19d1591a0..34579d166 100644 --- a/app/controllers/changeset_controller.rb +++ b/app/controllers/changeset_controller.rb @@ -35,17 +35,15 @@ class ChangesetController < ApplicationController end def fix_way(w, node_ids) - w.nds.each { |nd| - new_id = node_ids[nd.node_id] - nd.node_id = new_id unless new_id.nil? - } + w.nds = w.instance_eval { @nds }. + map { |nd| node_ids[nd] || nd } + return w end def fix_rel(r, ids) - r.members.each { |memb| - new_id = ids[memb.member_type][memb.member_id] - nd.member_id = new_id unless new_id.nil? - } + r.members = r.instance_eval { @members }. + map { |memb| [memb[0], ids[memb[0]][memb[1].to_i] || memb[1], memb[2]] } + return r end def upload @@ -69,17 +67,19 @@ class ChangesetController < ApplicationController end doc.find('//osm/create/way').each do |nd| way = Way.from_xml_node(nd, true) + fix_way(way, node_ids) raise OSM::APIPreconditionFailedError.new if !way.preconditions_ok? - create_prim way_ids, fix_way(way, node_ids), nd + create_prim way_ids, way, nd end doc.find('//osm/create/relation').each do |nd| relation = Relation.from_xml_node(nd, true) - raise OSM::APIPreconditionFailedError.new if !way.preconditions_ok? - create_prim relation_ids, fix_rel(relation, ids), nd + fix_rel(relation, ids) + raise OSM::APIPreconditionFailedError.new if !relation.preconditions_ok? + create_prim rel_ids, relation, nd end doc.find('//osm/modify/node').each do |nd| - unless NodeController.update_internal nil, Node.from_xml_node(nd) + unless NodeController.new.update_internal nil, Node.from_xml_node(nd) raise OSM::APIPreconditionFailedError.new end end From e7e5ae7aaab85222b0cc6677d483b8027a47844f Mon Sep 17 00:00:00 2001 From: Tom Hughes Date: Sun, 4 May 2008 14:50:53 +0000 Subject: [PATCH 028/381] The current_way_nodes table is already an InnoDB table. --- db/migrate/014_move_to_innodb.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/migrate/014_move_to_innodb.rb b/db/migrate/014_move_to_innodb.rb index da6442dab..c551b0ef8 100644 --- a/db/migrate/014_move_to_innodb.rb +++ b/db/migrate/014_move_to_innodb.rb @@ -1,6 +1,6 @@ class MoveToInnodb < ActiveRecord::Migration @@conv_tables = ['nodes', 'ways', 'way_tags', 'way_nodes', - 'current_way_nodes', 'current_way_tags', 'relation_members', + 'current_way_tags', 'relation_members', 'relations', 'relation_tags', 'current_relation_tags'] @@ver_tbl = ['nodes', 'ways', 'relations'] From 6ebb7045fe53d831502f054cf5007843e023d094 Mon Sep 17 00:00:00 2001 From: Gabriel Ebner Date: Sun, 4 May 2008 15:16:58 +0000 Subject: [PATCH 029/381] api06: Fix diff uploading (still doesn't give a useful response): Modification and deletion works fine now. Rollback also works apparently. Just auto increment doesn't get reset. --- app/controllers/changeset_controller.rb | 33 +++++++----------- app/controllers/node_controller.rb | 45 ++++--------------------- app/controllers/relation_controller.rb | 44 +++++------------------- app/controllers/way_controller.rb | 25 +++----------- app/models/node.rb | 25 ++++++++++++++ app/models/relation.rb | 28 +++++++++++++++ app/models/way.rb | 14 ++++++++ 7 files changed, 98 insertions(+), 116 deletions(-) diff --git a/app/controllers/changeset_controller.rb b/app/controllers/changeset_controller.rb index 34579d166..585821a55 100644 --- a/app/controllers/changeset_controller.rb +++ b/app/controllers/changeset_controller.rb @@ -78,34 +78,27 @@ class ChangesetController < ApplicationController create_prim rel_ids, relation, nd end - doc.find('//osm/modify/node').each do |nd| - unless NodeController.new.update_internal nil, Node.from_xml_node(nd) - raise OSM::APIPreconditionFailedError.new - end + doc.find('//osm/modify/relation').each do |nd| + new_relation = Relation.from_xml_node(nd) + Relation.find(new_relation.id).update_from new_relation, @user end doc.find('//osm/modify/way').each do |nd| - unless WayController.update_internal nil, fix_way(Way.from_xml_node(nd), node_ids) - raise OSM::APIPreconditionFailedError.new - end + new_way = Way.from_xml_node(nd) + Way.find(new_way.id).update_from new_way, @user end - doc.find('//osm/modify/relation').each do |nd| - unless RelationController.update_internal nil, fix_rel(Relation.from_xml_node(nd), ids) - raise OSM::APIPreconditionFailedError.new - end + doc.find('//osm/modify/node').each do |nd| + new_node = Node.from_xml_node(nd) + Node.find(new_node.id).update_from new_node, @user end - doc.find('//osm/delete/node').each do |nd| - unless NodeController.delete_internal nil, Node.from_xml_node(n) - raise OSM::APIPreconditionFailedError.new - end + doc.find('//osm/delete/relation').each do |nd| + Relation.find(nd['id']).delete_with_history(@user) end doc.find('//osm/delete/way').each do |nd| - Way.from_xml_node(nd).delete_with_relations_and_history(@user) + Way.find(nd['id']).delete_with_relations_and_history(@user) end - doc.find('//osm/delete/relation').each do |nd| - unless RelationController.delete_internal nil, fix_rel(Relation.from_xml_node(nd), ids) - raise OSM::APIPreconditionFailedError.new - end + doc.find('//osm/delete/node').each do |nd| + Node.find(nd['id']).delete_with_history(@user) end end diff --git a/app/controllers/node_controller.rb b/app/controllers/node_controller.rb index ae5245016..f6a673fe5 100644 --- a/app/controllers/node_controller.rb +++ b/app/controllers/node_controller.rb @@ -51,7 +51,7 @@ class NodeController < ApplicationController new_node = Node.from_xml(request.raw_post) if new_node and new_node.id == node.id - update_internal node, new_node + node.update_from(new_node, @user) render :nothing => true else render :nothing => true, :status => :bad_request @@ -61,51 +61,18 @@ class NodeController < ApplicationController end end - def update_internal(node, new_node) - node = Node.find(new_node.id) if node.nil? - - 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! - - return true - end - # Delete a node. Doesn't actually delete it, but retains its history in a wiki-like way. # FIXME remove all the fricking SQL def delete begin node = Node.find(params[:id]) - - res = delete_internal(node) - unless res - render :text => "", :status => :precondition_failed - else - render :text => "", :status => res - end + node.delete_with_history(@user) rescue ActiveRecord::RecordNotFound render :nothing => true, :status => :not_found - end - end - - def delete_internal(node) - 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 ]) - return false - 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]]) - return false - else - node.user_id = @user.id - node.visible = 0 - node.save_with_history! - - return :ok - end - else - return :gone + rescue OSM::APIAlreadyDeletedError + render :text => "", :status => :gone + rescue OSM::APIPreconditionFailedError + render :text => "", :status => :precondition_failed end end diff --git a/app/controllers/relation_controller.rb b/app/controllers/relation_controller.rb index 894ab40e8..caacd4fb9 100644 --- a/app/controllers/relation_controller.rb +++ b/app/controllers/relation_controller.rb @@ -51,22 +51,14 @@ class RelationController < ApplicationController 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 else render :nothing => true, :status => :bad_request end rescue ActiveRecord::RecordNotFound render :nothing => true, :status => :not_found + rescue OSM::APIPreconditionFailedError + render :text => "", :status => :precondition_failed rescue render :nothing => true, :status => :internal_server_error end @@ -76,13 +68,11 @@ class RelationController < ApplicationController #XXX check if member somewhere! begin relation = Relation.find(params[:id]) - - res = delete_internal(node) - unless res - render :text => "", :status => :precondition_failed - else - render :text => "", :status => res - end + relation.delete_with_history(@user) + rescue OSM::APIAlreadyDeletedError + render :text => "", :status => :gone + rescue OSM::APIPreconditionFailedError + render :text => "", :status => :precondition_failed rescue ActiveRecord::RecordNotFound render :nothing => true, :status => :not_found rescue @@ -90,24 +80,6 @@ class RelationController < ApplicationController end end - def delete_internal(relation) - 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]]) - return false - else - relation.user_id = @user.id - relation.tags = [] - relation.members = [] - relation.visible = false - relation.save_with_history! - - return :ok - end - else - return :gone - end - end - # ----------------------------------------------------------------- # full # diff --git a/app/controllers/way_controller.rb b/app/controllers/way_controller.rb index fd01008fa..3f5f66197 100644 --- a/app/controllers/way_controller.rb +++ b/app/controllers/way_controller.rb @@ -51,35 +51,18 @@ class WayController < ApplicationController new_way = Way.from_xml(request.raw_post) if new_way and new_way.id == way.id - unless update_internal(way, new_way) - render :text => "", :status => :precondition_failed - else - render :nothing => true - end + way.update_from(new_way, @user) + render :nothing => true else render :nothing => true, :status => :bad_request end + rescue OSM::APIPreconditionFailedError + render :text => "", :status => :precondition_failed rescue ActiveRecord::RecordNotFound render :nothing => true, :status => :not_found end end - def update_internal way, new_way - way = Way.find(new_way.id) if way.nil? - - if !new_way.preconditions_ok? - return false - else - way.user_id = @user.id - way.tags = new_way.tags - way.nds = new_way.nds - way.visible = true - way.save_with_history! - - return true - end - end - # This is the API call to delete a way def delete begin diff --git a/app/models/node.rb b/app/models/node.rb index 872c5c922..7a103efa5 100644 --- a/app/models/node.rb +++ b/app/models/node.rb @@ -122,6 +122,31 @@ class Node < GeoRecord end end + def delete_with_history(user) + if self.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 = ?", 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 = 1 AND member_type='node' and member_id=?", self.id]) + raise OSM::APIPreconditionFailedError.new + else + self.user_id = user.id + self.visible = 0 + save_with_history! + end + else + raise OSM::APIAlreadyDeletedError.new + end + end + + def update_from(new_node, user) + self.user_id = user.id + self.latitude = new_node.latitude + self.longitude = new_node.longitude + self.tags = new_node.tags + self.visible = true + save_with_history! + end + def to_xml doc = OSM::API.new.get_xml_doc doc.root << to_xml_node() diff --git a/app/models/relation.rb b/app/models/relation.rb index 2c155e9e1..559b78440 100644 --- a/app/models/relation.rb +++ b/app/models/relation.rb @@ -205,6 +205,34 @@ class Relation < ActiveRecord::Base end end + def delete_with_history(user) + if self.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=?", self.id ]) + raise OSM::APIPreconditionFailedError.new + else + self.user_id = user.id + self.tags = [] + self.members = [] + self.visible = false + save_with_history! + end + else + raise OSM::APIAlreadyDeletedError.new + end + end + + def update_from(new_relation, user) + if !new_relation.preconditions_ok? + raise OSM::APIPreconditionFailedError.new + else + self.user_id = user.id + self.tags = new_relation.tags + self.members = new_relation.members + self.visible = true + save_with_history! + end + end + def preconditions_ok? self.members.each do |m| if (m[0] == "node") diff --git a/app/models/way.rb b/app/models/way.rb index 0c927c11c..e16ec53e0 100644 --- a/app/models/way.rb +++ b/app/models/way.rb @@ -196,6 +196,18 @@ class Way < ActiveRecord::Base end end + def update_from(new_way, user) + if !new_way.preconditions_ok? + raise OSM::APIPreconditionFailedError.new + else + self.user_id = user.id + self.tags = new_way.tags + self.nds = new_way.nds + self.visible = true + save_with_history! + end + end + def preconditions_ok? return false if self.nds.empty? self.nds.each do |n| @@ -213,6 +225,8 @@ class Way < ActiveRecord::Base # FIXME # this should actually delete the relations, # not just throw a PreconditionFailed if it's a member of a relation!! + + # FIXME: this should probably renamed to delete_with_history 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 From e7c550cf3fb13e410f49439e569f603836b3d83e Mon Sep 17 00:00:00 2001 From: Gabriel Ebner Date: Sun, 4 May 2008 15:56:10 +0000 Subject: [PATCH 030/381] api06: Diff upload works now (but no integration with changesets whatsoever as of now) --- app/controllers/changeset_controller.rb | 73 ++++++++++++++++++++++--- 1 file changed, 65 insertions(+), 8 deletions(-) diff --git a/app/controllers/changeset_controller.rb b/app/controllers/changeset_controller.rb index 585821a55..f91ae09bf 100644 --- a/app/controllers/changeset_controller.rb +++ b/app/controllers/changeset_controller.rb @@ -61,47 +61,104 @@ class ChangesetController < ApplicationController node_ids, way_ids, rel_ids = {}, {}, {} ids = {"node"=>node_ids, "way"=>way_ids, "relation"=>rel_ids} + res = XML::Document.new + res.encoding = 'UTF-8' + root = XML::Node.new 'osm' + root['version'] = '0.6' + root['creator'] = 'OpenStreetMap.org' + res.root = root + + root << XML::Node.new_comment(" Warning: this is a 0.6 result document, " + + "not a normal OSM file. ") + Changeset.transaction do doc.find('//osm/create/node').each do |nd| - create_prim node_ids, Node.from_xml_node(nd, true), nd + elem = XML::Node.new 'node' + node = Node.from_xml_node(nd, true) + elem['old_id'] = nd['id'] + create_prim node_ids, node, nd + elem['new_id'] = node.id.to_s + elem['new_version'] = node.version.to_s + root << elem end doc.find('//osm/create/way').each do |nd| + elem = XML::Node.new 'way' way = Way.from_xml_node(nd, true) + elem['old_id'] = nd['id'] fix_way(way, node_ids) raise OSM::APIPreconditionFailedError.new if !way.preconditions_ok? create_prim way_ids, way, nd + elem['new_id'] = way.id.to_s + elem['new_version'] = way.version.to_s + root << elem end doc.find('//osm/create/relation').each do |nd| + elem = XML::Node.new 'relation' relation = Relation.from_xml_node(nd, true) + elem['old_id'] = nd['id'] fix_rel(relation, ids) raise OSM::APIPreconditionFailedError.new if !relation.preconditions_ok? create_prim rel_ids, relation, nd + elem['new_id'] = relation.id.to_s + elem['new_version'] = relation.version.to_s + root << elem end doc.find('//osm/modify/relation').each do |nd| + elem = XML::Node.new 'relation' new_relation = Relation.from_xml_node(nd) - Relation.find(new_relation.id).update_from new_relation, @user + relation = Relation.find(new_relation.id) + relation.update_from new_relation, @user + elem['old_id'] = elem['new_id'] = relation.id.to_s + elem['new_version'] = relation.version.to_s + root << elem end doc.find('//osm/modify/way').each do |nd| + elem = XML::Node.new 'way' new_way = Way.from_xml_node(nd) - Way.find(new_way.id).update_from new_way, @user + way = Way.find(new_way.id) + way.update_from new_way, @user + elem['old_id'] = elem['new_id'] = way.id.to_s + elem['new_version'] = way.version.to_s + root << elem end doc.find('//osm/modify/node').each do |nd| + elem = XML::Node.new 'node' new_node = Node.from_xml_node(nd) - Node.find(new_node.id).update_from new_node, @user + node = Node.find(new_node.id) + node.update_from new_node, @user + elem['old_id'] = elem['new_id'] = node.id.to_s + elem['new_version'] = node.version.to_s + root << elem end doc.find('//osm/delete/relation').each do |nd| - Relation.find(nd['id']).delete_with_history(@user) + elem = XML::Node.new 'relation' + relation = Relation.find(nd['id']) + relation.delete_with_history(@user) + elem['old_id'] = elem['new_id'] = relation.id.to_s + elem['new_version'] = relation.version.to_s + root << elem end doc.find('//osm/delete/way').each do |nd| - Way.find(nd['id']).delete_with_relations_and_history(@user) + elem = XML::Node.new 'way' + way = Way.find(nd['id']) + way.delete_with_relations_and_history(@user) + elem['old_id'] = elem['new_id'] = way.id.to_s + elem['new_version'] = way.version.to_s + root << elem end doc.find('//osm/delete/node').each do |nd| - Node.find(nd['id']).delete_with_history(@user) + elem = XML::Node.new 'node' + new_node = Node.from_xml_node(nd) + node = Node.find(nd['id']) + node.delete_with_history(@user) + elem['old_id'] = elem['new_id'] = node.id.to_s + elem['new_version'] = node.version.to_s + root << elem end end - render :text => "Ok, Fine. Upload worked without errors.\n", :status => 200 + render :text => res.to_s, :content_type => "text/xml" end end From 013ef53c97a1d2c8319f1ac624e8325a06828366 Mon Sep 17 00:00:00 2001 From: Gabriel Ebner Date: Mon, 5 May 2008 22:41:32 +0000 Subject: [PATCH 031/381] api06: Fix normal way and relation uploading (i.e. not as part of a diff). (I'm sorry that I couldn't commit it earlier, but I didn't have access to an internet connection until now.) --- app/models/relation.rb | 6 ++---- app/models/way.rb | 6 ++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/app/models/relation.rb b/app/models/relation.rb index 559b78440..58add6dd0 100644 --- a/app/models/relation.rb +++ b/app/models/relation.rb @@ -17,13 +17,11 @@ class Relation < ActiveRecord::Base doc = p.parse doc.find('//osm/relation').each do |pt| - relation = Relation.from_xml_node pt, create + return Relation.from_xml_node(pt, create) end rescue - relation = nil + return nil end - - return relation end def self.from_xml_node(pt, create=false) diff --git a/app/models/way.rb b/app/models/way.rb index e16ec53e0..255f4329f 100644 --- a/app/models/way.rb +++ b/app/models/way.rb @@ -18,13 +18,11 @@ class Way < ActiveRecord::Base doc = p.parse doc.find('//osm/way').each do |pt| - way = Way.from_xml_node pt, create + return Way.from_xml_node(pt, create) end rescue - way = nil + return nil end - - return way end def self.from_xml_node(pt, create=false) From 7a216c0ce66e0426613415d887937be729cb26d2 Mon Sep 17 00:00:00 2001 From: Christopher Schmidt Date: Sun, 11 May 2008 19:47:19 +0000 Subject: [PATCH 032/381] Make edits return version numbers as plain text strings --- app/controllers/node_controller.rb | 6 +++--- app/controllers/relation_controller.rb | 1 + app/controllers/way_controller.rb | 6 +++--- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/app/controllers/node_controller.rb b/app/controllers/node_controller.rb index f6a673fe5..956a8b8d9 100644 --- a/app/controllers/node_controller.rb +++ b/app/controllers/node_controller.rb @@ -15,7 +15,7 @@ class NodeController < ApplicationController node = Node.from_xml(request.raw_post, true) if node - node.version = 0 + node.version = 0 node.user_id = @user.id node.visible = true node.save_with_history! @@ -51,8 +51,8 @@ class NodeController < ApplicationController new_node = Node.from_xml(request.raw_post) if new_node and new_node.id == node.id - node.update_from(new_node, @user) - 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 diff --git a/app/controllers/relation_controller.rb b/app/controllers/relation_controller.rb index caacd4fb9..20f5372a6 100644 --- a/app/controllers/relation_controller.rb +++ b/app/controllers/relation_controller.rb @@ -52,6 +52,7 @@ class RelationController < ApplicationController if new_relation and new_relation.id == relation.id relation.update_from new_relation, user + render :text => relation.version.to_s, :content_type => "text/plain" else render :nothing => true, :status => :bad_request end diff --git a/app/controllers/way_controller.rb b/app/controllers/way_controller.rb index 3f5f66197..94a10d424 100644 --- a/app/controllers/way_controller.rb +++ b/app/controllers/way_controller.rb @@ -15,7 +15,7 @@ class WayController < ApplicationController if !way.preconditions_ok? render :text => "", :status => :precondition_failed else - way.version = 0 + way.version = 0 way.user_id = @user.id way.save_with_history! @@ -51,8 +51,8 @@ class WayController < ApplicationController new_way = Way.from_xml(request.raw_post) if new_way and new_way.id == way.id - way.update_from(new_way, @user) - render :nothing => true + way.update_from(new_way, @user) + render :text => way.version.to_s, :content_type => "text/plain" else render :nothing => true, :status => :bad_request end From e48cbc6a5f54a0e18dc581ccbac3b13284313125 Mon Sep 17 00:00:00 2001 From: Christopher Schmidt Date: Sun, 11 May 2008 20:03:17 +0000 Subject: [PATCH 033/381] Add support for accessing changesets as XML through the API via the changeset controller "read" method --- app/controllers/changeset_controller.rb | 9 +++++++ app/models/changeset.rb | 33 +++++++++++++++++++++++++ config/routes.rb | 1 + 3 files changed, 43 insertions(+) diff --git a/app/controllers/changeset_controller.rb b/app/controllers/changeset_controller.rb index f91ae09bf..3e88eeec3 100644 --- a/app/controllers/changeset_controller.rb +++ b/app/controllers/changeset_controller.rb @@ -45,6 +45,15 @@ class ChangesetController < ApplicationController map { |memb| [memb[0], ids[memb[0]][memb[1].to_i] || memb[1], memb[2]] } return r end + + 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 def upload if not request.put? diff --git a/app/models/changeset.rb b/app/models/changeset.rb index 85621dce2..983f7544c 100644 --- a/app/models/changeset.rb +++ b/app/models/changeset.rb @@ -70,4 +70,37 @@ class Changeset < ActiveRecord::Base 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? + + 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 + + return el1 + end end diff --git a/config/routes.rb b/config/routes.rb index 64293b34d..9196fae1b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -2,6 +2,7 @@ ActionController::Routing::Routes.draw do |map| # API map.connect "api/#{API_VERSION}/changeset/create", :controller => 'changeset', :action => 'create' + map.connect "api/#{API_VERSION}/changeset/:id", :controller => 'changeset', :action => 'read' map.connect "api/#{API_VERSION}/changeset/upload", :controller => 'changeset', :action => 'upload' map.connect "api/#{API_VERSION}/node/create", :controller => 'node', :action => 'create' From 9cbc370a7eb498993ddefbaa2be8b5b48155d8d5 Mon Sep 17 00:00:00 2001 From: Christopher Schmidt Date: Sun, 11 May 2008 20:07:47 +0000 Subject: [PATCH 034/381] add FIXME FIXME FIXME for serializing changes in a changeset --- app/models/changeset.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/models/changeset.rb b/app/models/changeset.rb index 983f7544c..9b92c62ea 100644 --- a/app/models/changeset.rb +++ b/app/models/changeset.rb @@ -101,6 +101,10 @@ class Changeset < ActiveRecord::Base el1['created_at'] = self.created_at.xmlschema + # FIXME FIXME FIXME: This does not include changes yet! There is + # currently no changeset_id column in the tables as far as I can tell, + # so this is just a scaffold to build on, not a complete to_xml + return el1 end end From 6f1aad0d0431703cd834e2b29becf20fcbac4dab Mon Sep 17 00:00:00 2001 From: Christopher Schmidt Date: Sun, 11 May 2008 21:18:42 +0000 Subject: [PATCH 035/381] add support for closing changesets --- app/controllers/changeset_controller.rb | 15 +++++++++++++++ config/routes.rb | 1 + 2 files changed, 16 insertions(+) diff --git a/app/controllers/changeset_controller.rb b/app/controllers/changeset_controller.rb index 3e88eeec3..9994a6b1e 100644 --- a/app/controllers/changeset_controller.rb +++ b/app/controllers/changeset_controller.rb @@ -54,6 +54,21 @@ class ChangesetController < ApplicationController render :nothing => true, :status => :not_found end end + + def close + begin + if not request.put? + render :nothing => true, :status => :method_not_allowed + return + end + changeset = Changeset.find(params[:id]) + changeset.open = false + changeset.save + render :nothing => true + rescue ActiveRecord::RecordNotFound + render :nothing => true, :status => :not_found + end + end def upload if not request.put? diff --git a/config/routes.rb b/config/routes.rb index 9196fae1b..581310068 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -3,6 +3,7 @@ ActionController::Routing::Routes.draw do |map| # API map.connect "api/#{API_VERSION}/changeset/create", :controller => 'changeset', :action => 'create' map.connect "api/#{API_VERSION}/changeset/:id", :controller => 'changeset', :action => 'read' + map.connect "api/#{API_VERSION}/changeset/:id/close", :controller => 'changeset', :action => 'close' map.connect "api/#{API_VERSION}/changeset/upload", :controller => 'changeset', :action => 'upload' map.connect "api/#{API_VERSION}/node/create", :controller => 'node', :action => 'create' From 412472212be935f8dbcd35b2d94c63466a90d218 Mon Sep 17 00:00:00 2001 From: Christopher Schmidt Date: Sun, 11 May 2008 21:21:30 +0000 Subject: [PATCH 036/381] change 'no nodes' response to use API_VERSION in output --- app/controllers/api_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/api_controller.rb b/app/controllers/api_controller.rb index 26cb93a48..6ede9ebe8 100644 --- a/app/controllers/api_controller.rb +++ b/app/controllers/api_controller.rb @@ -120,7 +120,7 @@ class ApiController < ApplicationController return end if node_ids.length == 0 - render :text => "", :content_type => "text/xml" + render :text => "", :content_type => "text/xml" return end From 6aa9ec599fb44589f17bebc256b52edcc2ca95b1 Mon Sep 17 00:00:00 2001 From: Christopher Schmidt Date: Sun, 11 May 2008 21:23:38 +0000 Subject: [PATCH 037/381] Make capabilities report the current version for from/to version identifier --- app/controllers/api_controller.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/api_controller.rb b/app/controllers/api_controller.rb index 6ede9ebe8..d7937cb01 100644 --- a/app/controllers/api_controller.rb +++ b/app/controllers/api_controller.rb @@ -252,8 +252,8 @@ class ApiController < ApplicationController 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; From 618908319ca4cc33f4e14c2caba245cd7076829a Mon Sep 17 00:00:00 2001 From: Christopher Schmidt Date: Sun, 11 May 2008 21:49:33 +0000 Subject: [PATCH 038/381] prevent update of any object without providing the correct/current version as part of the XML. This affects update-only at this time: DELETE doesn't work the same way because we don't provide the data as part of a DELETE --- app/controllers/node_controller.rb | 4 ++++ app/controllers/relation_controller.rb | 4 ++++ app/controllers/way_controller.rb | 5 +++++ 3 files changed, 13 insertions(+) diff --git a/app/controllers/node_controller.rb b/app/controllers/node_controller.rb index 956a8b8d9..fa6759c3b 100644 --- a/app/controllers/node_controller.rb +++ b/app/controllers/node_controller.rb @@ -49,6 +49,10 @@ class NodeController < ApplicationController begin node = Node.find(params[:id]) new_node = Node.from_xml(request.raw_post) + if new_node.version != node.version + render :text => "Version mismatch: Provided " + new_node.version.to_s + ", server had: " + node.version.to_s, :status => :bad_request + return + end if new_node and new_node.id == node.id node.update_from(new_node, @user) diff --git a/app/controllers/relation_controller.rb b/app/controllers/relation_controller.rb index 20f5372a6..d7b9de13c 100644 --- a/app/controllers/relation_controller.rb +++ b/app/controllers/relation_controller.rb @@ -49,6 +49,10 @@ class RelationController < ApplicationController begin relation = Relation.find(params[:id]) new_relation = Relation.from_xml(request.raw_post) + if new_relation.version != relation.version + render :text => "Version mismatch: Provided " + new_relation.version.to_s + ", server had: " + relation.version.to_s, :status => :bad_request + return + end if new_relation and new_relation.id == relation.id relation.update_from new_relation, user diff --git a/app/controllers/way_controller.rb b/app/controllers/way_controller.rb index 94a10d424..d3a1b039f 100644 --- a/app/controllers/way_controller.rb +++ b/app/controllers/way_controller.rb @@ -49,6 +49,11 @@ class WayController < ApplicationController begin way = Way.find(params[:id]) new_way = Way.from_xml(request.raw_post) + if new_way.version != way.version + render :text => "Version mismatch: Provided " + new_way.version.to_s + ", server had: " + way.version.to_s, :status => :bad_request + return + end + if new_way and new_way.id == way.id way.update_from(new_way, @user) From 604f9ddd46fcca6b66e1b5c3da286fcb7b86f864 Mon Sep 17 00:00:00 2001 From: Christopher Schmidt Date: Sun, 11 May 2008 22:26:11 +0000 Subject: [PATCH 039/381] spit out whether the changeset is open in the xml --- app/models/changeset.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/models/changeset.rb b/app/models/changeset.rb index 9b92c62ea..c9eeb0018 100644 --- a/app/models/changeset.rb +++ b/app/models/changeset.rb @@ -100,6 +100,7 @@ class Changeset < ActiveRecord::Base end el1['created_at'] = self.created_at.xmlschema + el1['open'] = self.open.to_s # FIXME FIXME FIXME: This does not include changes yet! There is # currently no changeset_id column in the tables as far as I can tell, From e9b86e914d1e09eb012974848f5edb438665d36f Mon Sep 17 00:00:00 2001 From: Christopher Schmidt Date: Sun, 11 May 2008 22:26:46 +0000 Subject: [PATCH 040/381] add to_xml method for oldnode --- app/models/old_node.rb | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/models/old_node.rb b/app/models/old_node.rb index 247a69923..5d9c6d7ef 100644 --- a/app/models/old_node.rb +++ b/app/models/old_node.rb @@ -1,4 +1,5 @@ class OldNode < GeoRecord + set_primary_keys :id, :version set_table_name 'nodes' validates_presence_of :user_id, :timestamp @@ -30,6 +31,12 @@ class OldNode < GeoRecord 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' From df11677afd55ef592330a05c43ed4baa81b60572 Mon Sep 17 00:00:00 2001 From: Christopher Schmidt Date: Sun, 11 May 2008 22:27:40 +0000 Subject: [PATCH 041/381] from_xml sets a version on nodes --- app/models/node.rb | 3 ++- app/models/relation.rb | 2 ++ app/models/way.rb | 2 ++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/app/models/node.rb b/app/models/node.rb index 7a103efa5..acd251918 100644 --- a/app/models/node.rb +++ b/app/models/node.rb @@ -66,7 +66,8 @@ class Node < GeoRecord def self.from_xml_node(pt, create=false) node = Node.new - + + node.version = pt['version'] node.lat = pt['lat'].to_f node.lon = pt['lon'].to_f diff --git a/app/models/relation.rb b/app/models/relation.rb index 58add6dd0..b7cb0f15b 100644 --- a/app/models/relation.rb +++ b/app/models/relation.rb @@ -31,6 +31,8 @@ class Relation < ActiveRecord::Base relation.id = pt['id'].to_i end + relation.version = pt['version'] + if create relation.timestamp = Time.now relation.visible = true diff --git a/app/models/way.rb b/app/models/way.rb index 255f4329f..de69f7565 100644 --- a/app/models/way.rb +++ b/app/models/way.rb @@ -31,6 +31,8 @@ class Way < ActiveRecord::Base if !create and pt['id'] != '0' way.id = pt['id'].to_i end + + way.version = pt['version'] if create way.timestamp = Time.now From 97cb1fd7fa2916974538fde2a6ea311a3f793989 Mon Sep 17 00:00:00 2001 From: Christopher Schmidt Date: Sun, 11 May 2008 23:02:06 +0000 Subject: [PATCH 042/381] undo composite keys change on old_ndoe --- app/models/old_node.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/app/models/old_node.rb b/app/models/old_node.rb index 5d9c6d7ef..a3650173a 100644 --- a/app/models/old_node.rb +++ b/app/models/old_node.rb @@ -1,5 +1,4 @@ class OldNode < GeoRecord - set_primary_keys :id, :version set_table_name 'nodes' validates_presence_of :user_id, :timestamp From 322f3d7576c325e1726db9561dd1a96dd5632cbb Mon Sep 17 00:00:00 2001 From: Christopher Schmidt Date: Sun, 11 May 2008 23:12:54 +0000 Subject: [PATCH 043/381] make error_message spit out the message text to the user as well as putting it in the header. --- app/controllers/application.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/controllers/application.rb b/app/controllers/application.rb index acb2f9162..8d082c2ca 100644 --- a/app/controllers/application.rb +++ b/app/controllers/application.rb @@ -61,7 +61,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 @@ -72,6 +72,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 From 697449d6f1976bdc31862e1508311620da246729 Mon Sep 17 00:00:00 2001 From: Christopher Schmidt Date: Mon, 12 May 2008 01:22:04 +0000 Subject: [PATCH 044/381] node.tags is now (apparently) the same as tags_as_hash. --- app/models/node.rb | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/app/models/node.rb b/app/models/node.rb index acd251918..5f4133819 100644 --- a/app/models/node.rb +++ b/app/models/node.rb @@ -186,11 +186,7 @@ class Node < GeoRecord end def tags_as_hash - hash = {} - Tags.split(self.tags) do |k,v| - hash[k] = v - end - hash + return tags end def tags From ec916b7429ec04731eb5c533c55915f627c9aa38 Mon Sep 17 00:00:00 2001 From: Gabriel Ebner Date: Mon, 12 May 2008 20:43:35 +0000 Subject: [PATCH 045/381] api06: Make diff uploading work again. --- config/routes.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/routes.rb b/config/routes.rb index 581310068..2c7f4d0fc 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -2,9 +2,9 @@ ActionController::Routing::Routes.draw do |map| # API map.connect "api/#{API_VERSION}/changeset/create", :controller => 'changeset', :action => 'create' + map.connect "api/#{API_VERSION}/changeset/upload", :controller => 'changeset', :action => 'upload' map.connect "api/#{API_VERSION}/changeset/:id", :controller => 'changeset', :action => 'read' map.connect "api/#{API_VERSION}/changeset/:id/close", :controller => 'changeset', :action => 'close' - map.connect "api/#{API_VERSION}/changeset/upload", :controller => 'changeset', :action => 'upload' 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+/ From 86b4d1bc2c05a68053c7485217144a7fcf8ef4e8 Mon Sep 17 00:00:00 2001 From: Gabriel Ebner Date: Mon, 12 May 2008 21:05:11 +0000 Subject: [PATCH 046/381] api06: Move version-checking into the models, raising an exception on mismatch (still not implemented for delete requests though.) --- app/controllers/node_controller.rb | 7 +++---- app/controllers/relation_controller.rb | 3 +++ app/controllers/way_controller.rb | 8 +++----- app/models/node.rb | 4 ++++ app/models/relation.rb | 2 ++ app/models/way.rb | 2 ++ lib/osm.rb | 9 +++++++++ 7 files changed, 26 insertions(+), 9 deletions(-) diff --git a/app/controllers/node_controller.rb b/app/controllers/node_controller.rb index fa6759c3b..379ee77c9 100644 --- a/app/controllers/node_controller.rb +++ b/app/controllers/node_controller.rb @@ -49,10 +49,6 @@ class NodeController < ApplicationController begin node = Node.find(params[:id]) new_node = Node.from_xml(request.raw_post) - if new_node.version != node.version - render :text => "Version mismatch: Provided " + new_node.version.to_s + ", server had: " + node.version.to_s, :status => :bad_request - return - end if new_node and new_node.id == node.id node.update_from(new_node, @user) @@ -60,6 +56,9 @@ class NodeController < ApplicationController else render :nothing => true, :status => :bad_request end + rescue OSM::APIVersionMismatchError ex + render :text => "Version mismatch: Provided " + ex.provided.to_s + + ", server had: " + ex.latest.to_s, :status => :bad_request rescue ActiveRecord::RecordNotFound render :nothing => true, :status => :not_found end diff --git a/app/controllers/relation_controller.rb b/app/controllers/relation_controller.rb index d7b9de13c..dabf6eaa8 100644 --- a/app/controllers/relation_controller.rb +++ b/app/controllers/relation_controller.rb @@ -64,6 +64,9 @@ class RelationController < ApplicationController render :nothing => true, :status => :not_found rescue OSM::APIPreconditionFailedError render :text => "", :status => :precondition_failed + rescue OSM::APIVersionMismatchError => ex + render :text => "Version mismatch: Provided " + ex.provided.to_s + + ", server had: " + ex.latest.to_s, :status => :bad_request rescue render :nothing => true, :status => :internal_server_error end diff --git a/app/controllers/way_controller.rb b/app/controllers/way_controller.rb index d3a1b039f..b22d35d0b 100644 --- a/app/controllers/way_controller.rb +++ b/app/controllers/way_controller.rb @@ -49,11 +49,6 @@ class WayController < ApplicationController begin way = Way.find(params[:id]) new_way = Way.from_xml(request.raw_post) - if new_way.version != way.version - render :text => "Version mismatch: Provided " + new_way.version.to_s + ", server had: " + way.version.to_s, :status => :bad_request - return - end - if new_way and new_way.id == way.id way.update_from(new_way, @user) @@ -63,6 +58,9 @@ class WayController < ApplicationController end rescue OSM::APIPreconditionFailedError render :text => "", :status => :precondition_failed + rescue OSM::APIVersionMismatchError => ex + render :text => "Version mismatch: Provided " + ex.provided.to_s + + ", server had: " + ex.latest.to_s, :status => :bad_request rescue ActiveRecord::RecordNotFound render :nothing => true, :status => :not_found end diff --git a/app/models/node.rb b/app/models/node.rb index 5f4133819..b027f2fd4 100644 --- a/app/models/node.rb +++ b/app/models/node.rb @@ -140,6 +140,10 @@ class Node < GeoRecord end def update_from(new_node, user) + if new_node.version != version + raise OSM::APIVersionMismatchError.new(new_node.version, version) + end + self.user_id = user.id self.latitude = new_node.latitude self.longitude = new_node.longitude diff --git a/app/models/relation.rb b/app/models/relation.rb index b7cb0f15b..984732c71 100644 --- a/app/models/relation.rb +++ b/app/models/relation.rb @@ -224,6 +224,8 @@ class Relation < ActiveRecord::Base def update_from(new_relation, user) if !new_relation.preconditions_ok? raise OSM::APIPreconditionFailedError.new + elsif new_relation.version != version + raise OSM::APIVersionMismatchError.new(new_relation.version, version) else self.user_id = user.id self.tags = new_relation.tags diff --git a/app/models/way.rb b/app/models/way.rb index de69f7565..a6192e2d7 100644 --- a/app/models/way.rb +++ b/app/models/way.rb @@ -199,6 +199,8 @@ class Way < ActiveRecord::Base def update_from(new_way, user) if !new_way.preconditions_ok? raise OSM::APIPreconditionFailedError.new + elsif new_way.version != version + raise OSM::APIVersionMismatchError.new(new_way.version, version) else self.user_id = user.id self.tags = new_way.tags diff --git a/lib/osm.rb b/lib/osm.rb index bd9351026..a8cb103d2 100644 --- a/lib/osm.rb +++ b/lib/osm.rb @@ -24,6 +24,15 @@ module OSM class APIAlreadyDeletedError < APIError end + # Raised when the provided version is not equal to the latest in the db. + class APIVersionMismatchError < APIError + def initialize(provided, latest) + @provided, @latest = provided, latest + end + + attr_reader :provided, :latest + end + # Helper methods for going to/from mercator and lat/lng. class Mercator include Math From ea1961e3ce54432e15a2468fb7b711937fd1517a Mon Sep 17 00:00:00 2001 From: Gabriel Ebner Date: Mon, 12 May 2008 21:10:38 +0000 Subject: [PATCH 047/381] api06: s/delete_with_relations_and_history/delete_with_history/g --- app/controllers/changeset_controller.rb | 2 +- app/controllers/way_controller.rb | 2 +- app/models/way.rb | 7 ++++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/app/controllers/changeset_controller.rb b/app/controllers/changeset_controller.rb index 9994a6b1e..db3a26295 100644 --- a/app/controllers/changeset_controller.rb +++ b/app/controllers/changeset_controller.rb @@ -167,7 +167,7 @@ class ChangesetController < ApplicationController doc.find('//osm/delete/way').each do |nd| elem = XML::Node.new 'way' way = Way.find(nd['id']) - way.delete_with_relations_and_history(@user) + way.delete_with_history(@user) elem['old_id'] = elem['new_id'] = way.id.to_s elem['new_version'] = way.version.to_s root << elem diff --git a/app/controllers/way_controller.rb b/app/controllers/way_controller.rb index b22d35d0b..21f2ea765 100644 --- a/app/controllers/way_controller.rb +++ b/app/controllers/way_controller.rb @@ -70,7 +70,7 @@ class WayController < ApplicationController def delete begin way = Way.find(params[:id]) - way.delete_with_relations_and_history(@user) + way.delete_with_history(@user) # if we get here, all is fine, otherwise something will catch below. render :nothing => true diff --git a/app/models/way.rb b/app/models/way.rb index a6192e2d7..ea027fb47 100644 --- a/app/models/way.rb +++ b/app/models/way.rb @@ -221,8 +221,7 @@ class Way < ActiveRecord::Base 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) + def delete_with_history(user) if self.visible # FIXME # this should actually delete the relations, @@ -246,6 +245,8 @@ class Way < ActiveRecord::Base end # delete a way and it's nodes that aren't part of other ways, with history + + # FIXME: merge the potlatch code to delete the relations def delete_with_relations_and_nodes_and_history(user) node_ids = self.nodes.collect {|node| node.id } @@ -266,7 +267,7 @@ class Way < ActiveRecord::Base self.user_id = user.id - self.delete_with_relations_and_history(user) + self.delete_with_history(user) end end From cddded9569d2c5b5c9e82a7bdbb3669be6e23b25 Mon Sep 17 00:00:00 2001 From: Gabriel Ebner Date: Mon, 12 May 2008 21:11:51 +0000 Subject: [PATCH 048/381] api06: Fix typo. --- app/controllers/node_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/node_controller.rb b/app/controllers/node_controller.rb index 379ee77c9..f1ee8f66b 100644 --- a/app/controllers/node_controller.rb +++ b/app/controllers/node_controller.rb @@ -56,7 +56,7 @@ class NodeController < ApplicationController else render :nothing => true, :status => :bad_request end - rescue OSM::APIVersionMismatchError ex + rescue OSM::APIVersionMismatchError => ex render :text => "Version mismatch: Provided " + ex.provided.to_s + ", server had: " + ex.latest.to_s, :status => :bad_request rescue ActiveRecord::RecordNotFound From 4a9ee4c7362071dfbaa9e1a59ea1720bc00d5ebb Mon Sep 17 00:00:00 2001 From: Christopher Schmidt Date: Mon, 12 May 2008 21:12:30 +0000 Subject: [PATCH 049/381] The ordering of these shouldn't have mattered, but it did, because I didn't put a regex filter on my :id, so "upload" was being treated as an ID (which clearly wouldn't work); This fix obviates the need for r7789, sorry about that. --- config/routes.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/config/routes.rb b/config/routes.rb index 2c7f4d0fc..5ca4f5a53 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -3,8 +3,8 @@ ActionController::Routing::Routes.draw do |map| # API map.connect "api/#{API_VERSION}/changeset/create", :controller => 'changeset', :action => 'create' map.connect "api/#{API_VERSION}/changeset/upload", :controller => 'changeset', :action => 'upload' - map.connect "api/#{API_VERSION}/changeset/:id", :controller => 'changeset', :action => 'read' - map.connect "api/#{API_VERSION}/changeset/:id/close", :controller => 'changeset', :action => 'close' + map.connect "api/#{API_VERSION}/changeset/:id", :controller => 'changeset', :action => 'read', :id => /\d+/ + map.connect "api/#{API_VERSION}/changeset/:id/close", :controller => 'changeset', :action => 'close', :id =>/\d+/ 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+/ @@ -59,6 +59,7 @@ ActionController::Routing::Routes.draw do |map| # Potlatch API + map.connect "api/0.5/amf", :controller =>'amf', :action =>'talk' map.connect "api/#{API_VERSION}/amf", :controller =>'amf', :action =>'talk' map.connect "api/#{API_VERSION}/swf/trackpoints", :controller =>'swf', :action =>'trackpoints' From 7151fa05e4d4f73956d7f9a7ad2db1f5b956a0c0 Mon Sep 17 00:00:00 2001 From: Gabriel Ebner Date: Tue, 13 May 2008 12:15:06 +0000 Subject: [PATCH 050/381] api06: simplify exception handling and add exception handling to the diff upload. --- app/controllers/changeset_controller.rb | 3 +++ app/controllers/node_controller.rb | 6 ++---- app/controllers/relation_controller.rb | 21 ++++----------------- app/controllers/way_controller.rb | 15 ++++++--------- lib/osm.rb | 8 ++++++++ 5 files changed, 23 insertions(+), 30 deletions(-) diff --git a/app/controllers/changeset_controller.rb b/app/controllers/changeset_controller.rb index db3a26295..322ce79f7 100644 --- a/app/controllers/changeset_controller.rb +++ b/app/controllers/changeset_controller.rb @@ -184,5 +184,8 @@ class ChangesetController < ApplicationController end render :text => res.to_s, :content_type => "text/xml" + + rescue OSM::APIError => ex + render ex.render_opts end end diff --git a/app/controllers/node_controller.rb b/app/controllers/node_controller.rb index f1ee8f66b..1e0deb140 100644 --- a/app/controllers/node_controller.rb +++ b/app/controllers/node_controller.rb @@ -72,10 +72,8 @@ class NodeController < ApplicationController node.delete_with_history(@user) rescue ActiveRecord::RecordNotFound render :nothing => true, :status => :not_found - rescue OSM::APIAlreadyDeletedError - render :text => "", :status => :gone - rescue OSM::APIPreconditionFailedError - render :text => "", :status => :precondition_failed + rescue OSM::APIError => ex + render ex.render_opts end end diff --git a/app/controllers/relation_controller.rb b/app/controllers/relation_controller.rb index dabf6eaa8..c49ecd4d7 100644 --- a/app/controllers/relation_controller.rb +++ b/app/controllers/relation_controller.rb @@ -49,10 +49,6 @@ class RelationController < ApplicationController begin relation = Relation.find(params[:id]) new_relation = Relation.from_xml(request.raw_post) - if new_relation.version != relation.version - render :text => "Version mismatch: Provided " + new_relation.version.to_s + ", server had: " + relation.version.to_s, :status => :bad_request - return - end if new_relation and new_relation.id == relation.id relation.update_from new_relation, user @@ -62,13 +58,8 @@ class RelationController < ApplicationController end rescue ActiveRecord::RecordNotFound render :nothing => true, :status => :not_found - rescue OSM::APIPreconditionFailedError - render :text => "", :status => :precondition_failed - rescue OSM::APIVersionMismatchError => ex - render :text => "Version mismatch: Provided " + ex.provided.to_s + - ", server had: " + ex.latest.to_s, :status => :bad_request - rescue - render :nothing => true, :status => :internal_server_error + rescue OSM::APIError => ex + render ex.render_opts end end @@ -77,14 +68,10 @@ class RelationController < ApplicationController begin relation = Relation.find(params[:id]) relation.delete_with_history(@user) - rescue OSM::APIAlreadyDeletedError - render :text => "", :status => :gone - rescue OSM::APIPreconditionFailedError - render :text => "", :status => :precondition_failed + 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 diff --git a/app/controllers/way_controller.rb b/app/controllers/way_controller.rb index 21f2ea765..cf1634fa5 100644 --- a/app/controllers/way_controller.rb +++ b/app/controllers/way_controller.rb @@ -40,6 +40,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 @@ -56,11 +58,8 @@ class WayController < ApplicationController else render :nothing => true, :status => :bad_request end - rescue OSM::APIPreconditionFailedError - render :text => "", :status => :precondition_failed - rescue OSM::APIVersionMismatchError => ex - render :text => "Version mismatch: Provided " + ex.provided.to_s + - ", server had: " + ex.latest.to_s, :status => :bad_request + rescue OSM::APIError => ex + render ex.render_opts rescue ActiveRecord::RecordNotFound render :nothing => true, :status => :not_found end @@ -74,10 +73,8 @@ class WayController < ApplicationController # 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 + rescue OSM::APIError => ex + render ex.render_opts rescue ActiveRecord::RecordNotFound render :nothing => true, :status => :not_found end diff --git a/lib/osm.rb b/lib/osm.rb index a8cb103d2..8798866e5 100644 --- a/lib/osm.rb +++ b/lib/osm.rb @@ -10,6 +10,7 @@ module OSM # The base class for API Errors. class APIError < RuntimeError + def render_opts { :text => "", :status => :internal_server_error } end end # Raised when an API object is not found. @@ -18,10 +19,12 @@ module OSM # Raised when a precondition to an API action fails sanity check. class APIPreconditionFailedError < APIError + def render_opts { :text => "", :status => :precondition_failed } end end # Raised when to delete an already-deleted object. class APIAlreadyDeletedError < APIError + def render_opts { :text => "", :status => :gone } end end # Raised when the provided version is not equal to the latest in the db. @@ -31,6 +34,11 @@ module OSM end attr_reader :provided, :latest + + def render_opts + { :text => "Version mismatch: Provided " + ex.provided.to_s + + ", server had: " + ex.latest.to_s, :status => :bad_request } + end end # Helper methods for going to/from mercator and lat/lng. From 2ff2cdf63d7f9874960c3efea01b4db2a80f9f68 Mon Sep 17 00:00:00 2001 From: Gabriel Ebner Date: Tue, 13 May 2008 12:27:38 +0000 Subject: [PATCH 051/381] api06: fix syntax errors. --- lib/osm.rb | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/osm.rb b/lib/osm.rb index 8798866e5..38053e12a 100644 --- a/lib/osm.rb +++ b/lib/osm.rb @@ -10,7 +10,9 @@ module OSM # The base class for API Errors. class APIError < RuntimeError - def render_opts { :text => "", :status => :internal_server_error } end + def render_opts + { :text => "", :status => :internal_server_error } + end end # Raised when an API object is not found. @@ -19,12 +21,16 @@ module OSM # Raised when a precondition to an API action fails sanity check. class APIPreconditionFailedError < APIError - def render_opts { :text => "", :status => :precondition_failed } end + def render_opts + { :text => "", :status => :precondition_failed } + end end # Raised when to delete an already-deleted object. class APIAlreadyDeletedError < APIError - def render_opts { :text => "", :status => :gone } end + def render_opts + { :text => "", :status => :gone } + end end # Raised when the provided version is not equal to the latest in the db. From e823ce2fc4b9909986987db6b1fc41eaff8c3499 Mon Sep 17 00:00:00 2001 From: Gabriel Ebner Date: Tue, 13 May 2008 12:32:43 +0000 Subject: [PATCH 052/381] api06: Fix OSM::APIVersionMismatchError.render_opts --- lib/osm.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/osm.rb b/lib/osm.rb index 38053e12a..c038ab2d5 100644 --- a/lib/osm.rb +++ b/lib/osm.rb @@ -42,8 +42,8 @@ module OSM attr_reader :provided, :latest def render_opts - { :text => "Version mismatch: Provided " + ex.provided.to_s + - ", server had: " + ex.latest.to_s, :status => :bad_request } + { :text => "Version mismatch: Provided " + provided.to_s + + ", server had: " + latest.to_s, :status => :bad_request } end end From 866c3ff78245994ce03cf1567afee20673dc8932 Mon Sep 17 00:00:00 2001 From: Gabriel Ebner Date: Tue, 13 May 2008 12:53:59 +0000 Subject: [PATCH 053/381] api06: diff upload: don't read the whole request into memory. Note: libxml-ruby-0.5.4 will segfault on this: https://rubyforge.org/tracker/index.php?func=detail&aid=20117&group_id=494&atid=1971 --- app/controllers/changeset_controller.rb | 189 +++++++++++++----------- 1 file changed, 102 insertions(+), 87 deletions(-) diff --git a/app/controllers/changeset_controller.rb b/app/controllers/changeset_controller.rb index 322ce79f7..ec5ef83b0 100644 --- a/app/controllers/changeset_controller.rb +++ b/app/controllers/changeset_controller.rb @@ -76,11 +76,7 @@ class ChangesetController < ApplicationController return end - # FIXME: this should really be done without loading the whole XML file - # into memory. - p = XML::Parser.new - p.string = request.raw_post - doc = p.parse + p = XML::Reader.new request.raw_post node_ids, way_ids, rel_ids = {}, {}, {} ids = {"node"=>node_ids, "way"=>way_ids, "relation"=>rel_ids} @@ -96,90 +92,109 @@ class ChangesetController < ApplicationController "not a normal OSM file. ") Changeset.transaction do - doc.find('//osm/create/node').each do |nd| - elem = XML::Node.new 'node' - node = Node.from_xml_node(nd, true) - elem['old_id'] = nd['id'] - create_prim node_ids, node, nd - elem['new_id'] = node.id.to_s - elem['new_version'] = node.version.to_s - root << elem - end - doc.find('//osm/create/way').each do |nd| - elem = XML::Node.new 'way' - way = Way.from_xml_node(nd, true) - elem['old_id'] = nd['id'] - fix_way(way, node_ids) - raise OSM::APIPreconditionFailedError.new if !way.preconditions_ok? - create_prim way_ids, way, nd - elem['new_id'] = way.id.to_s - elem['new_version'] = way.version.to_s - root << elem - end - doc.find('//osm/create/relation').each do |nd| - elem = XML::Node.new 'relation' - relation = Relation.from_xml_node(nd, true) - elem['old_id'] = nd['id'] - fix_rel(relation, ids) - raise OSM::APIPreconditionFailedError.new if !relation.preconditions_ok? - create_prim rel_ids, relation, nd - elem['new_id'] = relation.id.to_s - elem['new_version'] = relation.version.to_s - root << elem - end + while p.read == 1 + break if p.node_type == 15 # end element + next unless p.node_type == 1 # element - doc.find('//osm/modify/relation').each do |nd| - elem = XML::Node.new 'relation' - new_relation = Relation.from_xml_node(nd) - relation = Relation.find(new_relation.id) - relation.update_from new_relation, @user - elem['old_id'] = elem['new_id'] = relation.id.to_s - elem['new_version'] = relation.version.to_s - root << elem - end - doc.find('//osm/modify/way').each do |nd| - elem = XML::Node.new 'way' - new_way = Way.from_xml_node(nd) - way = Way.find(new_way.id) - way.update_from new_way, @user - elem['old_id'] = elem['new_id'] = way.id.to_s - elem['new_version'] = way.version.to_s - root << elem - end - doc.find('//osm/modify/node').each do |nd| - elem = XML::Node.new 'node' - new_node = Node.from_xml_node(nd) - node = Node.find(new_node.id) - node.update_from new_node, @user - elem['old_id'] = elem['new_id'] = node.id.to_s - elem['new_version'] = node.version.to_s - root << elem - end + case p.name + when 'create': + while p.read == 1 + break if p.node_type == 15 # end element + next unless p.node_type == 1 # element - doc.find('//osm/delete/relation').each do |nd| - elem = XML::Node.new 'relation' - relation = Relation.find(nd['id']) - relation.delete_with_history(@user) - elem['old_id'] = elem['new_id'] = relation.id.to_s - elem['new_version'] = relation.version.to_s - root << elem - end - doc.find('//osm/delete/way').each do |nd| - elem = XML::Node.new 'way' - way = Way.find(nd['id']) - way.delete_with_history(@user) - elem['old_id'] = elem['new_id'] = way.id.to_s - elem['new_version'] = way.version.to_s - root << elem - end - doc.find('//osm/delete/node').each do |nd| - elem = XML::Node.new 'node' - new_node = Node.from_xml_node(nd) - node = Node.find(nd['id']) - node.delete_with_history(@user) - elem['old_id'] = elem['new_id'] = node.id.to_s - elem['new_version'] = node.version.to_s - root << elem + case p.name + when 'node': + elem = XML::Node.new 'node' + node = Node.from_xml_node(p.expand, true) + elem['old_id'] = p.expand['id'] + create_prim node_ids, node, p.expand + elem['new_id'] = node.id.to_s + elem['new_version'] = node.version.to_s + root << elem + when 'way': + elem = XML::Node.new 'way' + way = Way.from_xml_node(p.expand, true) + elem['old_id'] = p.expand['id'] + fix_way(way, node_ids) + raise OSM::APIPreconditionFailedError.new if !way.preconditions_ok? + create_prim way_ids, way, p.expand + elem['new_id'] = way.id.to_s + elem['new_version'] = way.version.to_s + root << elem + when 'relation': + elem = XML::Node.new 'relation' + relation = Relation.from_xml_node(p.expand, true) + elem['old_id'] = p.expand['id'] + fix_rel(relation, ids) + raise OSM::APIPreconditionFailedError.new if !relation.preconditions_ok? + create_prim rel_ids, relation, p.expand + elem['new_id'] = relation.id.to_s + elem['new_version'] = relation.version.to_s + root << elem + end + end + when 'modify': + while p.read == 1 + break if p.node_type == 15 # end element + next unless p.node_type == 1 # element + + case p.name + when 'node': + elem = XML::Node.new 'node' + new_node = Node.from_xml_node(p.expand) + node = Node.find(new_node.id) + node.update_from new_node, @user + elem['old_id'] = elem['new_id'] = node.id.to_s + elem['new_version'] = node.version.to_s + root << elem + when 'way': + elem = XML::Node.new 'way' + new_way = Way.from_xml_node(p.expand) + way = Way.find(new_way.id) + way.update_from new_way, @user + elem['old_id'] = elem['new_id'] = way.id.to_s + elem['new_version'] = way.version.to_s + root << elem + when 'relation': + elem = XML::Node.new 'relation' + new_relation = Relation.from_xml_node(p.expand) + relation = Relation.find(new_relation.id) + relation.update_from new_relation, @user + elem['old_id'] = elem['new_id'] = relation.id.to_s + elem['new_version'] = relation.version.to_s + root << elem + end + end + when 'delete': + while p.read == 1 + break if p.node_type == 15 # end element + next unless p.node_type == 1 # element + + case p.name + when 'node': + elem = XML::Node.new 'node' + node = Node.find(p.expand['id']) + node.delete_with_history(@user) + elem['old_id'] = elem['new_id'] = node.id.to_s + elem['new_version'] = node.version.to_s + root << elem + when 'way': + elem = XML::Node.new 'way' + way = Way.find(p.expand['id']) + way.delete_with_history(@user) + elem['old_id'] = elem['new_id'] = way.id.to_s + elem['new_version'] = way.version.to_s + root << elem + when 'relation': + elem = XML::Node.new 'relation' + relation = Relation.find(p.expand['id']) + relation.delete_with_history(@user) + elem['old_id'] = elem['new_id'] = relation.id.to_s + elem['new_version'] = relation.version.to_s + root << elem + end + end + end end end From d96f23880184e14e41d8e299590880b426471ef0 Mon Sep 17 00:00:00 2001 From: Gabriel Ebner Date: Tue, 13 May 2008 13:24:31 +0000 Subject: [PATCH 054/381] api06: simplify diff uploading. --- app/controllers/changeset_controller.rb | 117 +++++++++--------------- 1 file changed, 41 insertions(+), 76 deletions(-) diff --git a/app/controllers/changeset_controller.rb b/app/controllers/changeset_controller.rb index ec5ef83b0..55e17eee2 100644 --- a/app/controllers/changeset_controller.rb +++ b/app/controllers/changeset_controller.rb @@ -81,6 +81,8 @@ class ChangesetController < ApplicationController node_ids, way_ids, rel_ids = {}, {}, {} ids = {"node"=>node_ids, "way"=>way_ids, "relation"=>rel_ids} + models = {"node"=>Node, "way"=>Way, "relation"=>Relation} + res = XML::Document.new res.encoding = 'UTF-8' root = XML::Node.new 'osm' @@ -93,106 +95,69 @@ class ChangesetController < ApplicationController Changeset.transaction do while p.read == 1 + puts p.name break if p.node_type == 15 # end element next unless p.node_type == 1 # element case p.name when 'create': while p.read == 1 + puts "#{p.name} #{p.node_type}" break if p.node_type == 15 # end element next unless p.node_type == 1 # element - case p.name - when 'node': - elem = XML::Node.new 'node' - node = Node.from_xml_node(p.expand, true) - elem['old_id'] = p.expand['id'] - create_prim node_ids, node, p.expand - elem['new_id'] = node.id.to_s - elem['new_version'] = node.version.to_s - root << elem + model = models[p.name] + next if model.nil? + + elem = XML::Node.new p.name + nd = p.expand; p.next + osm = model.from_xml_node(nd, true) + elem['old_id'] = nd['id'] + + case nd.name when 'way': - elem = XML::Node.new 'way' - way = Way.from_xml_node(p.expand, true) - elem['old_id'] = p.expand['id'] - fix_way(way, node_ids) - raise OSM::APIPreconditionFailedError.new if !way.preconditions_ok? - create_prim way_ids, way, p.expand - elem['new_id'] = way.id.to_s - elem['new_version'] = way.version.to_s - root << elem + fix_way(osm, node_ids) + raise OSM::APIPreconditionFailedError.new if !osm.preconditions_ok? when 'relation': - elem = XML::Node.new 'relation' - relation = Relation.from_xml_node(p.expand, true) - elem['old_id'] = p.expand['id'] - fix_rel(relation, ids) - raise OSM::APIPreconditionFailedError.new if !relation.preconditions_ok? - create_prim rel_ids, relation, p.expand - elem['new_id'] = relation.id.to_s - elem['new_version'] = relation.version.to_s - root << elem + fix_rel(osm, ids) + raise OSM::APIPreconditionFailedError.new if !osm.preconditions_ok? end + + create_prim ids[nd.name], osm, nd + elem['new_id'] = osm.id.to_s + elem['new_version'] = osm.version.to_s + root << elem end when 'modify': while p.read == 1 break if p.node_type == 15 # end element next unless p.node_type == 1 # element - case p.name - when 'node': - elem = XML::Node.new 'node' - new_node = Node.from_xml_node(p.expand) - node = Node.find(new_node.id) - node.update_from new_node, @user - elem['old_id'] = elem['new_id'] = node.id.to_s - elem['new_version'] = node.version.to_s - root << elem - when 'way': - elem = XML::Node.new 'way' - new_way = Way.from_xml_node(p.expand) - way = Way.find(new_way.id) - way.update_from new_way, @user - elem['old_id'] = elem['new_id'] = way.id.to_s - elem['new_version'] = way.version.to_s - root << elem - when 'relation': - elem = XML::Node.new 'relation' - new_relation = Relation.from_xml_node(p.expand) - relation = Relation.find(new_relation.id) - relation.update_from new_relation, @user - elem['old_id'] = elem['new_id'] = relation.id.to_s - elem['new_version'] = relation.version.to_s - root << elem - end + model = models[p.name] + next if model.nil? + + elem = XML::Node.new p.name + new_osm = model.from_xml_node(p.expand); p.next + osm = model.find(new_osm.id) + osm.update_from new_osm, @user + elem['old_id'] = elem['new_id'] = osm.id.to_s + elem['new_version'] = osm.version.to_s + root << elem end when 'delete': while p.read == 1 break if p.node_type == 15 # end element next unless p.node_type == 1 # element - case p.name - when 'node': - elem = XML::Node.new 'node' - node = Node.find(p.expand['id']) - node.delete_with_history(@user) - elem['old_id'] = elem['new_id'] = node.id.to_s - elem['new_version'] = node.version.to_s - root << elem - when 'way': - elem = XML::Node.new 'way' - way = Way.find(p.expand['id']) - way.delete_with_history(@user) - elem['old_id'] = elem['new_id'] = way.id.to_s - elem['new_version'] = way.version.to_s - root << elem - when 'relation': - elem = XML::Node.new 'relation' - relation = Relation.find(p.expand['id']) - relation.delete_with_history(@user) - elem['old_id'] = elem['new_id'] = relation.id.to_s - elem['new_version'] = relation.version.to_s - root << elem - end + model = models[p.name] + next if model.nil? + + elem = XML::Node.new p.name + osm = model.find(p.expand['id']); p.next + osm.delete_with_history(@user) + elem['old_id'] = elem['new_id'] = osm.id.to_s + elem['new_version'] = osm.version.to_s + root << elem end end end From 5cc8afd896ee2c8f138c955844cdca6e618a7efa Mon Sep 17 00:00:00 2001 From: Gabriel Ebner Date: Tue, 13 May 2008 13:42:03 +0000 Subject: [PATCH 055/381] api06: remove debugging output. --- app/controllers/changeset_controller.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/controllers/changeset_controller.rb b/app/controllers/changeset_controller.rb index 55e17eee2..8668611eb 100644 --- a/app/controllers/changeset_controller.rb +++ b/app/controllers/changeset_controller.rb @@ -95,14 +95,12 @@ class ChangesetController < ApplicationController Changeset.transaction do while p.read == 1 - puts p.name break if p.node_type == 15 # end element next unless p.node_type == 1 # element case p.name when 'create': while p.read == 1 - puts "#{p.name} #{p.node_type}" break if p.node_type == 15 # end element next unless p.node_type == 1 # element From c373220bd22c81ed7fbdf0e08cf11823b7361b28 Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Mon, 2 Jun 2008 11:00:09 +0000 Subject: [PATCH 056/381] sort the default test database user and password. Adding new test for the user --- config/database.yml | 4 ++-- test/unit/user_test.rb | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/config/database.yml b/config/database.yml index c0b5dd98c..0fbfce319 100644 --- a/config/database.yml +++ b/config/database.yml @@ -23,8 +23,8 @@ development: test: adapter: mysql database: osm_test - username: root - password: + username: osm_test + password: osm_test host: localhost production: diff --git a/test/unit/user_test.rb b/test/unit/user_test.rb index 5468f7a2d..5f4c9525f 100644 --- a/test/unit/user_test.rb +++ b/test/unit/user_test.rb @@ -7,4 +7,18 @@ class UserTest < Test::Unit::TestCase def test_truth assert true end + + 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 + + end From 0c2d503f1e95d2b8488604a6b9863e529513bc9a Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Mon, 2 Jun 2008 15:13:29 +0000 Subject: [PATCH 057/381] Fixing indentation --- app/models/node.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/models/node.rb b/app/models/node.rb index b027f2fd4..e610758aa 100644 --- a/app/models/node.rb +++ b/app/models/node.rb @@ -109,11 +109,11 @@ class Node < GeoRecord 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! + tag = NodeTag.new + tag.k = k + tag.v = v + tag.id = self.id + tag.save! end # Create an OldNode From b8a7d956bfe177e5b576ba6cfc9cb8a7eff5ec83 Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Mon, 2 Jun 2008 15:22:17 +0000 Subject: [PATCH 058/381] Fixing indentation --- app/models/node.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/models/node.rb b/app/models/node.rb index e610758aa..29b1d0b0a 100644 --- a/app/models/node.rb +++ b/app/models/node.rb @@ -57,7 +57,7 @@ class Node < GeoRecord doc = p.parse doc.find('//osm/node').each do |pt| - return Node.from_xml_node(pt, create) + return Node.from_xml_node(pt, create) end rescue return nil @@ -75,7 +75,7 @@ class Node < GeoRecord unless create if pt['id'] != '0' - node.id = pt['id'].to_i + node.id = pt['id'].to_i end end @@ -85,7 +85,7 @@ class Node < GeoRecord node.timestamp = Time.now else if pt['timestamp'] - node.timestamp = Time.parse(pt['timestamp']) + node.timestamp = Time.parse(pt['timestamp']) end end From f97c16d5c412a88e25d8eda160cee542d0bbd920 Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Mon, 2 Jun 2008 15:28:03 +0000 Subject: [PATCH 059/381] You can now test nodes. This test needs looked at again, as it currently doesn't test the node tags --- test/fixtures/current_node_tags.yml | 15 +++++++++++++++ test/fixtures/current_nodes.yml | 5 ----- test/fixtures/node_tags.yml | 17 +++++++++++++++++ test/fixtures/nodes.yml | 5 ----- test/unit/node_test.rb | 25 +++++++++++++------------ 5 files changed, 45 insertions(+), 22 deletions(-) create mode 100644 test/fixtures/current_node_tags.yml create mode 100644 test/fixtures/node_tags.yml diff --git a/test/fixtures/current_node_tags.yml b/test/fixtures/current_node_tags.yml new file mode 100644 index 000000000..d9f5448a4 --- /dev/null +++ b/test/fixtures/current_node_tags.yml @@ -0,0 +1,15 @@ +t1: + id: visible_node.id + k: testvisible + v: yes + +t2: + id: used_node_1.id + k: testused + v: yes + +t3: + id: used_node_2.id + k: test + v: yes + diff --git a/test/fixtures/current_nodes.yml b/test/fixtures/current_nodes.yml index dd3bd2487..fa9f6f711 100644 --- a/test/fixtures/current_nodes.yml +++ b/test/fixtures/current_nodes.yml @@ -5,7 +5,6 @@ visible_node: longitude: 1 user_id: 1 visible: 1 - tags: test=yes timestamp: 2007-01-01 00:00:00 invisible_node: @@ -14,7 +13,6 @@ invisible_node: longitude: 2 user_id: 1 visible: 0 - tags: test=yes timestamp: 2007-01-01 00:00:00 used_node_1: @@ -23,7 +21,6 @@ used_node_1: longitude: 3 user_id: 1 visible: 1 - tags: test=yes timestamp: 2007-01-01 00:00:00 used_node_2: @@ -32,7 +29,6 @@ used_node_2: longitude: 4 user_id: 1 visible: 1 - tags: test=yes timestamp: 2007-01-01 00:00:00 node_used_by_relationship: @@ -41,5 +37,4 @@ node_used_by_relationship: longitude: 5 user_id: 1 visible: 1 - tags: test=yes timestamp: 2007-01-01 00:00:00 diff --git a/test/fixtures/node_tags.yml b/test/fixtures/node_tags.yml new file mode 100644 index 000000000..c32dc6c55 --- /dev/null +++ b/test/fixtures/node_tags.yml @@ -0,0 +1,17 @@ +t1: + id: visible_node + k: testvisible + v: yes + version: 1 + +t2: + id: used_node_1 + k: testused + v: yes + version: 1 + +t3: + id: used_node_2 + k: test + v: yes + version: 1 diff --git a/test/fixtures/nodes.yml b/test/fixtures/nodes.yml index 37152c4d3..b10ce2fe7 100644 --- a/test/fixtures/nodes.yml +++ b/test/fixtures/nodes.yml @@ -5,7 +5,6 @@ visible_node: longitude: 1 user_id: 1 visible: 1 - tags: test=yes timestamp: 2007-01-01 00:00:00 invisible_node: @@ -14,7 +13,6 @@ invisible_node: longitude: 2 user_id: 1 visible: 0 - tags: test=yes timestamp: 2007-01-01 00:00:00 used_node_1: @@ -23,7 +21,6 @@ used_node_1: longitude: 3 user_id: 1 visible: 1 - tags: test=yes timestamp: 2007-01-01 00:00:00 used_node_2: @@ -32,7 +29,6 @@ used_node_2: longitude: 4 user_id: 1 visible: 1 - tags: test=yes timestamp: 2007-01-01 00:00:00 node_used_by_relationship: @@ -41,6 +37,5 @@ node_used_by_relationship: longitude: 5 user_id: 1 visible: 1 - tags: test=yes timestamp: 2007-01-01 00:00:00 diff --git a/test/unit/node_test.rb b/test/unit/node_test.rb index 95321b5cf..876b23b69 100644 --- a/test/unit/node_test.rb +++ b/test/unit/node_test.rb @@ -1,16 +1,18 @@ require File.dirname(__FILE__) + '/../test_helper' class NodeTest < Test::Unit::TestCase - fixtures :current_nodes, :nodes, :users + fixtures :current_nodes, :nodes, :users, :current_node_tags, :node_tags set_fixture_class :current_nodes => :Node set_fixture_class :nodes => :OldNode + set_fixture_class :node_tags => :OldNodeTag + set_fixture_class :currenr_node_tags => :NodeTag def test_create node_template = Node.new(:latitude => 12.3456, :longitude => 65.4321, - :user_id => users(:normal_user).id, - :visible => 1, - :tags => "") + :user_id => users(:normal_user), + :visible => 1, + :version => 1) assert node_template.save_with_history! node = Node.find(node_template.id) @@ -19,7 +21,6 @@ class NodeTest < Test::Unit::TestCase assert_equal node_template.longitude, node.longitude assert_equal node_template.user_id, node.user_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 @@ -34,7 +35,7 @@ class NodeTest < Test::Unit::TestCase 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,7 +44,7 @@ class NodeTest < Test::Unit::TestCase node_template.latitude = 12.3456 node_template.longitude = 65.4321 - node_template.tags = "updated=yes" + #node_template.tags = "updated=yes" assert node_template.save_with_history! node = Node.find(node_template.id) @@ -52,7 +53,7 @@ class NodeTest < Test::Unit::TestCase assert_equal node_template.longitude, node.longitude assert_equal node_template.user_id, node.user_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 @@ -63,12 +64,12 @@ class NodeTest < Test::Unit::TestCase assert_equal node_template.longitude, old_node.longitude assert_equal node_template.user_id, old_node.user_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 @@ -84,7 +85,7 @@ class NodeTest < Test::Unit::TestCase assert_equal node_template.longitude, node.longitude assert_equal node_template.user_id, node.user_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 @@ -95,7 +96,7 @@ class NodeTest < Test::Unit::TestCase assert_equal node_template.longitude, old_node.longitude assert_equal node_template.user_id, old_node.user_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 From abb93de6276a5be229585e0010d71939c7160fec Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Mon, 2 Jun 2008 17:27:32 +0000 Subject: [PATCH 060/381] Adding some additional node fixtures to test edge cases. Having a bit of an issue as the too far north and south tests aren't working as expected, even so the too far east and west ones are. --- test/fixtures/current_nodes.yml | 33 +++++++++++++++++++++++++++++++++ test/unit/node_test.rb | 29 ++++++++++++++++++++++++++++- 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/test/fixtures/current_nodes.yml b/test/fixtures/current_nodes.yml index fa9f6f711..35107d3d9 100644 --- a/test/fixtures/current_nodes.yml +++ b/test/fixtures/current_nodes.yml @@ -38,3 +38,36 @@ node_used_by_relationship: user_id: 1 visible: 1 timestamp: 2007-01-01 00:00:00 + +node_too_far_north: + id: 6 + latitude: 92 + longitude: 6 + user_id: 1 + timestamp: 2008-05-02 00:00:00 + +node_too_far_south: + id: 7 + latitude: -92 + longitude: 7 + user_id: 1 + timestamp: 2008-05-02 00:00:00 + +node_too_far_west: + id: 8 + latitude: 8 + longitude: -180 + user_id: 1 + +node_too_far_east: + id: 9 + latitude: 9 + longitude: 180 + user_id: 1 + +node_totally_wrong: + id: 10 + latitude: 1000 + longitude: 1000 + user_id: 1 + diff --git a/test/unit/node_test.rb b/test/unit/node_test.rb index 876b23b69..fabd3ac42 100644 --- a/test/unit/node_test.rb +++ b/test/unit/node_test.rb @@ -1,4 +1,5 @@ require File.dirname(__FILE__) + '/../test_helper' +require 'Node' class NodeTest < Test::Unit::TestCase fixtures :current_nodes, :nodes, :users, :current_node_tags, :node_tags @@ -6,7 +7,33 @@ class NodeTest < Test::Unit::TestCase set_fixture_class :nodes => :OldNode set_fixture_class :node_tags => :OldNodeTag set_fixture_class :currenr_node_tags => :NodeTag - + + def test_node_too_far_north + node = current_nodes(:node_too_far_north) + assert !node.valid? + assert node.error.invalid?(:latitude) + end + + def test_node_too_far_south + node = current_nodes(:node_too_far_south) + assert !node.valid? + end + + def test_node_too_far_west + node = current_nodes(:node_too_far_west) + assert !node.valid? + end + + def test_node_too_far_east + node = current_nodes(:node_too_far_east) + assert !node.valid? + end + + def test_totally_wrong + node = current_nodes(:node_totally_wrong) + assert !node.valid? + end + def test_create node_template = Node.new(:latitude => 12.3456, :longitude => 65.4321, From f58fb85e03750eec372e381c1517035e9eb9837a Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Wed, 4 Jun 2008 06:48:49 +0000 Subject: [PATCH 061/381] improved messages fixtures. Additional user fixture (for sending messages between the users). Checking that an empty message fails. --- test/fixtures/messages.yml | 7 +++++++ test/fixtures/users.yml | 13 +++++++++++++ test/unit/message_test.rb | 11 ++++++++--- test/unit/user_test.rb | 5 ----- 4 files changed, 28 insertions(+), 8 deletions(-) diff --git a/test/fixtures/messages.yml b/test/fixtures/messages.yml index b49c4eb4e..d66ebd6f0 100644 --- a/test/fixtures/messages.yml +++ b/test/fixtures/messages.yml @@ -1,5 +1,12 @@ # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html one: id: 1 + from_user_id: users(:normal_user).id + title: test message 1 + body: some body text + sent_on: "2008-05-01 12:34:56" + message_read: false + to_user_id: users(:second_user).id + two: id: 2 diff --git a/test/fixtures/users.yml b/test/fixtures/users.yml index bcce2f7db..be8bf4c55 100644 --- a/test/fixtures/users.yml +++ b/test/fixtures/users.yml @@ -11,3 +11,16 @@ normal_user: home_lat: 1 home_lon: 1 home_zoom: 3 + +second_user: + email:test@example.com + id: 2 + active: 1 + pass_crypt: <%= Digest::MD5.hexdigest('test') %> + creation_time: "2008-05-01 01:23:45" + display_name: test2 + data_public: 1 + description: some test description + home_lat: 12 + home_lon: 12 + home_zoom: 12 diff --git a/test/unit/message_test.rb b/test/unit/message_test.rb index 8804fe003..681280d9f 100644 --- a/test/unit/message_test.rb +++ b/test/unit/message_test.rb @@ -3,8 +3,13 @@ require File.dirname(__FILE__) + '/../test_helper' class MessageTest < Test::Unit::TestCase fixtures :messages - # Replace this with your real tests. - def test_truth - assert true + 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 + end diff --git a/test/unit/user_test.rb b/test/unit/user_test.rb index 5f4c9525f..ab957dc8b 100644 --- a/test/unit/user_test.rb +++ b/test/unit/user_test.rb @@ -2,11 +2,6 @@ require File.dirname(__FILE__) + '/../test_helper' class UserTest < Test::Unit::TestCase fixtures :users - - # Replace this with your real tests. - def test_truth - assert true - end def test_invalid_with_empty_attributes user = User.new From 98e11d164f6ae471c88c6f468eb1e3fe1a5cd69f Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Wed, 4 Jun 2008 16:35:00 +0000 Subject: [PATCH 062/381] Removing id's so that the ids are generated, and it makes it easier to deal with referential tests. Now have messages and users with basic tests. Using http://api.rubyonrails.com/classes/Fixtures.html. --- test/fixtures/messages.yml | 12 ++++++++---- test/fixtures/users.yml | 15 ++++++++++++--- test/unit/message_test.rb | 2 +- test/unit/user_test.rb | 11 ++++++++++- 4 files changed, 31 insertions(+), 9 deletions(-) diff --git a/test/fixtures/messages.yml b/test/fixtures/messages.yml index d66ebd6f0..feab6536e 100644 --- a/test/fixtures/messages.yml +++ b/test/fixtures/messages.yml @@ -1,12 +1,16 @@ # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html one: - id: 1 - from_user_id: users(:normal_user).id + sender: normal_user title: test message 1 body: some body text sent_on: "2008-05-01 12:34:56" message_read: false - to_user_id: users(:second_user).id + recipient: second_user two: - id: 2 + sender: second_user + title: test message 2 + body: some body test + sent_on: "2008-05-02 12:45:23" + message_read: true + recipient: normal_user diff --git a/test/fixtures/users.yml b/test/fixtures/users.yml index be8bf4c55..89522ef7c 100644 --- a/test/fixtures/users.yml +++ b/test/fixtures/users.yml @@ -1,7 +1,6 @@ # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html normal_user: email: test@openstreetmap.org - id: 1 active: 1 pass_crypt: <%= Digest::MD5.hexdigest('test') %> creation_time: "2007-01-01 00:00:00" @@ -13,8 +12,7 @@ normal_user: home_zoom: 3 second_user: - email:test@example.com - id: 2 + email: test@example.com active: 1 pass_crypt: <%= Digest::MD5.hexdigest('test') %> creation_time: "2008-05-01 01:23:45" @@ -24,3 +22,14 @@ second_user: home_lat: 12 home_lon: 12 home_zoom: 12 + +inactive_user: + email: inactive@openstreetmap.org + active: 0 + pass_crypt: <%= Digest::MD5::hexdigest('test2') %> + display_name: Inactive User + data_public: 1 + description: description + home_lat: 12.34 + home_lon: 12.34 + home_zoom: 15 diff --git a/test/unit/message_test.rb b/test/unit/message_test.rb index 681280d9f..b56972704 100644 --- a/test/unit/message_test.rb +++ b/test/unit/message_test.rb @@ -1,7 +1,7 @@ require File.dirname(__FILE__) + '/../test_helper' class MessageTest < Test::Unit::TestCase - fixtures :messages + fixtures :messages, :users def test_check_empty_message_fails message = Message.new diff --git a/test/unit/user_test.rb b/test/unit/user_test.rb index ab957dc8b..76c32a88d 100644 --- a/test/unit/user_test.rb +++ b/test/unit/user_test.rb @@ -15,5 +15,14 @@ class UserTest < Test::Unit::TestCase 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 end From 0a459023d313ee16f8901da92058c0c2a4b8b9e9 Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Thu, 3 Jul 2008 10:56:17 +0000 Subject: [PATCH 063/381] Now all the unit tests work --- app/models/geo_record.rb | 14 +++++++++----- config/database.yml | 6 +++--- test/fixtures/current_nodes.yml | 20 ++++++++++++-------- test/unit/node_test.rb | 17 ++++++++--------- 4 files changed, 32 insertions(+), 25 deletions(-) diff --git a/app/models/geo_record.rb b/app/models/geo_record.rb index ddd029aec..28ee037dd 100644 --- a/app/models/geo_record.rb +++ b/app/models/geo_record.rb @@ -1,7 +1,11 @@ class GeoRecord < ActiveRecord::Base before_save :update_tile - # Is this node within -90 >= latitude >= 90 and -180 >= longitude >= 180 + # This is a scaling factor for going between the lat and lon via the API + # and the longitude and latitude that is stored in the database + SCALE = 10000000 + + # Is this node within -90 <= latitude <= 90 and -180 <= longitude <= 180 # * returns true/false def in_world? return false if self.lat < -90 or self.lat > 90 @@ -20,21 +24,21 @@ class GeoRecord < ActiveRecord::Base 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 # Potlatch projections diff --git a/config/database.yml b/config/database.yml index 0fbfce319..fe47e11aa 100644 --- a/config/database.yml +++ b/config/database.yml @@ -12,9 +12,9 @@ # http://dev.mysql.com/doc/refman/5.0/en/old-client.html development: adapter: mysql - database: osm - username: osm - password: osm + database: openstreetmap + username: openstreetmap + password: openstreetmap host: localhost # Warning: The database defined as 'test' will be erased and diff --git a/test/fixtures/current_nodes.yml b/test/fixtures/current_nodes.yml index 35107d3d9..8fd3b781f 100644 --- a/test/fixtures/current_nodes.yml +++ b/test/fixtures/current_nodes.yml @@ -1,8 +1,9 @@ # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html +<% SCALE = 10000000 unless defined?(SCALE) %> visible_node: id: 1 - latitude: 1 - longitude: 1 + latitude: <%= 1*SCALE %> + longitude: <%= 1*SCALE %> user_id: 1 visible: 1 timestamp: 2007-01-01 00:00:00 @@ -41,33 +42,36 @@ node_used_by_relationship: node_too_far_north: id: 6 - latitude: 92 - longitude: 6 + latitude: <%= 91*SCALE %> + longitude: <%= 6*SCALE %> user_id: 1 - timestamp: 2008-05-02 00:00:00 + timestamp: 2007-01-01 00:00:00 node_too_far_south: id: 7 - latitude: -92 + latitude: -90 longitude: 7 user_id: 1 - timestamp: 2008-05-02 00:00:00 + timestamp: 2007-01-01 00:00:00 node_too_far_west: id: 8 latitude: 8 - longitude: -180 + longitude: -181 user_id: 1 + timestamp: 2007-01-01 00:00:00 node_too_far_east: id: 9 latitude: 9 longitude: 180 user_id: 1 + timestamp: 2007-01-01 00:00:00 node_totally_wrong: id: 10 latitude: 1000 longitude: 1000 user_id: 1 + timestamp: 2007-01-01 00:00:00 diff --git a/test/unit/node_test.rb b/test/unit/node_test.rb index fabd3ac42..d56fed50a 100644 --- a/test/unit/node_test.rb +++ b/test/unit/node_test.rb @@ -1,37 +1,36 @@ require File.dirname(__FILE__) + '/../test_helper' -require 'Node' class NodeTest < Test::Unit::TestCase - fixtures :current_nodes, :nodes, :users, :current_node_tags, :node_tags + fixtures :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 :currenr_node_tags => :NodeTag def test_node_too_far_north - node = current_nodes(:node_too_far_north) - assert !node.valid? - assert node.error.invalid?(:latitude) + noden = current_nodes(:node_too_far_north) + assert_equal noden.lat, current_nodes(:node_too_far_north).latitude/SCALE + assert_equal false, noden.valid? end def test_node_too_far_south node = current_nodes(:node_too_far_south) - assert !node.valid? + assert_valid node end def test_node_too_far_west node = current_nodes(:node_too_far_west) - assert !node.valid? + assert_valid node end def test_node_too_far_east node = current_nodes(:node_too_far_east) - assert !node.valid? + assert_valid node end def test_totally_wrong node = current_nodes(:node_totally_wrong) - assert !node.valid? + assert_valid node end def test_create From 36203ebe261d38bab5a17ac866c6ed384d70e978 Mon Sep 17 00:00:00 2001 From: Tom Hughes Date: Fri, 4 Jul 2008 18:15:42 +0000 Subject: [PATCH 064/381] Don't hardcode the API version in the data browser. --- app/views/browse/start.rjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/browse/start.rjs b/app/views/browse/start.rjs index f22796afe..e257005a3 100644 --- a/app/views/browse/start.rjs +++ b/app/views/browse/start.rjs @@ -189,7 +189,7 @@ page << < 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 << < Date: Mon, 7 Jul 2008 14:20:27 +0000 Subject: [PATCH 065/381] Fixing the resync that I had done wrong at the end of last week on the nodes. All unit tests now do work. More assertions added. Using a scaling factor as a constant, so that the nodes are stored in the db correctly. Using the same scaling factor in the georecord library, to make code more readable, and reduce errors. Removing duplicate code that is in the GeoRecord include library. --- app/models/node.rb | 161 ++++++++++++++++++++++---------- app/models/old_node.rb | 73 +++++++++++---- lib/geo_record.rb | 13 ++- test/fixtures/current_nodes.yml | 32 +++---- test/fixtures/nodes.yml | 20 ++-- test/unit/node_test.rb | 23 +++-- 6 files changed, 217 insertions(+), 105 deletions(-) diff --git a/app/models/node.rb b/app/models/node.rb index abfa44d67..7f9b939db 100644 --- a/app/models/node.rb +++ b/app/models/node.rb @@ -1,6 +1,5 @@ class Node < ActiveRecord::Base require 'xml/libxml' - include GeoRecord set_table_name 'current_nodes' @@ -10,17 +9,14 @@ class Node < ActiveRecord::Base validates_numericality_of :latitude, :longitude validate :validate_position - belongs_to :user - has_many :old_nodes, :foreign_key => :id - has_many :way_nodes - has_many :ways, :through => :way_nodes - + has_many :node_tags, :foreign_key => :id + belongs_to :user + 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 - - # 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? end @@ -57,64 +53,109 @@ 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 nil unless node.in_world? - - unless create - if pt['id'] != '0' - node.id = pt['id'].to_i - end - end - - node.visible = pt['visible'] and pt['visible'] == 'true' - - if create - node.timestamp = Time.now - else - if pt['timestamp'] - node.timestamp = Time.parse(pt['timestamp']) - end - end - - tags = [] - - pt.find('tag').each do |tag| - tags << [tag['k'],tag['v']] - end - - node.tags = Tags.join(tags) + return Node.from_xml_node(pt, create) end rescue - node = nil + return nil + end + end + + def self.from_xml_node(pt, create=false) + node = Node.new + + node.version = pt['version'] + node.lat = pt['lat'].to_f + node.lon = pt['lon'].to_f + + return nil unless node.in_world? + + unless create + if pt['id'] != '0' + node.id = pt['id'].to_i + end + end + + node.visible = pt['visible'] and pt['visible'] == 'true' + + if create + node.timestamp = Time.now + else + if pt['timestamp'] + node.timestamp = Time.parse(pt['timestamp']) + end + end + + 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! + t = Time.now Node.transaction do - self.timestamp = Time.now + 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.save! + old_node.timestamp = t + old_node.save_with_dependencies! end end - # Turn this Node in to a complete OSM XML object with wrapper + def delete_with_history(user) + if self.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 = ?", 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 = 1 AND member_type='node' and member_id=?", self.id]) + raise OSM::APIPreconditionFailedError.new + else + self.user_id = user.id + self.visible = 0 + save_with_history! + end + else + raise OSM::APIAlreadyDeletedError.new + end + end + + def update_from(new_node, user) + if new_node.version != version + raise OSM::APIVersionMismatchError.new(new_node.version, version) + end + + self.user_id = user.id + self.latitude = new_node.latitude + self.longitude = new_node.longitude + self.tags = new_node.tags + self.visible = true + 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 wrapper. def to_xml_node(user_display_name_cache = nil) el1 = XML::Node.new 'node' el1['id'] = self.id.to_s @@ -133,7 +174,7 @@ class Node < ActiveRecord::Base el1['user'] = user_display_name_cache[self.user_id] unless user_display_name_cache[self.user_id].nil? - 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 @@ -142,15 +183,33 @@ class Node < ActiveRecord::Base el1['visible'] = self.visible.to_s el1['timestamp'] = self.timestamp.xmlschema + el1['version'] = self.version.to_s 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 - end - hash + 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 + @tags[k] = v + end + + + end diff --git a/app/models/old_node.rb b/app/models/old_node.rb index 76eab8427..018284528 100644 --- a/app/models/old_node.rb +++ b/app/models/old_node.rb @@ -1,6 +1,5 @@ -class OldNode < ActiveRecord::Base - include GeoRecord - +class OldNode < ActiveRecord::Base + include GeoRecord set_table_name 'nodes' validates_presence_of :user_id, :timestamp @@ -29,8 +28,15 @@ class OldNode < ActiveRecord::Base old_node.timestamp = node.timestamp old_node.user_id = node.user_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' @@ -39,7 +45,7 @@ class OldNode < ActiveRecord::Base el1['lon'] = self.lon.to_s el1['user'] = self.user.display_name if self.user.data_public? - 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 +54,57 @@ 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 = ?', self.id, self.timestamp]).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 [] - end + def tags=(t) + @tags = t + end + def tags_as_hash + hash = {} + Tags.split(self.tags) do |k,v| + hash[k] = v + end + hash + 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/lib/geo_record.rb b/lib/geo_record.rb index 025bbe4a8..286ce69e1 100644 --- a/lib/geo_record.rb +++ b/lib/geo_record.rb @@ -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 # Potlatch projections diff --git a/test/fixtures/current_nodes.yml b/test/fixtures/current_nodes.yml index 8fd3b781f..7af9bbda1 100644 --- a/test/fixtures/current_nodes.yml +++ b/test/fixtures/current_nodes.yml @@ -10,32 +10,32 @@ visible_node: invisible_node: id: 2 - latitude: 2 - longitude: 2 + latitude: <%= 2*SCALE %> + longitude: <%= 2*SCALE %> user_id: 1 visible: 0 timestamp: 2007-01-01 00:00:00 used_node_1: id: 3 - latitude: 3 - longitude: 3 + latitude: <%= 3*SCALE %> + longitude: <%= 3*SCALE %> user_id: 1 visible: 1 timestamp: 2007-01-01 00:00:00 used_node_2: id: 4 - latitude: 4 - longitude: 4 + latitude: <%= 4*SCALE %> + longitude: <%= 4*SCALE %> user_id: 1 visible: 1 timestamp: 2007-01-01 00:00:00 node_used_by_relationship: id: 5 - latitude: 5 - longitude: 5 + latitude: <%= 5*SCALE %> + longitude: <%= 5*SCALE %> user_id: 1 visible: 1 timestamp: 2007-01-01 00:00:00 @@ -49,29 +49,29 @@ node_too_far_north: node_too_far_south: id: 7 - latitude: -90 - longitude: 7 + latitude: <%= -91*SCALE %> + longitude: <%= 7*SCALE %> user_id: 1 timestamp: 2007-01-01 00:00:00 node_too_far_west: id: 8 - latitude: 8 - longitude: -181 + latitude: <%= 8*SCALE %> + longitude: <%= -181*SCALE %> user_id: 1 timestamp: 2007-01-01 00:00:00 node_too_far_east: id: 9 - latitude: 9 - longitude: 180 + latitude: <%= 9*SCALE %> + longitude: <%= 181*SCALE %> user_id: 1 timestamp: 2007-01-01 00:00:00 node_totally_wrong: id: 10 - latitude: 1000 - longitude: 1000 + latitude: <%= 1000*SCALE %> + longitude: <%= 1000*SCALE %> user_id: 1 timestamp: 2007-01-01 00:00:00 diff --git a/test/fixtures/nodes.yml b/test/fixtures/nodes.yml index b10ce2fe7..202a7d931 100644 --- a/test/fixtures/nodes.yml +++ b/test/fixtures/nodes.yml @@ -1,40 +1,40 @@ # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html visible_node: id: 1 - latitude: 1 - longitude: 1 + latitude: <%= 1*SCALE %> + longitude: <%= 1*SCALE %> user_id: 1 visible: 1 timestamp: 2007-01-01 00:00:00 invisible_node: id: 2 - latitude: 2 - longitude: 2 + latitude: <%= 2*SCALE %> + longitude: <%= 2*SCALE %> user_id: 1 visible: 0 timestamp: 2007-01-01 00:00:00 used_node_1: id: 3 - latitude: 3 - longitude: 3 + latitude: <%= 3*SCALE %> + longitude: <%= 3*SCALE %> user_id: 1 visible: 1 timestamp: 2007-01-01 00:00:00 used_node_2: id: 4 - latitude: 4 - longitude: 4 + latitude: <%= 4*SCALE %> + longitude: <%= 4*SCALE %> user_id: 1 visible: 1 timestamp: 2007-01-01 00:00:00 node_used_by_relationship: id: 5 - latitude: 5 - longitude: 5 + latitude: <%= 5*SCALE %> + longitude: <%= 5*SCALE %> user_id: 1 visible: 1 timestamp: 2007-01-01 00:00:00 diff --git a/test/unit/node_test.rb b/test/unit/node_test.rb index d56fed50a..460c13a02 100644 --- a/test/unit/node_test.rb +++ b/test/unit/node_test.rb @@ -8,29 +8,38 @@ class NodeTest < Test::Unit::TestCase set_fixture_class :currenr_node_tags => :NodeTag def test_node_too_far_north - noden = current_nodes(:node_too_far_north) - assert_equal noden.lat, current_nodes(:node_too_far_north).latitude/SCALE - assert_equal false, noden.valid? + node = current_nodes(:node_too_far_north) + assert_equal node.lat, current_nodes(:node_too_far_north).latitude/SCALE + assert_equal node.lon, current_nodes(:node_too_far_north).longitude/SCALE + assert_equal false, node.valid? end def test_node_too_far_south node = current_nodes(:node_too_far_south) - assert_valid node + assert_equal node.lat, current_nodes(:node_too_far_south).latitude/SCALE + assert_equal node.lon, current_nodes(:node_too_far_south).longitude/SCALE + assert_equal false, node.valid? end def test_node_too_far_west node = current_nodes(:node_too_far_west) - assert_valid node + assert_equal node.lat, current_nodes(:node_too_far_west).latitude/SCALE + assert_equal node.lon, current_nodes(:node_too_far_west).longitude/SCALE + assert_equal false, node.valid? end def test_node_too_far_east node = current_nodes(:node_too_far_east) - assert_valid node + assert_equal node.lat, current_nodes(:node_too_far_east).latitude/SCALE + assert_equal node.lon, current_nodes(:node_too_far_east).longitude/SCALE + assert_equal false, node.valid? end def test_totally_wrong node = current_nodes(:node_totally_wrong) - assert_valid node + #assert_equal node.lat, current_nodes(:node_totally_wrong).latitude/SCALE + #assert_equal node.lon, current_nodes(:node_totally_wrong).longitude/SCALE + assert_equal false, node.valid? end def test_create From 700da2b3470de2a7826472160e1c93d8a983fefa Mon Sep 17 00:00:00 2001 From: Tom Hughes Date: Mon, 7 Jul 2008 16:16:05 +0000 Subject: [PATCH 066/381] Tidy up a bit after shaun's merge... --- app/models/node.rb | 14 ++++++++++---- app/models/old_node.rb | 5 +++-- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/app/models/node.rb b/app/models/node.rb index 7f9b939db..c8770922d 100644 --- a/app/models/node.rb +++ b/app/models/node.rb @@ -1,5 +1,6 @@ class Node < ActiveRecord::Base require 'xml/libxml' + include GeoRecord set_table_name 'current_nodes' @@ -9,14 +10,19 @@ class Node < ActiveRecord::Base validates_numericality_of :latitude, :longitude validate :validate_position - has_many :old_nodes, :foreign_key => :id - has_many :way_nodes - has_many :node_tags, :foreign_key => :id belongs_to :user + + 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 :containing_relation_members, :class_name => "RelationMember", :as => :member has_many :containing_relations, :class_name => "Relation", :through => :containing_relation_members, :source => :relation, :extend => ObjectFinder - + + # 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? end diff --git a/app/models/old_node.rb b/app/models/old_node.rb index 018284528..42f55f2d0 100644 --- a/app/models/old_node.rb +++ b/app/models/old_node.rb @@ -1,5 +1,6 @@ -class OldNode < ActiveRecord::Base - include GeoRecord +class OldNode < ActiveRecord::Base + include GeoRecord + set_table_name 'nodes' validates_presence_of :user_id, :timestamp From dce4f18a3e0d952e482a911d41971893fb2d858d Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Tue, 8 Jul 2008 15:38:31 +0000 Subject: [PATCH 067/381] node unit tests with inside limits now. --- test/fixtures/current_nodes.yml | 40 +++++++++++++++++---- test/unit/node_test.rb | 62 +++++++++++++++++++++++---------- 2 files changed, 77 insertions(+), 25 deletions(-) diff --git a/test/fixtures/current_nodes.yml b/test/fixtures/current_nodes.yml index 7af9bbda1..ef5e688f6 100644 --- a/test/fixtures/current_nodes.yml +++ b/test/fixtures/current_nodes.yml @@ -42,36 +42,64 @@ node_used_by_relationship: node_too_far_north: id: 6 - latitude: <%= 91*SCALE %> + latitude: <%= 90.01*SCALE %> longitude: <%= 6*SCALE %> user_id: 1 timestamp: 2007-01-01 00:00:00 +node_north_limit: + id: 11 + latitude: <%= 90*SCALE %> + longitude: <%= 11*SCALE %> + user_id: 1 + timestamp: 2008-07-08 14:50:00 + node_too_far_south: id: 7 - latitude: <%= -91*SCALE %> + latitude: <%= -90.01*SCALE %> longitude: <%= 7*SCALE %> user_id: 1 timestamp: 2007-01-01 00:00:00 +node_south_limit: + id: 12 + latitude: <%= -90*SCALE %> + longitude: <%= 12*SCALE %> + user_id: 1 + timestamp: 2008-07-08 15:02:18 + node_too_far_west: id: 8 latitude: <%= 8*SCALE %> - longitude: <%= -181*SCALE %> + longitude: <%= -180.01*SCALE %> user_id: 1 timestamp: 2007-01-01 00:00:00 +node_west_limit: + id: 13 + latitude: <%= 13*SCALE %> + longitude: <%= -180*SCALE %> + user_id: 1 + timestamp: 2008-07-08 15:17:37 + node_too_far_east: id: 9 latitude: <%= 9*SCALE %> - longitude: <%= 181*SCALE %> + longitude: <%= 180.01*SCALE %> user_id: 1 timestamp: 2007-01-01 00:00:00 +node_east_limit: + id: 14 + latitude: <%= 14*SCALE %> + longitude: <%= 180*SCALE %> + user_id: 1 + timestamp: 2008-07-08 15:46:16 + node_totally_wrong: id: 10 - latitude: <%= 1000*SCALE %> - longitude: <%= 1000*SCALE %> + latitude: <%= 200*SCALE %> + longitude: <%= 200*SCALE %> user_id: 1 timestamp: 2007-01-01 00:00:00 diff --git a/test/unit/node_test.rb b/test/unit/node_test.rb index 460c13a02..87b61a745 100644 --- a/test/unit/node_test.rb +++ b/test/unit/node_test.rb @@ -8,40 +8,64 @@ class NodeTest < Test::Unit::TestCase set_fixture_class :currenr_node_tags => :NodeTag def test_node_too_far_north - node = current_nodes(:node_too_far_north) - assert_equal node.lat, current_nodes(:node_too_far_north).latitude/SCALE - assert_equal node.lon, current_nodes(:node_too_far_north).longitude/SCALE - assert_equal false, node.valid? + 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 - node = current_nodes(:node_too_far_south) - assert_equal node.lat, current_nodes(:node_too_far_south).latitude/SCALE - assert_equal node.lon, current_nodes(:node_too_far_south).longitude/SCALE - assert_equal false, node.valid? + 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 - node = current_nodes(:node_too_far_west) - assert_equal node.lat, current_nodes(:node_too_far_west).latitude/SCALE - assert_equal node.lon, current_nodes(:node_too_far_west).longitude/SCALE - assert_equal false, node.valid? + 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 - node = current_nodes(:node_too_far_east) - assert_equal node.lat, current_nodes(:node_too_far_east).latitude/SCALE - assert_equal node.lon, current_nodes(:node_too_far_east).longitude/SCALE - assert_equal false, node.valid? + invalid_node_test(:node_too_far_east) + end + + def test_node_east_limit + valid_node_test(:node_east_limit) end def test_totally_wrong - node = current_nodes(:node_totally_wrong) - #assert_equal node.lat, current_nodes(:node_totally_wrong).latitude/SCALE - #assert_equal node.lon, current_nodes(:node_totally_wrong).longitude/SCALE + 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) + assert_equal node.lat, current_nodes(nod).latitude.to_f/SCALE + assert_equal node.lon, current_nodes(nod).longitude.to_f/SCALE + assert_equal node.timestamp, current_nodes(nod).timestamp + 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 = current_nodes(nod) + assert_equal node.lat, current_nodes(nod).latitude.to_f/SCALE + assert_equal node.lon, current_nodes(nod).longitude.to_f/SCALE + assert_equal node.timestamp, current_nodes(nod).timestamp assert_equal false, node.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, From c9a171c74581e0eb80794e2c9ed92e1b8faffa42 Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Tue, 8 Jul 2008 18:17:08 +0000 Subject: [PATCH 068/381] Removing the in_world test from the model since it is included in the geo_record library. Adding visible and version to the nodes and current_nodes fixtures. Adding old node test. Slightly different comparisions. --- app/models/old_node.rb | 9 +-- test/fixtures/current_nodes.yml | 38 ++++++++++++ test/fixtures/nodes.yml | 102 ++++++++++++++++++++++++++++++++ test/unit/node_test.rb | 28 ++++++--- test/unit/old_node_test.rb | 79 +++++++++++++++++++++++++ 5 files changed, 240 insertions(+), 16 deletions(-) create mode 100644 test/unit/old_node_test.rb diff --git a/app/models/old_node.rb b/app/models/old_node.rb index 42f55f2d0..6b6b71b53 100644 --- a/app/models/old_node.rb +++ b/app/models/old_node.rb @@ -14,12 +14,6 @@ class OldNode < ActiveRecord::Base 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 @@ -90,7 +84,8 @@ class OldNode < ActiveRecord::Base def tags=(t) @tags = t - end + end + def tags_as_hash hash = {} Tags.split(self.tags) do |k,v| diff --git a/test/fixtures/current_nodes.yml b/test/fixtures/current_nodes.yml index ef5e688f6..19fad704e 100644 --- a/test/fixtures/current_nodes.yml +++ b/test/fixtures/current_nodes.yml @@ -1,11 +1,14 @@ # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html <% SCALE = 10000000 unless defined?(SCALE) %> + visible_node: id: 1 latitude: <%= 1*SCALE %> longitude: <%= 1*SCALE %> user_id: 1 visible: 1 + version: 1 + tile: <%= QuadTile.tile_for_point(1,1) %> timestamp: 2007-01-01 00:00:00 invisible_node: @@ -14,6 +17,8 @@ invisible_node: longitude: <%= 2*SCALE %> user_id: 1 visible: 0 + version: 1 + tile: <%= QuadTile.tile_for_point(2,2) %> timestamp: 2007-01-01 00:00:00 used_node_1: @@ -22,6 +27,8 @@ used_node_1: longitude: <%= 3*SCALE %> user_id: 1 visible: 1 + version: 1 + tile: <%= QuadTile.tile_for_point(3,3) %> timestamp: 2007-01-01 00:00:00 used_node_2: @@ -30,6 +37,8 @@ used_node_2: longitude: <%= 4*SCALE %> user_id: 1 visible: 1 + version: 1 + tile: <%= QuadTile.tile_for_point(4,4) %> timestamp: 2007-01-01 00:00:00 node_used_by_relationship: @@ -38,6 +47,8 @@ node_used_by_relationship: longitude: <%= 5*SCALE %> user_id: 1 visible: 1 + version: 1 + tile: <%= QuadTile.tile_for_point(5,5) %> timestamp: 2007-01-01 00:00:00 node_too_far_north: @@ -45,6 +56,9 @@ node_too_far_north: latitude: <%= 90.01*SCALE %> longitude: <%= 6*SCALE %> user_id: 1 + visible: 1 + version: 1 + tile: <%= QuadTile.tile_for_point(90.01,6) %> timestamp: 2007-01-01 00:00:00 node_north_limit: @@ -52,6 +66,9 @@ node_north_limit: latitude: <%= 90*SCALE %> longitude: <%= 11*SCALE %> user_id: 1 + visible: 1 + version: 1 + tile: <%= QuadTile.tile_for_point(90,11) %> timestamp: 2008-07-08 14:50:00 node_too_far_south: @@ -59,6 +76,9 @@ node_too_far_south: latitude: <%= -90.01*SCALE %> longitude: <%= 7*SCALE %> user_id: 1 + visible: 1 + version: 1 + tile: <%= QuadTile.tile_for_point(-90.01,7) %> timestamp: 2007-01-01 00:00:00 node_south_limit: @@ -66,6 +86,9 @@ node_south_limit: latitude: <%= -90*SCALE %> longitude: <%= 12*SCALE %> user_id: 1 + visible: 1 + version: 1 + tile: <%= QuadTile.tile_for_point(-90,12) %> timestamp: 2008-07-08 15:02:18 node_too_far_west: @@ -73,6 +96,9 @@ node_too_far_west: latitude: <%= 8*SCALE %> longitude: <%= -180.01*SCALE %> user_id: 1 + visible: 1 + version: 1 + tile: <%= QuadTile.tile_for_point(8,-180.01) %> timestamp: 2007-01-01 00:00:00 node_west_limit: @@ -80,6 +106,9 @@ node_west_limit: latitude: <%= 13*SCALE %> longitude: <%= -180*SCALE %> user_id: 1 + visible: 1 + version: 1 + tile: <%= QuadTile.tile_for_point(13,-180) %> timestamp: 2008-07-08 15:17:37 node_too_far_east: @@ -87,6 +116,9 @@ node_too_far_east: latitude: <%= 9*SCALE %> longitude: <%= 180.01*SCALE %> user_id: 1 + visible: 1 + version: 1 + tile: <%= QuadTile.tile_for_point(9,180.01) %> timestamp: 2007-01-01 00:00:00 node_east_limit: @@ -94,6 +126,9 @@ node_east_limit: latitude: <%= 14*SCALE %> longitude: <%= 180*SCALE %> user_id: 1 + visible: 1 + version: 1 + tile: <%= QuadTile.tile_for_point(14,180) %> timestamp: 2008-07-08 15:46:16 node_totally_wrong: @@ -101,5 +136,8 @@ node_totally_wrong: latitude: <%= 200*SCALE %> longitude: <%= 200*SCALE %> user_id: 1 + visible: 1 + version: 1 + tile: <%= QuadTile.tile_for_point(200,200) %> timestamp: 2007-01-01 00:00:00 diff --git a/test/fixtures/nodes.yml b/test/fixtures/nodes.yml index 202a7d931..9699e395c 100644 --- a/test/fixtures/nodes.yml +++ b/test/fixtures/nodes.yml @@ -1,10 +1,14 @@ # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html +<% SCALE = 10000000 unless defined?(SCALE) %> + visible_node: id: 1 latitude: <%= 1*SCALE %> longitude: <%= 1*SCALE %> user_id: 1 visible: 1 + version: 1 + tile: <%= QuadTile.tile_for_point(1,1) %> timestamp: 2007-01-01 00:00:00 invisible_node: @@ -13,6 +17,8 @@ invisible_node: longitude: <%= 2*SCALE %> user_id: 1 visible: 0 + version: 1 + tile: <%= QuadTile.tile_for_point(2,2) %> timestamp: 2007-01-01 00:00:00 used_node_1: @@ -21,6 +27,8 @@ used_node_1: longitude: <%= 3*SCALE %> user_id: 1 visible: 1 + version: 1 + tile: <%= QuadTile.tile_for_point(3,3) %> timestamp: 2007-01-01 00:00:00 used_node_2: @@ -29,6 +37,8 @@ used_node_2: longitude: <%= 4*SCALE %> user_id: 1 visible: 1 + version: 1 + tile: <%= QuadTile.tile_for_point(4,4) %> timestamp: 2007-01-01 00:00:00 node_used_by_relationship: @@ -37,5 +47,97 @@ node_used_by_relationship: longitude: <%= 5*SCALE %> user_id: 1 visible: 1 + 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 %> + user_id: 1 + visible: 1 + 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 %> + user_id: 1 + visible: 1 + 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 %> + user_id: 1 + visible: 1 + 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 %> + user_id: 1 + visible: 1 + 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 %> + user_id: 1 + visible: 1 + 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 %> + user_id: 1 + visible: 1 + 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 %> + user_id: 1 + visible: 1 + 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 %> + user_id: 1 + visible: 1 + 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 %> + user_id: 1 + visible: 1 + version: 1 + tile: <%= QuadTile.tile_for_point(200,200) %> + timestamp: 2007-01-01 00:00:00 + diff --git a/test/unit/node_test.rb b/test/unit/node_test.rb index 87b61a745..b5ef30298 100644 --- a/test/unit/node_test.rb +++ b/test/unit/node_test.rb @@ -1,7 +1,7 @@ require File.dirname(__FILE__) + '/../test_helper' class NodeTest < Test::Unit::TestCase - fixtures :current_nodes, :users, :current_node_tags,:nodes, :node_tags + fixtures :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 @@ -48,21 +48,31 @@ class NodeTest < Test::Unit::TestCase # the fixture def valid_node_test(nod) node = current_nodes(nod) - assert_equal node.lat, current_nodes(nod).latitude.to_f/SCALE - assert_equal node.lon, current_nodes(nod).longitude.to_f/SCALE - assert_equal node.timestamp, current_nodes(nod).timestamp + 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.user_id, node.user_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 helpermethod will check to make sure that a node is outwith the world, + # 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) - assert_equal node.lat, current_nodes(nod).latitude.to_f/SCALE - assert_equal node.lon, current_nodes(nod).longitude.to_f/SCALE - assert_equal node.timestamp, current_nodes(nod).timestamp - assert_equal false, node.valid? + 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.user_id, node.user_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 diff --git a/test/unit/old_node_test.rb b/test/unit/old_node_test.rb new file mode 100644 index 000000000..85c2037c2 --- /dev/null +++ b/test/unit/old_node_test.rb @@ -0,0 +1,79 @@ +require File.dirname(__FILE__) + '/../test_helper' + +class OldNodeTest < Test::Unit::TestCase + fixtures :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 = 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.user_id, node.user_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.user_id, node.user_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 From cf78f3e6ddbd1c148e91de6500266240bfa24825 Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Tue, 15 Jul 2008 15:51:45 +0000 Subject: [PATCH 069/381] Can now run tests that use composite_primary_keys in the fixtures. Adding a new test to check that you can't add duplicate user preferences, and the display name of the user is guaranteed to be unique. --- test/fixtures/user_preferences.yml | 14 +++++++++----- test/test_helper.rb | 1 + test/unit/user_preference_test.rb | 24 +++++++++++++++++++++--- test/unit/user_test.rb | 11 +++++++++++ 4 files changed, 42 insertions(+), 8 deletions(-) diff --git a/test/fixtures/user_preferences.yml b/test/fixtures/user_preferences.yml index 5bf02933a..ab95c357d 100644 --- a/test/fixtures/user_preferences.yml +++ b/test/fixtures/user_preferences.yml @@ -1,7 +1,11 @@ # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html -# one: -# column: value -# -# two: -# column: value +a: + user: normal_user + k: "key" + v: "value" + +two: + user: normal_user + k: "some_key" + v: "some_value" diff --git a/test/test_helper.rb b/test/test_helper.rb index b1d7a8fcc..e954a9495 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -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 diff --git a/test/unit/user_preference_test.rb b/test/unit/user_preference_test.rb index bd4e80015..d591db69d 100644 --- a/test/unit/user_preference_test.rb +++ b/test/unit/user_preference_test.rb @@ -1,8 +1,26 @@ 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 + + end diff --git a/test/unit/user_test.rb b/test/unit/user_test.rb index 76c32a88d..732b5b73a 100644 --- a/test/unit/user_test.rb +++ b/test/unit/user_test.rb @@ -25,4 +25,15 @@ class UserTest < Test::Unit::TestCase 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 end From 8429c850518294c27fcfc21ef6acc9e05274a438 Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Tue, 15 Jul 2008 16:36:43 +0000 Subject: [PATCH 070/381] Check to see if the e-mail test works --- test/unit/user_test.rb | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/test/unit/user_test.rb b/test/unit/user_test.rb index 732b5b73a..2f31da709 100644 --- a/test/unit/user_test.rb +++ b/test/unit/user_test.rb @@ -36,4 +36,21 @@ class UserTest < Test::Unit::TestCase 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 } + 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 end From 679e59210caf049bec515f9b7e6fdc6071b1a18a Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Tue, 15 Jul 2008 17:51:26 +0000 Subject: [PATCH 071/381] Check to see if the e-mail test works --- test/unit/user_test.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/unit/user_test.rb b/test/unit/user_test.rb index 2f31da709..bb17368b4 100644 --- a/test/unit/user_test.rb +++ b/test/unit/user_test.rb @@ -53,4 +53,10 @@ class UserTest < Test::Unit::TestCase 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 = nil + asser user.valid, " should allow nil display name" + end end From 9694b977d3b54f917006aa812d875fc277fde41d Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Tue, 15 Jul 2008 18:04:35 +0000 Subject: [PATCH 072/381] fixing the new display name test --- test/unit/user_test.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/test/unit/user_test.rb b/test/unit/user_test.rb index bb17368b4..583b06e69 100644 --- a/test/unit/user_test.rb +++ b/test/unit/user_test.rb @@ -56,7 +56,13 @@ class UserTest < Test::Unit::TestCase 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 - asser user.valid, " should allow nil display name" + assert !user.valid? end end From b931d6f516d049ac42fcd70f33f130c252f5d0a9 Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Thu, 17 Jul 2008 10:44:06 +0000 Subject: [PATCH 073/381] testing the display name --- test/unit/user_test.rb | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/test/unit/user_test.rb b/test/unit/user_test.rb index 583b06e69..489f76a1d 100644 --- a/test/unit/user_test.rb +++ b/test/unit/user_test.rb @@ -63,6 +63,25 @@ class UserTest < Test::Unit::TestCase 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 + ok = [ "Name", "'me", "he\"", "#ping", "
"] + bad = [ "
", "test@example.com", "s/f", "/", ";", ".", ",", "?", "/;.,?" ] + 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" + end + end end From 0c3f15f5aa95d505ea4e220cb10bafd02752c021 Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Thu, 17 Jul 2008 11:18:33 +0000 Subject: [PATCH 074/381] new friends with test. Fix to the display name valid tests to make sure that they don't test the length --- test/unit/user_test.rb | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/test/unit/user_test.rb b/test/unit/user_test.rb index 489f76a1d..a8586280e 100644 --- a/test/unit/user_test.rb +++ b/test/unit/user_test.rb @@ -70,8 +70,13 @@ class UserTest < Test::Unit::TestCase 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", "
"] - bad = [ "
", "test@example.com", "s/f", "/", ";", ".", ",", "?", "/;.,?" ] + # These need to be 3 chars in length, otherwise the length test above + # should be used. + bad = [ "
", "test@example.com", "s/f", "aa/", "aa;", "aa.", "aa,", "aa?", "/;.,?" ] ok.each do |display_name| user = users(:normal_user) user.display_name = display_name @@ -82,6 +87,16 @@ class UserTest < Test::Unit::TestCase 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 false, 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 end From 411445075b6431cc3978682c77e53ed469a34e5b Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Thu, 17 Jul 2008 15:08:00 +0000 Subject: [PATCH 075/381] some tweaking to the user fixtures so that there is 2 near each other. new users nearby and friends with tests. --- test/fixtures/users.yml | 6 +++--- test/unit/user_test.rb | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/test/fixtures/users.yml b/test/fixtures/users.yml index 89522ef7c..5a788c25c 100644 --- a/test/fixtures/users.yml +++ b/test/fixtures/users.yml @@ -7,8 +7,8 @@ normal_user: display_name: test data_public: 0 description: test - home_lat: 1 - home_lon: 1 + home_lat: 12.1 + home_lon: 12.1 home_zoom: 3 second_user: @@ -30,6 +30,6 @@ inactive_user: display_name: Inactive User data_public: 1 description: description - home_lat: 12.34 + home_lat: 123.4 home_lon: 12.34 home_zoom: 15 diff --git a/test/unit/user_test.rb b/test/unit/user_test.rb index a8586280e..e12750a5f 100644 --- a/test/unit/user_test.rb +++ b/test/unit/user_test.rb @@ -99,4 +99,41 @@ class UserTest < Test::Unit::TestCase 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 + # make normal user a friend of second user + # it should be a one way friend accossitation + assert_equal 0, Friend.count + norm = users(:normal_user) + sec = users(:second_user) + friend = Friend.new + friend.user = norm + friend.friend_user_id = sec.id + friend.save + norm.clear_aggregation_cache + norm.clear_association_cache + sec.clear_aggregation_cache + sec.clear_association_cache + 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_all + assert_equal 0, Friend.count + end end From a14419f04f0d010e271de9c4774c4a9bb33faaab Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Fri, 18 Jul 2008 16:26:21 +0000 Subject: [PATCH 076/381] Adding message tests. Adding some international UTF-8 chars for the display name and email address tests. --- test/unit/message_test.rb | 13 +++++++++++++ test/unit/user_test.rb | 18 +++++++++--------- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/test/unit/message_test.rb b/test/unit/message_test.rb index b56972704..28c7605ff 100644 --- a/test/unit/message_test.rb +++ b/test/unit/message_test.rb @@ -3,6 +3,12 @@ require File.dirname(__FILE__) + '/../test_helper' class MessageTest < Test::Unit::TestCase fixtures :messages, :users + # 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? @@ -12,4 +18,11 @@ class MessageTest < Test::Unit::TestCase assert true, message.message_read end + def test_validating_msgs + message = messages(:one) + assert_equal true, message.valid? + massage = messages(:two) + assert_equal true, message.valid? + end + end diff --git a/test/unit/user_test.rb b/test/unit/user_test.rb index e12750a5f..587fc71fb 100644 --- a/test/unit/user_test.rb +++ b/test/unit/user_test.rb @@ -38,8 +38,10 @@ class UserTest < Test::Unit::TestCase 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 } - bad = %w{ hi ht@ n@ @.com help@.me.uk help"hi.me.uk } + 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) @@ -73,10 +75,12 @@ class UserTest < Test::Unit::TestCase # 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", "
"] + ok = [ "Name", "'me", "he\"", "#ping", "
", "*ho", "\"help\"@", + "vergrößern", "ルシステムã«ã‚‚対応ã—ã¾ã™", "è¼•è§¸æ–æ™ƒçš„éŠæˆ²" ] # These need to be 3 chars in length, otherwise the length test above # should be used. - bad = [ "
", "test@example.com", "s/f", "aa/", "aa;", "aa.", "aa,", "aa?", "/;.,?" ] + bad = [ "
", "test@example.com", "s/f", "aa/", "aa;", "aa.", + "aa,", "aa?", "/;.,?", "も対応ã—ã¾ã™/" ] ok.each do |display_name| user = users(:normal_user) user.display_name = display_name @@ -119,10 +123,6 @@ class UserTest < Test::Unit::TestCase friend.user = norm friend.friend_user_id = sec.id friend.save - norm.clear_aggregation_cache - norm.clear_association_cache - sec.clear_aggregation_cache - sec.clear_association_cache assert_equal [sec], norm.nearby assert_equal 1, norm.nearby.size assert_equal 1, Friend.count @@ -133,7 +133,7 @@ class UserTest < Test::Unit::TestCase 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_all + Friend.delete(friend) assert_equal 0, Friend.count end end From 1bcc2242c0a5bc58abb0ab23964d474af04923fa Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Fri, 18 Jul 2008 16:44:14 +0000 Subject: [PATCH 077/381] Make sure that there is a sender and a recipient of a message. New test to make sure that there a message also has a sender and a recipient, as the validates_associated doesn't do anything if they are nil. --- app/models/message.rb | 2 +- test/unit/message_test.rb | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/app/models/message.rb b/app/models/message.rb index 97e411192..ec712be25 100644 --- a/app/models/message.rb +++ b/app/models/message.rb @@ -2,7 +2,7 @@ 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_inclusion_of :message_read, :in => [ true, false ] validates_associated :sender, :recipient end diff --git a/test/unit/message_test.rb b/test/unit/message_test.rb index 28c7605ff..4de1a7b29 100644 --- a/test/unit/message_test.rb +++ b/test/unit/message_test.rb @@ -20,9 +20,15 @@ class MessageTest < Test::Unit::TestCase def test_validating_msgs message = messages(:one) - assert_equal true, message.valid? + assert message.valid? massage = messages(:two) - assert_equal true, message.valid? + assert message.valid? end + def test_invalid_send_recipient + message = messages(:one) + message.sender = nil + message.recipient = nil + assert !message.valid? + end end From ebb8e5524e54a9adff6a1f5b26c4b2f8c7524d78 Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Fri, 18 Jul 2008 17:10:24 +0000 Subject: [PATCH 078/381] First current node tag test --- test/unit/current_node_tag_test.rb | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 test/unit/current_node_tag_test.rb diff --git a/test/unit/current_node_tag_test.rb b/test/unit/current_node_tag_test.rb new file mode 100644 index 000000000..69b123b5a --- /dev/null +++ b/test/unit/current_node_tag_test.rb @@ -0,0 +1,10 @@ +require File.dirname(__FILE__) + '/../test_helper' + +class CurrentNodeTagTest < Test::Unit::TestCase + fixtures :current_node_tags, :nodes + + def test_tag_count + assert_equal 3, NodeTag.count + end + +end From 3d5ee2675b65030ef233ff95c58a70547c6f3995 Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Tue, 22 Jul 2008 14:06:53 +0000 Subject: [PATCH 079/381] Now just got one test to sort on the functional tests. All other tests are working and pass. Plenty more to write though. --- test/fixtures/current_ways.yml | 3 +++ test/fixtures/messages.yml | 8 ++++---- test/fixtures/user_preferences.yml | 4 ++-- test/fixtures/users.yml | 3 +++ test/fixtures/way_nodes.yml | 8 +++++++- test/functional/api_controller_test.rb | 2 +- test/functional/node_controller_test.rb | 2 +- test/functional/old_way_controller_test.rb | 6 ++++-- test/test_helper.rb | 3 ++- 9 files changed, 27 insertions(+), 12 deletions(-) diff --git a/test/fixtures/current_ways.yml b/test/fixtures/current_ways.yml index b129d7f45..9b5b5ab8a 100644 --- a/test/fixtures/current_ways.yml +++ b/test/fixtures/current_ways.yml @@ -3,16 +3,19 @@ visible_way: user_id: 1 timestamp: 2007-01-01 00:00:00 visible: 1 + version: 1 invisible_way: id: 2 user_id: 1 timestamp: 2007-01-01 00:00:00 visible: 0 + version: 1 used_way: id: 3 user_id: 1 timestamp: 2007-01-01 00:00:00 visible: 1 + version: 1 diff --git a/test/fixtures/messages.yml b/test/fixtures/messages.yml index feab6536e..22fab1863 100644 --- a/test/fixtures/messages.yml +++ b/test/fixtures/messages.yml @@ -1,16 +1,16 @@ # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html one: - sender: normal_user + from_user_id: 1 title: test message 1 body: some body text sent_on: "2008-05-01 12:34:56" message_read: false - recipient: second_user + to_user_id: 2 two: - sender: second_user + from_user_id: 2 title: test message 2 body: some body test sent_on: "2008-05-02 12:45:23" message_read: true - recipient: normal_user + to_user_id: 1 diff --git a/test/fixtures/user_preferences.yml b/test/fixtures/user_preferences.yml index ab95c357d..59ebd0542 100644 --- a/test/fixtures/user_preferences.yml +++ b/test/fixtures/user_preferences.yml @@ -1,11 +1,11 @@ # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html a: - user: normal_user + user_id: 1 k: "key" v: "value" two: - user: normal_user + user_id: 1 k: "some_key" v: "some_value" diff --git a/test/fixtures/users.yml b/test/fixtures/users.yml index 5a788c25c..28e1aca3d 100644 --- a/test/fixtures/users.yml +++ b/test/fixtures/users.yml @@ -1,5 +1,6 @@ # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html normal_user: + id: 1 email: test@openstreetmap.org active: 1 pass_crypt: <%= Digest::MD5.hexdigest('test') %> @@ -12,6 +13,7 @@ normal_user: home_zoom: 3 second_user: + id: 2 email: test@example.com active: 1 pass_crypt: <%= Digest::MD5.hexdigest('test') %> @@ -24,6 +26,7 @@ second_user: home_zoom: 12 inactive_user: + id: 3 email: inactive@openstreetmap.org active: 0 pass_crypt: <%= Digest::MD5::hexdigest('test2') %> diff --git a/test/fixtures/way_nodes.yml b/test/fixtures/way_nodes.yml index caeac16b1..c12a248a0 100644 --- a/test/fixtures/way_nodes.yml +++ b/test/fixtures/way_nodes.yml @@ -1,9 +1,15 @@ -t1: +t1a: id: 1 node_id: 3 sequence_id: 1 version: 1 +t1b: + id: 1 + node_id: 4 + sequence_id: 2 + version: 1 + t2: id: 2 node_id: 3 diff --git a/test/functional/api_controller_test.rb b/test/functional/api_controller_test.rb index 05cbe2af0..4c4787ff7 100644 --- a/test/functional/api_controller_test.rb +++ b/test/functional/api_controller_test.rb @@ -23,7 +23,7 @@ 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}" + bbox = "#{node.lat-0.1},#{node.lon-0.1},#{node.lat+0.1},#{node.lon+0.1}" get :map, :bbox => bbox if $VERBOSE print @response.body diff --git a/test/functional/node_controller_test.rb b/test/functional/node_controller_test.rb index a380eeb20..3f316d012 100644 --- a/test/functional/node_controller_test.rb +++ b/test/functional/node_controller_test.rb @@ -57,7 +57,7 @@ class NodeControllerTest < Test::Unit::TestCase assert_response :unauthorized # now set auth - basic_authorization("test@openstreetmap.org", "test"); + basic_authorization(users(:normal_user).email, "test"); # this should work delete :delete, :id => current_nodes(:visible_node).id diff --git a/test/functional/old_way_controller_test.rb b/test/functional/old_way_controller_test.rb index 374ea7dc2..b4e3c5127 100644 --- a/test/functional/old_way_controller_test.rb +++ b/test/functional/old_way_controller_test.rb @@ -17,11 +17,13 @@ class OldWayControllerTest < Test::Unit::TestCase # 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 chat a non-existent way is not returned get :history, :id => 0 assert_response :not_found diff --git a/test/test_helper.rb b/test/test_helper.rb index e954a9495..22cc0e15c 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -29,9 +29,10 @@ class Test::Unit::TestCase def self.api_fixtures fixtures :users - fixtures :current_nodes, :nodes + fixtures :current_nodes, :nodes, :current_node_tags set_fixture_class :current_nodes => :Node set_fixture_class :nodes => :OldNode + set_fixture_class :current_node_tags => :NodeTag fixtures :current_ways, :current_way_nodes, :current_way_tags set_fixture_class :current_ways => :Way From a0545ba4e524c96e0d7b751e2d856ab672dd78b3 Mon Sep 17 00:00:00 2001 From: Tom Hughes Date: Tue, 22 Jul 2008 14:16:07 +0000 Subject: [PATCH 080/381] Render nothing on a successful delete. --- app/controllers/node_controller.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/controllers/node_controller.rb b/app/controllers/node_controller.rb index 1e0deb140..4a7527734 100644 --- a/app/controllers/node_controller.rb +++ b/app/controllers/node_controller.rb @@ -70,6 +70,8 @@ class NodeController < ApplicationController begin node = Node.find(params[:id]) node.delete_with_history(@user) + + render :nothing => true rescue ActiveRecord::RecordNotFound render :nothing => true, :status => :not_found rescue OSM::APIError => ex From 796da2669d9bff39476631aa7dfaf348b6098169 Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Thu, 24 Jul 2008 10:56:29 +0000 Subject: [PATCH 081/381] Starting to test the output of the xml to make sure that it is what we expect to be returned by teh api for ways tests. --- test/functional/way_controller_test.rb | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/test/functional/way_controller_test.rb b/test/functional/way_controller_test.rb index 933dfb542..6fd3e234c 100644 --- a/test/functional/way_controller_test.rb +++ b/test/functional/way_controller_test.rb @@ -42,13 +42,29 @@ class WayControllerTest < Test::Unit::TestCase 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 + #print @response.body + # Needs to be updated when changing fixtures + # The generator should probably be defined in the environment.rb file + # in the same place as the api version + assert_select "osm[version=#{API_VERSION}][generator=\"OpenStreetMap server\"]", 1 + assert_select "osm way", 3 + assert_select "osm way nd", 3 + assert_select "osm way tag", 3 # 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 + #print @response.body + # Check the way is correctly returned + way = current_ways(:visible_way) + assert_select "osm way[id=#{way.id}][version=#{way.version}][visible=#{way.visible}]", 1 + assert_select "osm way nd[ref=#{way.way_nodes[0].node_id}]", 1 + # Check that the node is correctly returned + nd = current_ways(:visible_way).nodes + assert_equal 1, nd.count + nda = nd[0] + assert_select "osm node[id=#{nda.id}][version=#{nda.version}][lat=#{nda.lat}][lon=#{nda.lon}]", 1 end # ------------------------------------- From 87494900df0f40c734286139678d347af9c234b7 Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Mon, 18 Aug 2008 13:55:21 +0000 Subject: [PATCH 082/381] more testing of node tags --- config/database.yml | 9 ++++++--- test/fixtures/current_node_tags.yml | 6 +++--- test/unit/current_node_tag_test.rb | 12 +++++++++++- test/unit/node_test.rb | 2 +- 4 files changed, 21 insertions(+), 8 deletions(-) diff --git a/config/database.yml b/config/database.yml index fe47e11aa..a80e11b70 100644 --- a/config/database.yml +++ b/config/database.yml @@ -12,9 +12,12 @@ # http://dev.mysql.com/doc/refman/5.0/en/old-client.html development: adapter: mysql - database: openstreetmap - username: openstreetmap - password: openstreetmap + #database: openstreetmap + #username: openstreetmap + #password: openstreetmap + database: osm_test + username: osm_test + password: osm_test host: localhost # Warning: The database defined as 'test' will be erased and diff --git a/test/fixtures/current_node_tags.yml b/test/fixtures/current_node_tags.yml index d9f5448a4..ce68a5439 100644 --- a/test/fixtures/current_node_tags.yml +++ b/test/fixtures/current_node_tags.yml @@ -1,15 +1,15 @@ t1: - id: visible_node.id + id: 1 k: testvisible v: yes t2: - id: used_node_1.id + id: 2 k: testused v: yes t3: - id: used_node_2.id + id: 3 k: test v: yes diff --git a/test/unit/current_node_tag_test.rb b/test/unit/current_node_tag_test.rb index 69b123b5a..7fb1deff5 100644 --- a/test/unit/current_node_tag_test.rb +++ b/test/unit/current_node_tag_test.rb @@ -1,10 +1,20 @@ require File.dirname(__FILE__) + '/../test_helper' class CurrentNodeTagTest < Test::Unit::TestCase - fixtures :current_node_tags, :nodes + 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 3, NodeTag.count + node_tag_count(:visible_node, 1) + node_tag_count(:invisible_node, 1) + node_tag_count(:used_node_1, 1) + end + + def node_tag_count (node, count) + nod = current_nodes(node) + assert_equal count, nod.node_tags.count end end diff --git a/test/unit/node_test.rb b/test/unit/node_test.rb index b5ef30298..bb2b7dfa4 100644 --- a/test/unit/node_test.rb +++ b/test/unit/node_test.rb @@ -5,7 +5,7 @@ class NodeTest < Test::Unit::TestCase set_fixture_class :current_nodes => :Node set_fixture_class :nodes => :OldNode set_fixture_class :node_tags => :OldNodeTag - set_fixture_class :currenr_node_tags => :NodeTag + set_fixture_class :current_node_tags => :NodeTag def test_node_too_far_north invalid_node_test(:node_too_far_north) From 629843b96bcd0e58116f8d77b4b02f2c49912976 Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Thu, 25 Sep 2008 15:57:59 +0000 Subject: [PATCH 083/381] Minor fix for the migration that was moved --- db/migrate/017_populate_node_tags_and_remove.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/db/migrate/017_populate_node_tags_and_remove.rb b/db/migrate/017_populate_node_tags_and_remove.rb index 29a91c70b..9a87fe2ba 100644 --- a/db/migrate/017_populate_node_tags_and_remove.rb +++ b/db/migrate/017_populate_node_tags_and_remove.rb @@ -3,9 +3,9 @@ class PopulateNodeTagsAndRemove < ActiveRecord::Migration have_nodes = select_value("SELECT count(*) FROM current_nodes").to_i != 0 if have_nodes - prefix = File.join Dir.tmpdir, "013_populate_node_tags_and_remove.#{$$}." + prefix = File.join Dir.tmpdir, "017_populate_node_tags_and_remove.#{$$}." - cmd = "db/migrate/013_populate_node_tags_and_remove_helper" + cmd = "db/migrate/017_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` ' + From 6dfc83da03e77615c958af8f2992f2d6ef44f11b Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Thu, 25 Sep 2008 16:20:36 +0000 Subject: [PATCH 084/381] fixing the test that was broken in the resync --- test/unit/user_test.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/unit/user_test.rb b/test/unit/user_test.rb index 587fc71fb..486344fee 100644 --- a/test/unit/user_test.rb +++ b/test/unit/user_test.rb @@ -120,8 +120,8 @@ class UserTest < Test::Unit::TestCase norm = users(:normal_user) sec = users(:second_user) friend = Friend.new - friend.user = norm - friend.friend_user_id = sec.id + friend.befriender = norm + friend.befriendee = sec friend.save assert_equal [sec], norm.nearby assert_equal 1, norm.nearby.size From 56c972577b9cb4c4e435fe3025752d1cd9b26d56 Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Fri, 26 Sep 2008 15:17:30 +0000 Subject: [PATCH 085/381] fixing hard coded images/scripts, so that the site works better when deploy as a sub directory using passenger phusion. --- app/views/layouts/site.rhtml | 2 +- public/javascripts/map.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/layouts/site.rhtml b/app/views/layouts/site.rhtml index 7aa2db7b9..4437fa879 100644 --- a/app/views/layouts/site.rhtml +++ b/app/views/layouts/site.rhtml @@ -106,7 +106,7 @@ - + <%= link_to (image_tag "cc_button.png", :alt => "CC by-sa 2.0", :border => "0"), "http://creativecommons.org/licenses/by-sa/2.0/" %> diff --git a/public/javascripts/map.js b/public/javascripts/map.js index cd130c784..3e6d687b0 100644 --- a/public/javascripts/map.js +++ b/public/javascripts/map.js @@ -13,7 +13,7 @@ var nonamekeys = { }; OpenLayers._getScriptLocation = function () { - return "/openlayers/"; + return "openlayers/"; } function createMap(divName, options) { From 0ec26f14a04744242e93912b28c3cf7607efdce6 Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Mon, 29 Sep 2008 16:28:51 +0000 Subject: [PATCH 086/381] adding some version reading for the nodes --- app/controllers/node_controller.rb | 15 +++++++++++++++ config/routes.rb | 3 ++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/app/controllers/node_controller.rb b/app/controllers/node_controller.rb index 4a7527734..70bf59734 100644 --- a/app/controllers/node_controller.rb +++ b/app/controllers/node_controller.rb @@ -43,6 +43,21 @@ class NodeController < ApplicationController render :nothing => true, :status => :not_found end end + + # Dump a specific version of the node based on the given params[:id] and params[:version] + def version + begin + node = Node.find(:first, :conditions => { :id => params[:id], :version => params[:version] } ) + if node.visible + response.headers['Last-Modified'] = node.timestamp.rfc822 + render :text => node.to_xml.to_s, :content_type => "text/xml" + else + render :nothing => true, :status => :gone + end + rescue ActiveRecord::RecordNotFound + render :nothing => true, :status => :not_found + end + end # Update a node from given XML def update diff --git a/config/routes.rb b/config/routes.rb index a45ad1e0b..286e4055c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -10,6 +10,7 @@ ActionController::Routing::Routes.draw do |map| 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 } @@ -59,7 +60,7 @@ ActionController::Routing::Routes.draw do |map| # Potlatch API - map.connect "api/0.5/amf", :controller =>'amf', :action =>'talk' + map.connect "api/#{API_VERSION}/amf", :controller =>'amf', :action =>'talk' map.connect "api/#{API_VERSION}/amf", :controller =>'amf', :action =>'talk' map.connect "api/#{API_VERSION}/swf/trackpoints", :controller =>'swf', :action =>'trackpoints' From 20022e457f3f211f7274ea4f24443ea19c4d5c7f Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Tue, 30 Sep 2008 14:29:44 +0000 Subject: [PATCH 087/381] changing the changesets migrations, so that the user id is now the changeset id for the nodes/ways/relations --- db/migrate/020_add_changesets.rb | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/db/migrate/020_add_changesets.rb b/db/migrate/020_add_changesets.rb index 40455ec68..15122957b 100644 --- a/db/migrate/020_add_changesets.rb +++ b/db/migrate/020_add_changesets.rb @@ -1,4 +1,7 @@ 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, :limit => 20, :null => false @@ -23,10 +26,26 @@ class AddChangesets < ActiveRecord::Migration 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 user_id, user_id, creation_time, 0 from users;" + + @@conv_user_tables.each { |tbl| + rename_column tbl, :user_id, :changeset_id + #foreign keys too + add_foreign_key tbl, :changeset_id, [:id], :users + } end def self.down - drop_table "changesets" - drop_table "changeset_tags" + # It's not easy to generate the user ids from the changesets + raise IrreversibleMigration.new + #drop_table "changesets" + #drop_table "changeset_tags" end end From d7289a06deb1a644cbff7ba0606fe9e4c74830c7 Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Tue, 30 Sep 2008 15:24:25 +0000 Subject: [PATCH 088/381] using the wrong field name in one of the migrations --- db/migrate/020_add_changesets.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/migrate/020_add_changesets.rb b/db/migrate/020_add_changesets.rb index 15122957b..95a9bdbd6 100644 --- a/db/migrate/020_add_changesets.rb +++ b/db/migrate/020_add_changesets.rb @@ -33,7 +33,7 @@ class AddChangesets < ActiveRecord::Migration # 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 user_id, user_id, creation_time, 0 from users;" + "SELECT id, id, creation_time, 0 from users;" @@conv_user_tables.each { |tbl| rename_column tbl, :user_id, :changeset_id From 7252ffa8207855229123ae466ca7d29e72f8a6a0 Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Tue, 30 Sep 2008 15:44:55 +0000 Subject: [PATCH 089/381] wrong foreign key command in the migration --- db/migrate/020_add_changesets.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/migrate/020_add_changesets.rb b/db/migrate/020_add_changesets.rb index 95a9bdbd6..1c599de01 100644 --- a/db/migrate/020_add_changesets.rb +++ b/db/migrate/020_add_changesets.rb @@ -38,7 +38,7 @@ class AddChangesets < ActiveRecord::Migration @@conv_user_tables.each { |tbl| rename_column tbl, :user_id, :changeset_id #foreign keys too - add_foreign_key tbl, :changeset_id, [:id], :users + add_foreign_key tbl, [:changeset_id], :users } end From d19076d0ed7e077e33d1b7606b6186f99e994943 Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Tue, 30 Sep 2008 15:52:54 +0000 Subject: [PATCH 090/381] wrong foreign key command in the migration --- db/migrate/020_add_changesets.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/migrate/020_add_changesets.rb b/db/migrate/020_add_changesets.rb index 1c599de01..c21b50782 100644 --- a/db/migrate/020_add_changesets.rb +++ b/db/migrate/020_add_changesets.rb @@ -38,7 +38,7 @@ class AddChangesets < ActiveRecord::Migration @@conv_user_tables.each { |tbl| rename_column tbl, :user_id, :changeset_id #foreign keys too - add_foreign_key tbl, [:changeset_id], :users + add_foreign_key tbl, [:changeset_id], :users, [:id] } end From 40414cf324309d13d207f9339b7f40ce05a9fa2a Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Tue, 30 Sep 2008 18:21:56 +0000 Subject: [PATCH 091/381] changing the belongs_to and has_many items in the models to reflect the new foriegn keys in the db and the fact that a (old)node/way/relation now belong to a changeset, which in turn has a user. --- app/models/changeset.rb | 7 +++++++ app/models/node.rb | 2 +- app/models/old_node.rb | 2 +- app/models/old_relation.rb | 2 +- app/models/old_way.rb | 2 +- app/models/relation.rb | 2 +- app/models/way.rb | 2 +- 7 files changed, 13 insertions(+), 6 deletions(-) diff --git a/app/models/changeset.rb b/app/models/changeset.rb index c9eeb0018..04023ae2b 100644 --- a/app/models/changeset.rb +++ b/app/models/changeset.rb @@ -4,6 +4,13 @@ class Changeset < ActiveRecord::Base 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 def self.from_xml(xml, create=false) begin diff --git a/app/models/node.rb b/app/models/node.rb index 677023179..2efdb1bc9 100644 --- a/app/models/node.rb +++ b/app/models/node.rb @@ -10,7 +10,7 @@ class Node < ActiveRecord::Base validates_numericality_of :latitude, :longitude validate :validate_position - belongs_to :user + belongs_to :changeset has_many :old_nodes, :foreign_key => :id diff --git a/app/models/old_node.rb b/app/models/old_node.rb index 6b6b71b53..58a514bcd 100644 --- a/app/models/old_node.rb +++ b/app/models/old_node.rb @@ -8,7 +8,7 @@ class OldNode < ActiveRecord::Base validates_numericality_of :latitude, :longitude validate :validate_position - belongs_to :user + belongs_to :changeset def validate_position errors.add_to_base("Node is not in the world") unless in_world? diff --git a/app/models/old_relation.rb b/app/models/old_relation.rb index f5885f39f..9dca97a27 100644 --- a/app/models/old_relation.rb +++ b/app/models/old_relation.rb @@ -1,7 +1,7 @@ class OldRelation < ActiveRecord::Base set_table_name 'relations' - belongs_to :user + belongs_to :changeset def self.from_relation(relation) old_relation = OldRelation.new diff --git a/app/models/old_way.rb b/app/models/old_way.rb index 3c88c4673..2820d9438 100644 --- a/app/models/old_way.rb +++ b/app/models/old_way.rb @@ -1,7 +1,7 @@ class OldWay < ActiveRecord::Base set_table_name 'ways' - belongs_to :user + belongs_to :changeset def self.from_way(way) old_way = OldWay.new diff --git a/app/models/relation.rb b/app/models/relation.rb index e46da5ade..bb8f2d003 100644 --- a/app/models/relation.rb +++ b/app/models/relation.rb @@ -3,7 +3,7 @@ class Relation < ActiveRecord::Base set_table_name 'current_relations' - belongs_to :user + belongs_to :changeset has_many :old_relations, :foreign_key => 'id', :order => 'version' diff --git a/app/models/way.rb b/app/models/way.rb index 3bc8bcebe..8da959853 100644 --- a/app/models/way.rb +++ b/app/models/way.rb @@ -3,7 +3,7 @@ class Way < ActiveRecord::Base set_table_name 'current_ways' - belongs_to :user + belongs_to :changeset has_many :old_ways, :foreign_key => 'id', :order => 'version' From 5daeb1b0632f03f6c93490452cf7cff174b353ca Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Fri, 3 Oct 2008 13:57:20 +0000 Subject: [PATCH 092/381] Changing if not to unless to make code more readable. --- app/controllers/changeset_controller.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/changeset_controller.rb b/app/controllers/changeset_controller.rb index 8668611eb..8950126b4 100644 --- a/app/controllers/changeset_controller.rb +++ b/app/controllers/changeset_controller.rb @@ -57,7 +57,7 @@ class ChangesetController < ApplicationController def close begin - if not request.put? + unless request.put? render :nothing => true, :status => :method_not_allowed return end @@ -71,7 +71,7 @@ class ChangesetController < ApplicationController end def upload - if not request.put? + unless request.put? render :nothing => true, :status => :method_not_allowed return end From 328d47e506972fededfa1080967224928c36a4cf Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Fri, 3 Oct 2008 17:09:58 +0000 Subject: [PATCH 093/381] Fixing a bug in my foreign keys. I can now upload and download nodes and ways using JOSM in the new API. --- app/controllers/node_controller.rb | 5 ++++- app/controllers/way_controller.rb | 3 +-- app/models/node.rb | 18 +++++++++--------- app/models/old_node.rb | 6 +++--- app/models/old_way.rb | 2 +- app/models/way.rb | 16 ++++++++++------ db/migrate/020_add_changesets.rb | 2 +- 7 files changed, 29 insertions(+), 23 deletions(-) diff --git a/app/controllers/node_controller.rb b/app/controllers/node_controller.rb index 70bf59734..03ae64f41 100644 --- a/app/controllers/node_controller.rb +++ b/app/controllers/node_controller.rb @@ -13,10 +13,13 @@ class NodeController < ApplicationController def create if request.put? node = Node.from_xml(request.raw_post, true) + # FIXME remove debug + logger.debug request.raw_post + logger.debug node if node node.version = 0 - node.user_id = @user.id + #node.changeset_id = node.changeset node.visible = true node.save_with_history! diff --git a/app/controllers/way_controller.rb b/app/controllers/way_controller.rb index cf1634fa5..17f166ddc 100644 --- a/app/controllers/way_controller.rb +++ b/app/controllers/way_controller.rb @@ -12,11 +12,10 @@ class WayController < ApplicationController way = Way.from_xml(request.raw_post, true) if way - if !way.preconditions_ok? + unless way.preconditions_ok? render :text => "", :status => :precondition_failed else way.version = 0 - way.user_id = @user.id way.save_with_history! render :text => way.id.to_s, :content_type => "text/plain" diff --git a/app/models/node.rb b/app/models/node.rb index 2efdb1bc9..2f3001a49 100644 --- a/app/models/node.rb +++ b/app/models/node.rb @@ -5,7 +5,7 @@ class Node < ActiveRecord::Base set_table_name 'current_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 @@ -77,6 +77,7 @@ class Node < ActiveRecord::Base node.version = pt['version'] node.lat = pt['lat'].to_f node.lon = pt['lon'].to_f + node.changeset_id = pt['changeset'].to_i return nil unless node.in_world? @@ -151,7 +152,8 @@ class Node < ActiveRecord::Base raise OSM::APIVersionMismatchError.new(new_node.version, version) end - self.user_id = user.id + # FIXME logic need looked at + self.changeset_id = user.id self.latitude = new_node.latitude self.longitude = new_node.longitude self.tags = new_node.tags @@ -173,15 +175,15 @@ class Node < ActiveRecord::Base 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? + el1['user'] = user_display_name_cache[self.changeset.user_id] unless user_display_name_cache[self.changeset.user_id].nil? self.tags.each do |k,v| el2 = XML::Node.new('tag') @@ -219,6 +221,4 @@ class Node < ActiveRecord::Base @tags[k] = v end - - end diff --git a/app/models/old_node.rb b/app/models/old_node.rb index 58a514bcd..d4d9f1775 100644 --- a/app/models/old_node.rb +++ b/app/models/old_node.rb @@ -3,7 +3,7 @@ class OldNode < ActiveRecord::Base 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 @@ -21,7 +21,7 @@ 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 @@ -38,7 +38,7 @@ class OldNode < ActiveRecord::Base 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['user'] = self.changeset.user.display_name if self.changeset.user.data_public? self.tags.each do |k,v| el2 = XML::Node.new('tag') diff --git a/app/models/old_way.rb b/app/models/old_way.rb index 2820d9438..f297cfc1a 100644 --- a/app/models/old_way.rb +++ b/app/models/old_way.rb @@ -6,7 +6,7 @@ class OldWay < ActiveRecord::Base 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 diff --git a/app/models/way.rb b/app/models/way.rb index 8da959853..7c6e9d06b 100644 --- a/app/models/way.rb +++ b/app/models/way.rb @@ -3,6 +3,9 @@ class Way < ActiveRecord::Base set_table_name 'current_ways' + validates_presence_of :changeset_id, :timestamp + validates_inclusion_of :visible, :in => [ true, false ] + belongs_to :changeset has_many :old_ways, :foreign_key => 'id', :order => 'version' @@ -37,13 +40,14 @@ class Way < ActiveRecord::Base end way.version = pt['version'] + way.changeset_id = pt['changeset'] if create way.timestamp = Time.now way.visible = true else if pt['timestamp'] - way.timestamp = Time.parse(pt['timestamp']) + way.timestamp = Time.parse(pt['timestamp']) end end @@ -84,15 +88,15 @@ class Way < ActiveRecord::Base 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? + el1['user'] = user_display_name_cache[self.changeset.user_id] unless user_display_name_cache[self.changeset.user_id].nil? # make sure nodes are output in sequence_id order ordered_nodes = [] diff --git a/db/migrate/020_add_changesets.rb b/db/migrate/020_add_changesets.rb index c21b50782..75a909e80 100644 --- a/db/migrate/020_add_changesets.rb +++ b/db/migrate/020_add_changesets.rb @@ -38,7 +38,7 @@ class AddChangesets < ActiveRecord::Migration @@conv_user_tables.each { |tbl| rename_column tbl, :user_id, :changeset_id #foreign keys too - add_foreign_key tbl, [:changeset_id], :users, [:id] + add_foreign_key tbl, [:changeset_id], :changesets, [:id] } end From 5adc9a45ef4dc01f71ca429d93e664f00e761ea5 Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Tue, 7 Oct 2008 22:11:05 +0000 Subject: [PATCH 094/381] folder empty and components are dead and gone with rails 2 anyway, so deleting this folder. From b6a3e9671cf62e0a56985641e8bae7b6ea21edf5 Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Tue, 7 Oct 2008 22:40:28 +0000 Subject: [PATCH 095/381] Updating the fixtures for the new changesets --- test/fixtures/changesets.yml | 13 +++++++++++++ test/fixtures/current_nodes.yml | 28 ++++++++++++++-------------- test/fixtures/current_relations.yml | 6 +++--- test/fixtures/current_ways.yml | 6 +++--- test/fixtures/nodes.yml | 28 ++++++++++++++-------------- test/fixtures/relations.yml | 6 +++--- test/fixtures/ways.yml | 6 +++--- 7 files changed, 53 insertions(+), 40 deletions(-) create mode 100644 test/fixtures/changesets.yml diff --git a/test/fixtures/changesets.yml b/test/fixtures/changesets.yml new file mode 100644 index 000000000..e676ffa7a --- /dev/null +++ b/test/fixtures/changesets.yml @@ -0,0 +1,13 @@ +# 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" + open: 1 + +second_user_first_change: + id: 2 + user_id: 2 + created_at: "2008-05-01 01:23:45" + open: 1 + diff --git a/test/fixtures/current_nodes.yml b/test/fixtures/current_nodes.yml index 19fad704e..7407b2983 100644 --- a/test/fixtures/current_nodes.yml +++ b/test/fixtures/current_nodes.yml @@ -5,7 +5,7 @@ visible_node: id: 1 latitude: <%= 1*SCALE %> longitude: <%= 1*SCALE %> - user_id: 1 + changeset_id: 1 visible: 1 version: 1 tile: <%= QuadTile.tile_for_point(1,1) %> @@ -15,7 +15,7 @@ invisible_node: id: 2 latitude: <%= 2*SCALE %> longitude: <%= 2*SCALE %> - user_id: 1 + changeset_id: 1 visible: 0 version: 1 tile: <%= QuadTile.tile_for_point(2,2) %> @@ -25,7 +25,7 @@ used_node_1: id: 3 latitude: <%= 3*SCALE %> longitude: <%= 3*SCALE %> - user_id: 1 + changeset_id: 1 visible: 1 version: 1 tile: <%= QuadTile.tile_for_point(3,3) %> @@ -35,7 +35,7 @@ used_node_2: id: 4 latitude: <%= 4*SCALE %> longitude: <%= 4*SCALE %> - user_id: 1 + changeset_id: 1 visible: 1 version: 1 tile: <%= QuadTile.tile_for_point(4,4) %> @@ -45,7 +45,7 @@ node_used_by_relationship: id: 5 latitude: <%= 5*SCALE %> longitude: <%= 5*SCALE %> - user_id: 1 + changeset_id: 1 visible: 1 version: 1 tile: <%= QuadTile.tile_for_point(5,5) %> @@ -55,7 +55,7 @@ node_too_far_north: id: 6 latitude: <%= 90.01*SCALE %> longitude: <%= 6*SCALE %> - user_id: 1 + changeset_id: 1 visible: 1 version: 1 tile: <%= QuadTile.tile_for_point(90.01,6) %> @@ -65,7 +65,7 @@ node_north_limit: id: 11 latitude: <%= 90*SCALE %> longitude: <%= 11*SCALE %> - user_id: 1 + changeset_id: 1 visible: 1 version: 1 tile: <%= QuadTile.tile_for_point(90,11) %> @@ -75,7 +75,7 @@ node_too_far_south: id: 7 latitude: <%= -90.01*SCALE %> longitude: <%= 7*SCALE %> - user_id: 1 + changeset_id: 1 visible: 1 version: 1 tile: <%= QuadTile.tile_for_point(-90.01,7) %> @@ -85,7 +85,7 @@ node_south_limit: id: 12 latitude: <%= -90*SCALE %> longitude: <%= 12*SCALE %> - user_id: 1 + changeset_id: 1 visible: 1 version: 1 tile: <%= QuadTile.tile_for_point(-90,12) %> @@ -95,7 +95,7 @@ node_too_far_west: id: 8 latitude: <%= 8*SCALE %> longitude: <%= -180.01*SCALE %> - user_id: 1 + changeset_id: 1 visible: 1 version: 1 tile: <%= QuadTile.tile_for_point(8,-180.01) %> @@ -105,7 +105,7 @@ node_west_limit: id: 13 latitude: <%= 13*SCALE %> longitude: <%= -180*SCALE %> - user_id: 1 + changeset_id: 1 visible: 1 version: 1 tile: <%= QuadTile.tile_for_point(13,-180) %> @@ -115,7 +115,7 @@ node_too_far_east: id: 9 latitude: <%= 9*SCALE %> longitude: <%= 180.01*SCALE %> - user_id: 1 + changeset_id: 1 visible: 1 version: 1 tile: <%= QuadTile.tile_for_point(9,180.01) %> @@ -125,7 +125,7 @@ node_east_limit: id: 14 latitude: <%= 14*SCALE %> longitude: <%= 180*SCALE %> - user_id: 1 + changeset_id: 1 visible: 1 version: 1 tile: <%= QuadTile.tile_for_point(14,180) %> @@ -135,7 +135,7 @@ node_totally_wrong: id: 10 latitude: <%= 200*SCALE %> longitude: <%= 200*SCALE %> - user_id: 1 + changeset_id: 1 visible: 1 version: 1 tile: <%= QuadTile.tile_for_point(200,200) %> diff --git a/test/fixtures/current_relations.yml b/test/fixtures/current_relations.yml index c1f77d428..510df492c 100644 --- a/test/fixtures/current_relations.yml +++ b/test/fixtures/current_relations.yml @@ -1,17 +1,17 @@ visible_relation: id: 1 - user_id: 1 + changeset_id: 1 timestamp: 2007-01-01 00:00:00 visible: 1 invisible_relation: id: 2 - user_id: 1 + changeset_id: 1 timestamp: 2007-01-01 00:00:00 visible: 0 used_relation: id: 3 - user_id: 1 + changeset_id: 1 timestamp: 2007-01-01 00:00:00 visible: 1 diff --git a/test/fixtures/current_ways.yml b/test/fixtures/current_ways.yml index 9b5b5ab8a..bd31916e2 100644 --- a/test/fixtures/current_ways.yml +++ b/test/fixtures/current_ways.yml @@ -1,20 +1,20 @@ visible_way: id: 1 - user_id: 1 + changeset_id: 1 timestamp: 2007-01-01 00:00:00 visible: 1 version: 1 invisible_way: id: 2 - user_id: 1 + changeset_id: 1 timestamp: 2007-01-01 00:00:00 visible: 0 version: 1 used_way: id: 3 - user_id: 1 + changeset_id: 1 timestamp: 2007-01-01 00:00:00 visible: 1 version: 1 diff --git a/test/fixtures/nodes.yml b/test/fixtures/nodes.yml index 9699e395c..ddcf1d8f1 100644 --- a/test/fixtures/nodes.yml +++ b/test/fixtures/nodes.yml @@ -5,7 +5,7 @@ visible_node: id: 1 latitude: <%= 1*SCALE %> longitude: <%= 1*SCALE %> - user_id: 1 + changeset_id: 1 visible: 1 version: 1 tile: <%= QuadTile.tile_for_point(1,1) %> @@ -15,7 +15,7 @@ invisible_node: id: 2 latitude: <%= 2*SCALE %> longitude: <%= 2*SCALE %> - user_id: 1 + changeset_id: 1 visible: 0 version: 1 tile: <%= QuadTile.tile_for_point(2,2) %> @@ -25,7 +25,7 @@ used_node_1: id: 3 latitude: <%= 3*SCALE %> longitude: <%= 3*SCALE %> - user_id: 1 + changeset_id: 1 visible: 1 version: 1 tile: <%= QuadTile.tile_for_point(3,3) %> @@ -35,7 +35,7 @@ used_node_2: id: 4 latitude: <%= 4*SCALE %> longitude: <%= 4*SCALE %> - user_id: 1 + changeset_id: 1 visible: 1 version: 1 tile: <%= QuadTile.tile_for_point(4,4) %> @@ -45,7 +45,7 @@ node_used_by_relationship: id: 5 latitude: <%= 5*SCALE %> longitude: <%= 5*SCALE %> - user_id: 1 + changeset_id: 1 visible: 1 version: 1 tile: <%= QuadTile.tile_for_point(5,5) %> @@ -55,7 +55,7 @@ node_too_far_north: id: 6 latitude: <%= 90.01*SCALE %> longitude: <%= 6*SCALE %> - user_id: 1 + changeset_id: 1 visible: 1 version: 1 tile: <%= QuadTile.tile_for_point(90.01,6) %> @@ -65,7 +65,7 @@ node_north_limit: id: 11 latitude: <%= 90*SCALE %> longitude: <%= 11*SCALE %> - user_id: 1 + changeset_id: 1 visible: 1 version: 1 tile: <%= QuadTile.tile_for_point(90,11) %> @@ -75,7 +75,7 @@ node_too_far_south: id: 7 latitude: <%= -90.01*SCALE %> longitude: <%= 7*SCALE %> - user_id: 1 + changeset_id: 1 visible: 1 version: 1 tile: <%= QuadTile.tile_for_point(-90.01,7) %> @@ -85,7 +85,7 @@ node_south_limit: id: 12 latitude: <%= -90*SCALE %> longitude: <%= 12*SCALE %> - user_id: 1 + changeset_id: 1 visible: 1 version: 1 tile: <%= QuadTile.tile_for_point(-90,12) %> @@ -95,7 +95,7 @@ node_too_far_west: id: 8 latitude: <%= 8*SCALE %> longitude: <%= -180.01*SCALE %> - user_id: 1 + changeset_id: 1 visible: 1 version: 1 tile: <%= QuadTile.tile_for_point(8,-180.01) %> @@ -105,7 +105,7 @@ node_west_limit: id: 13 latitude: <%= 13*SCALE %> longitude: <%= -180*SCALE %> - user_id: 1 + changeset_id: 1 visible: 1 version: 1 tile: <%= QuadTile.tile_for_point(13,-180) %> @@ -115,7 +115,7 @@ node_too_far_east: id: 9 latitude: <%= 9*SCALE %> longitude: <%= 180.01*SCALE %> - user_id: 1 + changeset_id: 1 visible: 1 version: 1 tile: <%= QuadTile.tile_for_point(9,180.01) %> @@ -125,7 +125,7 @@ node_east_limit: id: 14 latitude: <%= 14*SCALE %> longitude: <%= 180*SCALE %> - user_id: 1 + changeset_id: 1 visible: 1 version: 1 tile: <%= QuadTile.tile_for_point(14,180) %> @@ -135,7 +135,7 @@ node_totally_wrong: id: 10 latitude: <%= 200*SCALE %> longitude: <%= 200*SCALE %> - user_id: 1 + changeset_id: 1 visible: 1 version: 1 tile: <%= QuadTile.tile_for_point(200,200) %> diff --git a/test/fixtures/relations.yml b/test/fixtures/relations.yml index cf1d1ff56..e4c3b18fa 100644 --- a/test/fixtures/relations.yml +++ b/test/fixtures/relations.yml @@ -1,20 +1,20 @@ visible_relation: id: 1 - user_id: 1 + changeset_id: 1 timestamp: 2007-01-01 00:00:00 visible: 1 version: 1 invisible_relation: id: 2 - user_id: 1 + changeset_id: 1 timestamp: 2007-01-01 00:00:00 visible: 0 version: 1 used_relation: id: 3 - user_id: 1 + changeset_id: 1 timestamp: 2007-01-01 00:00:00 visible: 1 version: 1 diff --git a/test/fixtures/ways.yml b/test/fixtures/ways.yml index c8cf6dcf4..bb1a43c80 100644 --- a/test/fixtures/ways.yml +++ b/test/fixtures/ways.yml @@ -1,20 +1,20 @@ visible_way: id: 1 - user_id: 1 + changeset_id: 1 timestamp: 2007-01-01 00:00:00 visible: 1 version: 1 invisible_way: id: 2 - user_id: 1 + changeset_id: 1 timestamp: 2007-01-01 00:00:00 visible: 0 version: 1 used_way: id: 3 - user_id: 1 + changeset_id: 1 timestamp: 2007-01-01 00:00:00 visible: 0 version: 1 From 6bf25141de8b1fa21198e3cfe85ff95f39231efe Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Tue, 7 Oct 2008 22:59:59 +0000 Subject: [PATCH 096/381] Adding the classic_pagination to svn as the svn external is no longer working --- vendor/plugins/classic_pagination/CHANGELOG | 152 +++++++ vendor/plugins/classic_pagination/README | 18 + vendor/plugins/classic_pagination/Rakefile | 22 + vendor/plugins/classic_pagination/init.rb | 33 ++ vendor/plugins/classic_pagination/install.rb | 1 + .../classic_pagination/lib/pagination.rb | 405 ++++++++++++++++++ .../lib/pagination_helper.rb | 135 ++++++ .../test/fixtures/companies.yml | 24 ++ .../test/fixtures/company.rb | 9 + .../test/fixtures/developer.rb | 7 + .../test/fixtures/developers.yml | 21 + .../test/fixtures/developers_projects.yml | 13 + .../test/fixtures/project.rb | 3 + .../test/fixtures/projects.yml | 7 + .../test/fixtures/replies.yml | 13 + .../classic_pagination/test/fixtures/reply.rb | 5 + .../test/fixtures/schema.sql | 42 ++ .../classic_pagination/test/fixtures/topic.rb | 3 + .../test/fixtures/topics.yml | 22 + .../plugins/classic_pagination/test/helper.rb | 117 +++++ .../test/pagination_helper_test.rb | 38 ++ .../test/pagination_test.rb | 177 ++++++++ 22 files changed, 1267 insertions(+) create mode 100644 vendor/plugins/classic_pagination/CHANGELOG create mode 100644 vendor/plugins/classic_pagination/README create mode 100644 vendor/plugins/classic_pagination/Rakefile create mode 100644 vendor/plugins/classic_pagination/init.rb create mode 100644 vendor/plugins/classic_pagination/install.rb create mode 100644 vendor/plugins/classic_pagination/lib/pagination.rb create mode 100644 vendor/plugins/classic_pagination/lib/pagination_helper.rb create mode 100644 vendor/plugins/classic_pagination/test/fixtures/companies.yml create mode 100644 vendor/plugins/classic_pagination/test/fixtures/company.rb create mode 100644 vendor/plugins/classic_pagination/test/fixtures/developer.rb create mode 100644 vendor/plugins/classic_pagination/test/fixtures/developers.yml create mode 100644 vendor/plugins/classic_pagination/test/fixtures/developers_projects.yml create mode 100644 vendor/plugins/classic_pagination/test/fixtures/project.rb create mode 100644 vendor/plugins/classic_pagination/test/fixtures/projects.yml create mode 100644 vendor/plugins/classic_pagination/test/fixtures/replies.yml create mode 100644 vendor/plugins/classic_pagination/test/fixtures/reply.rb create mode 100644 vendor/plugins/classic_pagination/test/fixtures/schema.sql create mode 100644 vendor/plugins/classic_pagination/test/fixtures/topic.rb create mode 100644 vendor/plugins/classic_pagination/test/fixtures/topics.yml create mode 100644 vendor/plugins/classic_pagination/test/helper.rb create mode 100644 vendor/plugins/classic_pagination/test/pagination_helper_test.rb create mode 100644 vendor/plugins/classic_pagination/test/pagination_test.rb diff --git a/vendor/plugins/classic_pagination/CHANGELOG b/vendor/plugins/classic_pagination/CHANGELOG new file mode 100644 index 000000000..d7d11f129 --- /dev/null +++ b/vendor/plugins/classic_pagination/CHANGELOG @@ -0,0 +1,152 @@ +* Exported the changelog of Pagination code for historical reference. + +* Imported some patches from Rails Trac (others closed as "wontfix"): + #8176, #7325, #7028, #4113. Documentation is much cleaner now and there + are some new unobtrusive features! + +* Extracted Pagination from Rails trunk (r6795) + +# +# ChangeLog for /trunk/actionpack/lib/action_controller/pagination.rb +# +# Generated by Trac 0.10.3 +# 05/20/07 23:48:02 +# + +09/03/06 23:28:54 david [4953] + * trunk/actionpack/lib/action_controller/pagination.rb (modified) + Docs and deprecation + +08/07/06 12:40:14 bitsweat [4715] + * trunk/actionpack/lib/action_controller/pagination.rb (modified) + Deprecate direct usage of @params. Update ActionView::Base for + instance var deprecation. + +06/21/06 02:16:11 rick [4476] + * trunk/actionpack/lib/action_controller/pagination.rb (modified) + Fix indent in pagination documentation. Closes #4990. [Kevin Clark] + +04/25/06 17:42:48 marcel [4268] + * trunk/actionpack/lib/action_controller/pagination.rb (modified) + Remove all remaining references to @params in the documentation. + +03/16/06 06:38:08 rick [3899] + * trunk/actionpack/lib/action_view/helpers/pagination_helper.rb (modified) + trivial documentation patch for #pagination_links [Francois + Beausoleil] closes #4258 + +02/20/06 03:15:22 david [3620] + * trunk/actionpack/lib/action_controller/pagination.rb (modified) + * trunk/actionpack/test/activerecord/pagination_test.rb (modified) + * trunk/activerecord/CHANGELOG (modified) + * trunk/activerecord/lib/active_record/base.rb (modified) + * trunk/activerecord/test/base_test.rb (modified) + Added :count option to pagination that'll make it possible for the + ActiveRecord::Base.count call to using something else than * for the + count. Especially important for count queries using DISTINCT #3839 + [skaes]. Added :select option to Base.count that'll allow you to + select something else than * to be counted on. Especially important + for count queries using DISTINCT (closes #3839) [skaes]. + +02/09/06 09:17:40 nzkoz [3553] + * trunk/actionpack/lib/action_controller/pagination.rb (modified) + * trunk/actionpack/test/active_record_unit.rb (added) + * trunk/actionpack/test/activerecord (added) + * trunk/actionpack/test/activerecord/active_record_assertions_test.rb (added) + * trunk/actionpack/test/activerecord/pagination_test.rb (added) + * trunk/actionpack/test/controller/active_record_assertions_test.rb (deleted) + * trunk/actionpack/test/fixtures/companies.yml (added) + * trunk/actionpack/test/fixtures/company.rb (added) + * trunk/actionpack/test/fixtures/db_definitions (added) + * trunk/actionpack/test/fixtures/db_definitions/sqlite.sql (added) + * trunk/actionpack/test/fixtures/developer.rb (added) + * trunk/actionpack/test/fixtures/developers_projects.yml (added) + * trunk/actionpack/test/fixtures/developers.yml (added) + * trunk/actionpack/test/fixtures/project.rb (added) + * trunk/actionpack/test/fixtures/projects.yml (added) + * trunk/actionpack/test/fixtures/replies.yml (added) + * trunk/actionpack/test/fixtures/reply.rb (added) + * trunk/actionpack/test/fixtures/topic.rb (added) + * trunk/actionpack/test/fixtures/topics.yml (added) + * Fix pagination problems when using include + * Introduce Unit Tests for pagination + * Allow count to work with :include by using count distinct. + + [Kevin Clark & Jeremy Hopple] + +11/05/05 02:10:29 bitsweat [2878] + * trunk/actionpack/lib/action_controller/pagination.rb (modified) + Update paginator docs. Closes #2744. + +10/16/05 15:42:03 minam [2649] + * trunk/actionpack/lib/action_controller/pagination.rb (modified) + Update/clean up AP documentation (rdoc) + +08/31/05 00:13:10 ulysses [2078] + * trunk/actionpack/CHANGELOG (modified) + * trunk/actionpack/lib/action_controller/pagination.rb (modified) + Add option to specify the singular name used by pagination. Closes + #1960 + +08/23/05 14:24:15 minam [2041] + * trunk/actionpack/CHANGELOG (modified) + * trunk/actionpack/lib/action_controller/pagination.rb (modified) + Add support for :include with pagination (subject to existing + constraints for :include with :limit and :offset) #1478 + [michael@schubert.cx] + +07/15/05 20:27:38 david [1839] + * trunk/actionpack/lib/action_controller/pagination.rb (modified) + * trunk/actionpack/lib/action_view/helpers/pagination_helper.rb (modified) + More pagination speed #1334 [Stefan Kaes] + +07/14/05 08:02:01 david [1832] + * trunk/actionpack/lib/action_controller/pagination.rb (modified) + * trunk/actionpack/lib/action_view/helpers/pagination_helper.rb (modified) + * trunk/actionpack/test/controller/addresses_render_test.rb (modified) + Made pagination faster #1334 [Stefan Kaes] + +04/13/05 05:40:22 david [1159] + * trunk/actionpack/CHANGELOG (modified) + * trunk/actionpack/lib/action_controller/pagination.rb (modified) + * trunk/activerecord/lib/active_record/base.rb (modified) + Fixed pagination to work with joins #1034 [scott@sigkill.org] + +04/02/05 09:11:17 david [1067] + * trunk/actionpack/CHANGELOG (modified) + * trunk/actionpack/lib/action_controller/pagination.rb (modified) + * trunk/actionpack/lib/action_controller/scaffolding.rb (modified) + * trunk/actionpack/lib/action_controller/templates/scaffolds/list.rhtml (modified) + * trunk/railties/lib/rails_generator/generators/components/scaffold/templates/controller.rb (modified) + * trunk/railties/lib/rails_generator/generators/components/scaffold/templates/view_list.rhtml (modified) + Added pagination for scaffolding (10 items per page) #964 + [mortonda@dgrmm.net] + +03/31/05 14:46:11 david [1048] + * trunk/actionpack/lib/action_view/helpers/pagination_helper.rb (modified) + Improved the message display on the exception handler pages #963 + [Johan Sorensen] + +03/27/05 00:04:07 david [1017] + * trunk/actionpack/CHANGELOG (modified) + * trunk/actionpack/lib/action_view/helpers/pagination_helper.rb (modified) + Fixed that pagination_helper would ignore :params #947 [Sebastian + Kanthak] + +03/22/05 13:09:44 david [976] + * trunk/actionpack/lib/action_view/helpers/pagination_helper.rb (modified) + Fixed documentation and prepared for 0.11.0 release + +03/21/05 14:35:36 david [967] + * trunk/actionpack/lib/action_controller/pagination.rb (modified) + * trunk/actionpack/lib/action_view/helpers/pagination_helper.rb (modified) + Tweaked the documentation + +03/20/05 23:12:05 david [949] + * trunk/actionpack/CHANGELOG (modified) + * trunk/actionpack/lib/action_controller.rb (modified) + * trunk/actionpack/lib/action_controller/pagination.rb (added) + * trunk/actionpack/lib/action_view/helpers/pagination_helper.rb (added) + * trunk/activesupport/lib/active_support/core_ext/kernel.rb (added) + Added pagination support through both a controller and helper add-on + #817 [Sam Stephenson] diff --git a/vendor/plugins/classic_pagination/README b/vendor/plugins/classic_pagination/README new file mode 100644 index 000000000..e94904974 --- /dev/null +++ b/vendor/plugins/classic_pagination/README @@ -0,0 +1,18 @@ +Pagination +========== + +To install: + + script/plugin install svn://errtheblog.com/svn/plugins/classic_pagination + +This code was extracted from Rails trunk after the release 1.2.3. +WARNING: this code is dead. It is unmaintained, untested and full of cruft. + +There is a much better pagination plugin called will_paginate. +Install it like this and glance through the README: + + script/plugin install svn://errtheblog.com/svn/plugins/will_paginate + +It doesn't have the same API, but is in fact much nicer. You can +have both plugins installed until you change your controller/view code that +handles pagination. Then, simply uninstall classic_pagination. diff --git a/vendor/plugins/classic_pagination/Rakefile b/vendor/plugins/classic_pagination/Rakefile new file mode 100644 index 000000000..c7e374b56 --- /dev/null +++ b/vendor/plugins/classic_pagination/Rakefile @@ -0,0 +1,22 @@ +require 'rake' +require 'rake/testtask' +require 'rake/rdoctask' + +desc 'Default: run unit tests.' +task :default => :test + +desc 'Test the classic_pagination plugin.' +Rake::TestTask.new(:test) do |t| + t.libs << 'lib' + t.pattern = 'test/**/*_test.rb' + t.verbose = true +end + +desc 'Generate documentation for the classic_pagination plugin.' +Rake::RDocTask.new(:rdoc) do |rdoc| + rdoc.rdoc_dir = 'rdoc' + rdoc.title = 'Pagination' + rdoc.options << '--line-numbers' << '--inline-source' + rdoc.rdoc_files.include('README') + rdoc.rdoc_files.include('lib/**/*.rb') +end diff --git a/vendor/plugins/classic_pagination/init.rb b/vendor/plugins/classic_pagination/init.rb new file mode 100644 index 000000000..25e552f2a --- /dev/null +++ b/vendor/plugins/classic_pagination/init.rb @@ -0,0 +1,33 @@ +#-- +# Copyright (c) 2004-2006 David Heinemeier Hansson +# +# 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. +#++ + +require 'pagination' +require 'pagination_helper' + +ActionController::Base.class_eval do + include ActionController::Pagination +end + +ActionView::Base.class_eval do + include ActionView::Helpers::PaginationHelper +end diff --git a/vendor/plugins/classic_pagination/install.rb b/vendor/plugins/classic_pagination/install.rb new file mode 100644 index 000000000..adf746f8b --- /dev/null +++ b/vendor/plugins/classic_pagination/install.rb @@ -0,0 +1 @@ +puts "\n\n" + File.read(File.dirname(__FILE__) + '/README') diff --git a/vendor/plugins/classic_pagination/lib/pagination.rb b/vendor/plugins/classic_pagination/lib/pagination.rb new file mode 100644 index 000000000..b6e9cf4bc --- /dev/null +++ b/vendor/plugins/classic_pagination/lib/pagination.rb @@ -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 @people + # 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 + # @person_pages Paginator instance. The current page is determined + # by the params[:page] 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 @person_pages + # and @people 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 @people 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] ||= Inflector.singularize(collection_id.to_s) + options[:class_name] ||= 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: + # :singular_name:: the singular name to use, if it can't be inferred by singularizing the collection name + # :class_name:: the class name to use, if it can't be inferred by + # camelizing the singular name + # :per_page:: the maximum number of items to include in a + # single page. Defaults to 10 + # :conditions:: optional conditions passed to Model.find(:all, *params) and + # Model.count + # :order:: optional order parameter passed to Model.find(:all, *params) + # :order_by:: (deprecated, used :order) optional order parameter passed to Model.find(:all, *params) + # :joins:: optional joins parameter passed to Model.find(:all, *params) + # and Model.count + # :join:: (deprecated, used :joins or :include) optional join parameter passed to Model.find(:all, *params) + # and Model.count + # :include:: optional eager loading parameter passed to Model.find(:all, *params) + # and Model.count + # :select:: :select parameter passed to Model.find(:all, *params) + # + # :count:: parameter passed as :select option to Model.count(*params) + # + # :group:: :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 :actions option). + # + # +options+ are the same as PaginationHelper#paginate, with the addition + # of: + # :actions:: 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/plugins/classic_pagination/lib/pagination_helper.rb b/vendor/plugins/classic_pagination/lib/pagination_helper.rb new file mode 100644 index 000000000..069d77566 --- /dev/null +++ b/vendor/plugins/classic_pagination/lib/pagination_helper.rb @@ -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 + # :name:: the routing name for this paginator + # (defaults to +page+) + # :prefix:: prefix for pagination links + # (i.e. Older Pages: 1 2 3 4) + # :suffix:: suffix for pagination links + # (i.e. 1 2 3 4 <- Older Pages) + # :window_size:: the number of pages to show around + # the current page (defaults to 2) + # :always_show_anchors:: whether or not the first and last + # pages should always be shown + # (defaults to +true+) + # :link_to_current_page:: whether or not the current page + # should be linked to (defaults to + # +false+) + # :params:: any additional routing parameters + # for page URLs + # + # ==== Examples + # # We'll assume we have a paginator setup in @person_pages... + # + # pagination_links(@person_pages) + # # => 1 2 3 ... 10 + # + # pagination_links(@person_pages, :link_to_current_page => true) + # # => 1 2 3 ... 10 + # + # pagination_links(@person_pages, :always_show_anchors => false) + # # => 1 2 3 + # + # pagination_links(@person_pages, :window_size => 1) + # # => 1 2 ... 10 + # + # pagination_links(@person_pages, :params => { :viewer => "flash" }) + # # => 1 2 3 ... + # # 10 + 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 + # :window_size:: the number of pages to show around + # the current page (defaults to +2+) + # :always_show_anchors:: whether or not the first and last + # pages should always be shown + # (defaults to +true+) + # :link_to_current_page:: 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/plugins/classic_pagination/test/fixtures/companies.yml b/vendor/plugins/classic_pagination/test/fixtures/companies.yml new file mode 100644 index 000000000..707f72abc --- /dev/null +++ b/vendor/plugins/classic_pagination/test/fixtures/companies.yml @@ -0,0 +1,24 @@ +thirty_seven_signals: + id: 1 + name: 37Signals + rating: 4 + +TextDrive: + id: 2 + name: TextDrive + rating: 4 + +PlanetArgon: + id: 3 + name: Planet Argon + rating: 4 + +Google: + id: 4 + name: Google + rating: 4 + +Ionist: + id: 5 + name: Ioni.st + rating: 4 \ No newline at end of file diff --git a/vendor/plugins/classic_pagination/test/fixtures/company.rb b/vendor/plugins/classic_pagination/test/fixtures/company.rb new file mode 100644 index 000000000..0d1c29b90 --- /dev/null +++ b/vendor/plugins/classic_pagination/test/fixtures/company.rb @@ -0,0 +1,9 @@ +class Company < ActiveRecord::Base + attr_protected :rating + set_sequence_name :companies_nonstd_seq + + validates_presence_of :name + def validate + errors.add('rating', 'rating should not be 2') if rating == 2 + end +end \ No newline at end of file diff --git a/vendor/plugins/classic_pagination/test/fixtures/developer.rb b/vendor/plugins/classic_pagination/test/fixtures/developer.rb new file mode 100644 index 000000000..f5e5b901f --- /dev/null +++ b/vendor/plugins/classic_pagination/test/fixtures/developer.rb @@ -0,0 +1,7 @@ +class Developer < ActiveRecord::Base + has_and_belongs_to_many :projects +end + +class DeVeLoPeR < ActiveRecord::Base + set_table_name "developers" +end diff --git a/vendor/plugins/classic_pagination/test/fixtures/developers.yml b/vendor/plugins/classic_pagination/test/fixtures/developers.yml new file mode 100644 index 000000000..308bf75de --- /dev/null +++ b/vendor/plugins/classic_pagination/test/fixtures/developers.yml @@ -0,0 +1,21 @@ +david: + id: 1 + name: David + salary: 80000 + +jamis: + id: 2 + name: Jamis + salary: 150000 + +<% for digit in 3..10 %> +dev_<%= digit %>: + id: <%= digit %> + name: fixture_<%= digit %> + salary: 100000 +<% end %> + +poor_jamis: + id: 11 + name: Jamis + salary: 9000 \ No newline at end of file diff --git a/vendor/plugins/classic_pagination/test/fixtures/developers_projects.yml b/vendor/plugins/classic_pagination/test/fixtures/developers_projects.yml new file mode 100644 index 000000000..cee359c7c --- /dev/null +++ b/vendor/plugins/classic_pagination/test/fixtures/developers_projects.yml @@ -0,0 +1,13 @@ +david_action_controller: + developer_id: 1 + project_id: 2 + joined_on: 2004-10-10 + +david_active_record: + developer_id: 1 + project_id: 1 + joined_on: 2004-10-10 + +jamis_active_record: + developer_id: 2 + project_id: 1 \ No newline at end of file diff --git a/vendor/plugins/classic_pagination/test/fixtures/project.rb b/vendor/plugins/classic_pagination/test/fixtures/project.rb new file mode 100644 index 000000000..2b53d39ed --- /dev/null +++ b/vendor/plugins/classic_pagination/test/fixtures/project.rb @@ -0,0 +1,3 @@ +class Project < ActiveRecord::Base + has_and_belongs_to_many :developers, :uniq => true +end diff --git a/vendor/plugins/classic_pagination/test/fixtures/projects.yml b/vendor/plugins/classic_pagination/test/fixtures/projects.yml new file mode 100644 index 000000000..02800c782 --- /dev/null +++ b/vendor/plugins/classic_pagination/test/fixtures/projects.yml @@ -0,0 +1,7 @@ +action_controller: + id: 2 + name: Active Controller + +active_record: + id: 1 + name: Active Record diff --git a/vendor/plugins/classic_pagination/test/fixtures/replies.yml b/vendor/plugins/classic_pagination/test/fixtures/replies.yml new file mode 100644 index 000000000..284c9c079 --- /dev/null +++ b/vendor/plugins/classic_pagination/test/fixtures/replies.yml @@ -0,0 +1,13 @@ +witty_retort: + id: 1 + topic_id: 1 + content: Birdman is better! + created_at: <%= 6.hours.ago.to_s(:db) %> + updated_at: nil + +another: + id: 2 + topic_id: 2 + content: Nuh uh! + created_at: <%= 1.hour.ago.to_s(:db) %> + updated_at: nil \ No newline at end of file diff --git a/vendor/plugins/classic_pagination/test/fixtures/reply.rb b/vendor/plugins/classic_pagination/test/fixtures/reply.rb new file mode 100644 index 000000000..ea84042b9 --- /dev/null +++ b/vendor/plugins/classic_pagination/test/fixtures/reply.rb @@ -0,0 +1,5 @@ +class Reply < ActiveRecord::Base + belongs_to :topic, :include => [:replies] + + validates_presence_of :content +end diff --git a/vendor/plugins/classic_pagination/test/fixtures/schema.sql b/vendor/plugins/classic_pagination/test/fixtures/schema.sql new file mode 100644 index 000000000..b4e7539d1 --- /dev/null +++ b/vendor/plugins/classic_pagination/test/fixtures/schema.sql @@ -0,0 +1,42 @@ +CREATE TABLE 'companies' ( + 'id' INTEGER PRIMARY KEY NOT NULL, + 'name' TEXT DEFAULT NULL, + 'rating' INTEGER DEFAULT 1 +); + +CREATE TABLE 'replies' ( + 'id' INTEGER PRIMARY KEY NOT NULL, + 'content' text, + 'created_at' datetime, + 'updated_at' datetime, + 'topic_id' integer +); + +CREATE TABLE 'topics' ( + 'id' INTEGER PRIMARY KEY NOT NULL, + 'title' varchar(255), + 'subtitle' varchar(255), + 'content' text, + 'created_at' datetime, + 'updated_at' datetime +); + +CREATE TABLE 'developers' ( + 'id' INTEGER PRIMARY KEY NOT NULL, + 'name' TEXT DEFAULT NULL, + 'salary' INTEGER DEFAULT 70000, + 'created_at' DATETIME DEFAULT NULL, + 'updated_at' DATETIME DEFAULT NULL +); + +CREATE TABLE 'projects' ( + 'id' INTEGER PRIMARY KEY NOT NULL, + 'name' TEXT DEFAULT NULL +); + +CREATE TABLE 'developers_projects' ( + 'developer_id' INTEGER NOT NULL, + 'project_id' INTEGER NOT NULL, + 'joined_on' DATE DEFAULT NULL, + 'access_level' INTEGER DEFAULT 1 +); diff --git a/vendor/plugins/classic_pagination/test/fixtures/topic.rb b/vendor/plugins/classic_pagination/test/fixtures/topic.rb new file mode 100644 index 000000000..0beeecf28 --- /dev/null +++ b/vendor/plugins/classic_pagination/test/fixtures/topic.rb @@ -0,0 +1,3 @@ +class Topic < ActiveRecord::Base + has_many :replies, :include => [:user], :dependent => :destroy +end diff --git a/vendor/plugins/classic_pagination/test/fixtures/topics.yml b/vendor/plugins/classic_pagination/test/fixtures/topics.yml new file mode 100644 index 000000000..61ea02d76 --- /dev/null +++ b/vendor/plugins/classic_pagination/test/fixtures/topics.yml @@ -0,0 +1,22 @@ +futurama: + id: 1 + title: Isnt futurama awesome? + subtitle: It really is, isnt it. + content: I like futurama + created_at: <%= 1.day.ago.to_s(:db) %> + updated_at: + +harvey_birdman: + id: 2 + title: Harvey Birdman is the king of all men + subtitle: yup + content: It really is + created_at: <%= 2.hours.ago.to_s(:db) %> + updated_at: + +rails: + id: 3 + title: Rails is nice + subtitle: It makes me happy + content: except when I have to hack internals to fix pagination. even then really. + created_at: <%= 20.minutes.ago.to_s(:db) %> diff --git a/vendor/plugins/classic_pagination/test/helper.rb b/vendor/plugins/classic_pagination/test/helper.rb new file mode 100644 index 000000000..3f76d5a76 --- /dev/null +++ b/vendor/plugins/classic_pagination/test/helper.rb @@ -0,0 +1,117 @@ +require 'test/unit' + +unless defined?(ActiveRecord) + plugin_root = File.join(File.dirname(__FILE__), '..') + + # first look for a symlink to a copy of the framework + if framework_root = ["#{plugin_root}/rails", "#{plugin_root}/../../rails"].find { |p| File.directory? p } + puts "found framework root: #{framework_root}" + # this allows for a plugin to be tested outside an app + $:.unshift "#{framework_root}/activesupport/lib", "#{framework_root}/activerecord/lib", "#{framework_root}/actionpack/lib" + else + # is the plugin installed in an application? + app_root = plugin_root + '/../../..' + + if File.directory? app_root + '/config' + puts 'using config/boot.rb' + ENV['RAILS_ENV'] = 'test' + require File.expand_path(app_root + '/config/boot') + else + # simply use installed gems if available + puts 'using rubygems' + require 'rubygems' + gem 'actionpack'; gem 'activerecord' + end + end + + %w(action_pack active_record action_controller active_record/fixtures action_controller/test_process).each {|f| require f} + + Dependencies.load_paths.unshift "#{plugin_root}/lib" +end + +# Define the connector +class ActiveRecordTestConnector + cattr_accessor :able_to_connect + cattr_accessor :connected + + # Set our defaults + self.connected = false + self.able_to_connect = true + + class << self + def setup + unless self.connected || !self.able_to_connect + setup_connection + load_schema + require_fixture_models + self.connected = true + end + rescue Exception => e # errors from ActiveRecord setup + $stderr.puts "\nSkipping ActiveRecord assertion tests: #{e}" + #$stderr.puts " #{e.backtrace.join("\n ")}\n" + self.able_to_connect = false + end + + private + + def setup_connection + if Object.const_defined?(:ActiveRecord) + defaults = { :database => ':memory:' } + begin + options = defaults.merge :adapter => 'sqlite3', :timeout => 500 + ActiveRecord::Base.establish_connection(options) + ActiveRecord::Base.configurations = { 'sqlite3_ar_integration' => options } + ActiveRecord::Base.connection + rescue Exception # errors from establishing a connection + $stderr.puts 'SQLite 3 unavailable; trying SQLite 2.' + options = defaults.merge :adapter => 'sqlite' + ActiveRecord::Base.establish_connection(options) + ActiveRecord::Base.configurations = { 'sqlite2_ar_integration' => options } + ActiveRecord::Base.connection + end + + Object.send(:const_set, :QUOTED_TYPE, ActiveRecord::Base.connection.quote_column_name('type')) unless Object.const_defined?(:QUOTED_TYPE) + else + raise "Can't setup connection since ActiveRecord isn't loaded." + end + end + + # Load actionpack sqlite tables + def load_schema + File.read(File.dirname(__FILE__) + "/fixtures/schema.sql").split(';').each do |sql| + ActiveRecord::Base.connection.execute(sql) unless sql.blank? + end + end + + def require_fixture_models + Dir.glob(File.dirname(__FILE__) + "/fixtures/*.rb").each {|f| require f} + end + end +end + +# Test case for inheritance +class ActiveRecordTestCase < Test::Unit::TestCase + # Set our fixture path + if ActiveRecordTestConnector.able_to_connect + self.fixture_path = "#{File.dirname(__FILE__)}/fixtures/" + self.use_transactional_fixtures = false + end + + def self.fixtures(*args) + super if ActiveRecordTestConnector.connected + end + + def run(*args) + super if ActiveRecordTestConnector.connected + end + + # Default so Test::Unit::TestCase doesn't complain + def test_truth + end +end + +ActiveRecordTestConnector.setup +ActionController::Routing::Routes.reload rescue nil +ActionController::Routing::Routes.draw do |map| + map.connect ':controller/:action/:id' +end diff --git a/vendor/plugins/classic_pagination/test/pagination_helper_test.rb b/vendor/plugins/classic_pagination/test/pagination_helper_test.rb new file mode 100644 index 000000000..d8394a793 --- /dev/null +++ b/vendor/plugins/classic_pagination/test/pagination_helper_test.rb @@ -0,0 +1,38 @@ +require File.dirname(__FILE__) + '/helper' +require File.dirname(__FILE__) + '/../init' + +class PaginationHelperTest < Test::Unit::TestCase + include ActionController::Pagination + include ActionView::Helpers::PaginationHelper + include ActionView::Helpers::UrlHelper + include ActionView::Helpers::TagHelper + + def setup + @controller = Class.new do + attr_accessor :url, :request + def url_for(options, *parameters_for_method_reference) + url + end + end + @controller = @controller.new + @controller.url = "http://www.example.com" + end + + def test_pagination_links + total, per_page, page = 30, 10, 1 + output = pagination_links Paginator.new(@controller, total, per_page, page) + assert_equal "1 2 3 ", output + end + + def test_pagination_links_with_prefix + total, per_page, page = 30, 10, 1 + output = pagination_links Paginator.new(@controller, total, per_page, page), :prefix => 'Newer ' + assert_equal "Newer 1 2 3 ", output + end + + def test_pagination_links_with_suffix + total, per_page, page = 30, 10, 1 + output = pagination_links Paginator.new(@controller, total, per_page, page), :suffix => 'Older' + assert_equal "1 2 3 Older", output + end +end diff --git a/vendor/plugins/classic_pagination/test/pagination_test.rb b/vendor/plugins/classic_pagination/test/pagination_test.rb new file mode 100644 index 000000000..16a6f1d84 --- /dev/null +++ b/vendor/plugins/classic_pagination/test/pagination_test.rb @@ -0,0 +1,177 @@ +require File.dirname(__FILE__) + '/helper' +require File.dirname(__FILE__) + '/../init' + +class PaginationTest < ActiveRecordTestCase + fixtures :topics, :replies, :developers, :projects, :developers_projects + + class PaginationController < ActionController::Base + if respond_to? :view_paths= + self.view_paths = [ "#{File.dirname(__FILE__)}/../fixtures/" ] + else + self.template_root = [ "#{File.dirname(__FILE__)}/../fixtures/" ] + end + + def simple_paginate + @topic_pages, @topics = paginate(:topics) + render :nothing => true + end + + def paginate_with_per_page + @topic_pages, @topics = paginate(:topics, :per_page => 1) + render :nothing => true + end + + def paginate_with_order + @topic_pages, @topics = paginate(:topics, :order => 'created_at asc') + render :nothing => true + end + + def paginate_with_order_by + @topic_pages, @topics = paginate(:topics, :order_by => 'created_at asc') + render :nothing => true + end + + def paginate_with_include_and_order + @topic_pages, @topics = paginate(:topics, :include => :replies, :order => 'replies.created_at asc, topics.created_at asc') + render :nothing => true + end + + def paginate_with_conditions + @topic_pages, @topics = paginate(:topics, :conditions => ["created_at > ?", 30.minutes.ago]) + render :nothing => true + end + + def paginate_with_class_name + @developer_pages, @developers = paginate(:developers, :class_name => "DeVeLoPeR") + render :nothing => true + end + + def paginate_with_singular_name + @developer_pages, @developers = paginate() + render :nothing => true + end + + def paginate_with_joins + @developer_pages, @developers = paginate(:developers, + :joins => 'LEFT JOIN developers_projects ON developers.id = developers_projects.developer_id', + :conditions => 'project_id=1') + render :nothing => true + end + + def paginate_with_join + @developer_pages, @developers = paginate(:developers, + :join => 'LEFT JOIN developers_projects ON developers.id = developers_projects.developer_id', + :conditions => 'project_id=1') + render :nothing => true + end + + def paginate_with_join_and_count + @developer_pages, @developers = paginate(:developers, + :join => 'd LEFT JOIN developers_projects ON d.id = developers_projects.developer_id', + :conditions => 'project_id=1', + :count => "d.id") + render :nothing => true + end + + def paginate_with_join_and_group + @developer_pages, @developers = paginate(:developers, + :join => 'INNER JOIN developers_projects ON developers.id = developers_projects.developer_id', + :group => 'developers.id') + render :nothing => true + end + + def rescue_errors(e) raise e end + + def rescue_action(e) raise end + + end + + def setup + @controller = PaginationController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + super + end + + # Single Action Pagination Tests + + def test_simple_paginate + get :simple_paginate + assert_equal 1, assigns(:topic_pages).page_count + assert_equal 3, assigns(:topics).size + end + + def test_paginate_with_per_page + get :paginate_with_per_page + assert_equal 1, assigns(:topics).size + assert_equal 3, assigns(:topic_pages).page_count + end + + def test_paginate_with_order + get :paginate_with_order + expected = [topics(:futurama), + topics(:harvey_birdman), + topics(:rails)] + assert_equal expected, assigns(:topics) + assert_equal 1, assigns(:topic_pages).page_count + end + + def test_paginate_with_order_by + get :paginate_with_order + expected = assigns(:topics) + get :paginate_with_order_by + assert_equal expected, assigns(:topics) + assert_equal 1, assigns(:topic_pages).page_count + end + + def test_paginate_with_conditions + get :paginate_with_conditions + expected = [topics(:rails)] + assert_equal expected, assigns(:topics) + assert_equal 1, assigns(:topic_pages).page_count + end + + def test_paginate_with_class_name + get :paginate_with_class_name + + assert assigns(:developers).size > 0 + assert_equal DeVeLoPeR, assigns(:developers).first.class + end + + def test_paginate_with_joins + get :paginate_with_joins + assert_equal 2, assigns(:developers).size + developer_names = assigns(:developers).map { |d| d.name } + assert developer_names.include?('David') + assert developer_names.include?('Jamis') + end + + def test_paginate_with_join_and_conditions + get :paginate_with_joins + expected = assigns(:developers) + get :paginate_with_join + assert_equal expected, assigns(:developers) + end + + def test_paginate_with_join_and_count + get :paginate_with_joins + expected = assigns(:developers) + get :paginate_with_join_and_count + assert_equal expected, assigns(:developers) + end + + def test_paginate_with_include_and_order + get :paginate_with_include_and_order + expected = Topic.find(:all, :include => 'replies', :order => 'replies.created_at asc, topics.created_at asc', :limit => 10) + assert_equal expected, assigns(:topics) + end + + def test_paginate_with_join_and_group + get :paginate_with_join_and_group + assert_equal 2, assigns(:developers).size + assert_equal 2, assigns(:developer_pages).item_count + developer_names = assigns(:developers).map { |d| d.name } + assert developer_names.include?('David') + assert developer_names.include?('Jamis') + end +end From 2347761266d8848dcdc42f9e5d9ef380fa04b89a Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Tue, 7 Oct 2008 23:49:53 +0000 Subject: [PATCH 097/381] some useful notes in code for ideas that require implementing. --- app/controllers/node_controller.rb | 5 +++++ app/models/changeset.rb | 10 ++++++++++ test/functional/node_controller_test.rb | 4 +++- 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/app/controllers/node_controller.rb b/app/controllers/node_controller.rb index 03ae64f41..bfd1031f0 100644 --- a/app/controllers/node_controller.rb +++ b/app/controllers/node_controller.rb @@ -87,6 +87,11 @@ class NodeController < ApplicationController def delete begin node = Node.find(params[:id]) + # FIXME we no longer care about the user, (or maybe we want to check + # that the user of the changeset is the same user as is making this + # little change?) we really care about the + # changeset which must be open, and that the version that we have been + # given is the one that is currently stored in the database node.delete_with_history(@user) render :nothing => true diff --git a/app/models/changeset.rb b/app/models/changeset.rb index 04023ae2b..7e2216f0e 100644 --- a/app/models/changeset.rb +++ b/app/models/changeset.rb @@ -11,6 +11,15 @@ class Changeset < ActiveRecord::Base has_many :old_nodes has_many :old_ways has_many :old_relations + + validates_presence_of :user_id, :created_at, :open + + # 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? + return open + end def self.from_xml(xml, create=false) begin @@ -83,6 +92,7 @@ class Changeset < ActiveRecord::Base 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 diff --git a/test/functional/node_controller_test.rb b/test/functional/node_controller_test.rb index 3f316d012..59b28e114 100644 --- a/test/functional/node_controller_test.rb +++ b/test/functional/node_controller_test.rb @@ -15,7 +15,9 @@ class NodeControllerTest < Test::Unit::TestCase 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"); + # FIXME we need to create a changeset first argh + # create a node with random lat/lon lat = rand(100)-50 + rand lon = rand(100)-50 + rand From e1b205913c9afd6b41684add7308cef4613112e2 Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Wed, 8 Oct 2008 15:42:42 +0000 Subject: [PATCH 098/381] Fix the data config, so that the development database is not the same as the test database. Make a good stab at getting the creates and updates for relations working, adding some new error exceptions. Some code indentation cleanup. --- app/controllers/node_controller.rb | 2 +- app/controllers/relation_controller.rb | 8 +++---- app/models/changeset.rb | 1 + app/models/node.rb | 6 +++-- app/models/old_relation.rb | 3 ++- app/models/relation.rb | 33 ++++++++++++++++---------- app/models/way.rb | 1 + config/database.yml | 9 +++---- lib/osm.rb | 14 +++++++++++ 9 files changed, 51 insertions(+), 26 deletions(-) diff --git a/app/controllers/node_controller.rb b/app/controllers/node_controller.rb index bfd1031f0..8e8c8446d 100644 --- a/app/controllers/node_controller.rb +++ b/app/controllers/node_controller.rb @@ -36,7 +36,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 diff --git a/app/controllers/relation_controller.rb b/app/controllers/relation_controller.rb index c49ecd4d7..b38d90c02 100644 --- a/app/controllers/relation_controller.rb +++ b/app/controllers/relation_controller.rb @@ -15,11 +15,11 @@ class RelationController < ApplicationController if !relation.preconditions_ok? render :text => "", :status => :precondition_failed else - relation.version = 0 - relation.user_id = @user.id + relation.version = 0 + #relation.user_id = @user.id relation.save_with_history! - render :text => relation.id.to_s, :content_type => "text/plain" + render :text => relation.id.to_s, :content_type => "text/plain" end else render :nothing => true, :status => :bad_request @@ -51,7 +51,7 @@ class RelationController < ApplicationController new_relation = Relation.from_xml(request.raw_post) if new_relation and new_relation.id == relation.id - relation.update_from new_relation, user + relation.update_from new_relation, @user render :text => relation.version.to_s, :content_type => "text/plain" else render :nothing => true, :status => :bad_request diff --git a/app/models/changeset.rb b/app/models/changeset.rb index 7e2216f0e..934d75e39 100644 --- a/app/models/changeset.rb +++ b/app/models/changeset.rb @@ -13,6 +13,7 @@ class Changeset < ActiveRecord::Base has_many :old_relations validates_presence_of :user_id, :created_at, :open + validates_inclusion_of :open, :in => [ true, false ] # Use a method like this, so that we can easily change how we # determine whether a changeset is open, without breaking code in at diff --git a/app/models/node.rb b/app/models/node.rb index 2f3001a49..d152be6d3 100644 --- a/app/models/node.rb +++ b/app/models/node.rb @@ -4,7 +4,7 @@ class Node < ActiveRecord::Base include GeoRecord set_table_name 'current_nodes' - + validates_presence_of :changeset_id, :timestamp validates_inclusion_of :visible, :in => [ true, false ] validates_numericality_of :latitude, :longitude @@ -172,7 +172,9 @@ class Node < ActiveRecord::Base 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.changeset.user_id) diff --git a/app/models/old_relation.rb b/app/models/old_relation.rb index 9dca97a27..428177755 100644 --- a/app/models/old_relation.rb +++ b/app/models/old_relation.rb @@ -6,7 +6,7 @@ class OldRelation < ActiveRecord::Base 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 @@ -93,6 +93,7 @@ class OldRelation < ActiveRecord::Base el1['timestamp'] = self.timestamp.xmlschema el1['user'] = self.user.display_name if self.user.data_public? el1['version'] = self.version.to_s + el1['changeset'] = self.changeset_id.to_s self.old_members.each do |member| e = XML::Node.new 'member' diff --git a/app/models/relation.rb b/app/models/relation.rb index bb8f2d003..658cf06f3 100644 --- a/app/models/relation.rb +++ b/app/models/relation.rb @@ -35,13 +35,14 @@ class Relation < ActiveRecord::Base end relation.version = pt['version'] + relation.changeset_id = pt['changeset'] if create relation.timestamp = Time.now relation.visible = true else if pt['timestamp'] - relation.timestamp = Time.parse(pt['timestamp']) + relation.timestamp = Time.parse(pt['timestamp']) end end @@ -68,18 +69,19 @@ class Relation < ActiveRecord::Base 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 + 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? + el1['user'] = user_display_name_cache[self.changeset.user_id] unless user_display_name_cache[self.changeset.user_id].nil? self.relation_members.each do |member| p=0 @@ -218,13 +220,14 @@ class Relation < ActiveRecord::Base def delete_with_history(user) if self.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=?", self.id ]) - raise OSM::APIPreconditionFailedError.new + raise OSM::APIPreconditionFailedError.new else - self.user_id = user.id - self.tags = [] - self.members = [] - self.visible = false - save_with_history! + #self.user_id = user.id + # FIXME we need to deal with changeset here, which is probably already dealt with + self.tags = [] + self.members = [] + self.visible = false + save_with_history! end else raise OSM::APIAlreadyDeletedError.new @@ -236,8 +239,14 @@ class Relation < ActiveRecord::Base raise OSM::APIPreconditionFailedError.new elsif new_relation.version != version raise OSM::APIVersionMismatchError.new(new_relation.version, version) + elsif new_relation.changeset.user_id != user.id + raise OSM::APIUserChangesetMismatchError.new + elsif not new_relation.changeset.open? + raise OSM::APIChangesetAlreadyClosedError.new else - self.user_id = user.id + # FIXME need to deal with changeset etc + #self.user_id = user.id + self.changeset = new_relation.changeset self.tags = new_relation.tags self.members = new_relation.members self.visible = true diff --git a/app/models/way.rb b/app/models/way.rb index 7c6e9d06b..9ddb603a8 100644 --- a/app/models/way.rb +++ b/app/models/way.rb @@ -85,6 +85,7 @@ class Way < ActiveRecord::Base 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? diff --git a/config/database.yml b/config/database.yml index a80e11b70..fe47e11aa 100644 --- a/config/database.yml +++ b/config/database.yml @@ -12,12 +12,9 @@ # http://dev.mysql.com/doc/refman/5.0/en/old-client.html development: adapter: mysql - #database: openstreetmap - #username: openstreetmap - #password: openstreetmap - database: osm_test - username: osm_test - password: osm_test + database: openstreetmap + username: openstreetmap + password: openstreetmap host: localhost # Warning: The database defined as 'test' will be erased and diff --git a/lib/osm.rb b/lib/osm.rb index a64aa8c48..3ddf10f98 100644 --- a/lib/osm.rb +++ b/lib/osm.rb @@ -33,6 +33,20 @@ module OSM 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 } + end + end + + # Raised when the changeset provided is already closed + class APIChangesetAlreadyClosedError < APIError + def render_opts + { :text => "The supplied changeset has already been closed", :status => :conflict } + end + end + # Raised when the provided version is not equal to the latest in the db. class APIVersionMismatchError < APIError def initialize(provided, latest) From 2b7a40069f7af2e72c81f9488411135f286515f9 Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Wed, 8 Oct 2008 18:03:41 +0000 Subject: [PATCH 099/381] adding the controller code to be able to get a specific version of a node, way or relation. Add some node checks on update. Some code indentation cleanup. --- app/controllers/old_node_controller.rb | 15 +++++++++++++++ app/controllers/old_relation_controller.rb | 16 ++++++++++++++++ app/controllers/old_way_controller.rb | 15 +++++++++++++++ app/controllers/relation_controller.rb | 1 + app/models/node.rb | 17 ++++++++++------- app/models/relation.rb | 2 +- lib/osm.rb | 2 +- 7 files changed, 59 insertions(+), 9 deletions(-) diff --git a/app/controllers/old_node_controller.rb b/app/controllers/old_node_controller.rb index e27898336..8c43941ec 100644 --- a/app/controllers/old_node_controller.rb +++ b/app/controllers/old_node_controller.rb @@ -22,4 +22,19 @@ 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]} ) + + 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 diff --git a/app/controllers/old_relation_controller.rb b/app/controllers/old_relation_controller.rb index 0b5aa89be..9307ee1ce 100644 --- a/app/controllers/old_relation_controller.rb +++ b/app/controllers/old_relation_controller.rb @@ -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,19 @@ 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]} ) + + 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 diff --git a/app/controllers/old_way_controller.rb b/app/controllers/old_way_controller.rb index e72c97a00..6cd8c8f22 100644 --- a/app/controllers/old_way_controller.rb +++ b/app/controllers/old_way_controller.rb @@ -22,4 +22,19 @@ 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]} ) + + 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 diff --git a/app/controllers/relation_controller.rb b/app/controllers/relation_controller.rb index b38d90c02..d87905059 100644 --- a/app/controllers/relation_controller.rb +++ b/app/controllers/relation_controller.rb @@ -46,6 +46,7 @@ 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) diff --git a/app/models/node.rb b/app/models/node.rb index d152be6d3..a25a19f70 100644 --- a/app/models/node.rb +++ b/app/models/node.rb @@ -134,13 +134,13 @@ class Node < ActiveRecord::Base def delete_with_history(user) if self.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 = ?", self.id ]) - raise OSM::APIPreconditionFailedError.new + raise OSM::APIPreconditionFailedError.new 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=?", self.id]) - raise OSM::APIPreconditionFailedError.new + raise OSM::APIPreconditionFailedError.new else - self.user_id = user.id - self.visible = 0 - save_with_history! + self.user_id = user.id + self.visible = 0 + save_with_history! end else raise OSM::APIAlreadyDeletedError.new @@ -150,10 +150,14 @@ class Node < ActiveRecord::Base def update_from(new_node, user) if new_node.version != version raise OSM::APIVersionMismatchError.new(new_node.version, version) + elsif new_node.changeset.user_id != user.id + raise OSM::APIUserChangesetMismatchError.new + elsif not new_node.changeset.open? + raise OSM::APIChangesetAlreadyClosedError.new end # FIXME logic need looked at - self.changeset_id = user.id + self.changeset_id = new_node.changeset_id self.latitude = new_node.latitude self.longitude = new_node.longitude self.tags = new_node.tags @@ -196,7 +200,6 @@ class Node < ActiveRecord::Base el1['visible'] = self.visible.to_s el1['timestamp'] = self.timestamp.xmlschema - el1['version'] = self.version.to_s return el1 end diff --git a/app/models/relation.rb b/app/models/relation.rb index 658cf06f3..93f0001da 100644 --- a/app/models/relation.rb +++ b/app/models/relation.rb @@ -246,7 +246,7 @@ class Relation < ActiveRecord::Base else # FIXME need to deal with changeset etc #self.user_id = user.id - self.changeset = new_relation.changeset + self.changeset_id = new_relation.changeset_id self.tags = new_relation.tags self.members = new_relation.members self.visible = true diff --git a/lib/osm.rb b/lib/osm.rb index 3ddf10f98..e0e83845e 100644 --- a/lib/osm.rb +++ b/lib/osm.rb @@ -57,7 +57,7 @@ module OSM def render_opts { :text => "Version mismatch: Provided " + provided.to_s + - ", server had: " + latest.to_s, :status => :bad_request } + ", server had: " + latest.to_s, :status => :conflict } end end From 2c5f6740c74dd754e162bac26db6e9b405234274 Mon Sep 17 00:00:00 2001 From: Matt Amos Date: Thu, 9 Oct 2008 10:19:16 +0000 Subject: [PATCH 100/381] Small fixes to unit tests where user_id has become changeset_id. --- test/unit/node_test.rb | 20 ++++++++++---------- test/unit/old_node_test.rb | 4 ++-- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/test/unit/node_test.rb b/test/unit/node_test.rb index bb2b7dfa4..7efbf9c06 100644 --- a/test/unit/node_test.rb +++ b/test/unit/node_test.rb @@ -1,7 +1,7 @@ require File.dirname(__FILE__) + '/../test_helper' class NodeTest < Test::Unit::TestCase - fixtures :current_nodes, :users, :current_node_tags, :nodes, :node_tags + 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 @@ -51,7 +51,7 @@ class NodeTest < Test::Unit::TestCase 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.user_id, node.user_id + 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 @@ -67,7 +67,7 @@ class NodeTest < Test::Unit::TestCase 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.user_id, node.user_id + 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 @@ -79,7 +79,7 @@ class NodeTest < Test::Unit::TestCase def test_create node_template = Node.new(:latitude => 12.3456, :longitude => 65.4321, - :user_id => users(:normal_user), + :changeset_id => changesets(:normal_user_first_change), :visible => 1, :version => 1) assert node_template.save_with_history! @@ -88,7 +88,7 @@ class NodeTest < Test::Unit::TestCase 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.timestamp.to_i, node.timestamp.to_i @@ -97,7 +97,7 @@ 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 @@ -120,7 +120,7 @@ class NodeTest < Test::Unit::TestCase 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 @@ -131,7 +131,7 @@ 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 @@ -152,7 +152,7 @@ class NodeTest < Test::Unit::TestCase 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 @@ -163,7 +163,7 @@ 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 diff --git a/test/unit/old_node_test.rb b/test/unit/old_node_test.rb index 85c2037c2..5623e9657 100644 --- a/test/unit/old_node_test.rb +++ b/test/unit/old_node_test.rb @@ -51,7 +51,7 @@ class OldNodeTest < Test::Unit::TestCase 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.user_id, node.user_id + 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 @@ -67,7 +67,7 @@ class OldNodeTest < Test::Unit::TestCase 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.user_id, node.user_id + 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 From 3d24694addd628cc55a3d2a24d736e61cbd55273 Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Thu, 9 Oct 2008 16:22:05 +0000 Subject: [PATCH 101/381] moving the conistency checks for updates and deletes to library, hopefully got the updates and deletes working now. --- app/controllers/node_controller.rb | 13 +++++++++---- app/controllers/relation_controller.rb | 8 +++++++- app/controllers/way_controller.rb | 11 ++++++++--- app/models/node.rb | 16 ++++++--------- app/models/relation.rb | 26 +++++++++++-------------- app/models/way.rb | 27 +++++++++++++------------- lib/geo_record.rb | 13 +++++++++++++ 7 files changed, 68 insertions(+), 46 deletions(-) diff --git a/app/controllers/node_controller.rb b/app/controllers/node_controller.rb index 8e8c8446d..29bf672e2 100644 --- a/app/controllers/node_controller.rb +++ b/app/controllers/node_controller.rb @@ -76,7 +76,7 @@ class NodeController < ApplicationController end rescue OSM::APIVersionMismatchError => ex render :text => "Version mismatch: Provided " + ex.provided.to_s + - ", server had: " + ex.latest.to_s, :status => :bad_request + ", server had: " + ex.latest.to_s, :status => :bad_request rescue ActiveRecord::RecordNotFound render :nothing => true, :status => :not_found end @@ -87,14 +87,19 @@ class NodeController < ApplicationController def delete begin node = Node.find(params[:id]) + new_node = Node.from_xml(request.raw_post) # FIXME we no longer care about the user, (or maybe we want to check # that the user of the changeset is the same user as is making this # little change?) we really care about the # changeset which must be open, and that the version that we have been # given is the one that is currently stored in the database - node.delete_with_history(@user) - - render :nothing => true + + if new_node and new_node.id == node.id + node.delete_with_history(new_node, @user) + render :nothing => true, :status => :success + else + render :nothing => true, :status => :bad_request + end rescue ActiveRecord::RecordNotFound render :nothing => true, :status => :not_found rescue OSM::APIError => ex diff --git a/app/controllers/relation_controller.rb b/app/controllers/relation_controller.rb index d87905059..b77d41ead 100644 --- a/app/controllers/relation_controller.rb +++ b/app/controllers/relation_controller.rb @@ -68,7 +68,13 @@ class RelationController < ApplicationController #XXX check if member somewhere! begin relation = Relation.find(params[:id]) - relation.delete_with_history(@user) + 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 :nothing => true, :status => :success + else + render :nothing => true, :status => :bad_request + end rescue OSM::APIError => ex render ex.render_opts rescue ActiveRecord::RecordNotFound diff --git a/app/controllers/way_controller.rb b/app/controllers/way_controller.rb index 17f166ddc..6f4704c77 100644 --- a/app/controllers/way_controller.rb +++ b/app/controllers/way_controller.rb @@ -68,10 +68,15 @@ class WayController < ApplicationController def delete begin way = Way.find(params[:id]) - way.delete_with_history(@user) + new_way = Way.from_xml(request.raw_post) + if new_way and new_way.id == way.id + way.delete_with_history(@user) - # if we get here, all is fine, otherwise something will catch below. - render :nothing => true + # if we get here, all is fine, otherwise something will catch below. + render :nothing => true + else + render :nothing => true, :status => :bad_request + end rescue OSM::APIError => ex render ex.render_opts rescue ActiveRecord::RecordNotFound diff --git a/app/models/node.rb b/app/models/node.rb index a25a19f70..39e1228ac 100644 --- a/app/models/node.rb +++ b/app/models/node.rb @@ -131,14 +131,16 @@ class Node < ActiveRecord::Base end end - def delete_with_history(user) + # Should probably be renamed delete_from to come in line with update + def delete_with_history(new_node, user) if self.visible + 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 = 1 AND current_way_nodes.node_id = ?", 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 = 1 AND member_type='node' and member_id=?", self.id]) raise OSM::APIPreconditionFailedError.new else - self.user_id = user.id + self.changeset_id = new_node.changeset_id self.visible = 0 save_with_history! end @@ -148,15 +150,9 @@ class Node < ActiveRecord::Base end def update_from(new_node, user) - if new_node.version != version - raise OSM::APIVersionMismatchError.new(new_node.version, version) - elsif new_node.changeset.user_id != user.id - raise OSM::APIUserChangesetMismatchError.new - elsif not new_node.changeset.open? - raise OSM::APIChangesetAlreadyClosedError.new - end + check_consistency(self, new_node, user) - # FIXME logic need looked at + # FIXME logic needs to be double checked self.changeset_id = new_node.changeset_id self.latitude = new_node.latitude self.longitude = new_node.longitude diff --git a/app/models/relation.rb b/app/models/relation.rb index 93f0001da..c8ee89d37 100644 --- a/app/models/relation.rb +++ b/app/models/relation.rb @@ -217,13 +217,15 @@ class Relation < ActiveRecord::Base end end - def delete_with_history(user) + def delete_with_history(new_relation, user) if self.visible + check_consistency(self, new_relation, user) 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=?", self.id ]) raise OSM::APIPreconditionFailedError.new else #self.user_id = user.id # FIXME we need to deal with changeset here, which is probably already dealt with + self.changeset_id = new_relation.changeset_id self.tags = [] self.members = [] self.visible = false @@ -235,23 +237,17 @@ class Relation < ActiveRecord::Base end def update_from(new_relation, user) + check_consistency(self, new_relation, user) if !new_relation.preconditions_ok? raise OSM::APIPreconditionFailedError.new - elsif new_relation.version != version - raise OSM::APIVersionMismatchError.new(new_relation.version, version) - elsif new_relation.changeset.user_id != user.id - raise OSM::APIUserChangesetMismatchError.new - elsif not new_relation.changeset.open? - raise OSM::APIChangesetAlreadyClosedError.new - else - # FIXME need to deal with changeset etc - #self.user_id = user.id - self.changeset_id = new_relation.changeset_id - self.tags = new_relation.tags - self.members = new_relation.members - self.visible = true - save_with_history! end + # FIXME need to deal with changeset etc + #self.user_id = user.id + 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 preconditions_ok? diff --git a/app/models/way.rb b/app/models/way.rb index 9ddb603a8..05b412b29 100644 --- a/app/models/way.rb +++ b/app/models/way.rb @@ -206,17 +206,15 @@ class Way < ActiveRecord::Base end def update_from(new_way, user) + check_consistency(self, new_way, user) if !new_way.preconditions_ok? raise OSM::APIPreconditionFailedError.new - elsif new_way.version != version - raise OSM::APIVersionMismatchError.new(new_way.version, version) - else - self.user_id = user.id - self.tags = new_way.tags - self.nds = new_way.nds - self.visible = true - save_with_history! end + self.changeset_id = changeset_id + self.tags = new_way.tags + self.nds = new_way.nds + self.visible = true + save_with_history! end def preconditions_ok? @@ -230,11 +228,13 @@ class Way < ActiveRecord::Base return true end - def delete_with_history(user) + def delete_with_history(new_way, user) + check_consistency(self, new_way, user) if self.visible - # FIXME - # this should actually delete the relations, - # not just throw a PreconditionFailed if it's a member of a relation!! + # FIXME + # this should actually delete the relations, + # not just throw a PreconditionFailed if it's a member of a relation!! + # WHY?? The editor should decide whether the node is in the relation or not! # FIXME: this should probably renamed to delete_with_history if RelationMember.find(:first, :joins => "INNER JOIN current_relations ON current_relations.id=current_relation_members.id", @@ -242,7 +242,7 @@ class Way < ActiveRecord::Base raise OSM::APIPreconditionFailedError # end FIXME else - self.user_id = user.id + self.changeset_id = new_way.changeset_id self.tags = [] self.nds = [] self.visible = false @@ -265,6 +265,7 @@ class Way < ActiveRecord::Base n.save_with_history! end + # FIXME needs more information passed in so that the changeset can be updated self.user_id = user.id self.delete_with_history(user) diff --git a/lib/geo_record.rb b/lib/geo_record.rb index 2740eab0c..3eab72b2d 100644 --- a/lib/geo_record.rb +++ b/lib/geo_record.rb @@ -42,6 +42,19 @@ module GeoRecord return self.longitude.to_f / SCALE end + # 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. 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.version, old.version) + elsif new.changeset.user_id != user.id + raise OSM::APIUserChangesetMismatchError.new + elsif not new.changeset.is_open? + raise OSM::APIChangesetAlreadyClosedError.new + end + end private def lat2y(a) From 34bc4686cddd3b962388a9bd9551a950031645ac Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Fri, 10 Oct 2008 07:58:33 +0000 Subject: [PATCH 102/381] Some comment about where to add code to get some more of the functional changesets working. --- test/functional/relation_controller_test.rb | 7 ++++++- test/functional/way_controller_test.rb | 2 ++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/test/functional/relation_controller_test.rb b/test/functional/relation_controller_test.rb index 202a015a8..3c7025235 100644 --- a/test/functional/relation_controller_test.rb +++ b/test/functional/relation_controller_test.rb @@ -45,6 +45,9 @@ class RelationControllerTest < Test::Unit::TestCase get :relations_for_node, :id => current_nodes(:node_used_by_relationship).id assert_response :success # FIXME check whether this contains the stuff we want! + # see the test_read in way_controller_test.rb for the assert_select + assert_select "osm[version=#{API_VERSION}][generator=\"OpenStreetMap server\"]", 1 + assert_select "osm relation" if $VERBOSE print @response.body end @@ -80,6 +83,8 @@ class RelationControllerTest < Test::Unit::TestCase def test_create basic_authorization "test@openstreetmap.org", "test" + + # FIXME create a new changeset and use the id that is returned for the next step # create an relation without members content "" @@ -97,7 +102,7 @@ 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 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" diff --git a/test/functional/way_controller_test.rb b/test/functional/way_controller_test.rb index 6fd3e234c..b4f4599a4 100644 --- a/test/functional/way_controller_test.rb +++ b/test/functional/way_controller_test.rb @@ -76,6 +76,8 @@ class WayControllerTest < Test::Unit::TestCase nid2 = current_nodes(:used_node_2).id basic_authorization "test@openstreetmap.org", "test" + # FIXME create a new changeset and use the id that is returned for the next step + # create a way with pre-existing nodes content "" put :create From 4da22eee1acaa77e9059baf1eaa71f302d2ba20f Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Fri, 10 Oct 2008 13:27:21 +0000 Subject: [PATCH 103/381] reverting the javascript change in changeset 10926, that needs to be specific to each rails setup (if run out of another dir), until we turn the file into an erb that can insert the rails root into the javascript file --- public/javascripts/map.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/public/javascripts/map.js b/public/javascripts/map.js index 3e6d687b0..c2ddf9c39 100644 --- a/public/javascripts/map.js +++ b/public/javascripts/map.js @@ -13,7 +13,9 @@ var nonamekeys = { }; OpenLayers._getScriptLocation = function () { - return "openlayers/"; + // Should really have this file as an erb, so that this can return + // the real rails root + return "/openlayers/"; } function createMap(divName, options) { From 233a97bfcbee9c82027c049bb8bafac2693ec359 Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Fri, 10 Oct 2008 18:24:07 +0000 Subject: [PATCH 104/381] fix indentation. Get teh api version from the constant, rather than hard coding it. Using right save method, so that exception is thrown. Some comment questions. --- app/controllers/changeset_controller.rb | 115 ++++++++++++------------ app/models/changeset.rb | 1 + 2 files changed, 60 insertions(+), 56 deletions(-) diff --git a/app/controllers/changeset_controller.rb b/app/controllers/changeset_controller.rb index 8950126b4..8151d3a67 100644 --- a/app/controllers/changeset_controller.rb +++ b/app/controllers/changeset_controller.rb @@ -63,7 +63,7 @@ class ChangesetController < ApplicationController end changeset = Changeset.find(params[:id]) changeset.open = false - changeset.save + changeset.save! render :nothing => true rescue ActiveRecord::RecordNotFound render :nothing => true, :status => :not_found @@ -83,10 +83,13 @@ class ChangesetController < ApplicationController models = {"node"=>Node, "way"=>Way, "relation"=>Relation} + # FIXME shouldn't this be done through the + # res = OSM::API.new.get_xml_doc + # as everything else is? res = XML::Document.new res.encoding = 'UTF-8' root = XML::Node.new 'osm' - root['version'] = '0.6' + root['version'] = API_VERSION root['creator'] = 'OpenStreetMap.org' res.root = root @@ -95,69 +98,69 @@ class ChangesetController < ApplicationController Changeset.transaction do while p.read == 1 - break if p.node_type == 15 # end element - next unless p.node_type == 1 # element + break if p.node_type == 15 # end element + next unless p.node_type == 1 # element - case p.name - when 'create': - while p.read == 1 - break if p.node_type == 15 # end element - next unless p.node_type == 1 # element + case p.name + when 'create': + while p.read == 1 + break if p.node_type == 15 # end element + next unless p.node_type == 1 # element - model = models[p.name] - next if model.nil? + model = models[p.name] + next if model.nil? - elem = XML::Node.new p.name - nd = p.expand; p.next - osm = model.from_xml_node(nd, true) - elem['old_id'] = nd['id'] + elem = XML::Node.new p.name + nd = p.expand; p.next + osm = model.from_xml_node(nd, true) + elem['old_id'] = nd['id'] - case nd.name - when 'way': - fix_way(osm, node_ids) - raise OSM::APIPreconditionFailedError.new if !osm.preconditions_ok? - when 'relation': - fix_rel(osm, ids) - raise OSM::APIPreconditionFailedError.new if !osm.preconditions_ok? - end + case nd.name + when 'way': + fix_way(osm, node_ids) + raise OSM::APIPreconditionFailedError.new if !osm.preconditions_ok? + when 'relation': + fix_rel(osm, ids) + raise OSM::APIPreconditionFailedError.new if !osm.preconditions_ok? + end - create_prim ids[nd.name], osm, nd - elem['new_id'] = osm.id.to_s - elem['new_version'] = osm.version.to_s - root << elem - end - when 'modify': - while p.read == 1 - break if p.node_type == 15 # end element - next unless p.node_type == 1 # element + create_prim ids[nd.name], osm, nd + elem['new_id'] = osm.id.to_s + elem['new_version'] = osm.version.to_s + root << elem + end + when 'modify': + while p.read == 1 + break if p.node_type == 15 # end element + next unless p.node_type == 1 # element - model = models[p.name] - next if model.nil? + model = models[p.name] + next if model.nil? - elem = XML::Node.new p.name - new_osm = model.from_xml_node(p.expand); p.next - osm = model.find(new_osm.id) - osm.update_from new_osm, @user - elem['old_id'] = elem['new_id'] = osm.id.to_s - elem['new_version'] = osm.version.to_s - root << elem - end - when 'delete': - while p.read == 1 - break if p.node_type == 15 # end element - next unless p.node_type == 1 # element + elem = XML::Node.new p.name + new_osm = model.from_xml_node(p.expand); p.next + osm = model.find(new_osm.id) + osm.update_from new_osm, @user + elem['old_id'] = elem['new_id'] = osm.id.to_s + elem['new_version'] = osm.version.to_s + root << elem + end + when 'delete': + while p.read == 1 + break if p.node_type == 15 # end element + next unless p.node_type == 1 # element - model = models[p.name] - next if model.nil? + model = models[p.name] + next if model.nil? - elem = XML::Node.new p.name - osm = model.find(p.expand['id']); p.next - osm.delete_with_history(@user) - elem['old_id'] = elem['new_id'] = osm.id.to_s - elem['new_version'] = osm.version.to_s - root << elem - end - end + elem = XML::Node.new p.name + osm = model.find(p.expand['id']); p.next + osm.delete_with_history(@user) + elem['old_id'] = elem['new_id'] = osm.id.to_s + elem['new_version'] = osm.version.to_s + root << elem + end + end end end diff --git a/app/models/changeset.rb b/app/models/changeset.rb index 934d75e39..caa167c9e 100644 --- a/app/models/changeset.rb +++ b/app/models/changeset.rb @@ -71,6 +71,7 @@ class Changeset < ActiveRecord::Base Changeset.transaction do # fixme update modified_at time? + # FIXME there is no modified_at time, should it be added self.save! end From 24b21e4a29d28a3174965d449d36465ecda9ead7 Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Sun, 12 Oct 2008 22:32:51 +0000 Subject: [PATCH 105/381] additional consistency checks. making the error message for the number of nodes use the configured number of nodes, rather than a hard coded number. minor improvements to the way controller functional tests. Not sure if they should be turned into integration tests. --- app/controllers/api_controller.rb | 2 +- app/controllers/way_controller.rb | 4 ++++ lib/geo_record.rb | 8 +++++++- lib/osm.rb | 7 +++++++ test/functional/way_controller_test.rb | 12 ++++++++++-- 5 files changed, 29 insertions(+), 4 deletions(-) diff --git a/app/controllers/api_controller.rb b/app/controllers/api_controller.rb index 2f040a92b..8b876d3a7 100644 --- a/app/controllers/api_controller.rb +++ b/app/controllers/api_controller.rb @@ -116,7 +116,7 @@ class ApiController < ApplicationController 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 diff --git a/app/controllers/way_controller.rb b/app/controllers/way_controller.rb index 6f4704c77..08270094d 100644 --- a/app/controllers/way_controller.rb +++ b/app/controllers/way_controller.rb @@ -12,6 +12,10 @@ class WayController < ApplicationController way = Way.from_xml(request.raw_post, true) if way + # FIXME move some of this to the model. The controller shouldn't need to + # know about the fact that the first version number is 0 on creation + # it will also allow use to run a variation on the check_consistency + # so that we don't get exceptions thrown when the changesets are not right unless way.preconditions_ok? render :text => "", :status => :precondition_failed else diff --git a/lib/geo_record.rb b/lib/geo_record.rb index 3eab72b2d..273419757 100644 --- a/lib/geo_record.rb +++ b/lib/geo_record.rb @@ -45,10 +45,16 @@ module GeoRecord # 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. This will throw an exception if there is an inconsistency + # 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.version, old.version) + elsif new.changeset.nil? + raise OSM::APIChangesetMissingError.new + elsif new.changeset.empty? + raise OSM::APIChangesetMissingError.new elsif new.changeset.user_id != user.id raise OSM::APIUserChangesetMismatchError.new elsif not new.changeset.is_open? diff --git a/lib/osm.rb b/lib/osm.rb index e0e83845e..82fc835b4 100644 --- a/lib/osm.rb +++ b/lib/osm.rb @@ -46,6 +46,13 @@ module OSM { :text => "The supplied changeset has already been closed", :status => :conflict } 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 } + end + end # Raised when the provided version is not equal to the latest in the db. class APIVersionMismatchError < APIError diff --git a/test/functional/way_controller_test.rb b/test/functional/way_controller_test.rb index b4f4599a4..558e45489 100644 --- a/test/functional/way_controller_test.rb +++ b/test/functional/way_controller_test.rb @@ -109,6 +109,7 @@ class WayControllerTest < Test::Unit::TestCase def test_create_invalid basic_authorization "test@openstreetmap.org", "test" + # FIXME All of these will fail because they don't have a valid changeset # create a way with non-existing node content "" put :create @@ -137,9 +138,16 @@ 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 :success + assert_response :bad_request + + # Now try without having a changeset + content "" + delete :delete, :id => current_ways(:visible_way).id + assert_response :bad_request + + # Now try and get a changeset # this won't work since the way is already deleted delete :delete, :id => current_ways(:invisible_way).id From 734cbb967de8e04f15e01ee2103033064dd290d4 Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Mon, 13 Oct 2008 12:08:26 +0000 Subject: [PATCH 106/381] fixing the data browser to be able to cope with the changeset change. changeset browsing still needs to be implemented. Also giving a nice error message for 404 not found instead of rendering nothing. --- app/controllers/browse_controller.rb | 30 +++++++++++++++----------- app/views/browse/_common_details.rhtml | 4 ++-- app/views/browse/not_found.rhtml | 1 + 3 files changed, 21 insertions(+), 14 deletions(-) create mode 100644 app/views/browse/not_found.rhtml diff --git a/app/controllers/browse_controller.rb b/app/controllers/browse_controller.rb index f3a04519c..408846c96 100644 --- a/app/controllers/browse_controller.rb +++ b/app/controllers/browse_controller.rb @@ -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,13 @@ 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 end diff --git a/app/views/browse/_common_details.rhtml b/app/views/browse/_common_details.rhtml index ee5f22cee..d7b340e9d 100644 --- a/app/views/browse/_common_details.rhtml +++ b/app/views/browse/_common_details.rhtml @@ -3,10 +3,10 @@ <%= h(common_details.timestamp) %> -<% if common_details.user.data_public %> +<% if common_details.changeset.user.data_public %> Edited by: - <%= link_to h(common_details.user.display_name), :controller => "user", :action => "view", :display_name => common_details.user.display_name %> + <%= link_to h(common_details.changeset.user.display_name), :controller => "user", :action => "view", :display_name => common_details.changeset.user.display_name %> <% end %> diff --git a/app/views/browse/not_found.rhtml b/app/views/browse/not_found.rhtml new file mode 100644 index 000000000..1322a0a6f --- /dev/null +++ b/app/views/browse/not_found.rhtml @@ -0,0 +1 @@ +

Sorry, the <%= @type -%> with the id <%= params[:id] -%>, could not be found.

From a2498928e470b358f69902d9fdc72a1a80aa5bf8 Mon Sep 17 00:00:00 2001 From: Matt Amos Date: Mon, 13 Oct 2008 14:08:31 +0000 Subject: [PATCH 107/381] Fixed node functional tests. A couple of changes to the old_node model and node controller for changesets. --- app/controllers/node_controller.rb | 2 +- app/models/old_node.rb | 1 + lib/geo_record.rb | 2 -- test/fixtures/node_tags.yml | 6 +++--- test/functional/node_controller_test.rb | 15 +++++++++++---- test/test_helper.rb | 7 +++++-- 6 files changed, 21 insertions(+), 12 deletions(-) diff --git a/app/controllers/node_controller.rb b/app/controllers/node_controller.rb index 29bf672e2..62e680388 100644 --- a/app/controllers/node_controller.rb +++ b/app/controllers/node_controller.rb @@ -96,7 +96,7 @@ class NodeController < ApplicationController if new_node and new_node.id == node.id node.delete_with_history(new_node, @user) - render :nothing => true, :status => :success + render :nothing => true else render :nothing => true, :status => :bad_request end diff --git a/app/models/old_node.rb b/app/models/old_node.rb index d4d9f1775..e7d803044 100644 --- a/app/models/old_node.rb +++ b/app/models/old_node.rb @@ -38,6 +38,7 @@ class OldNode < ActiveRecord::Base el1['id'] = self.id.to_s el1['lat'] = self.lat.to_s el1['lon'] = self.lon.to_s + el1['changeset'] = self.changeset.id.to_s el1['user'] = self.changeset.user.display_name if self.changeset.user.data_public? self.tags.each do |k,v| diff --git a/lib/geo_record.rb b/lib/geo_record.rb index 273419757..645ddc93a 100644 --- a/lib/geo_record.rb +++ b/lib/geo_record.rb @@ -53,8 +53,6 @@ module GeoRecord raise OSM::APIVersionMismatchError.new(new.version, old.version) elsif new.changeset.nil? raise OSM::APIChangesetMissingError.new - elsif new.changeset.empty? - raise OSM::APIChangesetMissingError.new elsif new.changeset.user_id != user.id raise OSM::APIUserChangesetMismatchError.new elsif not new.changeset.is_open? diff --git a/test/fixtures/node_tags.yml b/test/fixtures/node_tags.yml index c32dc6c55..8c80128b0 100644 --- a/test/fixtures/node_tags.yml +++ b/test/fixtures/node_tags.yml @@ -1,17 +1,17 @@ t1: - id: visible_node + id: 1 k: testvisible v: yes version: 1 t2: - id: used_node_1 + id: 3 k: testused v: yes version: 1 t3: - id: used_node_2 + id: 4 k: test v: yes version: 1 diff --git a/test/functional/node_controller_test.rb b/test/functional/node_controller_test.rb index 59b28e114..a8ef700a3 100644 --- a/test/functional/node_controller_test.rb +++ b/test/functional/node_controller_test.rb @@ -21,10 +21,14 @@ class NodeControllerTest < Test::Unit::TestCase # create a node with random lat/lon lat = rand(100)-50 + rand lon = rand(100)-50 + rand - content("") + # normal user has a changeset open, so we'll use that. + changeset = changesets(:normal_user_first_change) + # create a minimal xml file + content("") 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) @@ -32,7 +36,7 @@ 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 @@ -61,11 +65,13 @@ class NodeControllerTest < Test::Unit::TestCase # now set auth basic_authorization(users(:normal_user).email, "test"); - # this should work + # delete now takes a payload + content(nodes(:visible_node).to_xml) delete :delete, :id => current_nodes(:visible_node).id assert_response :success # 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 @@ -74,6 +80,7 @@ class NodeControllerTest < Test::Unit::TestCase assert_response :not_found # this won't work since the node is in use + content(nodes(:used_node_1).to_xml) delete :delete, :id => current_nodes(:used_node_1).id assert_response :precondition_failed end @@ -84,6 +91,6 @@ class NodeControllerTest < Test::Unit::TestCase end def content(c) - @request.env["RAW_POST_DATA"] = c + @request.env["RAW_POST_DATA"] = c.to_s end end diff --git a/test/test_helper.rb b/test/test_helper.rb index 22cc0e15c..0c03aac34 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -27,12 +27,15 @@ class Test::Unit::TestCase # Load standard fixtures needed to test API methods def self.api_fixtures - fixtures :users + fixtures :users, :changesets - fixtures :current_nodes, :nodes, :current_node_tags + fixtures :current_nodes, :nodes 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 From 2d373c6f2095e11fe8a426cbafc350eb47864868 Mon Sep 17 00:00:00 2001 From: Matt Amos Date: Mon, 13 Oct 2008 14:29:30 +0000 Subject: [PATCH 108/381] Fix to_xml method of old_relation model. --- app/models/old_relation.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/old_relation.rb b/app/models/old_relation.rb index 428177755..10c76a758 100644 --- a/app/models/old_relation.rb +++ b/app/models/old_relation.rb @@ -91,7 +91,7 @@ class OldRelation < 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? + el1['user'] = self.changeset.user.display_name if self.changeset.user.data_public? el1['version'] = self.version.to_s el1['changeset'] = self.changeset_id.to_s From b91b514cf22f04910c83b6dcc254908ed37d7413 Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Mon, 13 Oct 2008 14:34:04 +0000 Subject: [PATCH 109/381] moving the check consistency to it's own file so that checks will be able to be loaded into the way and relation models, without loading the node stuff. --- app/models/node.rb | 1 + app/models/old_node.rb | 1 + app/models/old_relation.rb | 2 ++ app/models/old_way.rb | 2 ++ app/models/relation.rb | 2 ++ app/models/way.rb | 2 ++ lib/consistency_validations.rb | 19 +++++++++++++++++++ lib/geo_record.rb | 17 ----------------- 8 files changed, 29 insertions(+), 17 deletions(-) create mode 100644 lib/consistency_validations.rb diff --git a/app/models/node.rb b/app/models/node.rb index 39e1228ac..e7058a5ad 100644 --- a/app/models/node.rb +++ b/app/models/node.rb @@ -2,6 +2,7 @@ class Node < ActiveRecord::Base require 'xml/libxml' include GeoRecord + include ConsistencyValidations set_table_name 'current_nodes' diff --git a/app/models/old_node.rb b/app/models/old_node.rb index e7d803044..8b3ba784b 100644 --- a/app/models/old_node.rb +++ b/app/models/old_node.rb @@ -1,5 +1,6 @@ class OldNode < ActiveRecord::Base include GeoRecord + include ConsistencyValidations set_table_name 'nodes' diff --git a/app/models/old_relation.rb b/app/models/old_relation.rb index 10c76a758..3a7cc29b2 100644 --- a/app/models/old_relation.rb +++ b/app/models/old_relation.rb @@ -1,4 +1,6 @@ class OldRelation < ActiveRecord::Base + include ConsistencyValidations + set_table_name 'relations' belongs_to :changeset diff --git a/app/models/old_way.rb b/app/models/old_way.rb index f297cfc1a..13c935fe6 100644 --- a/app/models/old_way.rb +++ b/app/models/old_way.rb @@ -1,4 +1,6 @@ class OldWay < ActiveRecord::Base + include ConsistencyValidations + set_table_name 'ways' belongs_to :changeset diff --git a/app/models/relation.rb b/app/models/relation.rb index c8ee89d37..081c44b25 100644 --- a/app/models/relation.rb +++ b/app/models/relation.rb @@ -1,6 +1,8 @@ class Relation < ActiveRecord::Base require 'xml/libxml' + include ConsistencyValidations + set_table_name 'current_relations' belongs_to :changeset diff --git a/app/models/way.rb b/app/models/way.rb index 05b412b29..6a5ad58ab 100644 --- a/app/models/way.rb +++ b/app/models/way.rb @@ -1,5 +1,7 @@ class Way < ActiveRecord::Base require 'xml/libxml' + + include ConsistencyValidations set_table_name 'current_ways' diff --git a/lib/consistency_validations.rb b/lib/consistency_validations.rb new file mode 100644 index 000000000..4becce89c --- /dev/null +++ b/lib/consistency_validations.rb @@ -0,0 +1,19 @@ +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.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 + end + end +end diff --git a/lib/geo_record.rb b/lib/geo_record.rb index 645ddc93a..2740eab0c 100644 --- a/lib/geo_record.rb +++ b/lib/geo_record.rb @@ -42,23 +42,6 @@ module GeoRecord return self.longitude.to_f / SCALE end - # 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.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 - end - end private def lat2y(a) From b38b2c2d0fcbcf94b668f3405c12f939c7318103 Mon Sep 17 00:00:00 2001 From: Matt Amos Date: Mon, 13 Oct 2008 14:38:32 +0000 Subject: [PATCH 110/381] Fixed xml method of old way model. --- app/models/old_way.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/models/old_way.rb b/app/models/old_way.rb index 13c935fe6..44155d05c 100644 --- a/app/models/old_way.rb +++ b/app/models/old_way.rb @@ -96,8 +96,9 @@ 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? + el1['user'] = self.changeset.user.display_name if self.changeset.user.data_public? 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' From 127bb4523e111bca6b7bb1d2d0125d069a1f6b78 Mon Sep 17 00:00:00 2001 From: Matt Amos Date: Mon, 13 Oct 2008 15:23:48 +0000 Subject: [PATCH 111/381] Fixed problems with the relations functional tests. --- app/controllers/relation_controller.rb | 2 +- app/models/old_relation.rb | 6 +++ app/models/relation.rb | 2 +- test/fixtures/current_relations.yml | 3 ++ test/functional/relation_controller_test.rb | 49 ++++++++++++++------- 5 files changed, 43 insertions(+), 19 deletions(-) diff --git a/app/controllers/relation_controller.rb b/app/controllers/relation_controller.rb index b77d41ead..09c878325 100644 --- a/app/controllers/relation_controller.rb +++ b/app/controllers/relation_controller.rb @@ -71,7 +71,7 @@ class RelationController < ApplicationController 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 :nothing => true, :status => :success + render :nothing => true else render :nothing => true, :status => :bad_request end diff --git a/app/models/old_relation.rb b/app/models/old_relation.rb index 3a7cc29b2..b7e7248d9 100644 --- a/app/models/old_relation.rb +++ b/app/models/old_relation.rb @@ -88,6 +88,12 @@ 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 diff --git a/app/models/relation.rb b/app/models/relation.rb index 081c44b25..dd2d1c7d5 100644 --- a/app/models/relation.rb +++ b/app/models/relation.rb @@ -77,7 +77,7 @@ class Relation < ActiveRecord::Base 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? + 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.changeset.user_id] = nil diff --git a/test/fixtures/current_relations.yml b/test/fixtures/current_relations.yml index 510df492c..e4c3b18fa 100644 --- a/test/fixtures/current_relations.yml +++ b/test/fixtures/current_relations.yml @@ -3,15 +3,18 @@ visible_relation: changeset_id: 1 timestamp: 2007-01-01 00:00:00 visible: 1 + version: 1 invisible_relation: id: 2 changeset_id: 1 timestamp: 2007-01-01 00:00:00 visible: 0 + version: 1 used_relation: id: 3 changeset_id: 1 timestamp: 2007-01-01 00:00:00 visible: 1 + version: 1 diff --git a/test/functional/relation_controller_test.rb b/test/functional/relation_controller_test.rb index 3c7025235..b45c80874 100644 --- a/test/functional/relation_controller_test.rb +++ b/test/functional/relation_controller_test.rb @@ -6,9 +6,6 @@ class RelationController; def rescue_action(e) raise e end; end class RelationControllerTest < Test::Unit::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 @@ -21,7 +18,7 @@ class RelationControllerTest < Test::Unit::TestCase end def content(c) - @request.env["RAW_POST_DATA"] = c + @request.env["RAW_POST_DATA"] = c.to_s end # ------------------------------------- @@ -84,10 +81,11 @@ class RelationControllerTest < Test::Unit::TestCase def test_create basic_authorization "test@openstreetmap.org", "test" - # FIXME create a new changeset and use the id that is returned for the next step + # put the relation in a dummy fixture changset + changeset_id = changesets(:normal_user_first_change).id # create an relation without members - content "" + content "" put :create # hope for success assert_response :success, @@ -102,6 +100,8 @@ 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 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, @@ -113,8 +113,9 @@ class RelationControllerTest < Test::Unit::TestCase # create an relation with a node as member nid = current_nodes(:used_node_1).id - content "" + - "" + content "" + + "" + + "" put :create # hope for success assert_response :success, @@ -129,7 +130,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" @@ -141,9 +144,10 @@ class RelationControllerTest < Test::Unit::TestCase # 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 "" + - "" + - "" + content "" + + "" + + "" + + "" put :create # hope for success assert_response :success, @@ -158,7 +162,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" @@ -175,8 +181,13 @@ 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 "" + content "" + + "" + + "" put :create # expect failure assert_response :precondition_failed, @@ -188,8 +199,6 @@ class RelationControllerTest < Test::Unit::TestCase # ------------------------------------- def test_delete - return true - # first try to delete relation without auth delete :delete, :id => current_relations(:visible_relation).id assert_response :unauthorized @@ -197,11 +206,17 @@ 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 + + # 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 # 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 From fb5f39f19ae00dad9ec896e216e8e813de42c151 Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Mon, 13 Oct 2008 15:39:21 +0000 Subject: [PATCH 112/381] Creating consistency check for creation of nodes, way and relations. Moving some creation code from the controller to the model, and adding error handling on create errors. --- app/controllers/node_controller.rb | 27 ++++++++++++-------------- app/controllers/relation_controller.rb | 23 ++++++++++------------ app/controllers/way_controller.rb | 26 ++++++++++--------------- app/models/node.rb | 7 +++++++ app/models/relation.rb | 10 ++++++++++ app/models/way.rb | 10 ++++++++++ lib/consistency_validations.rb | 11 +++++++++++ 7 files changed, 70 insertions(+), 44 deletions(-) diff --git a/app/controllers/node_controller.rb b/app/controllers/node_controller.rb index 62e680388..309b930b7 100644 --- a/app/controllers/node_controller.rb +++ b/app/controllers/node_controller.rb @@ -11,24 +11,21 @@ class NodeController < ApplicationController # Create a node from XML. def create - if request.put? - node = Node.from_xml(request.raw_post, true) - # FIXME remove debug - logger.debug request.raw_post - logger.debug node + begin + if request.put? + node = Node.from_xml(request.raw_post, true) - if node - node.version = 0 - #node.changeset_id = node.changeset - node.visible = true - node.save_with_history! - - 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 diff --git a/app/controllers/relation_controller.rb b/app/controllers/relation_controller.rb index 09c878325..4b3fdf34f 100644 --- a/app/controllers/relation_controller.rb +++ b/app/controllers/relation_controller.rb @@ -8,24 +8,21 @@ 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.version = 0 - #relation.user_id = @user.id - relation.save_with_history! + begin + if request.put? + relation = Relation.from_xml(request.raw_post, true) + if relation + relation.create_with_history @user render :text => relation.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 diff --git a/app/controllers/way_controller.rb b/app/controllers/way_controller.rb index 08270094d..e7cf0f7f1 100644 --- a/app/controllers/way_controller.rb +++ b/app/controllers/way_controller.rb @@ -8,27 +8,21 @@ class WayController < ApplicationController after_filter :compress_output def create - if request.put? - way = Way.from_xml(request.raw_post, true) - - if way - # FIXME move some of this to the model. The controller shouldn't need to - # know about the fact that the first version number is 0 on creation - # it will also allow use to run a variation on the check_consistency - # so that we don't get exceptions thrown when the changesets are not right - unless way.preconditions_ok? - render :text => "", :status => :precondition_failed - else - way.version = 0 - 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 + render ex.render_opts end end diff --git a/app/models/node.rb b/app/models/node.rb index e7058a5ad..d6a5143db 100644 --- a/app/models/node.rb +++ b/app/models/node.rb @@ -161,6 +161,13 @@ class Node < ActiveRecord::Base self.visible = true save_with_history! end + + def create_with_history(user) + check_create_consistency(self, user) + self.version = 0 + self.visible = true + save_with_history! + end def to_xml doc = OSM::API.new.get_xml_doc diff --git a/app/models/relation.rb b/app/models/relation.rb index dd2d1c7d5..195090e4b 100644 --- a/app/models/relation.rb +++ b/app/models/relation.rb @@ -251,6 +251,16 @@ class Relation < ActiveRecord::Base 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 diff --git a/app/models/way.rb b/app/models/way.rb index 6a5ad58ab..b2bdfb39b 100644 --- a/app/models/way.rb +++ b/app/models/way.rb @@ -219,6 +219,16 @@ class Way < ActiveRecord::Base save_with_history! end + def create_with_history(user) + check_create_consistency(self, user) + if !self.preconditions_ok? + raise OSM::APIPreconditionsFailedError.new + end + self.version = 0 + self.visible = true + save_with_history! + end + def preconditions_ok? return false if self.nds.empty? self.nds.each do |n| diff --git a/lib/consistency_validations.rb b/lib/consistency_validations.rb index 4becce89c..8fd6c257d 100644 --- a/lib/consistency_validations.rb +++ b/lib/consistency_validations.rb @@ -16,4 +16,15 @@ module ConsistencyValidations raise OSM::APIChangesetAlreadyClosedError.new 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 + end + end end From 1e5782f5795b5e22cae191c2956d18defce6dd66 Mon Sep 17 00:00:00 2001 From: Matt Amos Date: Mon, 13 Oct 2008 15:40:32 +0000 Subject: [PATCH 113/381] Fixed up way controller tests for API 0.6. --- app/controllers/way_controller.rb | 2 +- test/fixtures/changesets.yml | 5 ++++ test/functional/way_controller_test.rb | 41 ++++++++++++++++++++------ 3 files changed, 38 insertions(+), 10 deletions(-) diff --git a/app/controllers/way_controller.rb b/app/controllers/way_controller.rb index e7cf0f7f1..ac017ca6b 100644 --- a/app/controllers/way_controller.rb +++ b/app/controllers/way_controller.rb @@ -68,7 +68,7 @@ class WayController < ApplicationController way = Way.find(params[:id]) new_way = Way.from_xml(request.raw_post) if new_way and new_way.id == way.id - way.delete_with_history(@user) + way.delete_with_history(new_way, @user) # if we get here, all is fine, otherwise something will catch below. render :nothing => true diff --git a/test/fixtures/changesets.yml b/test/fixtures/changesets.yml index e676ffa7a..de6f4f786 100644 --- a/test/fixtures/changesets.yml +++ b/test/fixtures/changesets.yml @@ -11,3 +11,8 @@ second_user_first_change: created_at: "2008-05-01 01:23:45" open: 1 +normal_user_closed_change: + id: 3 + user_id: 1 + created_at: "2007-01-01 00:00:00" + open: 0 diff --git a/test/functional/way_controller_test.rb b/test/functional/way_controller_test.rb index 558e45489..fbc05086f 100644 --- a/test/functional/way_controller_test.rb +++ b/test/functional/way_controller_test.rb @@ -18,7 +18,7 @@ class WayControllerTest < Test::Unit::TestCase end def content(c) - @request.env["RAW_POST_DATA"] = c + @request.env["RAW_POST_DATA"] = c.to_s end # ------------------------------------- @@ -76,10 +76,13 @@ class WayControllerTest < Test::Unit::TestCase nid2 = current_nodes(:used_node_2).id basic_authorization "test@openstreetmap.org", "test" - # FIXME create a new changeset and use the id that is returned for the next step + # use the first user's open changeset + changeset_id = changesets(:normal_user_first_change).id # create a way with pre-existing nodes - content "" + content "" + + "" + + "" put :create # hope for success assert_response :success, @@ -96,7 +99,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" @@ -109,20 +114,34 @@ class WayControllerTest < Test::Unit::TestCase def test_create_invalid basic_authorization "test@openstreetmap.org", "test" - # FIXME All of these will fail because they don't have a valid changeset + # 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 "" + content "" + + "" 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 "" + content "" + + "" 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 "" + + "" + put :create + # expect failure + assert_response :precondition_failed, + "way upload to closed changeset did not return 'precondition failed'" end # ------------------------------------- @@ -130,7 +149,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 @@ -143,13 +161,18 @@ class WayControllerTest < Test::Unit::TestCase assert_response :bad_request # Now try without having a changeset - content "" + content "" delete :delete, :id => current_ways(:visible_way).id assert_response :bad_request # Now try and get a changeset + changeset_id = changesets(:normal_user_first_change).id + content current_ways(:visible_way).to_xml + delete :delete, :id => current_ways(:visible_way).id + assert_response :success # 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 From 3549d8900fae286019f9a4ad89d860e4eda76fb6 Mon Sep 17 00:00:00 2001 From: Matt Amos Date: Mon, 13 Oct 2008 15:46:22 +0000 Subject: [PATCH 114/381] All API 0.6 functional tests now pass. --- app/models/way.rb | 2 +- test/functional/way_controller_test.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/models/way.rb b/app/models/way.rb index b2bdfb39b..341bc6e3e 100644 --- a/app/models/way.rb +++ b/app/models/way.rb @@ -222,7 +222,7 @@ class Way < ActiveRecord::Base def create_with_history(user) check_create_consistency(self, user) if !self.preconditions_ok? - raise OSM::APIPreconditionsFailedError.new + raise OSM::APIPreconditionFailedError.new end self.version = 0 self.visible = true diff --git a/test/functional/way_controller_test.rb b/test/functional/way_controller_test.rb index fbc05086f..58bb6a9a1 100644 --- a/test/functional/way_controller_test.rb +++ b/test/functional/way_controller_test.rb @@ -140,8 +140,8 @@ class WayControllerTest < Test::Unit::TestCase "" put :create # expect failure - assert_response :precondition_failed, - "way upload to closed changeset did not return 'precondition failed'" + assert_response :conflict, + "way upload to closed changeset did not return 'conflict'" end # ------------------------------------- From 1d4a957c7941a8d82c5c529017de61d71067ccca Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Mon, 13 Oct 2008 16:11:57 +0000 Subject: [PATCH 115/381] fix so that changeset will be closed without an exception due to double validation --- app/models/changeset.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/changeset.rb b/app/models/changeset.rb index caa167c9e..070314e7f 100644 --- a/app/models/changeset.rb +++ b/app/models/changeset.rb @@ -12,7 +12,7 @@ class Changeset < ActiveRecord::Base has_many :old_ways has_many :old_relations - validates_presence_of :user_id, :created_at, :open + validates_presence_of :user_id, :created_at validates_inclusion_of :open, :in => [ true, false ] # Use a method like this, so that we can easily change how we From 9e1946b2837abe146777773177f4c48ffde8c06f Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Mon, 13 Oct 2008 17:05:52 +0000 Subject: [PATCH 116/381] now you will actually save the changeset on updating ways. Showing the changeset in the data browser detail. --- app/models/way.rb | 2 +- app/views/browse/_common_details.rhtml | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/app/models/way.rb b/app/models/way.rb index 341bc6e3e..f26d7658b 100644 --- a/app/models/way.rb +++ b/app/models/way.rb @@ -212,7 +212,7 @@ class Way < ActiveRecord::Base if !new_way.preconditions_ok? raise OSM::APIPreconditionFailedError.new end - self.changeset_id = changeset_id + self.changeset_id = new_way.changeset_id self.tags = new_way.tags self.nds = new_way.nds self.visible = true diff --git a/app/views/browse/_common_details.rhtml b/app/views/browse/_common_details.rhtml index d7b340e9d..968594bb6 100644 --- a/app/views/browse/_common_details.rhtml +++ b/app/views/browse/_common_details.rhtml @@ -10,6 +10,11 @@ <% end %> + + In changeset: + <%= common_details.changeset_id %> + Tags: From cf24a5a3ee68905c5f55cf6f17c5d2ea983cb34f Mon Sep 17 00:00:00 2001 From: Matt Amos Date: Mon, 13 Oct 2008 17:50:15 +0000 Subject: [PATCH 117/381] Added a bunch more tests on the API 0.6. Fixed node/way/relation from_xml code to disallow duplicate tag keys. --- app/controllers/node_controller.rb | 5 +- app/models/node.rb | 9 +- app/models/relation.rb | 5 + app/models/way.rb | 5 + test/functional/node_controller_test.rb | 142 +++++++++++++++++++++++- 5 files changed, 157 insertions(+), 9 deletions(-) diff --git a/app/controllers/node_controller.rb b/app/controllers/node_controller.rb index 309b930b7..f1023e78f 100644 --- a/app/controllers/node_controller.rb +++ b/app/controllers/node_controller.rb @@ -71,9 +71,8 @@ class NodeController < ApplicationController else render :nothing => true, :status => :bad_request end - rescue OSM::APIVersionMismatchError => ex - render :text => "Version mismatch: Provided " + ex.provided.to_s + - ", server had: " + ex.latest.to_s, :status => :bad_request + rescue OSM::APIError => ex + render ex.render_opts rescue ActiveRecord::RecordNotFound render :nothing => true, :status => :not_found end diff --git a/app/models/node.rb b/app/models/node.rb index d6a5143db..3b59ac80e 100644 --- a/app/models/node.rb +++ b/app/models/node.rb @@ -75,7 +75,7 @@ class Node < ActiveRecord::Base def self.from_xml_node(pt, create=false) node = Node.new - node.version = pt['version'] + node.version = pt['version'].to_i node.lat = pt['lat'].to_f node.lon = pt['lon'].to_f node.changeset_id = pt['changeset'].to_i @@ -138,7 +138,7 @@ class Node < ActiveRecord::Base 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 = 1 AND current_way_nodes.node_id = ?", 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 = 1 AND member_type='node' and member_id=?", self.id]) + 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=? ", self.id]) raise OSM::APIPreconditionFailedError.new else self.changeset_id = new_node.changeset_id @@ -227,6 +227,11 @@ class Node < ActiveRecord::Base 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 if @tags.include? k + @tags[k] = v end diff --git a/app/models/relation.rb b/app/models/relation.rb index 195090e4b..436c1e32e 100644 --- a/app/models/relation.rb +++ b/app/models/relation.rb @@ -182,6 +182,11 @@ 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 if @tags.include? k + @tags[k] = v end diff --git a/app/models/way.rb b/app/models/way.rb index f26d7658b..90458006e 100644 --- a/app/models/way.rb +++ b/app/models/way.rb @@ -169,6 +169,11 @@ class Way < 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 if @tags.include? k + @tags[k] = v end diff --git a/test/functional/node_controller_test.rb b/test/functional/node_controller_test.rb index a8ef700a3..085cd3078 100644 --- a/test/functional/node_controller_test.rb +++ b/test/functional/node_controller_test.rb @@ -16,7 +16,6 @@ class NodeControllerTest < Test::Unit::TestCase def test_create # cannot read password from fixture as it is stored as MD5 digest basic_authorization(users(:normal_user).email, "test"); - # FIXME we need to create a changeset first argh # create a node with random lat/lon lat = rand(100)-50 + rand @@ -57,7 +56,6 @@ 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 @@ -65,7 +63,18 @@ class NodeControllerTest < Test::Unit::TestCase # now set auth basic_authorization(users(:normal_user).email, "test"); - # delete now takes a payload + # 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 @@ -79,12 +88,116 @@ 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 + + # 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 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'" + end def basic_authorization(user, pass) @request.env["HTTP_AUTHORIZATION"] = "Basic %s" % Base64.encode64("#{user}:#{pass}") @@ -93,4 +206,25 @@ class NodeControllerTest < Test::Unit::TestCase def content(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 From 2c16177174d276335babcca5439cd4e97af62ffa Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Mon, 13 Oct 2008 20:48:58 +0000 Subject: [PATCH 118/381] adding changeset stuff for the data browser --- app/controllers/browse_controller.rb | 11 +++++++ app/models/changeset.rb | 4 +++ app/views/browse/_changeset_details.rhtml | 19 ++++++++++++ app/views/browse/changeset.rhtml | 38 +++++++++++++++++++++++ config/routes.rb | 1 + 5 files changed, 73 insertions(+) create mode 100644 app/views/browse/_changeset_details.rhtml create mode 100644 app/views/browse/changeset.rhtml diff --git a/app/controllers/browse_controller.rb b/app/controllers/browse_controller.rb index 408846c96..94a8a09a0 100644 --- a/app/controllers/browse_controller.rb +++ b/app/controllers/browse_controller.rb @@ -112,4 +112,15 @@ class BrowseController < ApplicationController render :action => "not_found", :status => :not_found end end + + def changeset + begin + @changeset = Changeset.find(params[:id]) + + @title = "Changeset | #{@changeset.id}" + rescue ActiveRecord::RecordNotFound + @type = "changeset" + render :action => "not_found", :status => :not_found + end + end end diff --git a/app/models/changeset.rb b/app/models/changeset.rb index 070314e7f..dc6c0e014 100644 --- a/app/models/changeset.rb +++ b/app/models/changeset.rb @@ -47,6 +47,10 @@ class Changeset < ActiveRecord::Base return cs end + def tags_as_hash + return tags + end + def tags unless @tags @tags = {} diff --git a/app/views/browse/_changeset_details.rhtml b/app/views/browse/_changeset_details.rhtml new file mode 100644 index 000000000..d8a911f8a --- /dev/null +++ b/app/views/browse/_changeset_details.rhtml @@ -0,0 +1,19 @@ + + + <%= render :partial => "common_details", :object => changeset_details %> + + <% unless node_details.ways.empty? and node_details.containing_relation_members.empty? %> + + + + + <% end %> + +
Part of: + + <% node_details.ways.each do |way| %> + + <% end %> + <%= render :partial => "containing_relation", :collection => node_details.containing_relation_members %> +
<%= link_to "Way " + way.id.to_s, :action => "way", :id => way.id.to_s %>
+
diff --git a/app/views/browse/changeset.rhtml b/app/views/browse/changeset.rhtml new file mode 100644 index 000000000..4ecdb8e99 --- /dev/null +++ b/app/views/browse/changeset.rhtml @@ -0,0 +1,38 @@ + + + + + + + + + + + + <% if @changeset.user.data_public? %> + + + + + <% end %> + <% unless @changeset.tags_as_hash.empty? %> + + + + + <% end %> +
+ <%= link_to "Download XML", :controller => "changeset", :action => "read" %> + + <% # render :partial => "map", :object => @node %> + +
+

Changeset: <%= h(@changeset.id) %>

+
+ <%= render :partial => "navigation" %> +
+
Created at:<%= h(@changeset.created_at) %>
Belongs to:<%= link_to h(@changeset.user.display_name), :controller => "user", :action => "view", :display_name => @changeset.user.display_name %>
Tags: + + <%= render :partial => "tag", :collection => @changeset.tags_as_hash %> +
+
diff --git a/config/routes.rb b/config/routes.rb index 286e4055c..db59a28c8 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -73,6 +73,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 From b56f57ec43dfefa4171dc0cefe26d9e75d4ca2bc Mon Sep 17 00:00:00 2001 From: Matt Amos Date: Tue, 14 Oct 2008 14:27:12 +0000 Subject: [PATCH 119/381] Fixed up delete methods on nodes, ways and relations to return the new version number and added some more tests. --- app/controllers/node_controller.rb | 14 +++---- app/controllers/relation_controller.rb | 4 +- app/controllers/way_controller.rb | 7 ++-- app/models/node.rb | 2 +- app/models/relation.rb | 8 +--- app/models/way.rb | 11 +----- lib/osm.rb | 15 ++++++++ test/functional/node_controller_test.rb | 42 +++++++++++++++++++-- test/functional/relation_controller_test.rb | 18 +++++++++ test/functional/way_controller_test.rb | 15 +++++++- 10 files changed, 100 insertions(+), 36 deletions(-) diff --git a/app/controllers/node_controller.rb b/app/controllers/node_controller.rb index f1023e78f..eed00a016 100644 --- a/app/controllers/node_controller.rb +++ b/app/controllers/node_controller.rb @@ -78,21 +78,17 @@ class NodeController < ApplicationController 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]) new_node = Node.from_xml(request.raw_post) - # FIXME we no longer care about the user, (or maybe we want to check - # that the user of the changeset is the same user as is making this - # little change?) we really care about the - # changeset which must be open, and that the version that we have been - # given is the one that is currently stored in the database if new_node and new_node.id == node.id - node.delete_with_history(new_node, @user) - render :nothing => true + node.delete_with_history!(new_node, @user) + render :text => node.version.to_s, :content_type => "text/plain" else render :nothing => true, :status => :bad_request end diff --git a/app/controllers/relation_controller.rb b/app/controllers/relation_controller.rb index 4b3fdf34f..7ce58dae6 100644 --- a/app/controllers/relation_controller.rb +++ b/app/controllers/relation_controller.rb @@ -67,8 +67,8 @@ class RelationController < ApplicationController relation = Relation.find(params[:id]) 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 :nothing => true + relation.delete_with_history!(new_relation, @user) + render :text => relation.version.to_s, :content_type => "text/plain" else render :nothing => true, :status => :bad_request end diff --git a/app/controllers/way_controller.rb b/app/controllers/way_controller.rb index ac017ca6b..b00658cf0 100644 --- a/app/controllers/way_controller.rb +++ b/app/controllers/way_controller.rb @@ -67,11 +67,10 @@ class WayController < ApplicationController begin way = Way.find(params[:id]) new_way = Way.from_xml(request.raw_post) - if new_way and new_way.id == way.id - way.delete_with_history(new_way, @user) - # if we get here, all is fine, otherwise something will catch below. - render :nothing => true + 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 diff --git a/app/models/node.rb b/app/models/node.rb index 3b59ac80e..c2a61906b 100644 --- a/app/models/node.rb +++ b/app/models/node.rb @@ -133,7 +133,7 @@ class Node < ActiveRecord::Base end # Should probably be renamed delete_from to come in line with update - def delete_with_history(new_node, user) + def delete_with_history!(new_node, user) if self.visible 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 = 1 AND current_way_nodes.node_id = ?", self.id ]) diff --git a/app/models/relation.rb b/app/models/relation.rb index 436c1e32e..db4dd52a6 100644 --- a/app/models/relation.rb +++ b/app/models/relation.rb @@ -224,14 +224,12 @@ class Relation < ActiveRecord::Base end end - def delete_with_history(new_relation, user) + def delete_with_history!(new_relation, user) if self.visible check_consistency(self, new_relation, user) - 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=?", self.id ]) + 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=? ", self.id ]) raise OSM::APIPreconditionFailedError.new else - #self.user_id = user.id - # FIXME we need to deal with changeset here, which is probably already dealt with self.changeset_id = new_relation.changeset_id self.tags = [] self.members = [] @@ -248,8 +246,6 @@ class Relation < ActiveRecord::Base if !new_relation.preconditions_ok? raise OSM::APIPreconditionFailedError.new end - # FIXME need to deal with changeset etc - #self.user_id = user.id self.changeset_id = new_relation.changeset_id self.tags = new_relation.tags self.members = new_relation.members diff --git a/app/models/way.rb b/app/models/way.rb index 90458006e..591dee9a2 100644 --- a/app/models/way.rb +++ b/app/models/way.rb @@ -245,19 +245,12 @@ class Way < ActiveRecord::Base return true end - def delete_with_history(new_way, user) + def delete_with_history!(new_way, user) check_consistency(self, new_way, user) if self.visible - # FIXME - # this should actually delete the relations, - # not just throw a PreconditionFailed if it's a member of a relation!! - # WHY?? The editor should decide whether the node is in the relation or not! - - # FIXME: this should probably renamed to delete_with_history 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]) + :conditions => [ "visible = 1 AND member_type='way' and member_id=? ", self.id]) raise OSM::APIPreconditionFailedError - # end FIXME else self.changeset_id = new_way.changeset_id self.tags = [] diff --git a/lib/osm.rb b/lib/osm.rb index 82fc835b4..ae8b81f5b 100644 --- a/lib/osm.rb +++ b/lib/osm.rb @@ -68,6 +68,21 @@ module OSM 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 } + end + end + # Helper methods for going to/from mercator and lat/lng. class Mercator include Math diff --git a/test/functional/node_controller_test.rb b/test/functional/node_controller_test.rb index 085cd3078..dbc00cbb2 100644 --- a/test/functional/node_controller_test.rb +++ b/test/functional/node_controller_test.rb @@ -79,6 +79,11 @@ class NodeControllerTest < Test::Unit::TestCase 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 @@ -92,12 +97,14 @@ class NodeControllerTest < Test::Unit::TestCase # 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 + assert_response :precondition_failed, + "shouldn't be able to delete a node used in a relation (#{@response.body})" end ## @@ -197,7 +204,36 @@ class NodeControllerTest < Test::Unit::TestCase put :update, :id => current_nodes(:visible_node).id assert_response :bad_request, "adding duplicate tags to a node should fail with 'bad request'" - end + 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 "" + + '' + + '' + 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}") diff --git a/test/functional/relation_controller_test.rb b/test/functional/relation_controller_test.rb index b45c80874..b54d9bc98 100644 --- a/test/functional/relation_controller_test.rb +++ b/test/functional/relation_controller_test.rb @@ -210,16 +210,34 @@ class RelationControllerTest < Test::Unit::TestCase delete :delete, :id => current_relations(:visible_relation).id assert_response :bad_request + # 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 diff --git a/test/functional/way_controller_test.rb b/test/functional/way_controller_test.rb index 58bb6a9a1..d889be2ba 100644 --- a/test/functional/way_controller_test.rb +++ b/test/functional/way_controller_test.rb @@ -165,17 +165,28 @@ class WayControllerTest < Test::Unit::TestCase delete :delete, :id => current_ways(:visible_way).id assert_response :bad_request - # Now try and get a changeset - changeset_id = changesets(:normal_user_first_change).id + # 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 From 44034cd78197a48e561104ceafa5b441748fbedf Mon Sep 17 00:00:00 2001 From: Matt Amos Date: Tue, 14 Oct 2008 14:34:17 +0000 Subject: [PATCH 120/381] Added some more functional tests for way and relation delete methods. --- test/functional/relation_controller_test.rb | 36 +++++++++++++++++++++ test/functional/way_controller_test.rb | 23 +++++++++++++ 2 files changed, 59 insertions(+) diff --git a/test/functional/relation_controller_test.rb b/test/functional/relation_controller_test.rb index b54d9bc98..ffd65f6cc 100644 --- a/test/functional/relation_controller_test.rb +++ b/test/functional/relation_controller_test.rb @@ -210,6 +210,22 @@ class RelationControllerTest < Test::Unit::TestCase delete :delete, :id => current_relations(:visible_relation).id assert_response :bad_request + # try to delete without specifying a changeset + content "" + delete :delete, :id => current_relations(:visible_relation).id + assert_response :conflict + + # 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 @@ -243,4 +259,24 @@ class RelationControllerTest < Test::Unit::TestCase assert_response :not_found 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/way_controller_test.rb b/test/functional/way_controller_test.rb index d889be2ba..2049ba0cb 100644 --- a/test/functional/way_controller_test.rb +++ b/test/functional/way_controller_test.rb @@ -165,6 +165,17 @@ class WayControllerTest < Test::Unit::TestCase 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 @@ -192,4 +203,16 @@ class WayControllerTest < Test::Unit::TestCase assert_response :not_found 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 From 45a9d9433767938a9d313f745f9459400132704a Mon Sep 17 00:00:00 2001 From: Matt Amos Date: Tue, 14 Oct 2008 16:40:17 +0000 Subject: [PATCH 121/381] Cleaned up some unreachable code. Added first test on the 'old' node controller. --- app/controllers/node_controller.rb | 15 --- app/models/old_node.rb | 2 +- test/functional/old_node_controller_test.rb | 141 ++++++++++++++++++++ 3 files changed, 142 insertions(+), 16 deletions(-) create mode 100644 test/functional/old_node_controller_test.rb diff --git a/app/controllers/node_controller.rb b/app/controllers/node_controller.rb index eed00a016..9763be5d4 100644 --- a/app/controllers/node_controller.rb +++ b/app/controllers/node_controller.rb @@ -44,21 +44,6 @@ class NodeController < ApplicationController end end - # Dump a specific version of the node based on the given params[:id] and params[:version] - def version - begin - node = Node.find(:first, :conditions => { :id => params[:id], :version => params[:version] } ) - if node.visible - response.headers['Last-Modified'] = node.timestamp.rfc822 - render :text => node.to_xml.to_s, :content_type => "text/xml" - else - render :nothing => true, :status => :gone - end - rescue ActiveRecord::RecordNotFound - render :nothing => true, :status => :not_found - end - end - # Update a node from given XML def update begin diff --git a/app/models/old_node.rb b/app/models/old_node.rb index 8b3ba784b..91b5a1a8e 100644 --- a/app/models/old_node.rb +++ b/app/models/old_node.rb @@ -61,7 +61,7 @@ class OldNode < ActiveRecord::Base clear_aggregation_cache clear_association_cache #ok from here - @attributes.update(OldNode.find(:first, :conditions => ['id = ? AND timestamp = ?', self.id, self.timestamp]).instance_variable_get('@attributes')) + @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 diff --git a/test/functional/old_node_controller_test.rb b/test/functional/old_node_controller_test.rb new file mode 100644 index 000000000..50d69e9a4 --- /dev/null +++ b/test/functional/old_node_controller_test.rb @@ -0,0 +1,141 @@ +require File.dirname(__FILE__) + '/../test_helper' +require 'old_node_controller' + +# Re-raise errors caught by the controller. +class OldNodeController; def rescue_action(e) raise e end; end + +class OldNodeControllerTest < Test::Unit::TestCase + api_fixtures + + def setup + @controller = OldNodeController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + end + + # + # 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 + + ## + # 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" + assert_equal a.longitude, b.longitude, "longitude" + assert_equal a.changeset_id, b.changeset_id, "changeset ID" + assert_equal a.visible, b.visible, "visible" + assert_equal a.version, b.version, "version" + assert_equal a.tags, b.tags, "tags" + 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 + + ## + # 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 + +end From e7147d0d7adae704ead9f4985bcda918e622d833 Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Tue, 14 Oct 2008 18:33:03 +0000 Subject: [PATCH 122/381] more work on the data browser to show the changesets. Also showing the version number. Would be good to have an additional route for showing a specific version of the node/way/relation from the changeset. --- app/controllers/browse_controller.rb | 2 + app/views/browse/_changeset_details.rhtml | 63 ++++++++++++++++++++--- app/views/browse/_common_details.rhtml | 11 ++-- app/views/browse/changeset.rhtml | 23 +-------- 4 files changed, 67 insertions(+), 32 deletions(-) diff --git a/app/controllers/browse_controller.rb b/app/controllers/browse_controller.rb index 94a8a09a0..2c6c3dc5f 100644 --- a/app/controllers/browse_controller.rb +++ b/app/controllers/browse_controller.rb @@ -118,6 +118,8 @@ class BrowseController < ApplicationController @changeset = Changeset.find(params[:id]) @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 diff --git a/app/views/browse/_changeset_details.rhtml b/app/views/browse/_changeset_details.rhtml index d8a911f8a..27335dd88 100644 --- a/app/views/browse/_changeset_details.rhtml +++ b/app/views/browse/_changeset_details.rhtml @@ -1,19 +1,68 @@ - <%= render :partial => "common_details", :object => changeset_details %> - - <% unless node_details.ways.empty? and node_details.containing_relation_members.empty? %> + + + + + + <% if changeset_details.user.data_public? %> + + + + + <% end %> + + <% unless changeset_details.tags_as_hash.empty? %> - + + + <% end %> + + <% unless changeset_details.old_nodes.empty? %> + + + + + <% end %> + + <% unless changeset_details.old_ways.empty? %> + + + <% end %> + + <% unless changeset_details.old_relations.empty? %> + + + + + <% end %>
Created at:<%= h(changeset_details.created_at) %>
Belongs to:<%= link_to h(changeset_details.user.display_name), :controller => "user", :action => "view", :display_name => changeset_details.user.display_name %>
Part of:Tags: - <% node_details.ways.each do |way| %> - + <%= render :partial => "tag", :collection => changeset_details.tags_as_hash %> +
<%= link_to "Way " + way.id.to_s, :action => "way", :id => way.id.to_s %>
+
Has the following nodes: + + <% changeset_details.old_nodes.each do |node| %> + <% end %> - <%= render :partial => "containing_relation", :collection => node_details.containing_relation_members %> +
<%= link_to "Node #{node.id.to_s}, version #{node.version.to_s}", :action => "node", :id => node.id.to_s %>
+
Has the following ways: + + <% changeset_details.old_ways.each do |way| %> + + <% end %> + <%= + #render :partial => "containing_relation", :collection => changeset_details.containing_relation_members + %>
<%= link_to "Way #{way.id.to_s}, version #{way.version.to_s}", :action => "way", :id => way.id.to_s %>
Has the following relations: + + <% changeset_details.old_relations.each do |relation| %> + + <% end %> +
<%= link_to "Relation #{relation.id.to_s}, version #{relation.version.to_s}", :action => "relation", :id => relation.id.to_s %>
+
diff --git a/app/views/browse/_common_details.rhtml b/app/views/browse/_common_details.rhtml index 968594bb6..1f9f9ffe6 100644 --- a/app/views/browse/_common_details.rhtml +++ b/app/views/browse/_common_details.rhtml @@ -3,17 +3,22 @@ <%= h(common_details.timestamp) %> -<% if common_details.changeset.user.data_public %> +<% if common_details.changeset.user.data_public? %> Edited by: <%= link_to h(common_details.changeset.user.display_name), :controller => "user", :action => "view", :display_name => common_details.changeset.user.display_name %> <% end %> + + Version: + <%= h(common_details.version) %> + + In changeset: - <%= common_details.changeset_id %> -<%= link_to common_details.changeset_id, :action => :changeset %> + <% unless common_details.tags_as_hash.empty? %> diff --git a/app/views/browse/changeset.rhtml b/app/views/browse/changeset.rhtml index 4ecdb8e99..9345eb087 100644 --- a/app/views/browse/changeset.rhtml +++ b/app/views/browse/changeset.rhtml @@ -9,30 +9,9 @@ - - Created at: - <%= h(@changeset.created_at) %> - - - <% if @changeset.user.data_public? %> - - Belongs to: - <%= link_to h(@changeset.user.display_name), :controller => "user", :action => "view", :display_name => @changeset.user.display_name %> - - <% end %> - <% unless @changeset.tags_as_hash.empty? %> - - Tags: - - - <%= render :partial => "tag", :collection => @changeset.tags_as_hash %> -
- - - <% end %> + <%= render :partial => "changeset_details", :object => @changeset %>
<%= link_to "Download XML", :controller => "changeset", :action => "read" %> - <% # render :partial => "map", :object => @node %> From 6a276202e559903cd0e21fb25b8f72a01cbd9314 Mon Sep 17 00:00:00 2001 From: Matt Amos Date: Wed, 15 Oct 2008 11:12:26 +0000 Subject: [PATCH 123/381] Added more fixtures and tests for node versioning calls. --- test/fixtures/changesets.yml | 6 +++ test/fixtures/current_node_tags.yml | 14 +++++++ test/fixtures/current_nodes.yml | 9 +++++ test/fixtures/node_tags.yml | 32 +++++++++++++++- test/fixtures/nodes.yml | 39 ++++++++++++++++++++ test/functional/old_node_controller_test.rb | 41 ++++++++++++++++++--- test/unit/current_node_tag_test.rb | 4 +- 7 files changed, 137 insertions(+), 8 deletions(-) diff --git a/test/fixtures/changesets.yml b/test/fixtures/changesets.yml index de6f4f786..7d97f2abf 100644 --- a/test/fixtures/changesets.yml +++ b/test/fixtures/changesets.yml @@ -16,3 +16,9 @@ normal_user_closed_change: user_id: 1 created_at: "2007-01-01 00:00:00" open: 0 + +normal_user_version_change: + id: 4 + user_id: 1 + created_at: "2008-01-01 00:00:00" + open: 1 diff --git a/test/fixtures/current_node_tags.yml b/test/fixtures/current_node_tags.yml index ce68a5439..8a699e9d4 100644 --- a/test/fixtures/current_node_tags.yml +++ b/test/fixtures/current_node_tags.yml @@ -13,3 +13,17 @@ t3: 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 diff --git a/test/fixtures/current_nodes.yml b/test/fixtures/current_nodes.yml index 7407b2983..7e4f33a07 100644 --- a/test/fixtures/current_nodes.yml +++ b/test/fixtures/current_nodes.yml @@ -141,3 +141,12 @@ node_totally_wrong: 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: 1 + version: 4 + tile: <%= QuadTile.tile_for_point(1,1) %> + timestamp: 2008-01-01 00:04:00 diff --git a/test/fixtures/node_tags.yml b/test/fixtures/node_tags.yml index 8c80128b0..c6a31186b 100644 --- a/test/fixtures/node_tags.yml +++ b/test/fixtures/node_tags.yml @@ -6,7 +6,7 @@ t1: t2: id: 3 - k: testused + k: test v: yes version: 1 @@ -15,3 +15,33 @@ t3: 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 diff --git a/test/fixtures/nodes.yml b/test/fixtures/nodes.yml index ddcf1d8f1..c58cda962 100644 --- a/test/fixtures/nodes.yml +++ b/test/fixtures/nodes.yml @@ -141,3 +141,42 @@ node_totally_wrong: 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: 1 + 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: 1 + 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: 1 + 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: 1 + version: 4 + tile: <%= QuadTile.tile_for_point(1,1) %> + timestamp: 2008-01-01 00:04:00 diff --git a/test/functional/old_node_controller_test.rb b/test/functional/old_node_controller_test.rb index 50d69e9a4..4b0cf0363 100644 --- a/test/functional/old_node_controller_test.rb +++ b/test/functional/old_node_controller_test.rb @@ -83,18 +83,47 @@ class OldNodeControllerTest < Test::Unit::TestCase 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 + ## # 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" - assert_equal a.longitude, b.longitude, "longitude" - assert_equal a.changeset_id, b.changeset_id, "changeset ID" - assert_equal a.visible, b.visible, "visible" - assert_equal a.version, b.version, "version" - assert_equal a.tags, b.tags, "tags" + 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 ## diff --git a/test/unit/current_node_tag_test.rb b/test/unit/current_node_tag_test.rb index 7fb1deff5..98bceb4e5 100644 --- a/test/unit/current_node_tag_test.rb +++ b/test/unit/current_node_tag_test.rb @@ -6,10 +6,12 @@ class CurrentNodeTagTest < Test::Unit::TestCase set_fixture_class :current_node_tags => :NodeTag def test_tag_count - assert_equal 3, NodeTag.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) From 2939aa9398694d304067a2fd409104ed53afc57d Mon Sep 17 00:00:00 2001 From: Andy Allan Date: Wed, 15 Oct 2008 13:17:10 +0000 Subject: [PATCH 124/381] utf-8 wide-char testing of message title - this will come in useful for other things e.g. k/v pairs --- test/unit/message_test.rb | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/test/unit/message_test.rb b/test/unit/message_test.rb index 4de1a7b29..362bcc430 100644 --- a/test/unit/message_test.rb +++ b/test/unit/message_test.rb @@ -3,6 +3,8 @@ require File.dirname(__FILE__) + '/../test_helper' class MessageTest < Test::Unit::TestCase fixtures :messages, :users + EURO = "\xe2\x82\xac" #euro symbol + # This needs to be updated when new fixtures are added # or removed. def test_check_message_count @@ -31,4 +33,30 @@ class MessageTest < Test::Unit::TestCase message.recipient = nil assert !message.valid? 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 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 From 25c3310be4c0b497d93e92a4d8141a50c12c4cd2 Mon Sep 17 00:00:00 2001 From: Andy Allan Date: Wed, 15 Oct 2008 15:23:56 +0000 Subject: [PATCH 125/381] Checking foreign key constraints on message recipients and senders --- test/unit/message_test.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/unit/message_test.rb b/test/unit/message_test.rb index 362bcc430..e95c698c9 100644 --- a/test/unit/message_test.rb +++ b/test/unit/message_test.rb @@ -32,6 +32,11 @@ class MessageTest < Test::Unit::TestCase 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 From a5e5cbc73999d004998e1e90b1b19796a94b077d Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Wed, 15 Oct 2008 16:20:43 +0000 Subject: [PATCH 126/381] Adding a lower and upper length on various web site varchar fields. Fixes one test. --- app/models/diary_entry.rb | 2 ++ app/models/message.rb | 1 + app/models/trace.rb | 2 ++ app/models/tracetag.rb | 1 + app/models/user.rb | 5 +++-- app/models/user_preference.rb | 3 +++ 6 files changed, 12 insertions(+), 2 deletions(-) diff --git a/app/models/diary_entry.rb b/app/models/diary_entry.rb index 5b3d34766..fe48ee8dc 100644 --- a/app/models/diary_entry.rb +++ b/app/models/diary_entry.rb @@ -3,6 +3,8 @@ class DiaryEntry < ActiveRecord::Base has_many :diary_comments, :order => "id" validates_presence_of :title, :body + validates_length_of :title, :within => 1..255 + validates_length_of :language, :within => 2..3 validates_numericality_of :latitude, :allow_nil => true validates_numericality_of :longitude, :allow_nil => true validates_associated :user diff --git a/app/models/message.rb b/app/models/message.rb index ec712be25..a85de2231 100644 --- a/app/models/message.rb +++ b/app/models/message.rb @@ -3,6 +3,7 @@ class Message < ActiveRecord::Base belongs_to :recipient, :class_name => "User", :foreign_key => :to_user_id 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 end diff --git a/app/models/trace.rb b/app/models/trace.rb index 10e867bad..1b44e2187 100644 --- a/app/models/trace.rb +++ b/app/models/trace.rb @@ -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] diff --git a/app/models/tracetag.rb b/app/models/tracetag.rb index f1d5967d5..f9833e141 100644 --- a/app/models/tracetag.rb +++ b/app/models/tracetag.rb @@ -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 diff --git a/app/models/user.rb b/app/models/user.rb index 9b5bfd595..525d018ac 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -15,8 +15,9 @@ class User < ActiveRecord::Base 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 diff --git a/app/models/user_preference.rb b/app/models/user_preference.rb index 3985a527e..28ef40f1d 100644 --- a/app/models/user_preference.rb +++ b/app/models/user_preference.rb @@ -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 wrapper. def to_xml_node From 8651215ec3a01204fa6acdbf0dbe72b9926e586a Mon Sep 17 00:00:00 2001 From: Andy Allan Date: Wed, 15 Oct 2008 16:56:56 +0000 Subject: [PATCH 127/381] set the db encoding to utf-8, otherwise data is stored incorrectly in db. Can be seen by roundtrip truncating at 255 bytes instead of 255 chars. Connection setting (here) is independant of mysql setup --- config/database.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/config/database.yml b/config/database.yml index fe47e11aa..cc3f9a1a5 100644 --- a/config/database.yml +++ b/config/database.yml @@ -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'. @@ -26,6 +27,7 @@ test: username: osm_test password: osm_test host: localhost + encoding: utf8 production: adapter: mysql @@ -33,4 +35,4 @@ production: username: osm password: osm host: localhost - + encoding: utf8 From 70ecb3ceefbbc84267bbb8cdbeec3e2f7ad861c3 Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Thu, 16 Oct 2008 07:11:28 +0000 Subject: [PATCH 128/381] adding the Last-Modified response header to the version action --- app/controllers/old_node_controller.rb | 2 ++ app/controllers/old_relation_controller.rb | 2 ++ app/controllers/old_way_controller.rb | 4 +++- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/app/controllers/old_node_controller.rb b/app/controllers/old_node_controller.rb index 8c43941ec..56397625c 100644 --- a/app/controllers/old_node_controller.rb +++ b/app/controllers/old_node_controller.rb @@ -26,6 +26,8 @@ class OldNodeController < ApplicationController 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 diff --git a/app/controllers/old_relation_controller.rb b/app/controllers/old_relation_controller.rb index 9307ee1ce..84d0b0c90 100644 --- a/app/controllers/old_relation_controller.rb +++ b/app/controllers/old_relation_controller.rb @@ -25,6 +25,8 @@ class OldRelationController < ApplicationController 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 diff --git a/app/controllers/old_way_controller.rb b/app/controllers/old_way_controller.rb index 6cd8c8f22..da4e26d67 100644 --- a/app/controllers/old_way_controller.rb +++ b/app/controllers/old_way_controller.rb @@ -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 @@ -27,6 +27,8 @@ class OldWayController < ApplicationController 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 From 89c677d8819ed2087a9427fb234253c0fff56a55 Mon Sep 17 00:00:00 2001 From: Matt Amos Date: Thu, 16 Oct 2008 13:02:09 +0000 Subject: [PATCH 129/381] Fixed fixtures and added new tests for ways and way_nodes. --- test/fixtures/current_way_nodes.yml | 5 ++ test/fixtures/current_ways.yml | 6 ++ test/fixtures/way_nodes.yml | 54 +++++++++++++-- test/fixtures/ways.yml | 28 ++++++++ test/functional/old_node_controller_test.rb | 14 ---- test/functional/old_way_controller_test.rb | 76 +++++++++++++++++++++ test/test_helper.rb | 14 ++++ 7 files changed, 177 insertions(+), 20 deletions(-) diff --git a/test/fixtures/current_way_nodes.yml b/test/fixtures/current_way_nodes.yml index ce394edbe..66aae0f20 100644 --- a/test/fixtures/current_way_nodes.yml +++ b/test/fixtures/current_way_nodes.yml @@ -12,3 +12,8 @@ t3: id: 3 node_id: 3 sequence_id: 1 + +t4: + id: 4 + node_id: 15 + sequence_id: 1 diff --git a/test/fixtures/current_ways.yml b/test/fixtures/current_ways.yml index bd31916e2..cf25b4f46 100644 --- a/test/fixtures/current_ways.yml +++ b/test/fixtures/current_ways.yml @@ -19,3 +19,9 @@ used_way: visible: 1 version: 1 +way_with_versions: + id: 4 + changeset_id: 4 + timestamp: 2008-01-01 00:01:00 + visible: 1 + version: 4 diff --git a/test/fixtures/way_nodes.yml b/test/fixtures/way_nodes.yml index c12a248a0..0b43f6a9c 100644 --- a/test/fixtures/way_nodes.yml +++ b/test/fixtures/way_nodes.yml @@ -3,12 +3,6 @@ t1a: node_id: 3 sequence_id: 1 version: 1 - -t1b: - id: 1 - node_id: 4 - sequence_id: 2 - version: 1 t2: id: 2 @@ -21,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 diff --git a/test/fixtures/ways.yml b/test/fixtures/ways.yml index bb1a43c80..c1ce7222b 100644 --- a/test/fixtures/ways.yml +++ b/test/fixtures/ways.yml @@ -19,3 +19,31 @@ used_way: visible: 0 version: 1 +way_with_versions_v1: + id: 4 + changeset_id: 4 + timestamp: 2008-01-01 00:01:00 + visible: 1 + version: 1 + +way_with_versions_v2: + id: 4 + changeset_id: 4 + timestamp: 2008-01-01 00:02:00 + visible: 1 + version: 2 + +way_with_versions: + id: 4 + changeset_id: 4 + timestamp: 2008-01-01 00:03:00 + visible: 1 + version: 3 + +way_with_versions_v4: + id: 4 + changeset_id: 4 + timestamp: 2008-01-01 00:04:00 + visible: 1 + version: 4 + diff --git a/test/functional/old_node_controller_test.rb b/test/functional/old_node_controller_test.rb index 4b0cf0363..ca9a114b1 100644 --- a/test/functional/old_node_controller_test.rb +++ b/test/functional/old_node_controller_test.rb @@ -153,18 +153,4 @@ class OldNodeControllerTest < Test::Unit::TestCase @request.env["RAW_POST_DATA"] = c.to_s 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 - end diff --git a/test/functional/old_way_controller_test.rb b/test/functional/old_way_controller_test.rb index b4e3c5127..c6d4ce240 100644 --- a/test/functional/old_way_controller_test.rb +++ b/test/functional/old_way_controller_test.rb @@ -24,10 +24,86 @@ class OldWayControllerTest < Test::Unit::TestCase 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 + + ## + # + 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 + + ## + # 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_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}" + 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 end diff --git a/test/test_helper.rb b/test/test_helper.rb index 0c03aac34..63567cc9b 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -58,5 +58,19 @@ class Test::Unit::TestCase set_fixture_class :relation_tags => :OldRelationTag 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 + # Add more helper methods to be used by all tests here... end From df496b44cd5d52f96bd090815f4f23b4e0f4452f Mon Sep 17 00:00:00 2001 From: Matt Amos Date: Fri, 17 Oct 2008 11:06:58 +0000 Subject: [PATCH 130/381] More functional tests, this time for way_tags. --- test/functional/old_way_controller_test.rb | 3 +- test/functional/way_controller_test.rb | 84 ++++++++++++++++++++++ 2 files changed, 86 insertions(+), 1 deletion(-) diff --git a/test/functional/old_way_controller_test.rb b/test/functional/old_way_controller_test.rb index c6d4ce240..b357b0987 100644 --- a/test/functional/old_way_controller_test.rb +++ b/test/functional/old_way_controller_test.rb @@ -74,7 +74,8 @@ class OldWayControllerTest < Test::Unit::TestCase 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" diff --git a/test/functional/way_controller_test.rb b/test/functional/way_controller_test.rb index 2049ba0cb..8a057c293 100644 --- a/test/functional/way_controller_test.rb +++ b/test/functional/way_controller_test.rb @@ -203,6 +203,90 @@ class WayControllerTest < Test::Unit::TestCase 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'" + 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'" + 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 = "" + + # all of these keys have the same unicode decoding, but are binary + # not equal. libxml should make these identical as it decodes the + # XML document... + [ "addr:housenumber", + "addr\xc0\xbahousenumber", + "addr\xe0\x80\xbahousenumber", + "addr\xf0\x80\x80\xbahousenumber" ].each do |key| + tag_xml = XML::Node.new("tag") + tag_xml['k'] = key + tag_xml['v'] = "1" + + # add all new tags to the way + way_str << "" + end + way_str << ""; + + # 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'" + end + ## # update the changeset_id of a node element def update_changeset(xml, changeset_id) From 654470b896c59e27bec9c3adcf391f2f923110e9 Mon Sep 17 00:00:00 2001 From: Matt Amos Date: Fri, 17 Oct 2008 11:21:40 +0000 Subject: [PATCH 131/381] Altered a functional test on way_tags. --- test/functional/way_controller_test.rb | 29 ++++++++++++++------------ 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/test/functional/way_controller_test.rb b/test/functional/way_controller_test.rb index 8a057c293..0eb77c2e4 100644 --- a/test/functional/way_controller_test.rb +++ b/test/functional/way_controller_test.rb @@ -257,34 +257,37 @@ class WayControllerTest < Test::Unit::TestCase # 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. + # + # NOTE: I'm not sure this test is working correctly, as a lot of the tag + # keys seem to come out as "addr��housenumber". It might be something to + # do with Ruby's unicode handling...? def test_invalid_duplicate_tags # setup auth basic_authorization(users(:normal_user).email, "test") # add the tag into the existing xml way_str = "" + way_str << "" # all of these keys have the same unicode decoding, but are binary # not equal. libxml should make these identical as it decodes the # XML document... - [ "addr:housenumber", - "addr\xc0\xbahousenumber", + [ "addr\xc0\xbahousenumber", "addr\xe0\x80\xbahousenumber", "addr\xf0\x80\x80\xbahousenumber" ].each do |key| - tag_xml = XML::Node.new("tag") - tag_xml['k'] = key - tag_xml['v'] = "1" + # copy the XML doc to add the tags + way_str_copy = way_str.clone # add all new tags to the way - way_str << "" - end - way_str << ""; + way_str_copy << "" + way_str_copy << "
"; - # 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'" + # try and upload it + content way_str_copy + put :create + assert_response :bad_request, + "adding new duplicate tags to a way should fail with 'bad request'" + end end ## From 3e9ceb0c3816a1e13c82914d16abf2997b482751 Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Sun, 19 Oct 2008 07:09:04 +0000 Subject: [PATCH 132/381] more testing of the api. Changing the generator so that it is a constant to come in line with the server url and the api version. Adding tracepoints per page to the capabilities api call. Better error message for api call. --- app/controllers/api_controller.rb | 10 ++-- config/environment.rb | 3 ++ config/routes.rb | 2 + lib/osm.rb | 2 +- test/functional/api_controller_test.rb | 65 ++++++++++++++++++++++++-- 5 files changed, 75 insertions(+), 7 deletions(-) diff --git a/app/controllers/api_controller.rb b/app/controllers/api_controller.rb index 8b876d3a7..f7c9f591f 100644 --- a/app/controllers/api_controller.rb +++ b/app/controllers/api_controller.rb @@ -11,10 +11,10 @@ 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'] def trackpoints @@ -109,6 +109,7 @@ class ApiController < ApplicationController return end + # 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 = 1", :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 @@ -245,7 +246,7 @@ 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", :status => :bad_request end end @@ -260,6 +261,9 @@ class ApiController < ApplicationController 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 doc.root << api diff --git a/config/environment.rb b/config/environment.rb index 08c433788..e42e87eb7 100644 --- a/config/environment.rb +++ b/config/environment.rb @@ -10,6 +10,9 @@ RAILS_GEM_VERSION = '2.0.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.6' diff --git a/config/routes.rb b/config/routes.rb index db59a28c8..5b2ca38b9 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -25,6 +25,8 @@ ActionController::Routing::Routes.draw do |map| 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 + # FIXME Wouldn't capabilites be better placed somewhere else in this file + # and without the #{API_VERSION}, so that clients can always find it? 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+/ diff --git a/lib/osm.rb b/lib/osm.rb index ae8b81f5b..e9d3c9464 100644 --- a/lib/osm.rb +++ b/lib/osm.rb @@ -249,7 +249,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 diff --git a/test/functional/api_controller_test.rb b/test/functional/api_controller_test.rb index 4c4787ff7..c722bbf60 100644 --- a/test/functional/api_controller_test.rb +++ b/test/functional/api_controller_test.rb @@ -23,12 +23,71 @@ class ApiControllerTest < Test::Unit::TestCase def test_map node = current_nodes(:used_node_1) - bbox = "#{node.lat-0.1},#{node.lon-0.1},#{node.lat+0.1},#{node.lon+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 + 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=1]" + end + # Should also test for the ways and relation + end + end + + def test_map_without_bbox + get :map + 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 + end + + def test_traces_without_bbox + get :trackpoints + 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 + 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 + end + 0.upto(10) do |i| + get :trackpoints, :page => i, :bbox => "-0.1,-0.1,0.1,0.1" + assert_response :success + end + end + + def test_traces_bbox_too_big + bad = %w{ -0.1,-0.1,1.1,1.1 10,10,11,11 } + bad.each do |bbox| + get :trackpoints, :bbox => bbox + assert_response :bad_request + 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 + 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 From 138c20322d74cd0418b45e385851c9df784fbe7b Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Mon, 20 Oct 2008 15:05:12 +0000 Subject: [PATCH 133/381] Adding some api tests for both the map and trackpoints requests. Needs some better bboxes. Adding new changeset controller fnctional tests. --- test/functional/api_controller_test.rb | 78 ++++++++++++++++---- test/functional/changeset_controller_test.rb | 61 +++++++++++++++ 2 files changed, 124 insertions(+), 15 deletions(-) create mode 100644 test/functional/changeset_controller_test.rb diff --git a/test/functional/api_controller_test.rb b/test/functional/api_controller_test.rb index c722bbf60..3e86affae 100644 --- a/test/functional/api_controller_test.rb +++ b/test/functional/api_controller_test.rb @@ -6,11 +6,21 @@ class ApiController; def rescue_action(e) raise e end; end class ApiControllerTest < Test::Unit::TestCase api_fixtures + + def setup @controller = ApiController.new @request = ActionController::TestRequest.new @response = ActionController::TestResponse.new + @badbigbbox = %w{ -0.1,-0.1,1.1,1.1 10,10,11,11 } + @badmalformedbbox = %w{ -0.1 hello S0.1,W0.1,N0.1,E0.1 + 10N2W10.1N2.1W } + @badlatmixedbbox = %w{} + @badlonmixedbbox = %w{} + @badlatlonoutboundsbbox = %w{ -190.2,-190.2,-190.1,-190.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 } end def basic_authorization(user, pass) @@ -47,15 +57,11 @@ class ApiControllerTest < Test::Unit::TestCase end def test_map_without_bbox - get :map - 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 - end - - def test_traces_without_bbox - get :trackpoints - 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 + ["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 + end end def test_traces_page_less_than_0 @@ -70,12 +76,54 @@ class ApiControllerTest < Test::Unit::TestCase end end - def test_traces_bbox_too_big - bad = %w{ -0.1,-0.1,1.1,1.1 10,10,11,11 } - bad.each do |bbox| - get :trackpoints, :bbox => bbox - assert_response :bad_request - 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 + 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 + 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 + 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 + 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 + end + end + end + + 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 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 + end end end diff --git a/test/functional/changeset_controller_test.rb b/test/functional/changeset_controller_test.rb new file mode 100644 index 000000000..1e050a71e --- /dev/null +++ b/test/functional/changeset_controller_test.rb @@ -0,0 +1,61 @@ +require File.dirname(__FILE__) + '/../test_helper' +require 'changeset_controller' + +# Re-raise errors caught by the controller. +class ChangesetController; def rescue_action(e) raise e end; end + + class ChangesetControllerTest < Test::Unit::TestCase + api_fixtures + + + + def setup + @controller = ChangesetController.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.to_s + end + + # ----------------------- + # Test simple changeset creation + # ----------------------- + + def test_create + basic_authorization "test@openstreetmap.org", "test" + + # Create the first user's changeset + content "" + + "" + + "" + put :create + + assert_response :success, "Creation of changeset did not return sucess status" + newid = @response.body + end + + def test_create_invalid + basic_authorization "test@openstreetmap.org", "test" + content "" + put :create + end + + def test_read + + end + + def test_close + + end + + def test_upload + + end + +end From 6889ee7564f2d795bf05fd9f3a9676632fd953a0 Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Mon, 20 Oct 2008 20:40:06 +0000 Subject: [PATCH 134/381] Making the capabilities api version indipendent --- config/routes.rb | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/config/routes.rb b/config/routes.rb index 5b2ca38b9..e49cdf74e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,6 +1,8 @@ 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/upload", :controller => 'changeset', :action => 'upload' map.connect "api/#{API_VERSION}/changeset/:id", :controller => 'changeset', :action => 'read', :id => /\d+/ @@ -25,9 +27,6 @@ ActionController::Routing::Routes.draw do |map| 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 - # FIXME Wouldn't capabilites be better placed somewhere else in this file - # and without the #{API_VERSION}, so that clients can always find it? - 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+/ From a8eb3a6ada2fafe77deba8456b61811b162e5faa Mon Sep 17 00:00:00 2001 From: Matt Amos Date: Tue, 21 Oct 2008 09:31:36 +0000 Subject: [PATCH 135/381] Removed debugging code. --- app/models/changeset.rb | 1 - test/functional/api_controller_test.rb | 2 -- 2 files changed, 3 deletions(-) diff --git a/app/models/changeset.rb b/app/models/changeset.rb index dc6c0e014..446ca351d 100644 --- a/app/models/changeset.rb +++ b/app/models/changeset.rb @@ -40,7 +40,6 @@ class Changeset < ActiveRecord::Base end end rescue Exception => ex - print "noes "+ ex.to_s + "\n" cs = nil end diff --git a/test/functional/api_controller_test.rb b/test/functional/api_controller_test.rb index 3e86affae..e9d1632da 100644 --- a/test/functional/api_controller_test.rb +++ b/test/functional/api_controller_test.rb @@ -7,8 +7,6 @@ class ApiController; def rescue_action(e) raise e end; end class ApiControllerTest < Test::Unit::TestCase api_fixtures - - def setup @controller = ApiController.new @request = ActionController::TestRequest.new From 6339473cb77e4e0998b959a0835a8f42767f608d Mon Sep 17 00:00:00 2001 From: Matt Amos Date: Tue, 21 Oct 2008 10:52:52 +0000 Subject: [PATCH 136/381] Added tests for ways_for_node. Altered behaviour to not return deleted ways. Fixed ways fixture where most recent history != current. --- app/controllers/way_controller.rb | 10 ++++- app/models/way.rb | 2 + test/fixtures/ways.yml | 2 +- test/functional/old_node_controller_test.rb | 14 ------- test/functional/old_way_controller_test.rb | 12 ------ test/functional/way_controller_test.rb | 45 ++++++++++++++------- test/test_helper.rb | 28 +++++++++++++ 7 files changed, 70 insertions(+), 43 deletions(-) diff --git a/app/controllers/way_controller.rb b/app/controllers/way_controller.rb index b00658cf0..25d535d79 100644 --- a/app/controllers/way_controller.rb +++ b/app/controllers/way_controller.rb @@ -125,13 +125,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" diff --git a/app/models/way.rb b/app/models/way.rb index 591dee9a2..1304d8a18 100644 --- a/app/models/way.rb +++ b/app/models/way.rb @@ -51,6 +51,8 @@ class Way < ActiveRecord::Base if pt['timestamp'] way.timestamp = Time.parse(pt['timestamp']) end + # if visible isn't present then it defaults to true + way.visible = (pt['visible'] or true) end pt.find('tag').each do |tag| diff --git a/test/fixtures/ways.yml b/test/fixtures/ways.yml index c1ce7222b..4f0af9854 100644 --- a/test/fixtures/ways.yml +++ b/test/fixtures/ways.yml @@ -16,7 +16,7 @@ used_way: id: 3 changeset_id: 1 timestamp: 2007-01-01 00:00:00 - visible: 0 + visible: 1 version: 1 way_with_versions_v1: diff --git a/test/functional/old_node_controller_test.rb b/test/functional/old_node_controller_test.rb index ca9a114b1..cdacca52b 100644 --- a/test/functional/old_node_controller_test.rb +++ b/test/functional/old_node_controller_test.rb @@ -112,20 +112,6 @@ class OldNodeControllerTest < Test::Unit::TestCase assert_nodes_are_equal current_node, old_node 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 - ## # 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. diff --git a/test/functional/old_way_controller_test.rb b/test/functional/old_way_controller_test.rb index b357b0987..c47b45dfc 100644 --- a/test/functional/old_way_controller_test.rb +++ b/test/functional/old_way_controller_test.rb @@ -95,16 +95,4 @@ class OldWayControllerTest < Test::Unit::TestCase 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_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}" - 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 - end diff --git a/test/functional/way_controller_test.rb b/test/functional/way_controller_test.rb index 0eb77c2e4..895e7532f 100644 --- a/test/functional/way_controller_test.rb +++ b/test/functional/way_controller_test.rb @@ -37,21 +37,11 @@ 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 - # Needs to be updated when changing fixtures - # The generator should probably be defined in the environment.rb file - # in the same place as the api version - assert_select "osm[version=#{API_VERSION}][generator=\"OpenStreetMap server\"]", 1 - assert_select "osm way", 3 - assert_select "osm way nd", 3 - assert_select "osm way tag", 3 - - # check the "full" mode + ## + # check the "full" mode + def test_full get :full, :id => current_ways(:visible_way).id assert_response :success # FIXME check whether this contains the stuff we want! @@ -290,6 +280,33 @@ class WayControllerTest < Test::Unit::TestCase end 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) diff --git a/test/test_helper.rb b/test/test_helper.rb index 63567cc9b..713bbf648 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -72,5 +72,33 @@ class Test::Unit::TestCase 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 + # Add more helper methods to be used by all tests here... end From 498d695064b7b856d5e65fcc9e888d17827d004c Mon Sep 17 00:00:00 2001 From: Matt Amos Date: Tue, 21 Oct 2008 11:38:13 +0000 Subject: [PATCH 137/381] Added test for relations_for_node where the relation had been deleted. --- app/controllers/relation_controller.rb | 2 +- test/fixtures/current_relation_members.yml | 6 ++++ test/functional/relation_controller_test.rb | 38 ++++++++++++++++----- 3 files changed, 36 insertions(+), 10 deletions(-) diff --git a/app/controllers/relation_controller.rb b/app/controllers/relation_controller.rb index 7ce58dae6..da5129467 100644 --- a/app/controllers/relation_controller.rb +++ b/app/controllers/relation_controller.rb @@ -185,7 +185,7 @@ class RelationController < ApplicationController 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" diff --git a/test/fixtures/current_relation_members.yml b/test/fixtures/current_relation_members.yml index bddc8a0dd..5696a365f 100644 --- a/test/fixtures/current_relation_members.yml +++ b/test/fixtures/current_relation_members.yml @@ -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 diff --git a/test/functional/relation_controller_test.rb b/test/functional/relation_controller_test.rb index ffd65f6cc..939a73df1 100644 --- a/test/functional/relation_controller_test.rb +++ b/test/functional/relation_controller_test.rb @@ -37,18 +37,34 @@ 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 + ## + # 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 + node_id = current_nodes(:node_used_by_relationship).id + + # fetch all the relations which contain that node + get :relations_for_node, :id => node_id assert_response :success - # FIXME check whether this contains the stuff we want! - # see the test_read in way_controller_test.rb for the assert_select - assert_select "osm[version=#{API_VERSION}][generator=\"OpenStreetMap server\"]", 1 - assert_select "osm relation" - if $VERBOSE - print @response.body - end + # count one osm element + assert_select "osm[version=#{API_VERSION}][generator=\"OpenStreetMap server\"]", 1 + + # we should have only two relations + assert_select "osm>relation", 2 + + # and each of them should contain the node we originally searched for + [ :visible_relation, + :used_relation ].each do |r| + relation_id = current_relations(r).id + assert_select "osm>relation#?", relation_id + assert_select "osm>relation#?>member[type=\"node\"][ref=#{node_id}]", relation_id + end + end + + def test_relations_for_way # check the "relations for way" mode get :relations_for_way, :id => current_ways(:used_way).id assert_response :success @@ -56,7 +72,9 @@ class RelationControllerTest < Test::Unit::TestCase if $VERBOSE print @response.body end + end + def test_relations_for_relation # check the "relations for relation" mode get :relations_for_relation, :id => current_relations(:used_relation).id assert_response :success @@ -64,7 +82,9 @@ class RelationControllerTest < Test::Unit::TestCase if $VERBOSE print @response.body end + end + def test_full # check the "full" mode get :full, :id => current_relations(:visible_relation).id assert_response :success From 5dd3eb769a71784f52c977fec73a28b10cbcb7f5 Mon Sep 17 00:00:00 2001 From: Matt Amos Date: Tue, 21 Oct 2008 11:40:06 +0000 Subject: [PATCH 138/381] Refactored test a bit to make it nicer. --- test/functional/relation_controller_test.rb | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/test/functional/relation_controller_test.rb b/test/functional/relation_controller_test.rb index 939a73df1..1c6439a96 100644 --- a/test/functional/relation_controller_test.rb +++ b/test/functional/relation_controller_test.rb @@ -49,15 +49,17 @@ class RelationControllerTest < Test::Unit::TestCase get :relations_for_node, :id => node_id assert_response :success + # the results we expect + expected_relations = [ :visible_relation, :used_relation ] + # count one osm element assert_select "osm[version=#{API_VERSION}][generator=\"OpenStreetMap server\"]", 1 - # we should have only two relations - assert_select "osm>relation", 2 + # 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 - [ :visible_relation, - :used_relation ].each do |r| + expected_relations.each do |r| relation_id = current_relations(r).id assert_select "osm>relation#?", relation_id assert_select "osm>relation#?>member[type=\"node\"][ref=#{node_id}]", relation_id From 417ded9da545299026b96f5f81f79897f8e3769b Mon Sep 17 00:00:00 2001 From: Matt Amos Date: Tue, 21 Oct 2008 14:00:08 +0000 Subject: [PATCH 139/381] Added min_lat > max_lat test. --- test/functional/api_controller_test.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/functional/api_controller_test.rb b/test/functional/api_controller_test.rb index e9d1632da..4cd20d3dd 100644 --- a/test/functional/api_controller_test.rb +++ b/test/functional/api_controller_test.rb @@ -14,7 +14,7 @@ class ApiControllerTest < Test::Unit::TestCase @badbigbbox = %w{ -0.1,-0.1,1.1,1.1 10,10,11,11 } @badmalformedbbox = %w{ -0.1 hello S0.1,W0.1,N0.1,E0.1 10N2W10.1N2.1W } - @badlatmixedbbox = %w{} + @badlatmixedbbox = %w{ 0,0.1,0.1,0 } @badlonmixedbbox = %w{} @badlatlonoutboundsbbox = %w{ -190.2,-190.2,-190.1,-190.1 -190.1,89.9,-190,90 } @goodbbox = %w{ -0.1,-0.1,0.1,0.1 51.1,-0.1,51.2,0 From e13b35e0cbf6b1c0ec429ed61ee6b9d8c4de176c Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Tue, 21 Oct 2008 14:28:52 +0000 Subject: [PATCH 140/381] making it clearer when a test case fails, as to which test case it is that is failing. Adding some more bboxes for testing. --- lib/map_boundary.rb | 1 + test/functional/api_controller_test.rb | 30 +++++++++++++------------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/lib/map_boundary.rb b/lib/map_boundary.rb index 9b39c9342..da7e44b32 100644 --- a/lib/map_boundary.rb +++ b/lib/map_boundary.rb @@ -17,6 +17,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 diff --git a/test/functional/api_controller_test.rb b/test/functional/api_controller_test.rb index 4cd20d3dd..f51bdc7b6 100644 --- a/test/functional/api_controller_test.rb +++ b/test/functional/api_controller_test.rb @@ -11,14 +11,14 @@ class ApiControllerTest < Test::Unit::TestCase @controller = ApiController.new @request = ActionController::TestRequest.new @response = ActionController::TestResponse.new - @badbigbbox = %w{ -0.1,-0.1,1.1,1.1 10,10,11,11 } + @badbigbbox = %w{ -0.1,-0.1,1.1,1.1 10,10,11,11 } @badmalformedbbox = %w{ -0.1 hello S0.1,W0.1,N0.1,E0.1 10N2W10.1N2.1W } - @badlatmixedbbox = %w{ 0,0.1,0.1,0 } - @badlonmixedbbox = %w{} - @badlatlonoutboundsbbox = %w{ -190.2,-190.2,-190.1,-190.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 } + @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 } end def basic_authorization(user, pass) @@ -58,7 +58,7 @@ class ApiControllerTest < Test::Unit::TestCase ["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 + 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 @@ -66,11 +66,11 @@ class ApiControllerTest < Test::Unit::TestCase -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 + 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 + assert_response :success, "The page number was #{i} and should have been accepted" end end @@ -79,7 +79,7 @@ class ApiControllerTest < Test::Unit::TestCase [ "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 + 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 @@ -89,7 +89,7 @@ class ApiControllerTest < Test::Unit::TestCase [ "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 + 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 @@ -99,7 +99,7 @@ class ApiControllerTest < Test::Unit::TestCase [ "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 + assert_equal "The minimum longitude must be less than the maximum longitude, but it wasn't", @response.body, "bbox: #{bbox}" end end end @@ -109,7 +109,7 @@ class ApiControllerTest < Test::Unit::TestCase ["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 + assert_equal "The minimum latitude must be less than the maximum latitude, but it wasn't", @response.body, "bbox: #{bbox}" end end end @@ -119,8 +119,8 @@ class ApiControllerTest < Test::Unit::TestCase [ "trackpoints", "map" ].each do |tq| get tq, :bbox => bbox #print @request.to_yaml - assert_response :bad_request, "The 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 + 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 From 6df7fdbb9eb563f8ccf68f24c3d210b27c93cd83 Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Tue, 21 Oct 2008 16:18:18 +0000 Subject: [PATCH 141/381] Adding new browse controller test, which is mostly stubs just now. Also adding the version number of the recently changed nodes in the data browser. --- app/views/browse/index.rhtml | 4 +- test/functional/browse_controller_test.rb | 77 +++++++++++++++++++++++ 2 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 test/functional/browse_controller_test.rb diff --git a/app/views/browse/index.rhtml b/app/views/browse/index.rhtml index 2cd5cc9da..e9d830a10 100644 --- a/app/views/browse/index.rhtml +++ b/app/views/browse/index.rhtml @@ -1,11 +1,11 @@

<%= @nodes.length %> Recently Changed Nodes

-
    +
      <% @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})" %>
    • <%= link_to h(name), :action => "node", :id => node.id %>
    • <% end %> diff --git a/test/functional/browse_controller_test.rb b/test/functional/browse_controller_test.rb new file mode 100644 index 000000000..81dbede57 --- /dev/null +++ b/test/functional/browse_controller_test.rb @@ -0,0 +1,77 @@ +require File.dirname(__FILE__) + '/../test_helper' +require 'browse_controller' + +# Re-raise errors caught by the controller. +class BrowseController; def rescue_action(e) raise e end; end + + class BrowseControllerTest < Test::Unit::TestCase + api_fixtures + + + + def setup + @controller = BrowseController.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.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 From 136c5a7bf1e850d188a86cfe65ace4c4c457fcb8 Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Wed, 22 Oct 2008 09:40:16 +0000 Subject: [PATCH 142/381] Fixing the error messages --- db/migrate/017_populate_node_tags_and_remove_helper.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/db/migrate/017_populate_node_tags_and_remove_helper.c b/db/migrate/017_populate_node_tags_and_remove_helper.c index 5a0fbb6cd..12203d9cf 100644 --- a/db/migrate/017_populate_node_tags_and_remove_helper.c +++ b/db/migrate/017_populate_node_tags_and_remove_helper.c @@ -7,9 +7,9 @@ static void exit_mysql_err(MYSQL *mysql) { const char *err = mysql_error(mysql); if (err) { - fprintf(stderr, "013_populate_node_tags_and_remove_helper: MySQL error: %s\n", err); + fprintf(stderr, "017_populate_node_tags_and_remove_helper: MySQL error: %s\n", err); } else { - fprintf(stderr, "013_populate_node_tags_and_remove_helper: MySQL error\n"); + fprintf(stderr, "017_populate_node_tags_and_remove_helper: MySQL error\n"); } abort(); exit(EXIT_FAILURE); @@ -192,7 +192,7 @@ int main(int argc, char **argv) { struct data data, *d = &data; if (argc != 8) { - printf("Usage: 013_populate_node_tags_and_remove_helper host user passwd database port socket prefix\n"); + printf("Usage: 017_populate_node_tags_and_remove_helper host user passwd database port socket prefix\n"); exit(EXIT_FAILURE); } From dc2a95903722644f6109244cec9a2d1e195fef0e Mon Sep 17 00:00:00 2001 From: Matt Amos Date: Thu, 23 Oct 2008 15:14:17 +0000 Subject: [PATCH 143/381] Added tests for changeset upload code. Refactored diff reading code and put it into /lib. Changed the route of a changeset upload to explicitly refer to the changeset it applies to (i.e: resource). --- app/controllers/changeset_controller.rb | 114 ++----- app/models/node.rb | 16 +- app/models/relation.rb | 18 + app/models/way.rb | 17 + config/routes.rb | 3 +- lib/diff_reader.rb | 167 ++++++++++ lib/osm.rb | 26 ++ test/functional/changeset_controller_test.rb | 331 ++++++++++++++++++- 8 files changed, 596 insertions(+), 96 deletions(-) create mode 100644 lib/diff_reader.rb diff --git a/app/controllers/changeset_controller.rb b/app/controllers/changeset_controller.rb index 8151d3a67..a24f61e28 100644 --- a/app/controllers/changeset_controller.rb +++ b/app/controllers/changeset_controller.rb @@ -2,6 +2,7 @@ class ChangesetController < ApplicationController require 'xml/libxml' + require 'diff_reader' before_filter :authorize, :only => [:create, :update, :delete, :upload] before_filter :check_write_availability, :only => [:create, :update, :delete, :upload] @@ -70,102 +71,37 @@ class ChangesetController < ApplicationController end 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: ??? the new document? updated diffs? def upload - unless request.put? + # 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 - p = XML::Reader.new request.raw_post - - node_ids, way_ids, rel_ids = {}, {}, {} - ids = {"node"=>node_ids, "way"=>way_ids, "relation"=>rel_ids} - - models = {"node"=>Node, "way"=>Way, "relation"=>Relation} - - # FIXME shouldn't this be done through the - # res = OSM::API.new.get_xml_doc - # as everything else is? - res = XML::Document.new - res.encoding = 'UTF-8' - root = XML::Node.new 'osm' - root['version'] = API_VERSION - root['creator'] = 'OpenStreetMap.org' - res.root = root - - root << XML::Node.new_comment(" Warning: this is a 0.6 result document, " + - "not a normal OSM file. ") - + changeset = Changeset.find(params[:id]) + + diff_reader = DiffReader.new(request.raw_post, changeset) Changeset.transaction do - while p.read == 1 - break if p.node_type == 15 # end element - next unless p.node_type == 1 # element - - case p.name - when 'create': - while p.read == 1 - break if p.node_type == 15 # end element - next unless p.node_type == 1 # element - - model = models[p.name] - next if model.nil? - - elem = XML::Node.new p.name - nd = p.expand; p.next - osm = model.from_xml_node(nd, true) - elem['old_id'] = nd['id'] - - case nd.name - when 'way': - fix_way(osm, node_ids) - raise OSM::APIPreconditionFailedError.new if !osm.preconditions_ok? - when 'relation': - fix_rel(osm, ids) - raise OSM::APIPreconditionFailedError.new if !osm.preconditions_ok? - end - - create_prim ids[nd.name], osm, nd - elem['new_id'] = osm.id.to_s - elem['new_version'] = osm.version.to_s - root << elem - end - when 'modify': - while p.read == 1 - break if p.node_type == 15 # end element - next unless p.node_type == 1 # element - - model = models[p.name] - next if model.nil? - - elem = XML::Node.new p.name - new_osm = model.from_xml_node(p.expand); p.next - osm = model.find(new_osm.id) - osm.update_from new_osm, @user - elem['old_id'] = elem['new_id'] = osm.id.to_s - elem['new_version'] = osm.version.to_s - root << elem - end - when 'delete': - while p.read == 1 - break if p.node_type == 15 # end element - next unless p.node_type == 1 # element - - model = models[p.name] - next if model.nil? - - elem = XML::Node.new p.name - osm = model.find(p.expand['id']); p.next - osm.delete_with_history(@user) - elem['old_id'] = elem['new_id'] = osm.id.to_s - elem['new_version'] = osm.version.to_s - root << elem - end - end - end + result = diff_reader.commit + render :text => result.to_s, :content_type => "text/xml" end - - render :text => res.to_s, :content_type => "text/xml" - + + rescue ActiveRecord::RecordNotFound + render :nothing => true, :status => :not_found rescue OSM::APIError => ex render ex.render_opts end diff --git a/app/models/node.rb b/app/models/node.rb index c2a61906b..67efeca2c 100644 --- a/app/models/node.rb +++ b/app/models/node.rb @@ -75,20 +75,25 @@ class Node < ActiveRecord::Base def self.from_xml_node(pt, create=false) node = Node.new - node.version = pt['version'].to_i node.lat = pt['lat'].to_f node.lon = pt['lon'].to_f node.changeset_id = pt['changeset'].to_i return nil unless node.in_world? + # version must be present unless creating + return nil unless create or not pt['version'].nil? + node.version = pt['version'].to_i + unless create if pt['id'] != '0' node.id = pt['id'].to_i end end - node.visible = pt['visible'] and pt['visible'] == 'true' + # visible if it says it is, or as the default if the attribute + # is missing. + node.visible = pt['visible'].nil? or pt['visible'] == 'true' if create node.timestamp = Time.now @@ -235,4 +240,11 @@ class Node < ActiveRecord::Base @tags[k] = v 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 + end diff --git a/app/models/relation.rb b/app/models/relation.rb index db4dd52a6..81f178997 100644 --- a/app/models/relation.rb +++ b/app/models/relation.rb @@ -320,4 +320,22 @@ class Relation < ActiveRecord::Base 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 + end diff --git a/app/models/way.rb b/app/models/way.rb index 1304d8a18..b413ccb28 100644 --- a/app/models/way.rb +++ b/app/models/way.rb @@ -299,4 +299,21 @@ 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 + end diff --git a/config/routes.rb b/config/routes.rb index e49cdf74e..39e2a1e74 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -4,7 +4,7 @@ ActionController::Routing::Routes.draw do |map| 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/upload", :controller => 'changeset', :action => 'upload' + map.connect "api/#{API_VERSION}/changeset/:id/upload", :controller => 'changeset', :action => 'upload', :id => /\d+/ map.connect "api/#{API_VERSION}/changeset/:id", :controller => 'changeset', :action => 'read', :id => /\d+/ map.connect "api/#{API_VERSION}/changeset/:id/close", :controller => 'changeset', :action => 'close', :id =>/\d+/ @@ -22,6 +22,7 @@ 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 } diff --git a/lib/diff_reader.rb b/lib/diff_reader.rb new file mode 100644 index 000000000..eca6d438c --- /dev/null +++ b/lib/diff_reader.rb @@ -0,0 +1,167 @@ +## +# 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 + + # loop at the top level, within the 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) + + # set the initial version to zero and save (which increments it) + new.version = 0 + new.save_with_history! + + # 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 + xml_result["new_version"] = new.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 "Unknown action #{action_name}, choices are create, modify, delete." + end + end + + # return the XML document to be rendered back to the client + return result + end + +end diff --git a/lib/osm.rb b/lib/osm.rb index e9d3c9464..246fedf54 100644 --- a/lib/osm.rb +++ b/lib/osm.rb @@ -54,6 +54,32 @@ module OSM 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 } + end + end + + # Raised when bad XML is encountered which stops things parsing as + # they should. + class APIBadXMLError < APIError + def initialize(model, xml) + @model, @xml = model, xml + end + + def render_opts + { :text => "Cannot parse valid #{@model} from xml string #{@xml}", + :status => :bad_request } + end + end + # Raised when the provided version is not equal to the latest in the db. class APIVersionMismatchError < APIError def initialize(provided, latest) diff --git a/test/functional/changeset_controller_test.rb b/test/functional/changeset_controller_test.rb index 1e050a71e..946d139d8 100644 --- a/test/functional/changeset_controller_test.rb +++ b/test/functional/changeset_controller_test.rb @@ -44,6 +44,7 @@ class ChangesetController; def rescue_action(e) raise e end; end basic_authorization "test@openstreetmap.org", "test" content "" put :create + assert_response :bad_request, "creating a invalid changeset should fail" end def test_read @@ -53,9 +54,331 @@ class ChangesetController; def rescue_action(e) raise e end; end def test_close end - - def test_upload - + + ## + # 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 + + # 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 + + # 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 "osm[version=#{API_VERSION}][generator=\"OpenStreetMap server\"]", 1 + assert_select "osm>node", 1 + assert_select "osm>way", 1 + assert_select "osm>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("//osm/node").first["new_id"].to_i + new_way_id = doc.find("//osm/way").first["new_id"].to_i + new_rel_id = doc.find("//osm/relation").first["new_id"].to_i + + # check the old IDs are all present and negative one + assert_equal -1, doc.find("//osm/node").first["old_id"].to_i + assert_equal -1, doc.find("//osm/way").first["old_id"].to_i + assert_equal -1, doc.find("//osm/relation").first["old_id"].to_i + + # check the versions are present and equal one + assert_equal 1, doc.find("//osm/node").first["new_version"].to_i + assert_equal 1, doc.find("//osm/way").first["new_version"].to_i + assert_equal 1, doc.find("//osm/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 + + # 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 "osm[version=#{API_VERSION}][generator=\"OpenStreetMap server\"]", 1 + assert_select "osm>node", 1 + assert_select "osm>way", 1 + assert_select "osm>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("//osm/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 + # 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 + + # 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 + + # 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 + + # 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 + end From b45dd632836632b596cdfae8d73cb8c53a47c20a Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Sun, 26 Oct 2008 23:43:37 +0000 Subject: [PATCH 144/381] cover the other extreme in the map bounary sanitizeation. Ading some documentation, which is parsed when running rake doc:app. Fixing up a couple of tests. --- app/controllers/api_controller.rb | 19 ++++++++++++++++- app/controllers/message_controller.rb | 8 +++++++ doc/README_FOR_APP | 18 +++++++++++----- lib/map_boundary.rb | 11 +++++----- test/functional/api_controller_test.rb | 29 ++++++++++++++------------ 5 files changed, 61 insertions(+), 24 deletions(-) diff --git a/app/controllers/api_controller.rb b/app/controllers/api_controller.rb index f7c9f591f..c337038a0 100644 --- a/app/controllers/api_controller.rb +++ b/app/controllers/api_controller.rb @@ -16,7 +16,8 @@ class ApiController < ApplicationController # Number of GPS trace/trackpoints returned per-page 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 @@ -205,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 @@ -250,6 +262,11 @@ class ApiController < ApplicationController 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 diff --git a/app/controllers/message_controller.rb b/app/controllers/message_controller.rb index 85c0ac328..f3f3bf9ea 100644 --- a/app/controllers/message_controller.rb +++ b/app/controllers/message_controller.rb @@ -4,6 +4,9 @@ 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. def new @title = 'send message' if params[:message] @@ -22,6 +25,7 @@ class MessageController < ApplicationController end 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(/^/, '> ')}" @@ -32,6 +36,7 @@ class MessageController < ApplicationController render :nothing => true, :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 ]) @@ -41,6 +46,7 @@ class MessageController < ApplicationController render :nothing => true, :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 +55,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,6 +64,7 @@ class MessageController < ApplicationController end end + # Set the message as being read or unread. def mark if params[:message_id] id = params[:message_id] diff --git a/doc/README_FOR_APP b/doc/README_FOR_APP index 129a6f24b..6c4e6b0fc 100644 --- a/doc/README_FOR_APP +++ b/doc/README_FOR_APP @@ -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/map_boundary.rb b/lib/map_boundary.rb index da7e44b32..153d65780 100644 --- a/lib/map_boundary.rb +++ b/lib/map_boundary.rb @@ -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 diff --git a/test/functional/api_controller_test.rb b/test/functional/api_controller_test.rb index f51bdc7b6..0ee49517f 100644 --- a/test/functional/api_controller_test.rb +++ b/test/functional/api_controller_test.rb @@ -12,13 +12,15 @@ class ApiControllerTest < Test::Unit::TestCase @request = ActionController::TestRequest.new @response = ActionController::TestResponse.new @badbigbbox = %w{ -0.1,-0.1,1.1,1.1 10,10,11,11 } - @badmalformedbbox = %w{ -0.1 hello S0.1,W0.1,N0.1,E0.1 + @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 } + #@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 } + -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) @@ -114,16 +116,17 @@ class ApiControllerTest < Test::Unit::TestCase end end - 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 + # 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 def test_capabilities get :capabilities From 46d529135013091286f3931223b387c571aecdb9 Mon Sep 17 00:00:00 2001 From: Matt Amos Date: Mon, 27 Oct 2008 16:48:09 +0000 Subject: [PATCH 145/381] Updated comment to reflect implementation. --- app/controllers/changeset_controller.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/controllers/changeset_controller.rb b/app/controllers/changeset_controller.rb index a24f61e28..bb8c3cef2 100644 --- a/app/controllers/changeset_controller.rb +++ b/app/controllers/changeset_controller.rb @@ -81,7 +81,8 @@ class ChangesetController < ApplicationController # Furthermore, each element in the diff can only reference the current # changeset. # - # Returns: ??? the new document? updated diffs? + # 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 From 8a57904398175d9143cec737282f5066793bdbc3 Mon Sep 17 00:00:00 2001 From: Matt Amos Date: Mon, 27 Oct 2008 17:50:28 +0000 Subject: [PATCH 146/381] Implemented osmChange diff downloads for changesets and a couple of tests. --- app/controllers/changeset_controller.rb | 73 ++++++++++++++ config/routes.rb | 1 + test/functional/changeset_controller_test.rb | 100 +++++++++++++++++++ 3 files changed, 174 insertions(+) diff --git a/app/controllers/changeset_controller.rb b/app/controllers/changeset_controller.rb index bb8c3cef2..7ac4eb91a 100644 --- a/app/controllers/changeset_controller.rb +++ b/app/controllers/changeset_controller.rb @@ -106,4 +106,77 @@ class ChangesetController < ApplicationController 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 + end diff --git a/config/routes.rb b/config/routes.rb index 39e2a1e74..139611bc6 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -5,6 +5,7 @@ ActionController::Routing::Routes.draw do |map| 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", :controller => 'changeset', :action => 'read', :id => /\d+/ map.connect "api/#{API_VERSION}/changeset/:id/close", :controller => 'changeset', :action => 'close', :id =>/\d+/ diff --git a/test/functional/changeset_controller_test.rb b/test/functional/changeset_controller_test.rb index 946d139d8..848b6d5f5 100644 --- a/test/functional/changeset_controller_test.rb +++ b/test/functional/changeset_controller_test.rb @@ -381,4 +381,104 @@ EOF "shouldn't be able to upload an element without version: #{@response.body}" 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 "" + + "" + + "" + put :create + assert_response :success + changeset_id = @response.body.to_i + + # add a diff to it + diff = < + + + + + + + + + + + +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 + + ## + # 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 "" + + "" + + "" + put :create + assert_response :success + changeset_id = @response.body.to_i + + # add a diff to it + diff = < + + + + + + + + + + + + + + + + + + +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 + end From e18c33f733007e898786ee63c7f40a854b4772c6 Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Mon, 27 Oct 2008 18:45:28 +0000 Subject: [PATCH 147/381] Prepare for Rails 2.1+, as we will no longer be able to use a symbol in the set_fixture_class method, instead we must use a Class. This change doesn't affect Rails 2.0.2, as currently used. All tests still work. :-) --- test/test_helper.rb | 33 +++++++++++++++--------------- test/unit/current_node_tag_test.rb | 4 ++-- test/unit/node_test.rb | 8 ++++---- test/unit/old_node_test.rb | 8 ++++---- 4 files changed, 27 insertions(+), 26 deletions(-) diff --git a/test/test_helper.rb b/test/test_helper.rb index 713bbf648..a170aed4f 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -27,35 +27,36 @@ class Test::Unit::TestCase # Load standard fixtures needed to test API methods def self.api_fixtures + print "setting up the api_fixtures" fixtures :users, :changesets 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 + 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 end ## diff --git a/test/unit/current_node_tag_test.rb b/test/unit/current_node_tag_test.rb index 98bceb4e5..143fa2442 100644 --- a/test/unit/current_node_tag_test.rb +++ b/test/unit/current_node_tag_test.rb @@ -2,8 +2,8 @@ require File.dirname(__FILE__) + '/../test_helper' class CurrentNodeTagTest < Test::Unit::TestCase fixtures :current_node_tags, :current_nodes - set_fixture_class :current_nodes => :Node - set_fixture_class :current_node_tags => :NodeTag + set_fixture_class :current_nodes => Node + set_fixture_class :current_node_tags => NodeTag def test_tag_count assert_equal 6, NodeTag.count diff --git a/test/unit/node_test.rb b/test/unit/node_test.rb index 7efbf9c06..2c6515cb7 100644 --- a/test/unit/node_test.rb +++ b/test/unit/node_test.rb @@ -2,10 +2,10 @@ require File.dirname(__FILE__) + '/../test_helper' class NodeTest < Test::Unit::TestCase 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 + 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) diff --git a/test/unit/old_node_test.rb b/test/unit/old_node_test.rb index 5623e9657..bdd685388 100644 --- a/test/unit/old_node_test.rb +++ b/test/unit/old_node_test.rb @@ -1,11 +1,11 @@ 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 - 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) From 427becc1db048c05576887e74b26c4118fa32d77 Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Tue, 28 Oct 2008 12:27:10 +0000 Subject: [PATCH 148/381] Moving all the functional tests to the Rails 2 way, which uses less code by having an additional sub-class to take care of the duplicate code in every test. There is a bug in rails 2.0.2, which means that super doesn't get called in the setup method in the functional tests. This is fixed in Rails 2.1.2, thus a version bump should really be done. --- test/functional/api_controller_test.rb | 9 ++------- test/functional/browse_controller_test.rb | 13 +------------ test/functional/changeset_controller_test.rb | 13 +------------ test/functional/geocoder_controller_test.rb | 10 +--------- test/functional/message_controller_test.rb | 10 +--------- test/functional/node_controller_test.rb | 11 +---------- test/functional/old_node_controller_test.rb | 11 +---------- test/functional/old_relation_controller_test.rb | 12 +----------- test/functional/old_way_controller_test.rb | 11 +---------- test/functional/relation_controller_test.rb | 11 +---------- test/functional/way_controller_test.rb | 11 +---------- test/test_helper.rb | 2 +- 12 files changed, 13 insertions(+), 111 deletions(-) diff --git a/test/functional/api_controller_test.rb b/test/functional/api_controller_test.rb index 0ee49517f..b6bd2bf1d 100644 --- a/test/functional/api_controller_test.rb +++ b/test/functional/api_controller_test.rb @@ -1,16 +1,11 @@ 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 } diff --git a/test/functional/browse_controller_test.rb b/test/functional/browse_controller_test.rb index 81dbede57..65e851011 100644 --- a/test/functional/browse_controller_test.rb +++ b/test/functional/browse_controller_test.rb @@ -1,19 +1,8 @@ require File.dirname(__FILE__) + '/../test_helper' require 'browse_controller' -# Re-raise errors caught by the controller. -class BrowseController; def rescue_action(e) raise e end; end - - class BrowseControllerTest < Test::Unit::TestCase +class BrowseControllerTest < ActionController::TestCase api_fixtures - - - - def setup - @controller = BrowseController.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}") diff --git a/test/functional/changeset_controller_test.rb b/test/functional/changeset_controller_test.rb index 848b6d5f5..5e26c2071 100644 --- a/test/functional/changeset_controller_test.rb +++ b/test/functional/changeset_controller_test.rb @@ -1,19 +1,8 @@ require File.dirname(__FILE__) + '/../test_helper' require 'changeset_controller' -# Re-raise errors caught by the controller. -class ChangesetController; def rescue_action(e) raise e end; end - - class ChangesetControllerTest < Test::Unit::TestCase +class ChangesetControllerTest < ActionController::TestCase api_fixtures - - - - def setup - @controller = ChangesetController.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}") diff --git a/test/functional/geocoder_controller_test.rb b/test/functional/geocoder_controller_test.rb index 3faadc740..f63fe518d 100644 --- a/test/functional/geocoder_controller_test.rb +++ b/test/functional/geocoder_controller_test.rb @@ -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 diff --git a/test/functional/message_controller_test.rb b/test/functional/message_controller_test.rb index 54c8a18d1..96f509cb0 100644 --- a/test/functional/message_controller_test.rb +++ b/test/functional/message_controller_test.rb @@ -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 diff --git a/test/functional/node_controller_test.rb b/test/functional/node_controller_test.rb index dbc00cbb2..9e5621f8b 100644 --- a/test/functional/node_controller_test.rb +++ b/test/functional/node_controller_test.rb @@ -1,18 +1,9 @@ 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(users(:normal_user).email, "test"); diff --git a/test/functional/old_node_controller_test.rb b/test/functional/old_node_controller_test.rb index cdacca52b..f1328e650 100644 --- a/test/functional/old_node_controller_test.rb +++ b/test/functional/old_node_controller_test.rb @@ -1,18 +1,9 @@ require File.dirname(__FILE__) + '/../test_helper' require 'old_node_controller' -# Re-raise errors caught by the controller. -class OldNodeController; def rescue_action(e) raise e end; end - -class OldNodeControllerTest < Test::Unit::TestCase +class OldNodeControllerTest < ActionController::TestCase api_fixtures - def setup - @controller = OldNodeController.new - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new - end - # # TODO: test history # diff --git a/test/functional/old_relation_controller_test.rb b/test/functional/old_relation_controller_test.rb index b8bf464b6..a52211e2e 100644 --- a/test/functional/old_relation_controller_test.rb +++ b/test/functional/old_relation_controller_test.rb @@ -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 diff --git a/test/functional/old_way_controller_test.rb b/test/functional/old_way_controller_test.rb index c47b45dfc..31da1d2c7 100644 --- a/test/functional/old_way_controller_test.rb +++ b/test/functional/old_way_controller_test.rb @@ -1,18 +1,9 @@ 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. # ------------------------------------- diff --git a/test/functional/relation_controller_test.rb b/test/functional/relation_controller_test.rb index 1c6439a96..812b35fb1 100644 --- a/test/functional/relation_controller_test.rb +++ b/test/functional/relation_controller_test.rb @@ -1,18 +1,9 @@ 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 - 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 diff --git a/test/functional/way_controller_test.rb b/test/functional/way_controller_test.rb index 895e7532f..19b7fd54c 100644 --- a/test/functional/way_controller_test.rb +++ b/test/functional/way_controller_test.rb @@ -1,18 +1,9 @@ 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 diff --git a/test/test_helper.rb b/test/test_helper.rb index a170aed4f..c01c31fca 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -27,7 +27,7 @@ class Test::Unit::TestCase # Load standard fixtures needed to test API methods def self.api_fixtures - print "setting up the api_fixtures" + #print "setting up the api_fixtures" fixtures :users, :changesets fixtures :current_nodes, :nodes From 79284559afc15f462b0a93e3ff581b350728bc70 Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Tue, 28 Oct 2008 12:49:51 +0000 Subject: [PATCH 149/381] Adding stub functional test files for all the controllers that should have tests written for them. --- test/functional/amf_controller_test.rb | 8 ++++++++ test/functional/changeset_tag_controller_test.rb | 8 ++++++++ test/functional/diary_entry_controller_test.rb | 8 ++++++++ test/functional/export_controller_test.rb | 8 ++++++++ test/functional/friend_controller_test.rb | 8 ++++++++ test/functional/search_controller_test.rb | 8 ++++++++ test/functional/site_controller_test.rb | 8 ++++++++ test/functional/swf_controller_test.rb | 8 ++++++++ test/functional/trace_controller_test.rb | 8 ++++++++ test/functional/user_controller_test.rb | 8 ++++++++ 10 files changed, 80 insertions(+) create mode 100644 test/functional/amf_controller_test.rb create mode 100644 test/functional/changeset_tag_controller_test.rb create mode 100644 test/functional/diary_entry_controller_test.rb create mode 100644 test/functional/export_controller_test.rb create mode 100644 test/functional/friend_controller_test.rb create mode 100644 test/functional/search_controller_test.rb create mode 100644 test/functional/site_controller_test.rb create mode 100644 test/functional/swf_controller_test.rb create mode 100644 test/functional/trace_controller_test.rb create mode 100644 test/functional/user_controller_test.rb diff --git a/test/functional/amf_controller_test.rb b/test/functional/amf_controller_test.rb new file mode 100644 index 000000000..1d17a5bea --- /dev/null +++ b/test/functional/amf_controller_test.rb @@ -0,0 +1,8 @@ +require File.dirname(__FILE__) + '/../test_helper' + +class AmfControllerTest < ActionController::TestCase + # Replace this with your real tests. + def test_truth + assert true + end +end diff --git a/test/functional/changeset_tag_controller_test.rb b/test/functional/changeset_tag_controller_test.rb new file mode 100644 index 000000000..db9710e63 --- /dev/null +++ b/test/functional/changeset_tag_controller_test.rb @@ -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 index 000000000..7ebf439f4 --- /dev/null +++ b/test/functional/diary_entry_controller_test.rb @@ -0,0 +1,8 @@ +require File.dirname(__FILE__) + '/../test_helper' + +class DiaryEntryControllerTest < ActionController::TestCase + # Replace this with your real tests. + def test_truth + assert true + end +end diff --git a/test/functional/export_controller_test.rb b/test/functional/export_controller_test.rb new file mode 100644 index 000000000..8a9794114 --- /dev/null +++ b/test/functional/export_controller_test.rb @@ -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 index 000000000..d1f0e7df0 --- /dev/null +++ b/test/functional/friend_controller_test.rb @@ -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 diff --git a/test/functional/search_controller_test.rb b/test/functional/search_controller_test.rb new file mode 100644 index 000000000..a213253ec --- /dev/null +++ b/test/functional/search_controller_test.rb @@ -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 index 000000000..39a6464b2 --- /dev/null +++ b/test/functional/site_controller_test.rb @@ -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 index 000000000..862d3a8f0 --- /dev/null +++ b/test/functional/swf_controller_test.rb @@ -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 index 000000000..6b46dbced --- /dev/null +++ b/test/functional/trace_controller_test.rb @@ -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 index 000000000..2278aed0c --- /dev/null +++ b/test/functional/user_controller_test.rb @@ -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 From c05111fdca7030045e8f0289b973fba3e3e214a1 Mon Sep 17 00:00:00 2001 From: Matt Amos Date: Tue, 28 Oct 2008 14:49:51 +0000 Subject: [PATCH 150/381] Added tests for relations for way/relation and a bit of DRYing up. --- test/functional/relation_controller_test.rb | 49 +++++++++------------ 1 file changed, 21 insertions(+), 28 deletions(-) diff --git a/test/functional/relation_controller_test.rb b/test/functional/relation_controller_test.rb index 812b35fb1..ed5951365 100644 --- a/test/functional/relation_controller_test.rb +++ b/test/functional/relation_controller_test.rb @@ -34,14 +34,27 @@ class RelationControllerTest < ActionController::TestCase # 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 - node_id = current_nodes(:node_used_by_relationship).id - - # fetch all the relations which contain that node - get :relations_for_node, :id => node_id - assert_response :success + check_relations_for_element(:relations_for_node, "node", + current_nodes(:node_used_by_relationship).id, + [ :visible_relation, :used_relation ]) + end - # the results we expect - expected_relations = [ :visible_relation, :used_relation ] + 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 method, :id => id + assert_response :success # count one osm element assert_select "osm[version=#{API_VERSION}][generator=\"OpenStreetMap server\"]", 1 @@ -53,27 +66,7 @@ class RelationControllerTest < ActionController::TestCase expected_relations.each do |r| relation_id = current_relations(r).id assert_select "osm>relation#?", relation_id - assert_select "osm>relation#?>member[type=\"node\"][ref=#{node_id}]", relation_id - end - end - - def test_relations_for_way - # 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 - end - - def test_relations_for_relation - # check the "relations for relation" mode - get :relations_for_relation, :id => current_relations(:used_relation).id - assert_response :success - # FIXME check whether this contains the stuff we want! - if $VERBOSE - print @response.body + assert_select "osm>relation#?>member[type=\"#{type}\"][ref=#{id}]", relation_id end end From 94cf6ece2dc27909f2d01af099a3960e75ed6da5 Mon Sep 17 00:00:00 2001 From: Matt Amos Date: Tue, 28 Oct 2008 15:01:17 +0000 Subject: [PATCH 151/381] Made full test a bit more thorough. --- test/functional/way_controller_test.rb | 37 +++++++++++++++++--------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/test/functional/way_controller_test.rb b/test/functional/way_controller_test.rb index 19b7fd54c..be4c41a39 100644 --- a/test/functional/way_controller_test.rb +++ b/test/functional/way_controller_test.rb @@ -33,19 +33,30 @@ class WayControllerTest < ActionController::TestCase ## # check the "full" mode def test_full - get :full, :id => current_ways(:visible_way).id - assert_response :success - # FIXME check whether this contains the stuff we want! - #print @response.body - # Check the way is correctly returned - way = current_ways(:visible_way) - assert_select "osm way[id=#{way.id}][version=#{way.version}][visible=#{way.visible}]", 1 - assert_select "osm way nd[ref=#{way.way_nodes[0].node_id}]", 1 - # Check that the node is correctly returned - nd = current_ways(:visible_way).nodes - assert_equal 1, nd.count - nda = nd[0] - assert_select "osm node[id=#{nda.id}][version=#{nda.version}][lat=#{nda.lat}][lon=#{nda.lon}]", 1 + Way.find(:all).each do |way| + get :full, :id => way.id + + # 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 # ------------------------------------- From 38f4e17865948228cd5f573d49fe2c0c28ef4cbf Mon Sep 17 00:00:00 2001 From: Andy Allan Date: Tue, 28 Oct 2008 18:34:05 +0000 Subject: [PATCH 152/381] message title shouldn't accept invalid utf-8 sequences - test currently fails --- test/unit/message_test.rb | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/test/unit/message_test.rb b/test/unit/message_test.rb index e95c698c9..ea05b1307 100644 --- a/test/unit/message_test.rb +++ b/test/unit/message_test.rb @@ -51,6 +51,21 @@ class MessageTest < Test::Unit::TestCase 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| + assert_raise(ActiveRecord::RecordInvalid) { make_message(char, 1).save! } + end + end + def make_message(char, count) message = messages(:one) message.title = char * count From 252c2f70225595312151bcf77ee7c8f5aac0c831 Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Tue, 28 Oct 2008 20:42:48 +0000 Subject: [PATCH 153/381] Updating to use Rails 2.1.2. Moving the gem dependancies to the config/environment.rb file. Moving the vendor/plugins externals into our svn. --- config/environment.rb | 12 +- config/initializers/composite_primary_keys.rb | 3 - config/initializers/libxml.rb | 7 +- vendor/plugins/deadlock_retry/README | 10 + vendor/plugins/deadlock_retry/Rakefile | 10 + vendor/plugins/deadlock_retry/init.rb | 2 + .../deadlock_retry/lib/deadlock_retry.rb | 58 ++ .../test/deadlock_retry_test.rb | 65 ++ vendor/plugins/file_column/CHANGELOG | 69 ++ vendor/plugins/file_column/README | 54 ++ vendor/plugins/file_column/Rakefile | 36 + vendor/plugins/file_column/TODO | 6 + vendor/plugins/file_column/init.rb | 13 + vendor/plugins/file_column/lib/file_column.rb | 720 ++++++++++++++++++ .../file_column/lib/file_column_helper.rb | 150 ++++ vendor/plugins/file_column/lib/file_compat.rb | 28 + .../file_column/lib/magick_file_column.rb | 260 +++++++ .../file_column/lib/rails_file_column.rb | 19 + vendor/plugins/file_column/lib/test_case.rb | 124 +++ vendor/plugins/file_column/lib/validations.rb | 112 +++ .../plugins/file_column/test/abstract_unit.rb | 63 ++ vendor/plugins/file_column/test/connection.rb | 17 + .../test/file_column_helper_test.rb | 97 +++ .../file_column/test/file_column_test.rb | 650 ++++++++++++++++ .../file_column/test/fixtures/entry.rb | 32 + .../test/fixtures/invalid-image.jpg | 1 + .../file_column/test/fixtures/kerb.jpg | Bin 0 -> 87582 bytes .../file_column/test/fixtures/mysql.sql | 25 + .../file_column/test/fixtures/schema.rb | 10 + .../file_column/test/fixtures/skanthak.png | Bin 0 -> 12629 bytes .../plugins/file_column/test/magick_test.rb | 380 +++++++++ .../file_column/test/magick_view_only_test.rb | 21 + vendor/plugins/sql_session_store/LICENSE | 20 + vendor/plugins/sql_session_store/README | 60 ++ vendor/plugins/sql_session_store/Rakefile | 22 + .../generators/sql_session_store/USAGE | 17 + .../sql_session_store_generator.rb | 25 + .../sql_session_store/templates/migration.rb | 38 + vendor/plugins/sql_session_store/init.rb | 1 + vendor/plugins/sql_session_store/install.rb | 2 + .../sql_session_store/lib/mysql_session.rb | 132 ++++ .../sql_session_store/lib/oracle_session.rb | 143 ++++ .../lib/postgresql_session.rb | 136 ++++ .../sql_session_store/lib/sql_session.rb | 27 + .../lib/sql_session_store.rb | 116 +++ .../sql_session_store/lib/sqlite_session.rb | 133 ++++ 46 files changed, 3919 insertions(+), 7 deletions(-) delete mode 100644 config/initializers/composite_primary_keys.rb create mode 100644 vendor/plugins/deadlock_retry/README create mode 100644 vendor/plugins/deadlock_retry/Rakefile create mode 100644 vendor/plugins/deadlock_retry/init.rb create mode 100644 vendor/plugins/deadlock_retry/lib/deadlock_retry.rb create mode 100644 vendor/plugins/deadlock_retry/test/deadlock_retry_test.rb create mode 100644 vendor/plugins/file_column/CHANGELOG create mode 100644 vendor/plugins/file_column/README create mode 100644 vendor/plugins/file_column/Rakefile create mode 100644 vendor/plugins/file_column/TODO create mode 100644 vendor/plugins/file_column/init.rb create mode 100644 vendor/plugins/file_column/lib/file_column.rb create mode 100644 vendor/plugins/file_column/lib/file_column_helper.rb create mode 100644 vendor/plugins/file_column/lib/file_compat.rb create mode 100644 vendor/plugins/file_column/lib/magick_file_column.rb create mode 100644 vendor/plugins/file_column/lib/rails_file_column.rb create mode 100644 vendor/plugins/file_column/lib/test_case.rb create mode 100644 vendor/plugins/file_column/lib/validations.rb create mode 100644 vendor/plugins/file_column/test/abstract_unit.rb create mode 100644 vendor/plugins/file_column/test/connection.rb create mode 100644 vendor/plugins/file_column/test/file_column_helper_test.rb create mode 100755 vendor/plugins/file_column/test/file_column_test.rb create mode 100644 vendor/plugins/file_column/test/fixtures/entry.rb create mode 100644 vendor/plugins/file_column/test/fixtures/invalid-image.jpg create mode 100644 vendor/plugins/file_column/test/fixtures/kerb.jpg create mode 100644 vendor/plugins/file_column/test/fixtures/mysql.sql create mode 100644 vendor/plugins/file_column/test/fixtures/schema.rb create mode 100644 vendor/plugins/file_column/test/fixtures/skanthak.png create mode 100644 vendor/plugins/file_column/test/magick_test.rb create mode 100644 vendor/plugins/file_column/test/magick_view_only_test.rb create mode 100644 vendor/plugins/sql_session_store/LICENSE create mode 100755 vendor/plugins/sql_session_store/README create mode 100755 vendor/plugins/sql_session_store/Rakefile create mode 100755 vendor/plugins/sql_session_store/generators/sql_session_store/USAGE create mode 100755 vendor/plugins/sql_session_store/generators/sql_session_store/sql_session_store_generator.rb create mode 100755 vendor/plugins/sql_session_store/generators/sql_session_store/templates/migration.rb create mode 100755 vendor/plugins/sql_session_store/init.rb create mode 100755 vendor/plugins/sql_session_store/install.rb create mode 100755 vendor/plugins/sql_session_store/lib/mysql_session.rb create mode 100755 vendor/plugins/sql_session_store/lib/oracle_session.rb create mode 100755 vendor/plugins/sql_session_store/lib/postgresql_session.rb create mode 100644 vendor/plugins/sql_session_store/lib/sql_session.rb create mode 100755 vendor/plugins/sql_session_store/lib/sql_session_store.rb create mode 100755 vendor/plugins/sql_session_store/lib/sqlite_session.rb diff --git a/config/environment.rb b/config/environment.rb index e42e87eb7..e23f23bfa 100644 --- a/config/environment.rb +++ b/config/environment.rb @@ -5,7 +5,7 @@ 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' @@ -40,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.0.10' + config.gem 'libxml-ruby', :version => '>= 0.8.3', :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 diff --git a/config/initializers/composite_primary_keys.rb b/config/initializers/composite_primary_keys.rb deleted file mode 100644 index 430bcfac2..000000000 --- a/config/initializers/composite_primary_keys.rb +++ /dev/null @@ -1,3 +0,0 @@ -require 'rubygems' -gem 'composite_primary_keys', '= 0.9.93' -require 'composite_primary_keys' diff --git a/config/initializers/libxml.rb b/config/initializers/libxml.rb index a1870dbab..4f71b6d0f 100644 --- a/config/initializers/libxml.rb +++ b/config/initializers/libxml.rb @@ -1,7 +1,8 @@ -require 'rubygems' -gem 'libxml-ruby', '>= 0.8.3' -require 'libxml' +#require 'rubygems' +#gem 'libxml-ruby', '>= 0.8.3' +#require 'libxml' +# Is this really needed? LibXML::XML::Parser.register_error_handler do |message| raise message end diff --git a/vendor/plugins/deadlock_retry/README b/vendor/plugins/deadlock_retry/README new file mode 100644 index 000000000..b5937ce0e --- /dev/null +++ b/vendor/plugins/deadlock_retry/README @@ -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 index 000000000..8063a6ed4 --- /dev/null +++ b/vendor/plugins/deadlock_retry/Rakefile @@ -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 index 000000000..e090f68af --- /dev/null +++ b/vendor/plugins/deadlock_retry/init.rb @@ -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 index 000000000..413cb823c --- /dev/null +++ b/vendor/plugins/deadlock_retry/lib/deadlock_retry.rb @@ -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 < 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 index 000000000..db0f6195d --- /dev/null +++ b/vendor/plugins/deadlock_retry/test/deadlock_retry_test.rb @@ -0,0 +1,65 @@ +begin + require 'active_record' +rescue LoadError + if ENV['ACTIVERECORD_PATH'].nil? + abort < 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 index 000000000..07a6e9661 --- /dev/null +++ b/vendor/plugins/file_column/README @@ -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//filename.ext" + +* Newly uploaded files will be stored in "public/entry/tmp//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 index 000000000..0a2468248 --- /dev/null +++ b/vendor/plugins/file_column/Rakefile @@ -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 index 000000000..d46e9fa80 --- /dev/null +++ b/vendor/plugins/file_column/TODO @@ -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 index 000000000..d31ef1b9c --- /dev/null +++ b/vendor/plugins/file_column/init.rb @@ -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 index 000000000..791a5be3e --- /dev/null +++ b/vendor/plugins/file_column/lib/file_column.rb @@ -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 ActiveRecord::Base + # as class methods, so that you can use them in your models. + # + # == Generated Methods + # + # After calling "file_column :image" as in the example above, a number of instance methods + # will automatically be generated, all prefixed by "image": + # + # * Entry#image=(uploaded_file): 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). + # * Entry#image(subdir=nil): This will return an absolute path (as a + # string) to the currently uploaded file + # or nil if no file has been uploaded + # * Entry#image_relative_path(subdir=nil): 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. + # * Entry#image_just_uploaded?: 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 + # ActiveRecord::Base#attributes or ActiveRecord::Base#[] 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 0644 (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 :permissions options. The value + # you give here is passed directly to File::chmod, 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//test.png" and be moved to + # "public/entry/image//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 + # image("thumb"), 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 :fix_file_extensions 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 :mime_extensions 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 :file_exec 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/" for unsaved models. + # + # You can specify a custom root_path by setting the :root_path option. + # + # You can specify a custom storage_dir by setting the :storage_dir option. + # + # For setting a static storage_dir that doesn't change with respect to a particular + # instance, you assign :storage_dir 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 + # :store_dir 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 :store_dir. You can override this via the + # :tmp_base_dir 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 index 000000000..f4ebe38e7 --- /dev/null +++ b/vendor/plugins/file_column/lib/file_column_helper.rb @@ -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): + # + # + # + # + # 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 @entry) 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 @entry: + # + # /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 :absolute => + # true 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 :symbol will select a version defined in the model + # via FileColumn::Magick's :versions feature. + # * a geometry_string will dynamically create an + # image resized as specified by geometry_string. The image will + # be stored so that it does not have to be recomputed the next time the + # same version string is used. + # * some_hash will dynamically create an image + # that is created according to the options in some_hash. 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 :name => name 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 :versions 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 index 000000000..f284410a3 --- /dev/null +++ b/vendor/plugins/file_column/lib/file_compat.rb @@ -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 index 000000000..c4dc06fc3 --- /dev/null +++ b/vendor/plugins/file_column/lib/magick_file_column.rb @@ -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 :magick 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 :size 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 :magick option, you can set the :image_required + # 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 :name 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 :attributes 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 Proc object, you can also give a + # Symbol, 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. + # + # Note: 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 index 000000000..af8c95a84 --- /dev/null +++ b/vendor/plugins/file_column/lib/rails_file_column.rb @@ -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 index 000000000..1416a1e7f --- /dev/null +++ b/vendor/plugins/file_column/lib/test_case.rb @@ -0,0 +1,124 @@ +require 'test/unit' + +# Add the methods +upload+, the setup_file_fixtures and +# teardown_file_fixtures 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 + # + # * path The path to the file to upload. + # * content_type The MIME type of the file. If it is :guess, + # 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 + # setup 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 index 000000000..5b961eb9c --- /dev/null +++ b/vendor/plugins/file_column/lib/validations.rb @@ -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 ActiveRecord::Base + # 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: + # * :in => 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: + # * :in => 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 :min option. + # + # Required options: + # * :min => 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 index 000000000..22bc53b70 --- /dev/null +++ b/vendor/plugins/file_column/test/abstract_unit.rb @@ -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 index 000000000..a2f28baca --- /dev/null +++ b/vendor/plugins/file_column/test/connection.rb @@ -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 index 000000000..ffb2c43b8 --- /dev/null +++ b/vendor/plugins/file_column/test/file_column_helper_test.rb @@ -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 index 000000000..452b7815d --- /dev/null +++ b/vendor/plugins/file_column/test/file_column_test.rb @@ -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 index 000000000..b9f7c954d --- /dev/null +++ b/vendor/plugins/file_column/test/fixtures/entry.rb @@ -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 index 000000000..bd4933b43 --- /dev/null +++ b/vendor/plugins/file_column/test/fixtures/invalid-image.jpg @@ -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 index 0000000000000000000000000000000000000000..083138e4fe1702478499964ecd9fb3fbcfd351bf GIT binary patch literal 87582 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

      3Q=3cgdr&fN>PAx03Cs@q4q%H`>q7>OZ}6? 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 nOx<=ys)O30l)Qw!nAAE1f 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 index 0000000000000000000000000000000000000000..7415eb6e4cbffb07f962be309205c15763935e42 GIT binary patch literal 12629 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®X?*?>a>xXxx9zrVTV|*o%D{-6 z=&YL=Y`^W-fB*LN>D$LopFe&0n>;-J=l3t)pT0>S4o*^0MT*+IE;L;zy6t_8Bqf;) z<;3DaQ97m{&r2~=brXfCO?lSD=U}Q(71P6)3K2>sRphV#eto)a|5%q_9^a0fSMoEY zuS+^7#`*v0mn7+#+_vp@yG4X}!QsctDR3}z_f^&RHeUXG+OGBK%h!K(KL7SbUM}y} zvYIoqwN}is%!o!N#mSL?Z*%A|NaTeyl>vuJ$#Sr>(`WdG?M=VOMM2VR2sQS00000NkvXXu0mjf Dba$3x literal 0 HcmV?d00001 diff --git a/vendor/plugins/file_column/test/magick_test.rb b/vendor/plugins/file_column/test/magick_test.rb new file mode 100644 index 000000000..036271927 --- /dev/null +++ b/vendor/plugins/file_column/test/magick_test.rb @@ -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 index 000000000..a7daa6172 --- /dev/null +++ b/vendor/plugins/file_column/test/magick_view_only_test.rb @@ -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 index 000000000..5cb5c7b95 --- /dev/null +++ b/vendor/plugins/sql_session_store/LICENSE @@ -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 index 000000000..07b083343 --- /dev/null +++ b/vendor/plugins/sql_session_store/README @@ -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 index 000000000..0145def2f --- /dev/null +++ b/vendor/plugins/sql_session_store/Rakefile @@ -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 index 000000000..1e3f58a67 --- /dev/null +++ b/vendor/plugins/sql_session_store/generators/sql_session_store/USAGE @@ -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 index 000000000..6af6bd0bc --- /dev/null +++ b/vendor/plugins/sql_session_store/generators/sql_session_store/sql_session_store_generator.rb @@ -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 index 000000000..512650068 --- /dev/null +++ b/vendor/plugins/sql_session_store/generators/sql_session_store/templates/migration.rb @@ -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 index 000000000..956151ea7 --- /dev/null +++ b/vendor/plugins/sql_session_store/init.rb @@ -0,0 +1 @@ +require 'sql_session_store' diff --git a/vendor/plugins/sql_session_store/install.rb b/vendor/plugins/sql_session_store/install.rb new file mode 100755 index 000000000..f40549dfe --- /dev/null +++ b/vendor/plugins/sql_session_store/install.rb @@ -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 index 000000000..8c86384c9 --- /dev/null +++ b/vendor/plugins/sql_session_store/lib/mysql_session.rb @@ -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 index 000000000..0b82f6391 --- /dev/null +++ b/vendor/plugins/sql_session_store/lib/oracle_session.rb @@ -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 index 000000000..d922913aa --- /dev/null +++ b/vendor/plugins/sql_session_store/lib/postgresql_session.rb @@ -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 index 000000000..19d2ad51e --- /dev/null +++ b/vendor/plugins/sql_session_store/lib/sql_session.rb @@ -0,0 +1,27 @@ +# An ActiveRecord class which corresponds to the database table +# +sessions+. Functions +find_session+, +create_session+, +# +update_session+ and +destroy+ constitute the interface to class +# +SqlSessionStore+. + +class SqlSession < ActiveRecord::Base + # this class should not be reloaded + def self.reloadable? + false + end + + # retrieve session data for a given +session_id+ from the database, + # return nil if no such session exists + def self.find_session(session_id) + find :first, :conditions => "session_id='#{session_id}'" + end + + # create a new session with given +session_id+ and +data+ + def self.create_session(session_id, data) + new(:session_id => session_id, :data => data) + end + + # update session data and store it in the database + def update_session(data) + update_attribute('data', data) + end +end 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 index 000000000..8b0ff156f --- /dev/null +++ b/vendor/plugins/sql_session_store/lib/sql_session_store.rb @@ -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 index 000000000..822b23231 --- /dev/null +++ b/vendor/plugins/sql_session_store/lib/sqlite_session.rb @@ -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. From 9f3a4a5e0170a356b704b09220d533e0e17ccda0 Mon Sep 17 00:00:00 2001 From: Matt Amos Date: Wed, 29 Oct 2008 15:33:23 +0000 Subject: [PATCH 154/381] Better error message and test for invalid UTF-8 sequences. --- test/unit/message_test.rb | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/test/unit/message_test.rb b/test/unit/message_test.rb index ea05b1307..3b83bf95a 100644 --- a/test/unit/message_test.rb +++ b/test/unit/message_test.rb @@ -62,7 +62,21 @@ class MessageTest < Test::Unit::TestCase "\xe1\x82\x4a", # three-byte identifier, contination and (incorrectly) plain ASCII ] invalid_sequences.each do |char| - assert_raise(ActiveRecord::RecordInvalid) { make_message(char, 1).save! } + 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 From b4c46ba4b8796d21c38fe737953808139adae741 Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Wed, 29 Oct 2008 15:45:48 +0000 Subject: [PATCH 155/381] add a comment about why the error handler is required, as per mail from TomH. Also adding the option so that we are using the old style of migrations, rather than the 2.1+ migrations. --- config/environment.rb | 6 ++++++ config/initializers/libxml.rb | 7 ++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/config/environment.rb b/config/environment.rb index e23f23bfa..171bb6fc8 100644 --- a/config/environment.rb +++ b/config/environment.rb @@ -76,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 diff --git a/config/initializers/libxml.rb b/config/initializers/libxml.rb index 4f71b6d0f..3b5919f0f 100644 --- a/config/initializers/libxml.rb +++ b/config/initializers/libxml.rb @@ -1,8 +1,5 @@ -#require 'rubygems' -#gem 'libxml-ruby', '>= 0.8.3' -#require 'libxml' - -# Is this really needed? +# This is required otherwise libxml writes out memory errors to +# the standard output and exits uncleanly LibXML::XML::Parser.register_error_handler do |message| raise message end From 32b280eb8dfd11d03646cb89c339d2b938f06a6e Mon Sep 17 00:00:00 2001 From: Matt Amos Date: Wed, 29 Oct 2008 16:27:15 +0000 Subject: [PATCH 156/381] Added custom validator for UTF-8 strings. --- app/models/message.rb | 3 +++ lib/validators.rb | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 lib/validators.rb diff --git a/app/models/message.rb b/app/models/message.rb index a85de2231..464c55028 100644 --- a/app/models/message.rb +++ b/app/models/message.rb @@ -1,3 +1,5 @@ +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 @@ -6,4 +8,5 @@ class Message < ActiveRecord::Base validates_length_of :title, :within => 1..255 validates_inclusion_of :message_read, :in => [ true, false ] validates_associated :sender, :recipient + validates_as_utf8 :title end diff --git a/lib/validators.rb b/lib/validators.rb new file mode 100644 index 000000000..095fb7af9 --- /dev/null +++ b/lib/validators.rb @@ -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 From 9705105681afef0482763d830096c52b430fe943 Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Thu, 30 Oct 2008 18:50:45 +0000 Subject: [PATCH 157/381] Starting the gpx trace testing with additional fixtures --- test/fixtures/gpx_file_tags.yml | 4 ++++ test/fixtures/gpx_files.yml | 12 ++++++++++++ test/fixtures/gpx_points.yml | 9 +++++++++ test/functional/api_controller_test.rb | 17 +++++++++++++++++ test/test_helper.rb | 5 +++++ 5 files changed, 47 insertions(+) create mode 100644 test/fixtures/gpx_file_tags.yml create mode 100644 test/fixtures/gpx_files.yml create mode 100644 test/fixtures/gpx_points.yml diff --git a/test/fixtures/gpx_file_tags.yml b/test/fixtures/gpx_file_tags.yml new file mode 100644 index 000000000..d914bfb84 --- /dev/null +++ b/test/fixtures/gpx_file_tags.yml @@ -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 index 000000000..48c58dca9 --- /dev/null +++ b/test/fixtures/gpx_files.yml @@ -0,0 +1,12 @@ +first_trace_file: + id: 1 + user_id: 1 + visible: 1 + name: Fist Trace.gpx + size: + latitude: 1 + longitude: 1 + timestamp: "2008-10-29 10:10:10" + public: 1 + description: This is a trace + inserted: 1 diff --git a/test/fixtures/gpx_points.yml b/test/fixtures/gpx_points.yml new file mode 100644 index 000000000..13ee3552a --- /dev/null +++ b/test/fixtures/gpx_points.yml @@ -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/functional/api_controller_test.rb b/test/functional/api_controller_test.rb index b6bd2bf1d..8fb8dcc38 100644 --- a/test/functional/api_controller_test.rb +++ b/test/functional/api_controller_test.rb @@ -51,6 +51,23 @@ class ApiControllerTest < ActionController::TestCase end end + def test_tracepoints + node = gpx_files(:first_trace_file) + minlon = node.longitude-0.1 + minlat = node.latitude-0.1 + maxlon = node.longitude+0.1 + maxlat = node.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 diff --git a/test/test_helper.rb b/test/test_helper.rb index c01c31fca..f355bf785 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -57,6 +57,11 @@ class Test::Unit::TestCase set_fixture_class :relations => OldRelation set_fixture_class :relation_members => OldRelationMember set_fixture_class :relation_tags => OldRelationTag + + fixtures :gpx_files, :gpx_points, :gpx_file_tags + set_fixture_class :gpx_files => Trace + set_fixture_class :gpx_points => Tracepoint + set_fixture_class :gpx_file_tags => Tracetag end ## From 7418fc7e86827d2c0f184409bfa7ad90d1bf0021 Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Fri, 31 Oct 2008 18:02:12 +0000 Subject: [PATCH 158/381] Add a description meta tag for the all web pages. Wording probably needs to be improved. --- app/views/layouts/site.rhtml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/views/layouts/site.rhtml b/app/views/layouts/site.rhtml index 4437fa879..cf7ad9fc8 100644 --- a/app/views/layouts/site.rhtml +++ b/app/views/layouts/site.rhtml @@ -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." }) %> OpenStreetMap<%= ' | '+ h(@title) if @title %> From 635daf1773f5e2795c87619aa527adde965ce938 Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Mon, 3 Nov 2008 00:12:18 +0000 Subject: [PATCH 159/381] Adding a restriction to prevent more than 2000 nodes to be added to any way. Tests still need to be written. Capabilities api request also updated. --- app/controllers/api_controller.rb | 3 +++ app/models/way.rb | 3 +++ config/application.yml | 4 +++- lib/osm.rb | 15 +++++++++++++++ 4 files changed, 24 insertions(+), 1 deletion(-) diff --git a/app/controllers/api_controller.rb b/app/controllers/api_controller.rb index c337038a0..fa14cd79e 100644 --- a/app/controllers/api_controller.rb +++ b/app/controllers/api_controller.rb @@ -281,6 +281,9 @@ class ApiController < ApplicationController 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 diff --git a/app/models/way.rb b/app/models/way.rb index b413ccb28..9d4d8ba87 100644 --- a/app/models/way.rb +++ b/app/models/way.rb @@ -238,6 +238,9 @@ class Way < ActiveRecord::Base 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 diff --git a/config/application.yml b/config/application.yml index 85ebe9f21..7400a7b9a 100644 --- a/config/application.yml +++ b/config/application.yml @@ -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 diff --git a/lib/osm.rb b/lib/osm.rb index 246fedf54..b002ebbe8 100644 --- a/lib/osm.rb +++ b/lib/osm.rb @@ -108,6 +108,21 @@ module OSM :status => :bad_request } 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 } + end + end # Helper methods for going to/from mercator and lat/lng. class Mercator From e932a69bd7704272caf07bf46dc7ca90f79f0807 Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Mon, 3 Nov 2008 18:05:19 +0000 Subject: [PATCH 160/381] Additional api tests for the changes. Also making sure that you cannot request a change of 0 length, so that you can detect erronious input. --- app/controllers/api_controller.rb | 6 +- test/functional/api_controller_test.rb | 86 ++++++++++++++++++++++++-- 2 files changed, 83 insertions(+), 9 deletions(-) diff --git a/app/controllers/api_controller.rb b/app/controllers/api_controller.rb index fa14cd79e..1e4a11404 100644 --- a/app/controllers/api_controller.rb +++ b/app/controllers/api_controller.rb @@ -131,7 +131,7 @@ class ApiController < ApplicationController return end if node_ids.length == 0 - render :text => "", :content_type => "text/xml" + render :text => "", :content_type => "text/xml" return end @@ -230,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], @@ -258,7 +258,7 @@ class ApiController < ApplicationController render :text => doc.to_s, :content_type => "text/xml" else - render :text => "Requested zoom is invalid", :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 diff --git a/test/functional/api_controller_test.rb b/test/functional/api_controller_test.rb index 8fb8dcc38..6a0c2e2ac 100644 --- a/test/functional/api_controller_test.rb +++ b/test/functional/api_controller_test.rb @@ -40,7 +40,7 @@ class ApiControllerTest < ActionController::TestCase print @request.to_yaml print @response.body end - assert_response :success + 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 @@ -51,12 +51,23 @@ class ApiControllerTest < ActionController::TestCase 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 - node = gpx_files(:first_trace_file) - minlon = node.longitude-0.1 - minlat = node.latitude-0.1 - maxlon = node.longitude+0.1 - maxlat = node.latitude+0.1 + 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 @@ -140,6 +151,69 @@ class ApiControllerTest < ActionController::TestCase # 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 From 183ffc78648938ca079f7ea5f899d3e71e829333 Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Mon, 3 Nov 2008 18:54:19 +0000 Subject: [PATCH 161/381] Adding an extra test to make sure that the correct response is returned when an invalid changeset action is uploaded. --- lib/diff_reader.rb | 2 +- lib/osm.rb | 13 +++++++++++++ test/functional/changeset_controller_test.rb | 20 +++++++++++++++++++- 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/lib/diff_reader.rb b/lib/diff_reader.rb index eca6d438c..165e30e20 100644 --- a/lib/diff_reader.rb +++ b/lib/diff_reader.rb @@ -156,7 +156,7 @@ class DiffReader else # no other actions to choose from, so it must be the users fault! - raise "Unknown action #{action_name}, choices are create, modify, delete." + raise OSM::APIChangesetActionInvalid.new(action_name) end end diff --git a/lib/osm.rb b/lib/osm.rb index b002ebbe8..223e351f4 100644 --- a/lib/osm.rb +++ b/lib/osm.rb @@ -66,6 +66,19 @@ module OSM "#{@allowed} is allowed.", :status => :conflict } 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 } + end + end # Raised when bad XML is encountered which stops things parsing as # they should. diff --git a/test/functional/changeset_controller_test.rb b/test/functional/changeset_controller_test.rb index 5e26c2071..31ade9fce 100644 --- a/test/functional/changeset_controller_test.rb +++ b/test/functional/changeset_controller_test.rb @@ -233,7 +233,7 @@ EOF "can't upload a complex diff to changeset: #{@response.body}" # check the returned payload - assert_select "osm[version=#{API_VERSION}][generator=\"OpenStreetMap server\"]", 1 + assert_select "osm[version=#{API_VERSION}][generator=\"#{GENERATOR}\"]", 1 assert_select "osm>node", 1 assert_select "osm>way", 1 assert_select "osm>relation", 1 @@ -369,6 +369,24 @@ EOF 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 + 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 From 0ca779e5bd2b53e83331640d21945b9fb06b2c8c Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Tue, 4 Nov 2008 13:54:45 +0000 Subject: [PATCH 162/381] Minor fix (integer => bigint) to one of the migrations so that they will run fully again. --- db/migrate/001_create_osm_db.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/migrate/001_create_osm_db.rb b/db/migrate/001_create_osm_db.rb index 2c80dd8ad..0117c853e 100644 --- a/db/migrate/001_create_osm_db.rb +++ b/db/migrate/001_create_osm_db.rb @@ -110,7 +110,7 @@ class CreateOsmDb < ActiveRecord::Migration 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" + change_column "gpx_file_tags", "id", :bigint, :limit => 20, :null => false, :options => "AUTO_INCREMENT" create_table "gpx_files", myisam_table do |t| t.column "id", :bigint, :limit => 64, :null => false From 02fbcf5f06917d7dea546b90932da82b8ed29d2a Mon Sep 17 00:00:00 2001 From: Matt Amos Date: Tue, 4 Nov 2008 15:52:22 +0000 Subject: [PATCH 163/381] Added first attempt at bounding box support in changesets and tests for the same. --- app/models/changeset.rb | 43 +++++++++- app/models/node.rb | 24 ++++++ app/models/relation.rb | 3 + app/models/way.rb | 20 +++++ lib/diff_reader.rb | 5 +- test/functional/changeset_controller_test.rb | 85 +++++++++++++++++++- test/unit/way_test.rb | 16 ++++ 7 files changed, 189 insertions(+), 7 deletions(-) create mode 100644 test/unit/way_test.rb diff --git a/app/models/changeset.rb b/app/models/changeset.rb index 446ca351d..b00dfa8af 100644 --- a/app/models/changeset.rb +++ b/app/models/changeset.rb @@ -15,6 +15,9 @@ class Changeset < ActiveRecord::Base validates_presence_of :user_id, :created_at validates_inclusion_of :open, :in => [ true, false ] + # over-expansion factor to use when updating the bounding box + EXPAND = 0.1 + # 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 @@ -46,6 +49,35 @@ class Changeset < ActiveRecord::Base 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 + def tags_as_hash return tags end @@ -124,9 +156,14 @@ class Changeset < ActiveRecord::Base el1['created_at'] = self.created_at.xmlschema el1['open'] = self.open.to_s - # FIXME FIXME FIXME: This does not include changes yet! There is - # currently no changeset_id column in the tables as far as I can tell, - # so this is just a scaffold to build on, not a complete to_xml + el1['min_lon'] = (bbox[0] / SCALE).to_s unless bbox[0].nil? + el1['min_lat'] = (bbox[1] / SCALE).to_s unless bbox[1].nil? + el1['max_lon'] = (bbox[2] / SCALE).to_s unless bbox[2].nil? + el1['max_lat'] = (bbox[3] / 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 diff --git a/app/models/node.rb b/app/models/node.rb index 67efeca2c..e58a1d896 100644 --- a/app/models/node.rb +++ b/app/models/node.rb @@ -112,6 +112,12 @@ class Node < ActiveRecord::Base return node end + ## + # the bounding box around a node + def bbox + [ longitude, latitude, longitude, latitude ] + end + def save_with_history! t = Time.now Node.transaction do @@ -134,6 +140,9 @@ class Node < ActiveRecord::Base old_node = OldNode.from_node(self) old_node.timestamp = t old_node.save_with_dependencies! + + # save the changeset in case of bounding box updates + changeset.save! end end @@ -148,6 +157,10 @@ class Node < ActiveRecord::Base else self.changeset_id = new_node.changeset_id self.visible = 0 + + # update the changeset with the deleted position + changeset.update_bbox!(bbox) + save_with_history! end else @@ -158,12 +171,19 @@ class Node < ActiveRecord::Base 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 @@ -171,6 +191,10 @@ class Node < ActiveRecord::Base check_create_consistency(self, user) self.version = 0 self.visible = true + + # update the changeset to include the new location + changeset.update_bbox!(bbox) + save_with_history! end diff --git a/app/models/relation.rb b/app/models/relation.rb index 81f178997..9836ef4f1 100644 --- a/app/models/relation.rb +++ b/app/models/relation.rb @@ -221,6 +221,9 @@ class Relation < ActiveRecord::Base old_relation = OldRelation.from_relation(self) old_relation.timestamp = t old_relation.save_with_dependencies! + + # update the bbox of the changeset and save it too. + # FIXME: what is the bounding box of a relation? end end diff --git a/app/models/way.rb b/app/models/way.rb index 9d4d8ba87..4143291c1 100644 --- a/app/models/way.rb +++ b/app/models/way.rb @@ -179,9 +179,24 @@ class Way < ActiveRecord::Base @tags[k] = v end + ## + # 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 + 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 @@ -211,6 +226,11 @@ class Way < ActiveRecord::Base 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? + changeset.save! end end diff --git a/lib/diff_reader.rb b/lib/diff_reader.rb index 165e30e20..d793f63e7 100644 --- a/lib/diff_reader.rb +++ b/lib/diff_reader.rb @@ -99,9 +99,8 @@ class DiffReader # diff, so we must fix these before saving the element. new.fix_placeholders!(ids) - # set the initial version to zero and save (which increments it) - new.version = 0 - new.save_with_history! + # 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 diff --git a/test/functional/changeset_controller_test.rb b/test/functional/changeset_controller_test.rb index 31ade9fce..b9c8c63d0 100644 --- a/test/functional/changeset_controller_test.rb +++ b/test/functional/changeset_controller_test.rb @@ -36,8 +36,16 @@ class ChangesetControllerTest < ActionController::TestCase 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 def test_close @@ -487,5 +495,80 @@ EOF 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 "" + 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 "" + 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]", 1 + assert_select "osm>changeset[max_lon=1]", 1 + assert_select "osm>changeset[min_lat=2]", 1 + assert_select "osm>changeset[max_lat=2]", 1 + + # add another node to it + with_controller(NodeController.new) do + content "" + 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]", 1 + assert_select "osm>changeset[max_lon=2]", 1 + assert_select "osm>changeset[min_lat=1]", 1 + assert_select "osm>changeset[max_lat=2]", 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]", 1 + assert_select "osm>changeset[max_lon=3]", 1 + assert_select "osm>changeset[min_lat=1]", 1 + assert_select "osm>changeset[max_lat=3]", 1 + end + + #------------------------------------------------------------ + # utility functions + #------------------------------------------------------------ + + ## + # 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/unit/way_test.rb b/test/unit/way_test.rb new file mode 100644 index 000000000..cd565fd27 --- /dev/null +++ b/test/unit/way_test.rb @@ -0,0 +1,16 @@ +require File.dirname(__FILE__) + '/../test_helper' + +class WayTest < Test::Unit::TestCase + api_fixtures + + 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 + +end From ee1da786493f8e0ea330195e17361bcb4a97efb7 Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Wed, 5 Nov 2008 18:42:38 +0000 Subject: [PATCH 164/381] Fixing the migration so that it will better match the live databetter. --- db/migrate/001_create_osm_db.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/migrate/001_create_osm_db.rb b/db/migrate/001_create_osm_db.rb index 0117c853e..689ca3c20 100644 --- a/db/migrate/001_create_osm_db.rb +++ b/db/migrate/001_create_osm_db.rb @@ -110,7 +110,7 @@ class CreateOsmDb < ActiveRecord::Migration 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", :bigint, :limit => 20, :null => false, :options => "AUTO_INCREMENT" + change_column "gpx_file_tags", "id", :integer, :null => false, :options => "AUTO_INCREMENT" create_table "gpx_files", myisam_table do |t| t.column "id", :bigint, :limit => 64, :null => false From 89d8fc1bade02835c90167f9ae8bee9f123acc8b Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Thu, 6 Nov 2008 14:20:07 +0000 Subject: [PATCH 165/381] Removing depricated config option in the development env --- config/environments/development.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/config/environments/development.rb b/config/environments/development.rb index 09a451f9a..85c9a6080 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -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 From 6855b2604cb356b814d7853fb56988924af24eb3 Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Thu, 6 Nov 2008 17:15:22 +0000 Subject: [PATCH 166/381] Doing the update version part of the migration in chunks of 10000 instead of all at once so that MySQL doesn't choke with running out of table locks. --- db/migrate/018_move_to_innodb.rb | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/db/migrate/018_move_to_innodb.rb b/db/migrate/018_move_to_innodb.rb index c551b0ef8..d17da8fd5 100644 --- a/db/migrate/018_move_to_innodb.rb +++ b/db/migrate/018_move_to_innodb.rb @@ -19,8 +19,23 @@ class MoveToInnodb < ActiveRecord::Migration @@ver_tbl.each { |tbl| add_column "current_#{tbl}", "version", :bigint, :limit => 20, :null => false - execute "UPDATE current_#{tbl} SET version = " + - "(SELECT max(version) FROM #{tbl} WHERE #{tbl}.id = current_#{tbl}.id)" + # 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 From d43a327a950591278f56041ced57986582dbcfb2 Mon Sep 17 00:00:00 2001 From: Matt Amos Date: Thu, 6 Nov 2008 17:56:24 +0000 Subject: [PATCH 167/381] Implemented changeset bounding box handling for relations, based on the conversation on the mailing list. Its a bit complex... --- app/models/relation.rb | 121 ++++++++++++++++++-- test/functional/relation_controller_test.rb | 101 ++++++++++++++++ 2 files changed, 213 insertions(+), 9 deletions(-) diff --git a/app/models/relation.rb b/app/models/relation.rb index 9836ef4f1..cc7977833 100644 --- a/app/models/relation.rb +++ b/app/models/relation.rb @@ -192,13 +192,42 @@ class Relation < ActiveRecord::Base 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 - RelationTag.delete_all(['id = ?', self.id]) + 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 + + 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 @@ -207,14 +236,37 @@ class Relation < ActiveRecord::Base tag.save! end - members = self.members - RelationMember.delete_all(['id = ?', self.id]) - members.each do |n| + # 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 = self.members_as_hash + relation_members.each do |old_member| + key = [old_member.member_id.to_s, old_member.member_type] + if members.has_key? key + # i'd love to rely on rails' dirty handling here, but the + # relation members are always dirty because of the member_class + # handling. + if members[key] != old_member.member_role + old_member.member_role = members[key] + changed_members << key + old_member.save! + end + members.delete key + + else + changed_members << key + RelationMember.delete_all ['id = ? and member_id = ? and member_type = ?', self.id, old_member.member_id, old_member.member_type] + end + end + # any remaining members must be new additions + changed_members += members.keys + members.each do |k,v| mem = RelationMember.new mem.id = self.id - mem.member_type = n[0]; - mem.member_id = n[1]; - mem.member_role = n[2]; + mem.member_type = k[1]; + mem.member_id = k[0]; + mem.member_role = v; mem.save! end @@ -223,10 +275,50 @@ class Relation < ActiveRecord::Base old_relation.save_with_dependencies! # update the bbox of the changeset and save it too. - # FIXME: what is the bounding box of a relation? + # 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 + # add only changed members to the changeset + changed_members.each do |id, type| + update_changeset_element(type, id) + end + end + + # save the (maybe updated) changeset bounding box + changeset.save! end 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) if self.visible check_consistency(self, new_relation, user) @@ -234,7 +326,7 @@ class Relation < ActiveRecord::Base raise OSM::APIPreconditionFailedError.new else self.changeset_id = new_relation.changeset_id - self.tags = [] + self.tags = {} self.members = [] self.visible = false save_with_history! @@ -319,6 +411,17 @@ class Relation < ActiveRecord::Base return false end + ## + # members in a hash table [id,type] => role + def members_as_hash + h = Hash.new + members.each do |m| + # should be: h[[m.id, m.type]] = m.role, but someone prefers arrays + h[[m[1], m[0]]] = m[2] + end + return h + end + # Temporary method to match interface to nodes def tags_as_hash return self.tags diff --git a/test/functional/relation_controller_test.rb b/test/functional/relation_controller_test.rb index ed5951365..5f23702db 100644 --- a/test/functional/relation_controller_test.rb +++ b/test/functional/relation_controller_test.rb @@ -265,6 +265,107 @@ class RelationControllerTest < ActionController::TestCase 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 + + ## + # 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 "" + 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]}]", 1 + assert_select "osm>changeset[min_lat=#{bbox[1]}]", 1 + assert_select "osm>changeset[max_lon=#{bbox[2]}]", 1 + assert_select "osm>changeset[max_lat=#{bbox[3]}]", 1 + end + end + ## # update the changeset_id of a node element def update_changeset(xml, changeset_id) From 383f204e64df874e288d100e65980db6df4630b6 Mon Sep 17 00:00:00 2001 From: Matt Amos Date: Thu, 6 Nov 2008 18:42:06 +0000 Subject: [PATCH 168/381] Added a changeset/#id/include method to POST to to expand the bounding box. --- app/controllers/changeset_controller.rb | 75 +++++++++++++------- test/functional/changeset_controller_test.rb | 44 +++++++++++- 2 files changed, 94 insertions(+), 25 deletions(-) diff --git a/app/controllers/changeset_controller.rb b/app/controllers/changeset_controller.rb index 7ac4eb91a..c56e15c01 100644 --- a/app/controllers/changeset_controller.rb +++ b/app/controllers/changeset_controller.rb @@ -4,9 +4,9 @@ class ChangesetController < ApplicationController require 'xml/libxml' require 'diff_reader' - before_filter :authorize, :only => [:create, :update, :delete, :upload] - before_filter :check_write_availability, :only => [:create, :update, :delete, :upload] - before_filter :check_read_availability, :except => [:create, :update, :delete, :upload] + before_filter :authorize, :only => [:create, :update, :delete, :upload, :include] + before_filter :check_write_availability, :only => [:create, :update, :delete, :upload, :include] + before_filter :check_read_availability, :except => [:create, :update, :delete, :upload, :download] after_filter :compress_output # Create a changeset from XML. @@ -26,27 +26,6 @@ class ChangesetController < ApplicationController end end - def create_prim(ids, prim, nd) - prim.version = 0 - prim.user_id = @user.id - prim.visible = true - prim.save_with_history! - - ids[nd['id'].to_i] = prim.id - end - - def fix_way(w, node_ids) - w.nds = w.instance_eval { @nds }. - map { |nd| node_ids[nd] || nd } - return w - end - - def fix_rel(r, ids) - r.members = r.instance_eval { @members }. - map { |memb| [memb[0], ids[memb[0]][memb[1].to_i] || memb[1], memb[2]] } - return r - end - def read begin changeset = Changeset.find(params[:id]) @@ -71,6 +50,54 @@ class ChangesetController < ApplicationController end 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 include + # 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]) + + # 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 * SCALE + lat << n['lat'].to_f * 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. # diff --git a/test/functional/changeset_controller_test.rb b/test/functional/changeset_controller_test.rb index b9c8c63d0..150e53dae 100644 --- a/test/functional/changeset_controller_test.rb +++ b/test/functional/changeset_controller_test.rb @@ -49,7 +49,7 @@ class ChangesetControllerTest < ActionController::TestCase end def test_close - + # FIXME FIXME FIXME! end ## @@ -554,10 +554,52 @@ EOF assert_select "osm>changeset[max_lat=3]", 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 "" + 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 + #------------------------------------------------------------ # utility functions #------------------------------------------------------------ + ## + # call the include method and assert properties of the bbox + def check_after_include(changeset_id, lon, lat, bbox) + content "" + post :include, :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) From c760891d634eff7b95b98d2bd88fc0d5535220bb Mon Sep 17 00:00:00 2001 From: Matt Amos Date: Thu, 6 Nov 2008 19:10:19 +0000 Subject: [PATCH 169/381] Ooops. Added missing access control to changeset controller. --- app/controllers/changeset_controller.rb | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/app/controllers/changeset_controller.rb b/app/controllers/changeset_controller.rb index c56e15c01..9152df816 100644 --- a/app/controllers/changeset_controller.rb +++ b/app/controllers/changeset_controller.rb @@ -41,7 +41,13 @@ class ChangesetController < ApplicationController render :nothing => true, :status => :method_not_allowed return end + changeset = Changeset.find(params[:id]) + + unless @user.id == changeset.user_id + raise OSM::APIUserChangesetMismatchError + end + changeset.open = false changeset.save! render :nothing => true @@ -61,6 +67,12 @@ class ChangesetController < ApplicationController if request.post? cs = Changeset.find(params[:id]) + # check user credentials - only the user who opened a changeset + # may alter it. + unless @user.id == changeset.user_id + raise OSM::APIUserChangesetMismatchError + end + # keep an array of lons and lats lon = Array.new lat = Array.new @@ -120,6 +132,12 @@ class ChangesetController < ApplicationController return end + # access control - only the user who created a changeset may + # upload to it. + unless @user.id == changeset.user_id + raise OSM::APIUserChangesetMismatchError + end + changeset = Changeset.find(params[:id]) diff_reader = DiffReader.new(request.raw_post, changeset) From e1566637231c9605ab7f85feae06bf82225174df Mon Sep 17 00:00:00 2001 From: Andy Allan Date: Fri, 7 Nov 2008 11:20:25 +0000 Subject: [PATCH 170/381] Booleans should be true/false for compatibility with other db adapters --- test/fixtures/changesets.yml | 8 +++---- test/fixtures/current_nodes.yml | 30 ++++++++++++------------ test/fixtures/current_relations.yml | 6 ++--- test/fixtures/current_ways.yml | 8 +++---- test/fixtures/gpx_files.yml | 6 ++--- test/fixtures/nodes.yml | 36 ++++++++++++++--------------- test/fixtures/relations.yml | 6 ++--- test/fixtures/users.yml | 12 +++++----- test/fixtures/ways.yml | 14 +++++------ 9 files changed, 63 insertions(+), 63 deletions(-) diff --git a/test/fixtures/changesets.yml b/test/fixtures/changesets.yml index 7d97f2abf..03fc36ddf 100644 --- a/test/fixtures/changesets.yml +++ b/test/fixtures/changesets.yml @@ -3,22 +3,22 @@ normal_user_first_change: id: 1 user_id: 1 created_at: "2007-01-01 00:00:00" - open: 1 + open: true second_user_first_change: id: 2 user_id: 2 created_at: "2008-05-01 01:23:45" - open: 1 + open: true normal_user_closed_change: id: 3 user_id: 1 created_at: "2007-01-01 00:00:00" - open: 0 + open: false normal_user_version_change: id: 4 user_id: 1 created_at: "2008-01-01 00:00:00" - open: 1 + open: true diff --git a/test/fixtures/current_nodes.yml b/test/fixtures/current_nodes.yml index 7e4f33a07..2d4b35fd6 100644 --- a/test/fixtures/current_nodes.yml +++ b/test/fixtures/current_nodes.yml @@ -6,7 +6,7 @@ visible_node: latitude: <%= 1*SCALE %> longitude: <%= 1*SCALE %> changeset_id: 1 - visible: 1 + visible: true version: 1 tile: <%= QuadTile.tile_for_point(1,1) %> timestamp: 2007-01-01 00:00:00 @@ -16,7 +16,7 @@ invisible_node: latitude: <%= 2*SCALE %> longitude: <%= 2*SCALE %> changeset_id: 1 - visible: 0 + visible: false version: 1 tile: <%= QuadTile.tile_for_point(2,2) %> timestamp: 2007-01-01 00:00:00 @@ -26,7 +26,7 @@ used_node_1: latitude: <%= 3*SCALE %> longitude: <%= 3*SCALE %> changeset_id: 1 - visible: 1 + visible: true version: 1 tile: <%= QuadTile.tile_for_point(3,3) %> timestamp: 2007-01-01 00:00:00 @@ -36,7 +36,7 @@ used_node_2: latitude: <%= 4*SCALE %> longitude: <%= 4*SCALE %> changeset_id: 1 - visible: 1 + visible: true version: 1 tile: <%= QuadTile.tile_for_point(4,4) %> timestamp: 2007-01-01 00:00:00 @@ -46,7 +46,7 @@ node_used_by_relationship: latitude: <%= 5*SCALE %> longitude: <%= 5*SCALE %> changeset_id: 1 - visible: 1 + visible: true version: 1 tile: <%= QuadTile.tile_for_point(5,5) %> timestamp: 2007-01-01 00:00:00 @@ -56,7 +56,7 @@ node_too_far_north: latitude: <%= 90.01*SCALE %> longitude: <%= 6*SCALE %> changeset_id: 1 - visible: 1 + visible: true version: 1 tile: <%= QuadTile.tile_for_point(90.01,6) %> timestamp: 2007-01-01 00:00:00 @@ -66,7 +66,7 @@ node_north_limit: latitude: <%= 90*SCALE %> longitude: <%= 11*SCALE %> changeset_id: 1 - visible: 1 + visible: true version: 1 tile: <%= QuadTile.tile_for_point(90,11) %> timestamp: 2008-07-08 14:50:00 @@ -76,7 +76,7 @@ node_too_far_south: latitude: <%= -90.01*SCALE %> longitude: <%= 7*SCALE %> changeset_id: 1 - visible: 1 + visible: true version: 1 tile: <%= QuadTile.tile_for_point(-90.01,7) %> timestamp: 2007-01-01 00:00:00 @@ -86,7 +86,7 @@ node_south_limit: latitude: <%= -90*SCALE %> longitude: <%= 12*SCALE %> changeset_id: 1 - visible: 1 + visible: true version: 1 tile: <%= QuadTile.tile_for_point(-90,12) %> timestamp: 2008-07-08 15:02:18 @@ -96,7 +96,7 @@ node_too_far_west: latitude: <%= 8*SCALE %> longitude: <%= -180.01*SCALE %> changeset_id: 1 - visible: 1 + visible: true version: 1 tile: <%= QuadTile.tile_for_point(8,-180.01) %> timestamp: 2007-01-01 00:00:00 @@ -106,7 +106,7 @@ node_west_limit: latitude: <%= 13*SCALE %> longitude: <%= -180*SCALE %> changeset_id: 1 - visible: 1 + visible: true version: 1 tile: <%= QuadTile.tile_for_point(13,-180) %> timestamp: 2008-07-08 15:17:37 @@ -116,7 +116,7 @@ node_too_far_east: latitude: <%= 9*SCALE %> longitude: <%= 180.01*SCALE %> changeset_id: 1 - visible: 1 + visible: true version: 1 tile: <%= QuadTile.tile_for_point(9,180.01) %> timestamp: 2007-01-01 00:00:00 @@ -126,7 +126,7 @@ node_east_limit: latitude: <%= 14*SCALE %> longitude: <%= 180*SCALE %> changeset_id: 1 - visible: 1 + visible: true version: 1 tile: <%= QuadTile.tile_for_point(14,180) %> timestamp: 2008-07-08 15:46:16 @@ -136,7 +136,7 @@ node_totally_wrong: latitude: <%= 200*SCALE %> longitude: <%= 200*SCALE %> changeset_id: 1 - visible: 1 + visible: true version: 1 tile: <%= QuadTile.tile_for_point(200,200) %> timestamp: 2007-01-01 00:00:00 @@ -146,7 +146,7 @@ node_with_versions: latitude: <%= 1*SCALE %> longitude: <%= 1*SCALE %> changeset_id: 4 - visible: 1 + visible: true version: 4 tile: <%= QuadTile.tile_for_point(1,1) %> timestamp: 2008-01-01 00:04:00 diff --git a/test/fixtures/current_relations.yml b/test/fixtures/current_relations.yml index e4c3b18fa..165f1a21e 100644 --- a/test/fixtures/current_relations.yml +++ b/test/fixtures/current_relations.yml @@ -2,19 +2,19 @@ visible_relation: id: 1 changeset_id: 1 timestamp: 2007-01-01 00:00:00 - visible: 1 + visible: true version: 1 invisible_relation: id: 2 changeset_id: 1 timestamp: 2007-01-01 00:00:00 - visible: 0 + visible: false version: 1 used_relation: id: 3 changeset_id: 1 timestamp: 2007-01-01 00:00:00 - visible: 1 + visible: true version: 1 diff --git a/test/fixtures/current_ways.yml b/test/fixtures/current_ways.yml index cf25b4f46..44a54caac 100644 --- a/test/fixtures/current_ways.yml +++ b/test/fixtures/current_ways.yml @@ -2,26 +2,26 @@ visible_way: id: 1 changeset_id: 1 timestamp: 2007-01-01 00:00:00 - visible: 1 + visible: true version: 1 invisible_way: id: 2 changeset_id: 1 timestamp: 2007-01-01 00:00:00 - visible: 0 + visible: false version: 1 used_way: id: 3 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: 1 + visible: true version: 4 diff --git a/test/fixtures/gpx_files.yml b/test/fixtures/gpx_files.yml index 48c58dca9..3ab74c806 100644 --- a/test/fixtures/gpx_files.yml +++ b/test/fixtures/gpx_files.yml @@ -1,12 +1,12 @@ first_trace_file: id: 1 user_id: 1 - visible: 1 + visible: true name: Fist Trace.gpx size: latitude: 1 longitude: 1 timestamp: "2008-10-29 10:10:10" - public: 1 + public: true description: This is a trace - inserted: 1 + inserted: true diff --git a/test/fixtures/nodes.yml b/test/fixtures/nodes.yml index c58cda962..27f51f6a2 100644 --- a/test/fixtures/nodes.yml +++ b/test/fixtures/nodes.yml @@ -6,7 +6,7 @@ visible_node: latitude: <%= 1*SCALE %> longitude: <%= 1*SCALE %> changeset_id: 1 - visible: 1 + visible: true version: 1 tile: <%= QuadTile.tile_for_point(1,1) %> timestamp: 2007-01-01 00:00:00 @@ -16,7 +16,7 @@ invisible_node: latitude: <%= 2*SCALE %> longitude: <%= 2*SCALE %> changeset_id: 1 - visible: 0 + visible: false version: 1 tile: <%= QuadTile.tile_for_point(2,2) %> timestamp: 2007-01-01 00:00:00 @@ -26,7 +26,7 @@ used_node_1: latitude: <%= 3*SCALE %> longitude: <%= 3*SCALE %> changeset_id: 1 - visible: 1 + visible: true version: 1 tile: <%= QuadTile.tile_for_point(3,3) %> timestamp: 2007-01-01 00:00:00 @@ -36,7 +36,7 @@ used_node_2: latitude: <%= 4*SCALE %> longitude: <%= 4*SCALE %> changeset_id: 1 - visible: 1 + visible: true version: 1 tile: <%= QuadTile.tile_for_point(4,4) %> timestamp: 2007-01-01 00:00:00 @@ -46,7 +46,7 @@ node_used_by_relationship: latitude: <%= 5*SCALE %> longitude: <%= 5*SCALE %> changeset_id: 1 - visible: 1 + visible: true version: 1 tile: <%= QuadTile.tile_for_point(5,5) %> timestamp: 2007-01-01 00:00:00 @@ -56,7 +56,7 @@ node_too_far_north: latitude: <%= 90.01*SCALE %> longitude: <%= 6*SCALE %> changeset_id: 1 - visible: 1 + visible: true version: 1 tile: <%= QuadTile.tile_for_point(90.01,6) %> timestamp: 2007-01-01 00:00:00 @@ -66,7 +66,7 @@ node_north_limit: latitude: <%= 90*SCALE %> longitude: <%= 11*SCALE %> changeset_id: 1 - visible: 1 + visible: true version: 1 tile: <%= QuadTile.tile_for_point(90,11) %> timestamp: 2008-07-08 14:50:00 @@ -76,7 +76,7 @@ node_too_far_south: latitude: <%= -90.01*SCALE %> longitude: <%= 7*SCALE %> changeset_id: 1 - visible: 1 + visible: true version: 1 tile: <%= QuadTile.tile_for_point(-90.01,7) %> timestamp: 2007-01-01 00:00:00 @@ -86,7 +86,7 @@ node_south_limit: latitude: <%= -90*SCALE %> longitude: <%= 12*SCALE %> changeset_id: 1 - visible: 1 + visible: true version: 1 tile: <%= QuadTile.tile_for_point(-90,12) %> timestamp: 2008-07-08 15:02:18 @@ -96,7 +96,7 @@ node_too_far_west: latitude: <%= 8*SCALE %> longitude: <%= -180.01*SCALE %> changeset_id: 1 - visible: 1 + visible: true version: 1 tile: <%= QuadTile.tile_for_point(8,-180.01) %> timestamp: 2007-01-01 00:00:00 @@ -106,7 +106,7 @@ node_west_limit: latitude: <%= 13*SCALE %> longitude: <%= -180*SCALE %> changeset_id: 1 - visible: 1 + visible: true version: 1 tile: <%= QuadTile.tile_for_point(13,-180) %> timestamp: 2008-07-08 15:17:37 @@ -116,7 +116,7 @@ node_too_far_east: latitude: <%= 9*SCALE %> longitude: <%= 180.01*SCALE %> changeset_id: 1 - visible: 1 + visible: true version: 1 tile: <%= QuadTile.tile_for_point(9,180.01) %> timestamp: 2007-01-01 00:00:00 @@ -126,7 +126,7 @@ node_east_limit: latitude: <%= 14*SCALE %> longitude: <%= 180*SCALE %> changeset_id: 1 - visible: 1 + visible: true version: 1 tile: <%= QuadTile.tile_for_point(14,180) %> timestamp: 2008-07-08 15:46:16 @@ -136,7 +136,7 @@ node_totally_wrong: latitude: <%= 200*SCALE %> longitude: <%= 200*SCALE %> changeset_id: 1 - visible: 1 + visible: true version: 1 tile: <%= QuadTile.tile_for_point(200,200) %> timestamp: 2007-01-01 00:00:00 @@ -146,7 +146,7 @@ node_with_versions_v1: latitude: <%= 1*SCALE %> longitude: <%= 1*SCALE %> changeset_id: 4 - visible: 1 + visible: true version: 1 tile: <%= QuadTile.tile_for_point(1,1) %> timestamp: 2008-01-01 00:01:00 @@ -156,7 +156,7 @@ node_with_versions_v2: latitude: <%= 2*SCALE %> longitude: <%= 2*SCALE %> changeset_id: 4 - visible: 1 + visible: true version: 2 tile: <%= QuadTile.tile_for_point(1,1) %> timestamp: 2008-01-01 00:02:00 @@ -166,7 +166,7 @@ node_with_versions_v3: latitude: <%= 1*SCALE %> longitude: <%= 1*SCALE %> changeset_id: 4 - visible: 1 + visible: true version: 3 tile: <%= QuadTile.tile_for_point(1,1) %> timestamp: 2008-01-01 00:03:00 @@ -176,7 +176,7 @@ node_with_versions_v4: latitude: <%= 1*SCALE %> longitude: <%= 1*SCALE %> changeset_id: 4 - visible: 1 + visible: true version: 4 tile: <%= QuadTile.tile_for_point(1,1) %> timestamp: 2008-01-01 00:04:00 diff --git a/test/fixtures/relations.yml b/test/fixtures/relations.yml index e4c3b18fa..165f1a21e 100644 --- a/test/fixtures/relations.yml +++ b/test/fixtures/relations.yml @@ -2,19 +2,19 @@ visible_relation: id: 1 changeset_id: 1 timestamp: 2007-01-01 00:00:00 - visible: 1 + visible: true version: 1 invisible_relation: id: 2 changeset_id: 1 timestamp: 2007-01-01 00:00:00 - visible: 0 + visible: false version: 1 used_relation: id: 3 changeset_id: 1 timestamp: 2007-01-01 00:00:00 - visible: 1 + visible: true version: 1 diff --git a/test/fixtures/users.yml b/test/fixtures/users.yml index 28e1aca3d..b639d2265 100644 --- a/test/fixtures/users.yml +++ b/test/fixtures/users.yml @@ -2,11 +2,11 @@ normal_user: id: 1 email: test@openstreetmap.org - active: 1 + 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: 12.1 home_lon: 12.1 @@ -15,11 +15,11 @@ normal_user: second_user: id: 2 email: test@example.com - active: 1 + active: true pass_crypt: <%= Digest::MD5.hexdigest('test') %> creation_time: "2008-05-01 01:23:45" display_name: test2 - data_public: 1 + data_public: true description: some test description home_lat: 12 home_lon: 12 @@ -28,10 +28,10 @@ second_user: inactive_user: id: 3 email: inactive@openstreetmap.org - active: 0 + active: false pass_crypt: <%= Digest::MD5::hexdigest('test2') %> display_name: Inactive User - data_public: 1 + data_public: true description: description home_lat: 123.4 home_lon: 12.34 diff --git a/test/fixtures/ways.yml b/test/fixtures/ways.yml index 4f0af9854..80b1da642 100644 --- a/test/fixtures/ways.yml +++ b/test/fixtures/ways.yml @@ -2,48 +2,48 @@ visible_way: id: 1 changeset_id: 1 timestamp: 2007-01-01 00:00:00 - visible: 1 + visible: true version: 1 invisible_way: id: 2 changeset_id: 1 timestamp: 2007-01-01 00:00:00 - visible: 0 + visible: false version: 1 used_way: id: 3 changeset_id: 1 timestamp: 2007-01-01 00:00:00 - visible: 1 + visible: true version: 1 way_with_versions_v1: id: 4 changeset_id: 4 timestamp: 2008-01-01 00:01:00 - visible: 1 + visible: true version: 1 way_with_versions_v2: id: 4 changeset_id: 4 timestamp: 2008-01-01 00:02:00 - visible: 1 + visible: true version: 2 way_with_versions: id: 4 changeset_id: 4 timestamp: 2008-01-01 00:03:00 - visible: 1 + visible: true version: 3 way_with_versions_v4: id: 4 changeset_id: 4 timestamp: 2008-01-01 00:04:00 - visible: 1 + visible: true version: 4 From 3cb759f0856686fedd72a7f7295b8cf18f370e29 Mon Sep 17 00:00:00 2001 From: Andy Allan Date: Fri, 7 Nov 2008 11:21:46 +0000 Subject: [PATCH 171/381] users fixtures should all have creation times --- test/fixtures/users.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/test/fixtures/users.yml b/test/fixtures/users.yml index b639d2265..709139d68 100644 --- a/test/fixtures/users.yml +++ b/test/fixtures/users.yml @@ -30,6 +30,7 @@ inactive_user: 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 From 527b6f76e3e1f1a3b7f0464f290a886786f01e2b Mon Sep 17 00:00:00 2001 From: Matt Amos Date: Fri, 7 Nov 2008 11:36:00 +0000 Subject: [PATCH 172/381] D'oh! Fixed stupid errors in changeset controller. --- app/controllers/changeset_controller.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/controllers/changeset_controller.rb b/app/controllers/changeset_controller.rb index 9152df816..904330900 100644 --- a/app/controllers/changeset_controller.rb +++ b/app/controllers/changeset_controller.rb @@ -69,7 +69,7 @@ class ChangesetController < ApplicationController # check user credentials - only the user who opened a changeset # may alter it. - unless @user.id == changeset.user_id + unless @user.id == cs.user_id raise OSM::APIUserChangesetMismatchError end @@ -132,13 +132,13 @@ class ChangesetController < ApplicationController return end + changeset = Changeset.find(params[:id]) + # access control - only the user who created a changeset may # upload to it. unless @user.id == changeset.user_id raise OSM::APIUserChangesetMismatchError end - - changeset = Changeset.find(params[:id]) diff_reader = DiffReader.new(request.raw_post, changeset) Changeset.transaction do From 1140237caee259950b05f0a1223b1dac87200b9b Mon Sep 17 00:00:00 2001 From: Andy Allan Date: Fri, 7 Nov 2008 11:36:34 +0000 Subject: [PATCH 173/381] Booleans should be true/false - coding style comments welcome --- app/models/user.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index 525d018ac..00b6e15bd 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -4,9 +4,9 @@ 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" From 7058d836f75c845b7790c7ab6eb2f75844ce0c1c Mon Sep 17 00:00:00 2001 From: Andy Allan Date: Fri, 7 Nov 2008 13:24:18 +0000 Subject: [PATCH 174/381] use boolean flags when using boolean columns --- app/controllers/api_controller.rb | 8 ++++---- app/controllers/application.rb | 2 +- app/controllers/diary_entry_controller.rb | 6 +++--- app/controllers/trace_controller.rb | 10 +++++----- app/controllers/user_controller.rb | 8 ++++---- app/controllers/way_controller.rb | 2 +- app/models/node.rb | 6 +++--- app/models/relation.rb | 2 +- app/models/user.rb | 2 +- app/models/way.rb | 2 +- 10 files changed, 24 insertions(+), 24 deletions(-) diff --git a/app/controllers/api_controller.rb b/app/controllers/api_controller.rb index 1e4a11404..0724a3712 100644 --- a/app/controllers/api_controller.rb +++ b/app/controllers/api_controller.rb @@ -120,7 +120,7 @@ class ApiController < ApplicationController end # 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 = 1", :limit => APP_CONFIG['max_number_of_nodes']+1) + @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]) @@ -187,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. diff --git a/app/controllers/application.rb b/app/controllers/application.rb index 1c27cb4d5..579c50e95 100644 --- a/app/controllers/application.rb +++ b/app/controllers/application.rb @@ -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 diff --git a/app/controllers/diary_entry_controller.rb b/app/controllers/diary_entry_controller.rb index b425ef4b6..a965af2ff 100644 --- a/app/controllers/diary_entry_controller.rb +++ b/app/controllers/diary_entry_controller.rb @@ -54,7 +54,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" @@ -77,7 +77,7 @@ 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) @@ -100,7 +100,7 @@ class DiaryEntryController < ApplicationController 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]]) diff --git a/app/controllers/trace_controller.rb b/app/controllers/trace_controller.rb index 899df05df..6895a1bcf 100644 --- a/app/controllers/trace_controller.rb +++ b/app/controllers/trace_controller.rb @@ -12,7 +12,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 @@ -33,15 +33,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 @@ -51,7 +51,7 @@ class TraceController < ApplicationController conditions << @tag end - conditions[0] += " AND gpx_files.visible = 1" + conditions[0] += " AND gpx_files.visible = 1" #FIXME: use boolean true as parameter to active record @trace_pages, @traces = paginate(:traces, :include => [:user, :tags], diff --git a/app/controllers/user_controller.rb b/app/controllers/user_controller.rb index df2a799c3..196d5cea6 100644 --- a/app/controllers/user_controller.rb +++ b/app/controllers/user_controller.rb @@ -76,7 +76,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 @@ -216,7 +216,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 @@ -229,7 +229,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 @@ -251,7 +251,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." diff --git a/app/controllers/way_controller.rb b/app/controllers/way_controller.rb index 25d535d79..5b0a632f7 100644 --- a/app/controllers/way_controller.rb +++ b/app/controllers/way_controller.rb @@ -87,7 +87,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 diff --git a/app/models/node.rb b/app/models/node.rb index e58a1d896..4ee849838 100644 --- a/app/models/node.rb +++ b/app/models/node.rb @@ -53,7 +53,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 @@ -150,9 +150,9 @@ class Node < ActiveRecord::Base def delete_with_history!(new_node, user) if self.visible 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 = 1 AND current_way_nodes.node_id = ?", self.id ]) + 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 = 1 AND member_type='node' and member_id=? ", self.id]) + 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 diff --git a/app/models/relation.rb b/app/models/relation.rb index cc7977833..be990e589 100644 --- a/app/models/relation.rb +++ b/app/models/relation.rb @@ -322,7 +322,7 @@ class Relation < ActiveRecord::Base def delete_with_history!(new_relation, user) if self.visible check_consistency(self, new_relation, user) - 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=? ", self.id ]) + 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 else self.changeset_id = new_relation.changeset_id diff --git a/app/models/user.rb b/app/models/user.rb index 00b6e15bd..80faf68e9 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -81,7 +81,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 diff --git a/app/models/way.rb b/app/models/way.rb index 4143291c1..c9e695b32 100644 --- a/app/models/way.rb +++ b/app/models/way.rb @@ -274,7 +274,7 @@ class Way < ActiveRecord::Base check_consistency(self, new_way, user) if self.visible 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]) + :conditions => [ "visible = ? AND member_type='way' and member_id=? ", true, self.id]) raise OSM::APIPreconditionFailedError else self.changeset_id = new_way.changeset_id From 2700e5110f1ae0815b1a320fdba45a05ac794338 Mon Sep 17 00:00:00 2001 From: Andy Allan Date: Fri, 7 Nov 2008 13:27:03 +0000 Subject: [PATCH 175/381] add fixme --- app/controllers/trace_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/trace_controller.rb b/app/controllers/trace_controller.rb index 6895a1bcf..f0d8033f2 100644 --- a/app/controllers/trace_controller.rb +++ b/app/controllers/trace_controller.rb @@ -196,7 +196,7 @@ class TraceController < ApplicationController end def georss - conditions = ["gpx_files.public = 1"] + conditions = ["gpx_files.public = 1"] # fixme pass boolean true as parameter if params[:display_name] conditions[0] += " AND users.display_name = ?" From c253ce33997451b6b44777254b38a364d597c492 Mon Sep 17 00:00:00 2001 From: Andy Allan Date: Fri, 7 Nov 2008 13:51:09 +0000 Subject: [PATCH 176/381] some boolean correctness work --- app/controllers/message_controller.rb | 6 +++--- app/models/node.rb | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/controllers/message_controller.rb b/app/controllers/message_controller.rb index f3f3bf9ea..fc7a9101b 100644 --- a/app/controllers/message_controller.rb +++ b/app/controllers/message_controller.rb @@ -40,7 +40,7 @@ class MessageController < ApplicationController 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 @@ -70,10 +70,10 @@ class MessageController < ApplicationController 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 diff --git a/app/models/node.rb b/app/models/node.rb index 4ee849838..3481e28ca 100644 --- a/app/models/node.rb +++ b/app/models/node.rb @@ -156,7 +156,7 @@ class Node < ActiveRecord::Base raise OSM::APIPreconditionFailedError.new else self.changeset_id = new_node.changeset_id - self.visible = 0 + self.visible = false # update the changeset with the deleted position changeset.update_bbox!(bbox) From 748a7a0a03c4240f5aaebb4ee1c0884b2736b15a Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Fri, 7 Nov 2008 15:08:43 +0000 Subject: [PATCH 177/381] fix the indentation only --- db/migrate/017_populate_node_tags_and_remove.rb | 12 +++++------- .../017_populate_node_tags_and_remove_helper.c | 14 +++++++------- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/db/migrate/017_populate_node_tags_and_remove.rb b/db/migrate/017_populate_node_tags_and_remove.rb index 9a87fe2ba..f10bf16d2 100644 --- a/db/migrate/017_populate_node_tags_and_remove.rb +++ b/db/migrate/017_populate_node_tags_and_remove.rb @@ -8,18 +8,16 @@ class PopulateNodeTagsAndRemove < ActiveRecord::Migration cmd = "db/migrate/017_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 + system 'cc -O3 -Wall `mysql_config --cflags --libs` ' + + "#{src} -o #{cmd}" or fail end - conn_opts = ActiveRecord::Base.connection. - instance_eval { @connection_options } + 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 } + tempfiles = ['nodes', 'node_tags', 'current_nodes', 'current_node_tags']. + map { |base| prefix + base } nodes, node_tags, current_nodes, current_node_tags = tempfiles end diff --git a/db/migrate/017_populate_node_tags_and_remove_helper.c b/db/migrate/017_populate_node_tags_and_remove_helper.c index 12203d9cf..697b8fcd3 100644 --- a/db/migrate/017_populate_node_tags_and_remove_helper.c +++ b/db/migrate/017_populate_node_tags_and_remove_helper.c @@ -51,10 +51,10 @@ static void unescape(char *str) { if (*i == '\\') { i++; switch (tmp = *i++) { - case 's': *o++ = ';'; break; - case 'e': *o++ = '='; break; - case '\\': *o++ = '\\'; break; - default: *o++ = tmp; break; + case 's': *o++ = ';'; break; + case 'e': *o++ = '='; break; + case '\\': *o++ = '\\'; break; + default: *o++ = tmp; break; } } else { *o++ = *i++; @@ -115,7 +115,7 @@ static void proc_nodes(struct data *d, const char *tbl, FILE *out, FILE *out_tag 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); + 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]);*/ @@ -124,9 +124,9 @@ static void proc_nodes(struct data *d, const char *tbl, FILE *out, FILE *out_tag 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); + fprintf(out_tags, "\"%s\",\"%u\",", row[0], version); } else { - fprintf(out_tags, "\"%s\",", row[0]); + fprintf(out_tags, "\"%s\",", row[0]); } write_csv_col(out_tags, k, ','); From b875d14b7cd13432b0683ca548b341dcd8bddf80 Mon Sep 17 00:00:00 2001 From: Matt Amos Date: Fri, 7 Nov 2008 18:24:34 +0000 Subject: [PATCH 178/381] Added a query method for querying bounding boxes, times and users of changesets. Needs (much) more testing. --- app/controllers/changeset_controller.rb | 105 ++++++++++++++++++- app/models/node.rb | 2 +- test/fixtures/changesets.yml | 17 +++ test/fixtures/current_nodes.yml | 18 ++-- test/fixtures/nodes.yml | 18 ++-- test/functional/changeset_controller_test.rb | 8 ++ 6 files changed, 148 insertions(+), 20 deletions(-) diff --git a/app/controllers/changeset_controller.rb b/app/controllers/changeset_controller.rb index 904330900..29b5ef83e 100644 --- a/app/controllers/changeset_controller.rb +++ b/app/controllers/changeset_controller.rb @@ -6,9 +6,12 @@ class ChangesetController < ApplicationController before_filter :authorize, :only => [:create, :update, :delete, :upload, :include] before_filter :check_write_availability, :only => [:create, :update, :delete, :upload, :include] - before_filter :check_read_availability, :except => [:create, :update, :delete, :upload, :download] + 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 + # Create a changeset from XML. def create if request.put? @@ -224,4 +227,104 @@ class ChangesetController < ApplicationController 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']) + cond_merge conditions, conditions_user(params['user']) + cond_merge conditions, conditions_time(params['time']) + 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 + rescue String => s + render :text => s, :content_type => "text/plain", :status => :bad_request + end + + ## + # 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 "Bounding box should be min_lon,min_lat,max_lon,max_lat" unless bbox.count(',') == 3 + bbox = sanitise_boundaries(bbox.split(/,/)) + raise "Minimum longitude should be less than maximum." unless bbox[0] <= bbox[2] + raise "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] * SCALE, bbox[0] * SCALE, bbox[3]* SCALE, bbox[1] * SCALE] + else + return nil + end + end + + ## + # restrict changesets to those by a particular user + def conditions_user(user) + unless user.nil? + u = User.find(user.to_i) + raise OSM::APINotFoundError unless u.data_public? + 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 + from, to = time.split(/,/).collect { |t| Date.parse(t) } + return ['created_at > ? and created_at < ?', from, to] + else + # if there is no comma, assume its a lower limit on time + return ['created_at > ?', Date.parse(time)] + end + else + return nil + end + rescue ArgumentError => ex + raise ex.message.to_s + end + + ## + # restrict changes to those which are open + def conditions_open(open) + return open.nil? ? nil : ['open = ?', true] + end + end diff --git a/app/models/node.rb b/app/models/node.rb index 3481e28ca..cf7aedae8 100644 --- a/app/models/node.rb +++ b/app/models/node.rb @@ -83,7 +83,7 @@ class Node < ActiveRecord::Base # version must be present unless creating return nil unless create or not pt['version'].nil? - node.version = pt['version'].to_i + node.version = create ? 0 : pt['version'].to_i unless create if pt['id'] != '0' diff --git a/test/fixtures/changesets.yml b/test/fixtures/changesets.yml index 03fc36ddf..2047af8d5 100644 --- a/test/fixtures/changesets.yml +++ b/test/fixtures/changesets.yml @@ -1,9 +1,17 @@ +# 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" open: true + min_lon: <%= 1 * SCALE %> + min_lat: <%= 1 * SCALE %> + max_lon: <%= 5 * SCALE %> + max_lat: <%= 5 * SCALE %> second_user_first_change: id: 2 @@ -22,3 +30,12 @@ normal_user_version_change: user_id: 1 created_at: "2008-01-01 00:00:00" open: true + +# changeset to contain all the invalid stuff that is in the +# fixtures (nodes outside the world, etc...) +invalid_changeset: + id: 5 + user_id: 0 + created_at: "2008-01-01 00:00:00" + open: false + \ No newline at end of file diff --git a/test/fixtures/current_nodes.yml b/test/fixtures/current_nodes.yml index 2d4b35fd6..6f21fd47f 100644 --- a/test/fixtures/current_nodes.yml +++ b/test/fixtures/current_nodes.yml @@ -55,7 +55,7 @@ node_too_far_north: id: 6 latitude: <%= 90.01*SCALE %> longitude: <%= 6*SCALE %> - changeset_id: 1 + changeset_id: 5 visible: true version: 1 tile: <%= QuadTile.tile_for_point(90.01,6) %> @@ -65,7 +65,7 @@ node_north_limit: id: 11 latitude: <%= 90*SCALE %> longitude: <%= 11*SCALE %> - changeset_id: 1 + changeset_id: 5 visible: true version: 1 tile: <%= QuadTile.tile_for_point(90,11) %> @@ -75,7 +75,7 @@ node_too_far_south: id: 7 latitude: <%= -90.01*SCALE %> longitude: <%= 7*SCALE %> - changeset_id: 1 + changeset_id: 5 visible: true version: 1 tile: <%= QuadTile.tile_for_point(-90.01,7) %> @@ -85,7 +85,7 @@ node_south_limit: id: 12 latitude: <%= -90*SCALE %> longitude: <%= 12*SCALE %> - changeset_id: 1 + changeset_id: 5 visible: true version: 1 tile: <%= QuadTile.tile_for_point(-90,12) %> @@ -95,7 +95,7 @@ node_too_far_west: id: 8 latitude: <%= 8*SCALE %> longitude: <%= -180.01*SCALE %> - changeset_id: 1 + changeset_id: 5 visible: true version: 1 tile: <%= QuadTile.tile_for_point(8,-180.01) %> @@ -105,7 +105,7 @@ node_west_limit: id: 13 latitude: <%= 13*SCALE %> longitude: <%= -180*SCALE %> - changeset_id: 1 + changeset_id: 5 visible: true version: 1 tile: <%= QuadTile.tile_for_point(13,-180) %> @@ -115,7 +115,7 @@ node_too_far_east: id: 9 latitude: <%= 9*SCALE %> longitude: <%= 180.01*SCALE %> - changeset_id: 1 + changeset_id: 5 visible: true version: 1 tile: <%= QuadTile.tile_for_point(9,180.01) %> @@ -125,7 +125,7 @@ node_east_limit: id: 14 latitude: <%= 14*SCALE %> longitude: <%= 180*SCALE %> - changeset_id: 1 + changeset_id: 5 visible: true version: 1 tile: <%= QuadTile.tile_for_point(14,180) %> @@ -135,7 +135,7 @@ node_totally_wrong: id: 10 latitude: <%= 200*SCALE %> longitude: <%= 200*SCALE %> - changeset_id: 1 + changeset_id: 5 visible: true version: 1 tile: <%= QuadTile.tile_for_point(200,200) %> diff --git a/test/fixtures/nodes.yml b/test/fixtures/nodes.yml index 27f51f6a2..5b690696e 100644 --- a/test/fixtures/nodes.yml +++ b/test/fixtures/nodes.yml @@ -55,7 +55,7 @@ node_too_far_north: id: 6 latitude: <%= 90.01*SCALE %> longitude: <%= 6*SCALE %> - changeset_id: 1 + changeset_id: 5 visible: true version: 1 tile: <%= QuadTile.tile_for_point(90.01,6) %> @@ -65,7 +65,7 @@ node_north_limit: id: 11 latitude: <%= 90*SCALE %> longitude: <%= 11*SCALE %> - changeset_id: 1 + changeset_id: 5 visible: true version: 1 tile: <%= QuadTile.tile_for_point(90,11) %> @@ -75,7 +75,7 @@ node_too_far_south: id: 7 latitude: <%= -90.01*SCALE %> longitude: <%= 7*SCALE %> - changeset_id: 1 + changeset_id: 5 visible: true version: 1 tile: <%= QuadTile.tile_for_point(-90.01,7) %> @@ -85,7 +85,7 @@ node_south_limit: id: 12 latitude: <%= -90*SCALE %> longitude: <%= 12*SCALE %> - changeset_id: 1 + changeset_id: 5 visible: true version: 1 tile: <%= QuadTile.tile_for_point(-90,12) %> @@ -95,7 +95,7 @@ node_too_far_west: id: 8 latitude: <%= 8*SCALE %> longitude: <%= -180.01*SCALE %> - changeset_id: 1 + changeset_id: 5 visible: true version: 1 tile: <%= QuadTile.tile_for_point(8,-180.01) %> @@ -105,7 +105,7 @@ node_west_limit: id: 13 latitude: <%= 13*SCALE %> longitude: <%= -180*SCALE %> - changeset_id: 1 + changeset_id: 5 visible: true version: 1 tile: <%= QuadTile.tile_for_point(13,-180) %> @@ -115,7 +115,7 @@ node_too_far_east: id: 9 latitude: <%= 9*SCALE %> longitude: <%= 180.01*SCALE %> - changeset_id: 1 + changeset_id: 5 visible: true version: 1 tile: <%= QuadTile.tile_for_point(9,180.01) %> @@ -125,7 +125,7 @@ node_east_limit: id: 14 latitude: <%= 14*SCALE %> longitude: <%= 180*SCALE %> - changeset_id: 1 + changeset_id: 5 visible: true version: 1 tile: <%= QuadTile.tile_for_point(14,180) %> @@ -135,7 +135,7 @@ node_totally_wrong: id: 10 latitude: <%= 200*SCALE %> longitude: <%= 200*SCALE %> - changeset_id: 1 + changeset_id: 5 visible: true version: 1 tile: <%= QuadTile.tile_for_point(200,200) %> diff --git a/test/functional/changeset_controller_test.rb b/test/functional/changeset_controller_test.rb index 150e53dae..25ceca187 100644 --- a/test/functional/changeset_controller_test.rb +++ b/test/functional/changeset_controller_test.rb @@ -576,6 +576,14 @@ EOF check_after_include(changeset_id, -2, 5, [-2, -1, 4, 5]) end + ## + # check searching for changesets by bbox + def test_changeset_by_bbox + get :query, :bbox => "-10,-10, 10, 10" + assert_response :success, "can't get changesets in bbox" + # FIXME: write the actual test bit after fixing the fixtures! + end + #------------------------------------------------------------ # utility functions #------------------------------------------------------------ From 67a50e848c8d9998a88d153429ae9d1ae25d3baf Mon Sep 17 00:00:00 2001 From: Dave Stubbs Date: Sat, 8 Nov 2008 10:48:07 +0000 Subject: [PATCH 179/381] more boolean correcting --- app/controllers/trace_controller.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/controllers/trace_controller.rb b/app/controllers/trace_controller.rb index f0d8033f2..d94280a6a 100644 --- a/app/controllers/trace_controller.rb +++ b/app/controllers/trace_controller.rb @@ -51,7 +51,8 @@ class TraceController < ApplicationController conditions << @tag end - conditions[0] += " AND gpx_files.visible = 1" #FIXME: use boolean true as parameter to active record + conditions[0] += " AND gpx_files.visible = ?" + conditions << true @trace_pages, @traces = paginate(:traces, :include => [:user, :tags], @@ -196,7 +197,7 @@ class TraceController < ApplicationController end def georss - conditions = ["gpx_files.public = 1"] # fixme pass boolean true as parameter + conditions = ["gpx_files.public = ?", true] if params[:display_name] conditions[0] += " AND users.display_name = ?" From e598ee220f30bbe4b6033d087e25a8fee0bcebe2 Mon Sep 17 00:00:00 2001 From: Andy Allan Date: Sat, 8 Nov 2008 11:01:39 +0000 Subject: [PATCH 180/381] Change bigint id column to monkeypatching-friendly syntax --- db/migrate/020_add_changesets.rb | 7 +------ lib/migrate.rb | 1 + 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/db/migrate/020_add_changesets.rb b/db/migrate/020_add_changesets.rb index 75a909e80..772a5f20d 100644 --- a/db/migrate/020_add_changesets.rb +++ b/db/migrate/020_add_changesets.rb @@ -4,7 +4,6 @@ class AddChangesets < ActiveRecord::Migration def self.up create_table "changesets", innodb_table do |t| - t.column "id", :bigint, :limit => 20, :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 @@ -13,11 +12,7 @@ class AddChangesets < ActiveRecord::Migration t.column "min_lon", :integer, :null => true t.column "max_lon", :integer, :null => true end - - add_primary_key "changesets", ["id"] - # FIXME add indexes? - - change_column "changesets", "id", :bigint, :limit => 20, :null => false, :options => "AUTO_INCREMENT" + add_column :changesets, :id, :bigint_pk create_table "changeset_tags", innodb_table do |t| t.column "id", :bigint, :limit => 64, :null => false diff --git a/lib/migrate.rb b/lib/migrate.rb index 26e95a496..68ff04615 100644 --- a/lib/migrate.rb +++ b/lib/migrate.rb @@ -44,6 +44,7 @@ module ActiveRecord 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 end From c6592045c482d9086d5db6ebab87db49aa85cefa Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Sat, 8 Nov 2008 11:34:58 +0000 Subject: [PATCH 181/381] Adding some diary fixtures and test stubs --- test/fixtures/diary_comments.yml | 7 ++++ test/fixtures/diary_entries.yml | 21 ++++++++++++ .../functional/diary_entry_controller_test.rb | 33 +++++++++++++++++-- 3 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 test/fixtures/diary_comments.yml create mode 100644 test/fixtures/diary_entries.yml diff --git a/test/fixtures/diary_comments.yml b/test/fixtures/diary_comments.yml new file mode 100644 index 000000000..8bb9f49bd --- /dev/null +++ b/test/fixtures/diary_comments.yml @@ -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 index 000000000..5d07e5fa7 --- /dev/null +++ b/test/fixtures/diary_entries.yml @@ -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/functional/diary_entry_controller_test.rb b/test/functional/diary_entry_controller_test.rb index 7ebf439f4..7eebfa57e 100644 --- a/test/functional/diary_entry_controller_test.rb +++ b/test/functional/diary_entry_controller_test.rb @@ -1,8 +1,35 @@ require File.dirname(__FILE__) + '/../test_helper' class DiaryEntryControllerTest < ActionController::TestCase - # Replace this with your real tests. - def test_truth - assert true + 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 + + def test_showing_create_diary_entry + + end + + def test_editing_diary_entry + + end + + def test_editing_creating_diary_comment + + end + + def test_listing_diary_entries + + end + + def test_rss + + end + + def test_viewing_diary_entry + end end From fb627873c2b9df6dc740ad3c4ce1b8ff6c5ecdab Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Sat, 8 Nov 2008 12:16:42 +0000 Subject: [PATCH 182/381] fixing one cmd that was missed in the resync --- db/migrate/018_populate_node_tags_and_remove.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/migrate/018_populate_node_tags_and_remove.rb b/db/migrate/018_populate_node_tags_and_remove.rb index f10bf16d2..2a3f3c988 100644 --- a/db/migrate/018_populate_node_tags_and_remove.rb +++ b/db/migrate/018_populate_node_tags_and_remove.rb @@ -5,7 +5,7 @@ class PopulateNodeTagsAndRemove < ActiveRecord::Migration if have_nodes prefix = File.join Dir.tmpdir, "017_populate_node_tags_and_remove.#{$$}." - cmd = "db/migrate/017_populate_node_tags_and_remove_helper" + cmd = "db/migrate/018_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` ' + From 204fa8706495a4f78612d0e2a21f10b94649cc86 Mon Sep 17 00:00:00 2001 From: Andy Allan Date: Sat, 8 Nov 2008 12:30:42 +0000 Subject: [PATCH 183/381] move bigint_pk id column into table def, no need to be an add_column --- db/migrate/010_diary_comments.rb | 4 +--- db/migrate/021_add_changesets.rb | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/db/migrate/010_diary_comments.rb b/db/migrate/010_diary_comments.rb index be07d851a..43019a938 100644 --- a/db/migrate/010_diary_comments.rb +++ b/db/migrate/010_diary_comments.rb @@ -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 diff --git a/db/migrate/021_add_changesets.rb b/db/migrate/021_add_changesets.rb index 772a5f20d..9b769c772 100644 --- a/db/migrate/021_add_changesets.rb +++ b/db/migrate/021_add_changesets.rb @@ -4,6 +4,7 @@ class AddChangesets < ActiveRecord::Migration 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 @@ -12,7 +13,6 @@ class AddChangesets < ActiveRecord::Migration t.column "min_lon", :integer, :null => true t.column "max_lon", :integer, :null => true end - add_column :changesets, :id, :bigint_pk create_table "changeset_tags", innodb_table do |t| t.column "id", :bigint, :limit => 64, :null => false From 22005a38a696828c3c6ade4c49d7499afc06bc81 Mon Sep 17 00:00:00 2001 From: Matt Amos Date: Sat, 8 Nov 2008 14:07:34 +0000 Subject: [PATCH 184/381] Error message for version mismatch is now more informative. --- lib/consistency_validations.rb | 2 +- lib/osm.rb | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/consistency_validations.rb b/lib/consistency_validations.rb index 8fd6c257d..6e214f902 100644 --- a/lib/consistency_validations.rb +++ b/lib/consistency_validations.rb @@ -7,7 +7,7 @@ module ConsistencyValidations # 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.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 diff --git a/lib/osm.rb b/lib/osm.rb index 223e351f4..c132ff25c 100644 --- a/lib/osm.rb +++ b/lib/osm.rb @@ -95,15 +95,16 @@ module OSM # Raised when the provided version is not equal to the latest in the db. class APIVersionMismatchError < APIError - def initialize(provided, latest) - @provided, @latest = provided, latest + def initialize(id, type, provided, latest) + @id, @type, @provided, @latest = id, type, provided, latest end - attr_reader :provided, :latest + attr_reader :provided, :latest, :id, :type def render_opts { :text => "Version mismatch: Provided " + provided.to_s + - ", server had: " + latest.to_s, :status => :conflict } + ", server had: " + latest.to_s + " of " + type + " " + id.to_s, + :status => :conflict } end end From 7d786c32ed2918a1a5ce420c616e1fb038fb4fd9 Mon Sep 17 00:00:00 2001 From: Daniel Silverstone Date: Sat, 8 Nov 2008 14:11:45 +0000 Subject: [PATCH 185/381] Make the confrm button into a confirm button --- app/views/user/confirm.rhtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/user/confirm.rhtml b/app/views/user/confirm.rhtml index 5577b7068..7953ff822 100644 --- a/app/views/user/confirm.rhtml +++ b/app/views/user/confirm.rhtml @@ -4,7 +4,7 @@

      - + From 061d77726666beb8bccc27e0d410c4dc0ee4ef09 Mon Sep 17 00:00:00 2001 From: Andy Allan Date: Sat, 8 Nov 2008 15:01:14 +0000 Subject: [PATCH 186/381] Boolean correctness in migrations --- db/migrate/013_add_email_valid.rb | 2 +- db/migrate/015_add_user_visible.rb | 2 +- db/migrate/021_add_changesets.rb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/db/migrate/013_add_email_valid.rb b/db/migrate/013_add_email_valid.rb index b8af4bf6a..2192383bd 100644 --- a/db/migrate/013_add_email_valid.rb +++ b/db/migrate/013_add_email_valid.rb @@ -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 => true) end def self.down diff --git a/db/migrate/015_add_user_visible.rb b/db/migrate/015_add_user_visible.rb index d870dfffd..869f24c37 100644 --- a/db/migrate/015_add_user_visible.rb +++ b/db/migrate/015_add_user_visible.rb @@ -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/021_add_changesets.rb b/db/migrate/021_add_changesets.rb index 9b769c772..e0cf3904a 100644 --- a/db/migrate/021_add_changesets.rb +++ b/db/migrate/021_add_changesets.rb @@ -28,7 +28,7 @@ class AddChangesets < ActiveRecord::Migration # 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, 0 from users;" + "SELECT id, id, creation_time, false from users;" @@conv_user_tables.each { |tbl| rename_column tbl, :user_id, :changeset_id From 40b5b876f850e6a5e31463f90e268bfc2451c7a5 Mon Sep 17 00:00:00 2001 From: Harry Wood Date: Sat, 8 Nov 2008 16:41:51 +0000 Subject: [PATCH 187/381] message with a logo and link to Contact page --- public/404.html | 8 ++++++-- public/500.html | 9 +++++++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/public/404.html b/public/404.html index 0e1845619..16abbfca0 100644 --- a/public/404.html +++ b/public/404.html @@ -2,7 +2,11 @@ "http://www.w3.org/TR/html4/loose.dtd"> -

      File not found

      -

      Change this error message for pages not found in public/404.html

      + +
      +

      File not found

      +

      Couldn't find a file/directory/API operation by that name on the OpenStreetMap server (HTTP 404)

      +

      Feel free to contact the OpenStreetMap community if you have found a broken link / bug. Make a note of the exact URL of your request.

      +
      \ No newline at end of file diff --git a/public/500.html b/public/500.html index ab95f74c4..552024a2d 100644 --- a/public/500.html +++ b/public/500.html @@ -2,7 +2,12 @@ "http://www.w3.org/TR/html4/loose.dtd"> -

      Application error

      -

      Change this error message for exceptions thrown outside of an action (like in Dispatcher setups or broken Ruby code) in public/500.html

      + +
      +

      Application error

      +

      The OpenStreetMap server encountered an unexpected condition that prevented it from fulfilling the request (HTTP 500)

      +

      Feel free to contact the OpenStreetMap community if your problem persists. Make a note of the exact URL / post data of your request.

      +

      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)

      +
      \ No newline at end of file From fc9046ce6fcb3b4c4962285737767ea094b4cb00 Mon Sep 17 00:00:00 2001 From: Andy Allan Date: Sat, 8 Nov 2008 16:47:02 +0000 Subject: [PATCH 188/381] Moving AUTO_INCREMENTs to monkeypatching. Seems a bit hacky though --- db/migrate/001_create_osm_db.rb | 47 ++++++----------------------- db/migrate/002_cleanup_osm_db.rb | 8 ++--- db/migrate/004_user_enhancements.rb | 5 +-- db/migrate/006_tile_nodes.rb | 9 ++---- db/migrate/007_add_relations.rb | 5 +-- lib/migrate.rb | 2 ++ 6 files changed, 18 insertions(+), 58 deletions(-) diff --git a/db/migrate/001_create_osm_db.rb b/db/migrate/001_create_osm_db.rb index 689ca3c20..fdced3e1b 100644 --- a/db/migrate/001_create_osm_db.rb +++ b/db/migrate/001_create_osm_db.rb @@ -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 @@ -53,18 +53,14 @@ class CreateOsmDb < ActiveRecord::Migration execute "CREATE FULLTEXT INDEX `current_way_tags_v_idx` ON `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, :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 diff --git a/db/migrate/002_cleanup_osm_db.rb b/db/migrate/002_cleanup_osm_db.rb index b99055e52..1358febd8 100644 --- a/db/migrate/002_cleanup_osm_db.rb +++ b/db/migrate/002_cleanup_osm_db.rb @@ -150,15 +150,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 @@ -223,6 +219,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 diff --git a/db/migrate/004_user_enhancements.rb b/db/migrate/004_user_enhancements.rb index 92f01bf5d..a6e81d222 100644 --- a/db/migrate/004_user_enhancements.rb +++ b/db/migrate/004_user_enhancements.rb @@ -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 diff --git a/db/migrate/006_tile_nodes.rb b/db/migrate/006_tile_nodes.rb index 3a50cc9b0..f3a1edbb2 100644 --- a/db/migrate/006_tile_nodes.rb +++ b/db/migrate/006_tile_nodes.rb @@ -36,7 +36,7 @@ class TileNodes < ActiveRecord::Migration 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,11 +46,9 @@ 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 upgrade_table "current_nodes_v5", "current_nodes", Node @@ -85,7 +83,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 +92,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" diff --git a/db/migrate/007_add_relations.rb b/db/migrate/007_add_relations.rb index a30642e32..5dcdeb28d 100644 --- a/db/migrate/007_add_relations.rb +++ b/db/migrate/007_add_relations.rb @@ -27,15 +27,12 @@ class AddRelations < ActiveRecord::Migration execute "CREATE FULLTEXT INDEX `current_relation_tags_v_idx` ON `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 diff --git a/lib/migrate.rb b/lib/migrate.rb index 68ff04615..782436af3 100644 --- a/lib/migrate.rb +++ b/lib/migrate.rb @@ -45,6 +45,8 @@ module ActiveRecord 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 end From 9432e7ce27c55763d1d9d9d70d8aee1b94fbad3c Mon Sep 17 00:00:00 2001 From: Andy Allan Date: Sat, 8 Nov 2008 17:08:51 +0000 Subject: [PATCH 189/381] Fixing boo-boo in r11802 - but you can assign integers to booleans on all dbs --- db/migrate/013_add_email_valid.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/migrate/013_add_email_valid.rb b/db/migrate/013_add_email_valid.rb index 2192383bd..a01809701 100644 --- a/db/migrate/013_add_email_valid.rb +++ b/db/migrate/013_add_email_valid.rb @@ -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 => true) + 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 From c19d1b406b149040a37ef22f4f14eeddc29cf0ea Mon Sep 17 00:00:00 2001 From: Andy Allan Date: Sat, 8 Nov 2008 17:39:16 +0000 Subject: [PATCH 190/381] last of the auto_increments removed from the migrations --- db/migrate/001_create_osm_db.rb | 4 ++-- db/migrate/007_add_relations.rb | 2 +- lib/migrate.rb | 2 ++ 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/db/migrate/001_create_osm_db.rb b/db/migrate/001_create_osm_db.rb index fdced3e1b..998e12951 100644 --- a/db/migrate/001_create_osm_db.rb +++ b/db/migrate/001_create_osm_db.rb @@ -199,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 @@ -221,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 diff --git a/db/migrate/007_add_relations.rb b/db/migrate/007_add_relations.rb index 5dcdeb28d..dea777d5d 100644 --- a/db/migrate/007_add_relations.rb +++ b/db/migrate/007_add_relations.rb @@ -65,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 diff --git a/lib/migrate.rb b/lib/migrate.rb index 782436af3..77bc93646 100644 --- a/lib/migrate.rb +++ b/lib/migrate.rb @@ -47,6 +47,8 @@ module ActiveRecord 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 end From 91374368faffee13ded82b9718078d5593e32fed Mon Sep 17 00:00:00 2001 From: Andy Allan Date: Sat, 8 Nov 2008 18:04:36 +0000 Subject: [PATCH 191/381] Move db engine changing into db adaptor monkeypatch --- db/migrate/002_cleanup_osm_db.rb | 4 ++-- db/migrate/003_sql_session_store_setup.rb | 2 +- db/migrate/019_move_to_innodb.rb | 2 +- lib/migrate.rb | 8 ++++++++ 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/db/migrate/002_cleanup_osm_db.rb b/db/migrate/002_cleanup_osm_db.rb index 1358febd8..f283602ac 100644 --- a/db/migrate/002_cleanup_osm_db.rb +++ b/db/migrate/002_cleanup_osm_db.rb @@ -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 @@ -191,7 +191,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 diff --git a/db/migrate/003_sql_session_store_setup.rb b/db/migrate/003_sql_session_store_setup.rb index 7b1c75479..4de0dd4b1 100644 --- a/db/migrate/003_sql_session_store_setup.rb +++ b/db/migrate/003_sql_session_store_setup.rb @@ -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 diff --git a/db/migrate/019_move_to_innodb.rb b/db/migrate/019_move_to_innodb.rb index d17da8fd5..2e9f4adaa 100644 --- a/db/migrate/019_move_to_innodb.rb +++ b/db/migrate/019_move_to_innodb.rb @@ -14,7 +14,7 @@ class MoveToInnodb < ActiveRecord::Migration } @@conv_tables.each { |tbl| - execute "ALTER TABLE #{tbl} ENGINE = InnoDB" + change_engine (tbl, "InnoDB") } @@ver_tbl.each { |tbl| diff --git a/lib/migrate.rb b/lib/migrate.rb index 77bc93646..df7e5f7ae 100644 --- a/lib/migrate.rb +++ b/lib/migrate.rb @@ -73,6 +73,14 @@ 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 end end end From 550ce43250208b25d459247d3bbb5bbc25fa3ff4 Mon Sep 17 00:00:00 2001 From: Andy Allan Date: Sat, 8 Nov 2008 18:44:38 +0000 Subject: [PATCH 192/381] move fulltext indexes to monkeypatch, since not all dbs have a fulltext analogue --- db/migrate/001_create_osm_db.rb | 2 +- db/migrate/007_add_relations.rb | 2 +- lib/migrate.rb | 4 ++++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/db/migrate/001_create_osm_db.rb b/db/migrate/001_create_osm_db.rb index 998e12951..3e3377921 100644 --- a/db/migrate/001_create_osm_db.rb +++ b/db/migrate/001_create_osm_db.rb @@ -50,7 +50,7 @@ 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_pk_64, :null => false diff --git a/db/migrate/007_add_relations.rb b/db/migrate/007_add_relations.rb index dea777d5d..b4aff6e09 100644 --- a/db/migrate/007_add_relations.rb +++ b/db/migrate/007_add_relations.rb @@ -24,7 +24,7 @@ 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_pk_64, :null => false diff --git a/lib/migrate.rb b/lib/migrate.rb index df7e5f7ae..585b47b1c 100644 --- a/lib/migrate.rb +++ b/lib/migrate.rb @@ -81,6 +81,10 @@ module ActiveRecord 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 end end end From 46c0e364eea5a1016a37db54e8b8925059f16bfa Mon Sep 17 00:00:00 2001 From: Andy Allan Date: Sat, 8 Nov 2008 18:59:06 +0000 Subject: [PATCH 193/381] correcting typo on gps_points fixtures --- test/fixtures/{gpx_points.yml => gps_points.yml} | 0 test/test_helper.rb | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) rename test/fixtures/{gpx_points.yml => gps_points.yml} (100%) diff --git a/test/fixtures/gpx_points.yml b/test/fixtures/gps_points.yml similarity index 100% rename from test/fixtures/gpx_points.yml rename to test/fixtures/gps_points.yml diff --git a/test/test_helper.rb b/test/test_helper.rb index f355bf785..e38f74af5 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -58,9 +58,9 @@ class Test::Unit::TestCase set_fixture_class :relation_members => OldRelationMember set_fixture_class :relation_tags => OldRelationTag - fixtures :gpx_files, :gpx_points, :gpx_file_tags + fixtures :gpx_files, :gps_points, :gpx_file_tags set_fixture_class :gpx_files => Trace - set_fixture_class :gpx_points => Tracepoint + set_fixture_class :gps_points => Tracepoint set_fixture_class :gpx_file_tags => Tracetag end From 43b3998657c9807a68e71609c568748c8337b95e Mon Sep 17 00:00:00 2001 From: Harry Wood Date: Sat, 8 Nov 2008 19:03:25 +0000 Subject: [PATCH 194/381] cosmetic tweaks to user settings form + move 'Mapper since' field onto user profile view --- app/views/layouts/site.rhtml | 1 + app/views/user/account.rhtml | 61 +++++++++++++++++++++++------------- app/views/user/view.rhtml | 14 +++++++++ public/stylesheets/site.css | 13 ++++++-- 4 files changed, 65 insertions(+), 24 deletions(-) diff --git a/app/views/layouts/site.rhtml b/app/views/layouts/site.rhtml index cf7ad9fc8..a5416ff68 100644 --- a/app/views/layouts/site.rhtml +++ b/app/views/layouts/site.rhtml @@ -69,6 +69,7 @@ <% unless @user %>
      + HELLO! OpenStreetMap is a free editable map of the whole world. It is made by people like you.

      OpenStreetMap allows you to view, edit and use geographical data in a collaborative way from anywhere on Earth. diff --git a/app/views/user/account.rhtml b/app/views/user/account.rhtml index 501af7494..8a57ccf52 100644 --- a/app/views/user/account.rhtml +++ b/app/views/user/account.rhtml @@ -1,34 +1,53 @@ -

      User details

      +

      My settings

      <%= error_messages_for 'user' %> <% form_for :user, @user do |f| %> - - - - - - +
      Email<%= f.text_field :email %>
      Mapper since<%= @user.creation_time %> (<%= time_ago_in_words(@user.creation_time) %> ago)
      Display Name<%= f.text_field :display_name %>
      Password<%= f.password_field :pass_crypt, {:value => '', :size => 50, :maxlength => 255} %>
      Confirm Password<%= f.password_field :pass_crypt_confirmation, {:value => '', :size => 50, :maxlength => 255} %>
      + + + + + + + + + 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. + You will need to do this if you want to use the online editor and it is encouraged (find out why). +

      + This action cannot be reversed and all new users are now public by default. +

      + <%= button_to "Make all my edits public", :action => :go_public %> +

      + +<% end %> + + - class="nohome" <%end%> > + + + + + class="nohome" <%end%> > + +
      Display Name : <%= f.text_field :display_name %>
      Email : <%= f.text_field :email, {:size => 50, :maxlength => 255} %> (not displayed publicly)
      Password : <%= f.password_field :pass_crypt, {:value => '', :size => 30, :maxlength => 255} %>
      Confirm Password : <%= f.password_field :pass_crypt_confirmation, {:value => '', :size => 30, :maxlength => 255} %>
      Public editing : +<% if @user.data_public? %> + Enabled. Not anonymous (what's this?) +<% else %> -
      Description<%= f.text_area :description, :class => "editDescription" %>
      Your homeYou have not entered your home location.Latitude: <%= f.text_field :home_lat, :size => 20, :id => "home_lat" %> Longitude <%= f.text_field :home_lon, :size => 20, :id => "home_lon" %>
      Profile Description : <%= f.text_area :description, :rows => '5', :cols => '60' %>

      Home Location : You have not entered your home location.Latitude: <%= f.text_field :home_lat, :size => 20, :id => "home_lat" %> Longitude <%= f.text_field :home_lon, :size => 20, :id => "home_lon" %>

      Update home location when I click on the map? checked="checked" <% end %> id="updatehome" />

      -
      +


      <%= submit_tag 'Save Changes' %>
      -<%= submit_tag 'Save Changes' %> +
      + <% end %> <%= render :partial => 'friend_map' %> -

      Public editing

      -<% 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. -You will need to do this if you want to use the online editor and it is encouraged (find out why). -This action cannot be reversed and all new users are now public by default. -

      - <%= button_to "Make all my edits public", :action => :go_public %> -<% end %> +
      +
      +
      +<%= link_to 'return to profile', :controller => 'user', :action => @user.display_name %> +
      +
      \ No newline at end of file diff --git a/app/views/user/view.rhtml b/app/views/user/view.rhtml index 438de836d..66a7426f5 100644 --- a/app/views/user/view.rhtml +++ b/app/views/user/view.rhtml @@ -2,11 +2,13 @@

      <%= h(@this_user.display_name) %>

      <% if @user and @this_user.id == @user.id %> + <%= 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 %> + <%= 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 %> @@ -18,6 +20,12 @@ <% end %>
      +<% if @this_user != nil %> +

      +Mapper since : <%= @this_user.creation_time %> (<%= time_ago_in_words(@this_user.creation_time) %> ago) +

      +<% end %> +

      User image

      <% if @this_user.image %> <%= image_tag url_for_file_column(@this_user, "image") %> @@ -92,3 +100,9 @@ <% end %> <% end %> <% end %> + +
      +
      +<% 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 diff --git a/public/stylesheets/site.css b/public/stylesheets/site.css index 31c61de28..222c93280 100644 --- a/public/stylesheets/site.css +++ b/public/stylesheets/site.css @@ -527,6 +527,14 @@ input[type="submit"] { border: 1px solid black; } +#accountForm TD { + padding-bottom:10px; +} +#accountForm .fieldName { + text-align:right; + font-weight:bold; +} + .nohome .location { display: none; } @@ -539,9 +547,8 @@ input[type="submit"] { display: inline !important; } -.editDescription { - height: 10ex; - width: 30em; +.minorNote { + font-size:0.8em; } .nowrap { From ee656dd1f3ee556e9c6793a70781d4346abfe01e Mon Sep 17 00:00:00 2001 From: Daniel Silverstone Date: Sat, 8 Nov 2008 19:18:22 +0000 Subject: [PATCH 195/381] Support building a postgresql binding for maptile_for_point --- db/functions/Makefile | 21 +++++++++++--- db/functions/maptile.c | 62 ++++++++++++++++++++++++++++++++++++------ 2 files changed, 70 insertions(+), 13 deletions(-) diff --git a/db/functions/Makefile b/db/functions/Makefile index 7652862fa..1bdddce71 100644 --- a/db/functions/Makefile +++ b/db/functions/Makefile @@ -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 diff --git a/db/functions/maptile.c b/db/functions/maptile.c index f96f9c23e..2c15d2b41 100644 --- a/db/functions/maptile.c +++ b/db/functions/maptile.c @@ -1,3 +1,31 @@ +#ifndef USE_MYSQL +#ifndef USE_PGSQL +#error One of USE_MYSQL or USE_PGSQL must be defined +#endif +#endif + +#include + +/* 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 #include #include @@ -27,13 +55,29 @@ 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; - - 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; + + return internal_maptile_for_point(lat, lon, zoom); } +#endif + +#ifdef USE_PGSQL +#include +#include + +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); + +#ifdef PG_MODULE_MAGIC +PG_MODULE_MAGIC; +#endif + +#endif From 509b8d52074e7dce2ac078d16056f336536e8dac Mon Sep 17 00:00:00 2001 From: Daniel Silverstone Date: Sat, 8 Nov 2008 19:21:03 +0000 Subject: [PATCH 196/381] CREATE FUNCTION statement for pgsql --- db/functions/maptile.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/db/functions/maptile.c b/db/functions/maptile.c index 2c15d2b41..c2baac5d4 100644 --- a/db/functions/maptile.c +++ b/db/functions/maptile.c @@ -76,6 +76,16 @@ maptile_for_point(PG_FUNCTION_ARGS) 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 From 9e354f1b0f3c79e27c5623463344342184284423 Mon Sep 17 00:00:00 2001 From: Daniel Silverstone Date: Sat, 8 Nov 2008 19:33:41 +0000 Subject: [PATCH 197/381] Ignore the bits in tmp/ From eebadd141a6b0aa067131d6ae807fae5f4d3c07b Mon Sep 17 00:00:00 2001 From: Dave Stubbs Date: Sat, 8 Nov 2008 19:41:29 +0000 Subject: [PATCH 198/381] 1st amf_controller tests, and some basic sanity check fixes --- app/controllers/amf_controller.rb | 59 +++++-- test/functional/amf_controller_test.rb | 213 ++++++++++++++++++++++++- 2 files changed, 257 insertions(+), 15 deletions(-) diff --git a/app/controllers/amf_controller.rb b/app/controllers/amf_controller.rb index 935746ed4..0e77c11f7 100644 --- a/app/controllers/amf_controller.rb +++ b/app/controllers/amf_controller.rb @@ -28,6 +28,9 @@ class AmfController < ApplicationController include Potlatch + # Help methods for checking boundary sanity and area size + include MapBoundary + session :off before_filter :check_write_availability @@ -128,6 +131,15 @@ class AmfController < ApplicationController def whichways(xmin, ymin, xmax, ymax) #:doc: xmin -= 0.01; ymin -= 0.01 xmax += 0.01; ymax += 0.01 + + # check boundary is sane and area within defined + # see /config/application.yml + begin + check_boundaries(xmin, ymin, xmax, ymax) + rescue Exception => err + # FIXME: report an error rather than just return an empty result + return [[],[],[]] + end if POTLATCH_USE_SQL then way_ids = sql_find_way_ids_in_area(xmin, ymin, xmax, ymax) @@ -135,7 +147,7 @@ class AmfController < ApplicationController 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) + nodes_in_area = Node.find_by_area(ymin, xmin, ymax, xmax, :conditions => ["current_nodes.visible = ?", true], :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 @@ -143,8 +155,8 @@ class AmfController < ApplicationController 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") + relations = Relation.find_for_nodes(nodes_in_area.collect { |n| n.id }, :conditions => {:visible => true}) + + Relation.find_for_ways(way_ids, :conditions => {:visible => true}) relation_ids = relations.collect { |relation| relation.id }.uniq end @@ -158,7 +170,16 @@ class AmfController < ApplicationController 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) + # check boundary is sane and area within defined + # see /config/application.yml + begin + check_boundaries(xmin, ymin, xmax, ymax) + rescue Exception => err + # FIXME: report an error rather than just return an empty result + return [[]] + end + + 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 [way_ids] @@ -175,7 +196,15 @@ class AmfController < ApplicationController # 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) + 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_as_hash nodetags.delete('created_by') @@ -196,7 +225,7 @@ class AmfController < ApplicationController def getway_old(id, version) #:doc: if version < 0 - old_way = OldWay.find(:first, :conditions => ['visible = 1 AND id = ?', id], :order => 'version DESC') + old_way = OldWay.find(:first, :conditions => ['visible = ? AND id = ?', true, id], :order => 'version DESC') points = old_way.get_nodes_undelete else old_way = OldWay.find(:first, :conditions => ['id = ? AND version = ?', id, version]) @@ -241,7 +270,13 @@ class AmfController < ApplicationController # 2. list of members. def getrelation(relid) #:doc: - rel = Relation.find(relid) + begin + rel = Relation.find(relid) + rescue ActiveRecord::RecordNotFound + return [relid, {}, []] + end + + return [relid, {}, []] if rel.nil? or !rel.visible [relid, rel.tags, rel.members] end @@ -277,7 +312,7 @@ class AmfController < ApplicationController 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 + visible = (visible.to_i != 0) # create a new relation, or find the existing one if relid <= 0 @@ -563,8 +598,8 @@ class AmfController < ApplicationController 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 + 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 } @@ -575,7 +610,7 @@ class AmfController < ApplicationController SELECT current_nodes.id,current_nodes.latitude*0.0000001 AS lat,current_nodes.longitude*0.0000001 AS lon,current_nodes.tags FROM current_nodes LEFT OUTER JOIN current_way_nodes cwn ON cwn.node_id=current_nodes.id - WHERE current_nodes.visible=1 + WHERE current_nodes.visible=TRUE AND cwn.id IS NULL AND #{OSM.sql_for_area(ymin, xmin, ymax, xmax, "current_nodes.")} EOF @@ -612,7 +647,7 @@ class AmfController < ApplicationController 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 + AND current_nodes.visible=TRUE ORDER BY sequence_id EOF ActiveRecord::Base.connection.select_all(sql).each do |row| diff --git a/test/functional/amf_controller_test.rb b/test/functional/amf_controller_test.rb index 1d17a5bea..e0375c92f 100644 --- a/test/functional/amf_controller_test.rb +++ b/test/functional/amf_controller_test.rb @@ -1,8 +1,215 @@ require File.dirname(__FILE__) + '/../test_helper' +require 'stringio' +include Potlatch class AmfControllerTest < ActionController::TestCase - # Replace this with your real tests. - def test_truth - assert true + api_fixtures + + 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 map[0].include?(current_ways(:used_way).id) + assert !map[0].include?(current_ways(:invisible_way).id) + end + + def test_whichways_toobig + bbox = [-0.1,-0.1,1.1,1.1] + amf_content "whichways", "/1", bbox + post :amf_read + assert_response :success + amf_parse_response + + # FIXME: whichways needs to reject large bboxes and the test needs to check for this + map = amf_result "/1" + assert map[0].empty? and map[1].empty? and map[2].empty? + end + + def test_whichways_badlat + bboxes = [[0,0.1,0.1,0], [-0.1,80,0.1,70], [0.24,54.34,0.25,54.33]] + bboxes.each do |bbox| + amf_content "whichways", "/1", bbox + post :amf_read + assert_response :success + amf_parse_response + + # FIXME: whichways needs to reject bboxes with illegal lats and the test needs to check for this + map = amf_result "/1" + assert map[0].empty? and map[1].empty? and map[2].empty? + end + end + + def test_whichways_badlon + bboxes = [[80,-0.1,70,0.1], [54.34,0.24,54.33,0.25]] + bboxes.each do |bbox| + amf_content "whichways", "/1", bbox + post :amf_read + assert_response :success + amf_parse_response + + # FIXME: whichways needs to reject bboxes with illegal lons and the test needs to check for this + map = amf_result "/1" + assert map[0].empty? and map[1].empty? and map[2].empty? + 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 + ways = amf_result "/1" + assert ways[0].include?(current_ways(:invisible_way).id) + assert !ways[0].include?(current_ways(:used_way).id) + 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 + + ways = amf_result "/1" + assert ways[0].empty? + 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 + + # 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 + end From 79420631ceb2cb0f33e127b06e7b4f8a461b9281 Mon Sep 17 00:00:00 2001 From: Frederik Ramm Date: Sat, 8 Nov 2008 19:43:31 +0000 Subject: [PATCH 199/381] fix version number return for "modify" sections of diff uploads --- config/database.yml | 6 +++--- config/environment.rb | 2 +- config/environments/development.rb | 2 +- lib/diff_reader.rb | 6 ++++-- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/config/database.yml b/config/database.yml index cc3f9a1a5..09c9dbe6b 100644 --- a/config/database.yml +++ b/config/database.yml @@ -12,9 +12,9 @@ # http://dev.mysql.com/doc/refman/5.0/en/old-client.html development: adapter: mysql - database: openstreetmap - username: openstreetmap - password: openstreetmap + database: osm + username: osm + password: osm host: localhost encoding: utf8 diff --git a/config/environment.rb b/config/environment.rb index 171bb6fc8..2c9e4803c 100644 --- a/config/environment.rb +++ b/config/environment.rb @@ -60,7 +60,7 @@ Rails::Initializer.run do |config| # Force all environments to use the same logger level # (by default production uses :info, the others :debug) - # config.log_level = :debug + config.log_level = :debug # Your secret key for verifying cookie session data integrity. # If you change this key, all old sessions will become invalid! diff --git a/config/environments/development.rb b/config/environments/development.rb index 85c9a6080..d67452f0c 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -14,4 +14,4 @@ config.action_view.debug_rjs = true config.action_controller.perform_caching = 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/lib/diff_reader.rb b/lib/diff_reader.rb index d793f63e7..f7c51d797 100644 --- a/lib/diff_reader.rb +++ b/lib/diff_reader.rb @@ -129,8 +129,10 @@ class DiffReader 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 - xml_result["new_version"] = new.version.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 From 7a3f7bc2dd1cd89cb69a4b6f0f1e38859535891d Mon Sep 17 00:00:00 2001 From: Frederik Ramm Date: Sat, 8 Nov 2008 19:55:13 +0000 Subject: [PATCH 200/381] fix accidental commit of local config changes --- config/database.yml | 6 +++--- config/environment.rb | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/config/database.yml b/config/database.yml index 09c9dbe6b..cc3f9a1a5 100644 --- a/config/database.yml +++ b/config/database.yml @@ -12,9 +12,9 @@ # http://dev.mysql.com/doc/refman/5.0/en/old-client.html development: adapter: mysql - database: osm - username: osm - password: osm + database: openstreetmap + username: openstreetmap + password: openstreetmap host: localhost encoding: utf8 diff --git a/config/environment.rb b/config/environment.rb index 2c9e4803c..171bb6fc8 100644 --- a/config/environment.rb +++ b/config/environment.rb @@ -60,7 +60,7 @@ Rails::Initializer.run do |config| # Force all environments to use the same logger level # (by default production uses :info, the others :debug) - config.log_level = :debug + # config.log_level = :debug # Your secret key for verifying cookie session data integrity. # If you change this key, all old sessions will become invalid! From 4f15f5926792f5ba2b0efc39792c75830eb82199 Mon Sep 17 00:00:00 2001 From: Daniel Silverstone Date: Sun, 9 Nov 2008 10:41:48 +0000 Subject: [PATCH 201/381] Update DB readme for PgSQL a bit --- db/README | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/db/README b/db/README index 447c63651..f00029339 100644 --- a/db/README +++ b/db/README @@ -25,12 +25,12 @@ $ mysql -u -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 -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 ) + +> 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 =================================== From e26348fc18a921dbf740c3cad7bb79de5fd8f27d Mon Sep 17 00:00:00 2001 From: Harry Wood Date: Sun, 9 Nov 2008 10:57:05 +0000 Subject: [PATCH 202/381] oops remove my test --- app/views/layouts/site.rhtml | 1 - 1 file changed, 1 deletion(-) diff --git a/app/views/layouts/site.rhtml b/app/views/layouts/site.rhtml index a5416ff68..cf7ad9fc8 100644 --- a/app/views/layouts/site.rhtml +++ b/app/views/layouts/site.rhtml @@ -69,7 +69,6 @@ <% unless @user %>
      - HELLO! OpenStreetMap is a free editable map of the whole world. It is made by people like you.

      OpenStreetMap allows you to view, edit and use geographical data in a collaborative way from anywhere on Earth. From 771a4e5be3f947e84f89939bb9c4c1341f9164a0 Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Sun, 9 Nov 2008 11:13:10 +0000 Subject: [PATCH 203/381] bumping the version of composite primary keys for the amf controler as there are bugs in it --- config/environment.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/environment.rb b/config/environment.rb index 171bb6fc8..d2ab58ce4 100644 --- a/config/environment.rb +++ b/config/environment.rb @@ -45,7 +45,7 @@ Rails::Initializer.run do |config| # 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.0.10' + config.gem 'composite_primary_keys', :version => '1.1.0' config.gem 'libxml-ruby', :version => '>= 0.8.3', :lib => 'libxml' config.gem 'rmagick', :lib => 'RMagick' config.gem 'mysql' From ebbc770cbfcdf5b37ca88fa27b537919cef1aab7 Mon Sep 17 00:00:00 2001 From: Andy Allan Date: Sun, 9 Nov 2008 11:19:46 +0000 Subject: [PATCH 204/381] move index dropping to remove_index method --- db/migrate/019_move_to_innodb.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/db/migrate/019_move_to_innodb.rb b/db/migrate/019_move_to_innodb.rb index 2e9f4adaa..da0488ca5 100644 --- a/db/migrate/019_move_to_innodb.rb +++ b/db/migrate/019_move_to_innodb.rb @@ -6,8 +6,8 @@ class MoveToInnodb < ActiveRecord::Migration @@ver_tbl = ['nodes', 'ways', 'relations'] def self.up - execute 'DROP INDEX current_way_tags_v_idx ON current_way_tags' - execute 'DROP INDEX current_relation_tags_v_idx ON current_relation_tags' + 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 From b64fc35140674d868f9e66a748b663877d3fd285 Mon Sep 17 00:00:00 2001 From: Frederik Ramm Date: Sun, 9 Nov 2008 11:30:03 +0000 Subject: [PATCH 205/381] make diff upload result a "diffResult" rather than "osm" document --- lib/diff_reader.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/diff_reader.rb b/lib/diff_reader.rb index f7c51d797..6a053e4ad 100644 --- a/lib/diff_reader.rb +++ b/lib/diff_reader.rb @@ -79,6 +79,7 @@ class DiffReader 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 element (although we # don't actually check this...) From 5ff5bf60e56b2762ad08fa947af8e458e5974b6c Mon Sep 17 00:00:00 2001 From: Frederik Ramm Date: Sun, 9 Nov 2008 12:06:05 +0000 Subject: [PATCH 206/381] mini fix --- app/models/way.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/way.rb b/app/models/way.rb index c9e695b32..be6531def 100644 --- a/app/models/way.rb +++ b/app/models/way.rb @@ -113,7 +113,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 From 2a20c57a8e642c1117be2a608faaa91597b25726 Mon Sep 17 00:00:00 2001 From: Harry Wood Date: Sun, 9 Nov 2008 12:17:53 +0000 Subject: [PATCH 207/381] cosmetic tweaks to 'login' and 'create account' screens --- app/views/user/login.rhtml | 10 ++++++---- app/views/user/new.rhtml | 26 +++++++++++++++++--------- public/stylesheets/site.css | 6 ++++-- 3 files changed, 27 insertions(+), 15 deletions(-) diff --git a/app/views/user/login.rhtml b/app/views/user/login.rhtml index ff988f070..770ad8873 100644 --- a/app/views/user/login.rhtml +++ b/app/views/user/login.rhtml @@ -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])) %> +
      - - + + + +
      Email Address or username:<%= text_field('user', 'email',{:size => 50, :maxlength => 255}) %>
      Password:<%= password_field('user', 'password',{:size => 50, :maxlength => 255}) %>
      Email Address or username:<%= text_field('user', 'email',{:size => 50, :maxlength => 255}) %>
      Password:<%= password_field('user', 'password',{:size => 28, :maxlength => 255}) %> (<%= link_to 'Lost your password?', :controller => 'user', :action => 'lost_password' %>)
       
      <%= submit_tag 'Login' %>

      -<%= submit_tag 'Login' %> -<% end %> (<%= link_to 'Lost your password?', :controller => 'user', :action => 'lost_password' %>) +<% end %> diff --git a/app/views/user/new.rhtml b/app/views/user/new.rhtml index 5d4687edd..1b7f6e9b4 100644 --- a/app/views/user/new.rhtml +++ b/app/views/user/new.rhtml @@ -1,20 +1,28 @@

      Create a user account


      -Fill in the form and we'll send you a quick email to activate your account.

      +Fill in the form and we'll send you a quick email to activate your account. +

      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 this Creative Commons license (by-sa).

      <%= error_messages_for 'user' %> <% form_tag :action => 'save' do %> - - - - - - +
      Email Address<%= text_field('user', 'email',{:size => 50, :maxlength => 255}) %>
      Confirm Email Address<%= text_field('user', 'email_confirmation',{:size => 50, :maxlength => 255}) %>
      Display Name<%= text_field('user', 'display_name',{:size => 50, :maxlength => 255}) %>
      Password<%= password_field('user', 'pass_crypt',{:size => 50, :maxlength => 255}) %>
      Confirm Password<%= password_field('user', 'pass_crypt_confirmation',{:size => 50, :maxlength => 255}) %>
      + + + + + + + + + + +
      Email Address : <%= text_field('user', 'email',{:size => 50, :maxlength => 255}) %>
      Confirm Email Address : <%= text_field('user', 'email_confirmation',{:size => 50, :maxlength => 255}) %>
      Not displayed publicly (see privacy policy)
       
      Display Name : <%= text_field('user', 'display_name',{:size => 30, :maxlength => 255}) %>
       
      Password : <%= password_field('user', 'pass_crypt',{:size => 30, :maxlength => 255}) %>
      Confirm Password : <%= password_field('user', 'pass_crypt_confirmation',{:size => 30, :maxlength => 255}) %>
       


      - - + <% end %> diff --git a/public/stylesheets/site.css b/public/stylesheets/site.css index 222c93280..2bfe21d24 100644 --- a/public/stylesheets/site.css +++ b/public/stylesheets/site.css @@ -527,14 +527,16 @@ input[type="submit"] { border: 1px solid black; } -#accountForm TD { +#accountForm td { padding-bottom:10px; } -#accountForm .fieldName { + +.fieldName { text-align:right; font-weight:bold; } + .nohome .location { display: none; } From 9263c5500cbec408406d2d131d97a7d8eb63078e Mon Sep 17 00:00:00 2001 From: Frederik Ramm Date: Sun, 9 Nov 2008 13:44:08 +0000 Subject: [PATCH 208/381] require auth for close --- app/controllers/changeset_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/changeset_controller.rb b/app/controllers/changeset_controller.rb index 29b5ef83e..2709a594c 100644 --- a/app/controllers/changeset_controller.rb +++ b/app/controllers/changeset_controller.rb @@ -4,7 +4,7 @@ class ChangesetController < ApplicationController require 'xml/libxml' require 'diff_reader' - before_filter :authorize, :only => [:create, :update, :delete, :upload, :include] + 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 From c02c516047f20cdaa8120968e514420f6c2c6034 Mon Sep 17 00:00:00 2001 From: Frederik Ramm Date: Sun, 9 Nov 2008 15:52:10 +0000 Subject: [PATCH 209/381] fix SCALE, make changeset GET use floats --- app/controllers/changeset_controller.rb | 6 +++--- app/models/changeset.rb | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/controllers/changeset_controller.rb b/app/controllers/changeset_controller.rb index 2709a594c..8c042ef8d 100644 --- a/app/controllers/changeset_controller.rb +++ b/app/controllers/changeset_controller.rb @@ -84,8 +84,8 @@ class ChangesetController < ApplicationController # 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 * SCALE - lat << n['lat'].to_f * SCALE + 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 @@ -283,7 +283,7 @@ class ChangesetController < ApplicationController raise "Minimum longitude should be less than maximum." unless bbox[0] <= bbox[2] raise "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] * SCALE, bbox[0] * SCALE, bbox[3]* SCALE, bbox[1] * SCALE] + bbox[2] * GeoRecord::SCALE, bbox[0] * GeoRecord::SCALE, bbox[3]* GeoRecord::SCALE, bbox[1] * GeoRecord::SCALE] else return nil end diff --git a/app/models/changeset.rb b/app/models/changeset.rb index b00dfa8af..5938d0835 100644 --- a/app/models/changeset.rb +++ b/app/models/changeset.rb @@ -156,10 +156,10 @@ class Changeset < ActiveRecord::Base el1['created_at'] = self.created_at.xmlschema el1['open'] = self.open.to_s - el1['min_lon'] = (bbox[0] / SCALE).to_s unless bbox[0].nil? - el1['min_lat'] = (bbox[1] / SCALE).to_s unless bbox[1].nil? - el1['max_lon'] = (bbox[2] / SCALE).to_s unless bbox[2].nil? - el1['max_lat'] = (bbox[3] / SCALE).to_s unless bbox[3].nil? + 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 From 051f20e2771a44ddc5ed8d730f357cc323532233 Mon Sep 17 00:00:00 2001 From: Andy Allan Date: Sun, 9 Nov 2008 16:11:42 +0000 Subject: [PATCH 210/381] migrations: add specific method for nodes/ways/relations enum column --- db/migrate/007_add_relations.rb | 4 ++-- lib/migrate.rb | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/db/migrate/007_add_relations.rb b/db/migrate/007_add_relations.rb index b4aff6e09..c265fc3ad 100644 --- a/db/migrate/007_add_relations.rb +++ b/db/migrate/007_add_relations.rb @@ -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" @@ -41,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" diff --git a/lib/migrate.rb b/lib/migrate.rb index 585b47b1c..69538f1da 100644 --- a/lib/migrate.rb +++ b/lib/migrate.rb @@ -85,6 +85,10 @@ module ActiveRecord 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 end end end From 81ca9cfabf6f9f720127a1fcc68ec709af277730 Mon Sep 17 00:00:00 2001 From: Frederik Ramm Date: Sun, 9 Nov 2008 16:12:23 +0000 Subject: [PATCH 211/381] return user id AND name with changeset get request --- app/models/changeset.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/models/changeset.rb b/app/models/changeset.rb index 5938d0835..4a4d12124 100644 --- a/app/models/changeset.rb +++ b/app/models/changeset.rb @@ -145,6 +145,7 @@ class Changeset < ActiveRecord::Base 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') From 322d293007eb75eedae7bd6138dc5abc3143d972 Mon Sep 17 00:00:00 2001 From: Dave Stubbs Date: Sun, 9 Nov 2008 16:43:09 +0000 Subject: [PATCH 212/381] update nodes/ways/relation XML to include user id as well as display name --- app/models/node.rb | 5 ++++- app/models/old_node.rb | 5 ++++- app/models/old_relation.rb | 5 ++++- app/models/old_way.rb | 5 ++++- app/models/relation.rb | 5 ++++- app/models/way.rb | 5 ++++- 6 files changed, 24 insertions(+), 6 deletions(-) diff --git a/app/models/node.rb b/app/models/node.rb index cf7aedae8..391b50dcd 100644 --- a/app/models/node.rb +++ b/app/models/node.rb @@ -222,7 +222,10 @@ class Node < ActiveRecord::Base user_display_name_cache[self.changeset.user_id] = nil end - el1['user'] = user_display_name_cache[self.changeset.user_id] unless user_display_name_cache[self.changeset.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.tags.each do |k,v| el2 = XML::Node.new('tag') diff --git a/app/models/old_node.rb b/app/models/old_node.rb index 91b5a1a8e..03aff0fc6 100644 --- a/app/models/old_node.rb +++ b/app/models/old_node.rb @@ -40,7 +40,10 @@ class OldNode < ActiveRecord::Base el1['lat'] = self.lat.to_s el1['lon'] = self.lon.to_s el1['changeset'] = self.changeset.id.to_s - el1['user'] = self.changeset.user.display_name if self.changeset.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 self.tags.each do |k,v| el2 = XML::Node.new('tag') diff --git a/app/models/old_relation.rb b/app/models/old_relation.rb index b7e7248d9..491b444a6 100644 --- a/app/models/old_relation.rb +++ b/app/models/old_relation.rb @@ -99,7 +99,10 @@ class OldRelation < ActiveRecord::Base el1['id'] = self.id.to_s el1['visible'] = self.visible.to_s el1['timestamp'] = self.timestamp.xmlschema - el1['user'] = self.changeset.user.display_name if self.changeset.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 diff --git a/app/models/old_way.rb b/app/models/old_way.rb index 44155d05c..ce856e208 100644 --- a/app/models/old_way.rb +++ b/app/models/old_way.rb @@ -96,7 +96,10 @@ class OldWay < ActiveRecord::Base el1['id'] = self.id.to_s el1['visible'] = self.visible.to_s el1['timestamp'] = self.timestamp.xmlschema - el1['user'] = self.changeset.user.display_name if self.changeset.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 diff --git a/app/models/relation.rb b/app/models/relation.rb index be990e589..1bbb1d8e9 100644 --- a/app/models/relation.rb +++ b/app/models/relation.rb @@ -83,7 +83,10 @@ class Relation < ActiveRecord::Base user_display_name_cache[self.changeset.user_id] = nil end - el1['user'] = user_display_name_cache[self.changeset.user_id] unless user_display_name_cache[self.changeset.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 diff --git a/app/models/way.rb b/app/models/way.rb index be6531def..da7b8d733 100644 --- a/app/models/way.rb +++ b/app/models/way.rb @@ -101,7 +101,10 @@ class Way < ActiveRecord::Base user_display_name_cache[self.changeset.user_id] = nil end - el1['user'] = user_display_name_cache[self.changeset.user_id] unless user_display_name_cache[self.changeset.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 = [] From b6c354b31b8e7963bbd384045cb72cd3a88c613e Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Sun, 9 Nov 2008 16:49:27 +0000 Subject: [PATCH 213/381] fixing the tests due to a osm -> diffChange and moving to floats --- test/functional/changeset_controller_test.rb | 60 ++++++++++---------- test/functional/relation_controller_test.rb | 8 +-- 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/test/functional/changeset_controller_test.rb b/test/functional/changeset_controller_test.rb index 25ceca187..d81bf46b8 100644 --- a/test/functional/changeset_controller_test.rb +++ b/test/functional/changeset_controller_test.rb @@ -124,26 +124,26 @@ EOF "can't upload a simple valid creation to changeset: #{@response.body}" # check the returned payload - assert_select "osm[version=#{API_VERSION}][generator=\"OpenStreetMap server\"]", 1 - assert_select "osm>node", 1 - assert_select "osm>way", 1 - assert_select "osm>relation", 1 + 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("//osm/node").first["new_id"].to_i - new_way_id = doc.find("//osm/way").first["new_id"].to_i - new_rel_id = doc.find("//osm/relation").first["new_id"].to_i + 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("//osm/node").first["old_id"].to_i - assert_equal -1, doc.find("//osm/way").first["old_id"].to_i - assert_equal -1, doc.find("//osm/relation").first["old_id"].to_i + 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("//osm/node").first["new_version"].to_i - assert_equal 1, doc.find("//osm/way").first["new_version"].to_i - assert_equal 1, doc.find("//osm/relation").first["new_version"].to_i + 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" @@ -241,14 +241,14 @@ EOF "can't upload a complex diff to changeset: #{@response.body}" # check the returned payload - assert_select "osm[version=#{API_VERSION}][generator=\"#{GENERATOR}\"]", 1 - assert_select "osm>node", 1 - assert_select "osm>way", 1 - assert_select "osm>relation", 1 + 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("//osm/node").first["new_id"].to_i + 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" @@ -517,10 +517,10 @@ EOF # 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]", 1 - assert_select "osm>changeset[max_lon=1]", 1 - assert_select "osm>changeset[min_lat=2]", 1 - assert_select "osm>changeset[max_lat=2]", 1 + 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 @@ -532,10 +532,10 @@ EOF # 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]", 1 - assert_select "osm>changeset[max_lon=2]", 1 - assert_select "osm>changeset[min_lat=1]", 1 - assert_select "osm>changeset[max_lat=2]", 1 + 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 @@ -548,10 +548,10 @@ EOF # 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]", 1 - assert_select "osm>changeset[max_lon=3]", 1 - assert_select "osm>changeset[min_lat=1]", 1 - assert_select "osm>changeset[max_lat=3]", 1 + 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 ## diff --git a/test/functional/relation_controller_test.rb b/test/functional/relation_controller_test.rb index 5f23702db..4b265b503 100644 --- a/test/functional/relation_controller_test.rb +++ b/test/functional/relation_controller_test.rb @@ -359,10 +359,10 @@ class RelationControllerTest < ActionController::TestCase 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]}]", 1 - assert_select "osm>changeset[min_lat=#{bbox[1]}]", 1 - assert_select "osm>changeset[max_lon=#{bbox[2]}]", 1 - assert_select "osm>changeset[max_lat=#{bbox[3]}]", 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 From 436470b0576574971ea13b9754142047a6d109df Mon Sep 17 00:00:00 2001 From: Dave Stubbs Date: Sun, 9 Nov 2008 17:41:38 +0000 Subject: [PATCH 214/381] a few more amf tests and associated bug fixes --- app/controllers/amf_controller.rb | 19 ++++++++----- app/models/old_node.rb | 6 +---- test/functional/amf_controller_test.rb | 37 ++++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 12 deletions(-) diff --git a/app/controllers/amf_controller.rb b/app/controllers/amf_controller.rb index 0e77c11f7..f90a62a9a 100644 --- a/app/controllers/amf_controller.rb +++ b/app/controllers/amf_controller.rb @@ -61,6 +61,7 @@ class AmfController < ApplicationController 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()) @@ -75,6 +76,7 @@ class AmfController < ApplicationController when 'getpoi'; results[index]=AMF.putdata(index,getpoi(*args)) end end + logger.info("encoding AMF results") sendresponse(results) end @@ -218,23 +220,26 @@ class AmfController < ApplicationController # 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. def getway_old(id, version) #:doc: if version < 0 old_way = OldWay.find(:first, :conditions => ['visible = ? AND id = ?', true, id], :order => 'version DESC') - points = old_way.get_nodes_undelete + 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 + points = old_way.get_nodes_revert unless old_way.nil? end - old_way.tags['history'] = "Retrieved from v#{old_way.version}" - - [0, id, points, old_way.tags, old_way.version] + if old_way.nil? + return [0, id, [], {}, -1] + else + old_way.tags['history'] = "Retrieved from v#{old_way.version}" + return [0, id, points, old_way.tags, old_way.version] + end end # Find history of a way. Returns 'way', id, and diff --git a/app/models/old_node.rb b/app/models/old_node.rb index 03aff0fc6..badcd74a2 100644 --- a/app/models/old_node.rb +++ b/app/models/old_node.rb @@ -92,11 +92,7 @@ class OldNode < ActiveRecord::Base end def tags_as_hash - hash = {} - Tags.split(self.tags) do |k,v| - hash[k] = v - end - hash + return self.tags end # Pretend we're not in any ways diff --git a/test/functional/amf_controller_test.rb b/test/functional/amf_controller_test.rb index e0375c92f..9a12a01c4 100644 --- a/test/functional/amf_controller_test.rb +++ b/test/functional/amf_controller_test.rb @@ -154,6 +154,43 @@ class AmfControllerTest < ActionController::TestCase 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 + + + # ************************************************************ + # 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 From 2aa3daf0e091be9a3257e08f4059b6adfb05fc10 Mon Sep 17 00:00:00 2001 From: Dave Stubbs Date: Sun, 9 Nov 2008 18:48:16 +0000 Subject: [PATCH 215/381] amf get*_history tests and related fixes --- app/controllers/amf_controller.rb | 34 ++++++++++------ test/functional/amf_controller_test.rb | 56 ++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 12 deletions(-) diff --git a/app/controllers/amf_controller.rb b/app/controllers/amf_controller.rb index f90a62a9a..d04166aea 100644 --- a/app/controllers/amf_controller.rb +++ b/app/controllers/amf_controller.rb @@ -246,26 +246,36 @@ class AmfController < ApplicationController # 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 + 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 - ['way',wayid,history] + 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 + begin + 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.timestamp.to_i, old_node.timestamp.strftime("%d %b %Y, %H:%M"), old_node.visible ? 1 : 0, user, uid] + end - ['node',nodeid,history] + return ['node',nodeid,history] + rescue ActiveRecord::RecordNotFound + return ['node', nodeid, []] + end end # Get a relation with all tags and members. diff --git a/test/functional/amf_controller_test.rb b/test/functional/amf_controller_test.rb index 9a12a01c4..0cf56fca4 100644 --- a/test/functional/amf_controller_test.rb +++ b/test/functional/amf_controller_test.rb @@ -187,6 +187,62 @@ class AmfControllerTest < ActionController::TestCase 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' + assert_equal history[1], latest.id + assert_equal history[2].first[0], latest.timestamp.to_i + assert_equal history[2].last[0], nodes(:node_with_versions_v1).timestamp.to_i + 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 Helper functions From 4adeeb51adaecbb1dddf07c0f5253de050963e35 Mon Sep 17 00:00:00 2001 From: Andy Allan Date: Sun, 9 Nov 2008 19:30:22 +0000 Subject: [PATCH 216/381] remove indexes before renaming tables (some dbs have database-level index namespaces) and unsigned is also a mysqlism --- db/migrate/002_cleanup_osm_db.rb | 2 ++ db/migrate/005_tile_tracepoints.rb | 2 +- db/migrate/006_tile_nodes.rb | 8 ++++++-- lib/migrate.rb | 1 + 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/db/migrate/002_cleanup_osm_db.rb b/db/migrate/002_cleanup_osm_db.rb index f283602ac..b13a92099 100644 --- a/db/migrate/002_cleanup_osm_db.rb +++ b/db/migrate/002_cleanup_osm_db.rb @@ -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 diff --git a/db/migrate/005_tile_tracepoints.rb b/db/migrate/005_tile_tracepoints.rb index 51a4d1376..74d85d195 100644 --- a/db/migrate/005_tile_tracepoints.rb +++ b/db/migrate/005_tile_tracepoints.rb @@ -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" diff --git a/db/migrate/006_tile_nodes.rb b/db/migrate/006_tile_nodes.rb index f3a1edbb2..dc4755ac3 100644 --- a/db/migrate/006_tile_nodes.rb +++ b/db/migrate/006_tile_nodes.rb @@ -33,6 +33,8 @@ 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| @@ -49,12 +51,14 @@ class TileNodes < ActiveRecord::Migration add_index "current_nodes", ["timestamp"], :name => "current_nodes_timestamp_idx" add_index "current_nodes", ["tile"], :name => "current_nodes_tile_idx" - 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| @@ -72,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 diff --git a/lib/migrate.rb b/lib/migrate.rb index 69538f1da..38f8db6b3 100644 --- a/lib/migrate.rb +++ b/lib/migrate.rb @@ -49,6 +49,7 @@ module ActiveRecord 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 From 5ed41857f723daff3c9d9de810654a6d5c9b01ba Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Sun, 9 Nov 2008 19:51:56 +0000 Subject: [PATCH 217/381] adding first integration for testing user diaries, as multiple controllers are required. more work required. --- .../functional/diary_entry_controller_test.rb | 27 +++++++++++++++++- test/integration/user_diaries_test.rb | 28 +++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 test/integration/user_diaries_test.rb diff --git a/test/functional/diary_entry_controller_test.rb b/test/functional/diary_entry_controller_test.rb index 7eebfa57e..a8d0adf6d 100644 --- a/test/functional/diary_entry_controller_test.rb +++ b/test/functional/diary_entry_controller_test.rb @@ -1,6 +1,8 @@ require File.dirname(__FILE__) + '/../test_helper' +require 'app/controllers/user_controller.rb' class DiaryEntryControllerTest < ActionController::TestCase + fixtures :users, :diary_entries, :diary_comments def basic_authorization(user, pass) @request.env["HTTP_AUTHORIZATION"] = "Basic %s" % Base64.encode64("#{user}:#{pass}") end @@ -10,11 +12,32 @@ class DiaryEntryControllerTest < ActionController::TestCase end def test_showing_create_diary_entry + get :new + assert_response 302 + assert_redirected_to :controller => :user, :action => "login", :referer => "/diary_entry/new" + # can't really redirect to the + #follow_redirect + # Now login + #post :login, :user_email => "test@openstreetmap.org", :user_password => "test" + #get :controller => :users, :action => :new + #assert_response :success + #print @response.to_yaml + #assert_select "html" do + # assert_select "body" do + # assert_select "div#content" do + # assert_select "form" do + # assert_select "input[id=diary_entry_title]" + # end + # end + # end + #end + end def test_editing_diary_entry - + get :edit + assert :not_authorized end def test_editing_creating_diary_comment @@ -26,6 +49,8 @@ class DiaryEntryControllerTest < ActionController::TestCase end def test_rss + get :rss + assert :success end diff --git a/test/integration/user_diaries_test.rb b/test/integration/user_diaries_test.rb new file mode 100644 index 000000000..7ec743804 --- /dev/null +++ b/test/integration/user_diaries_test.rb @@ -0,0 +1,28 @@ +require 'test_helper' + +class UserDiariesTest < ActionController::IntegrationTest + fixtures :users, :diary_entries + + def test_showing_create_diary_entry + get '/user/test/diary/new' + assert_response 302 + assert_redirected_to :controller => :user, :action => "login", :referer => "/user/test/diary/new" + #follow_redirect + # Now login + #post :login, :user_email => "test@openstreetmap.org", :user_password => "test" + # + #get :controller => :users, :action => :new + #assert_response :success + #print @response.to_yaml + #assert_select "html" do + # assert_select "body" do + # assert_select "div#content" do + # assert_select "form" do + # assert_select "input[id=diary_entry_title]" + # end + # end + # end + #end + + end +end From 3065af398d20b9c8a1270075780dd66003f0d894 Mon Sep 17 00:00:00 2001 From: Richard Fairhurst Date: Sun, 9 Nov 2008 23:50:03 +0000 Subject: [PATCH 218/381] 0.6-friendly Potlatch (work in progress). DO NOT SET POTLATCH_USE_SQL=false as it won't work --- app/controllers/amf_controller.rb | 169 ++++++++++++++++++++---------- app/models/way.rb | 16 +-- config/initializers/potlatch.rb | 2 +- config/routes.rb | 3 +- lib/potlatch.rb | 4 + public/potlatch/potlatch.swf | Bin 166392 -> 167436 bytes 6 files changed, 128 insertions(+), 66 deletions(-) diff --git a/app/controllers/amf_controller.rb b/app/controllers/amf_controller.rb index d04166aea..4034ce10d 100644 --- a/app/controllers/amf_controller.rb +++ b/app/controllers/amf_controller.rb @@ -110,8 +110,9 @@ class AmfController < ApplicationController 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 'deleteway'; results[index]=AMF.putdata(index,deleteway(*args)) when 'putpoi'; results[index]=AMF.putdata(index,putpoi(*args)) + when 'startchangeset'; results[index]=AMF.putdata(index,startchangeset(*args)) end end sendresponse(results) @@ -119,6 +120,25 @@ class AmfController < ApplicationController private + # Start new changeset + + def startchangeset(usertoken, cstags, closeid, closecomment) + uid = getuserid(usertoken) + if !uid 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 + end + + # open a new changeset + cs = Changeset.new + cs.tags = cstags + cs.user_id = uid + cs.created_at = Time.now + cs.save_with_tags! + return [0,cs.id] + end + # Return presets (default tags, localisation etc.): # uses POTLATCH_PRESETS global, set up in OSM::Potlatch. @@ -144,17 +164,18 @@ class AmfController < ApplicationController end if POTLATCH_USE_SQL then - way_ids = sql_find_way_ids_in_area(xmin, ymin, xmax, ymax) + ways = sql_find_ways_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) + relation_ids = 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) + nodes_in_area = Node.find_by_area(ymin, xmin, ymax, xmax, :conditions => ["current_nodes.visible = ?", true], :include => :ways) # ** include causes problems way_ids = nodes_in_area.collect { |node| node.way_ids }.flatten.uniq + # ** get versions # 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] } + points = nodes_not_used_in_area.collect { |n| [n.id, n.lon, n.lat, n.tags] } # find the relations used by those nodes and ways relations = Relation.find_for_nodes(nodes_in_area.collect { |n| n.id }, :conditions => {:visible => true}) + @@ -162,7 +183,7 @@ class AmfController < ApplicationController relation_ids = relations.collect { |relation| relation.id }.uniq end - [way_ids, points, relation_ids] + [ways, points, relation_ids] end # Find deleted ways in current bounding box (similar to whichways, but ways @@ -188,12 +209,13 @@ class AmfController < ApplicationController 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) + 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 @@ -208,14 +230,15 @@ class AmfController < ApplicationController return [wayid,[],{}] if way.nil? or !way.visible points = way.nodes.collect do |node| - nodetags=node.tags_as_hash + 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] + [wayid, points, tags, version] end # Get an old version of a way, and all constituent nodes. @@ -224,6 +247,14 @@ class AmfController < ApplicationController # 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 @@ -235,10 +266,11 @@ class AmfController < ApplicationController end if old_way.nil? - return [0, id, [], {}, -1] + 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] + return [0, id, points, old_way.tags, old_way.version, (curway.version==old_way.version and curway.visible)] end end @@ -269,7 +301,7 @@ class AmfController < ApplicationController 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.timestamp.to_i, old_node.timestamp.strftime("%d %b %Y, %H:%M"), old_node.visible ? 1 : 0, user, uid] + [old_node.version, old_node.timestamp.strftime("%d %b %Y, %H:%M"), old_node.visible ? 1 : 0, user, uid] end return ['node',nodeid,history] @@ -282,7 +314,8 @@ class AmfController < ApplicationController # Returns: # 0. relation id, # 1. hash of tags, - # 2. list of members. + # 2. list of members, + # 3. version. def getrelation(relid) #:doc: begin @@ -291,9 +324,8 @@ class AmfController < ApplicationController return [relid, {}, []] end - return [relid, {}, []] if rel.nil? or !rel.visible - - [relid, rel.tags, rel.members] + return [relid, {}, [], nil] if rel.nil? or !rel.visible + [relid, rel.tags, rel.members, rel.version] end # Find relations with specified name/id. @@ -322,7 +354,7 @@ class AmfController < ApplicationController # 1. original relation id (unchanged), # 2. new relation id. - def putrelation(renumberednodes, renumberedways, usertoken, relid, tags, members, visible) #:doc: + def putrelation(renumberednodes, renumberedways, usertoken, changeset, 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 @@ -332,6 +364,7 @@ class AmfController < ApplicationController # create a new relation, or find the existing one if relid <= 0 rel = Relation.new + rel.version = 0 else rel = Relation.find(relid) end @@ -353,7 +386,7 @@ class AmfController < ApplicationController rel.members = typedmembers rel.tags = tags rel.visible = visible - rel.user_id = uid + rel.changeset_id = changeset # check it then save it # BUG: the following is commented out because it always fails on my @@ -375,9 +408,10 @@ 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 @@ -397,6 +431,7 @@ class AmfController < ApplicationController if originalway < 0 way = Way.new + way.version = 0 # otherwise +=1 breaks uniques = [] else way = Way.find(originalway) @@ -418,10 +453,11 @@ class AmfController < ApplicationController elsif id < 0 # Create new node node = Node.new + node.version = 0 # otherwise +=1 breaks savenode = true else node = Node.find(id) - nodetags=node.tags_as_hash + nodetags=node.tags nodetags.delete('created_by') if !fpcomp(lat, node.lat) or !fpcomp(lon, node.lon) or n[4] != nodetags or !node.visible? @@ -430,10 +466,10 @@ class AmfController < ApplicationController end if savenode - node.user_id = uid + node.changeset_id = changeset node.lat = lat node.lon = lon - node.tags = Tags.join(n[4]) + node.tags = n[4] node.visible = true node.save_with_history! @@ -453,20 +489,22 @@ class AmfController < ApplicationController deleteitemrelations(n, 'node') node = Node.find(n) - node.user_id = uid + node.changeset_id = changeset 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! + if way.tags!=attributes or way.nds!=nodes or !way.visible? + way.tags = attributes + way.nds = nodes + way.changeset_id = changeset + way.visible = true + way.save_with_history! + end - [0, originalway, way.id, renumberednodes] + [0, originalway, way.id, renumberednodes, way.version] end # Save POI to the database. @@ -474,9 +512,10 @@ class AmfController < ApplicationController # Returns: # 0. 0 (success), # 1. original node id (unchanged), - # 2. new node id. + # 2. new node id, + # 3. version. - def putpoi(usertoken, id, lon, lat, tags, visible) #:doc: + def putpoi(usertoken, changeset, 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 @@ -492,54 +531,55 @@ class AmfController < ApplicationController end else node = Node.new + node.version = 0 end - node.user_id = uid + node.changeset_id = changeset node.lat = lat node.lon = lon - node.tags = Tags.join(tags) + node.tags = tags node.visible = visible node.save_with_history! - [0, id, node.id] + [0, id, node.id, node.version] end # Read POI from database # (only called on revert: POIs are usually read by whichways). # - # Returns array of id, long, lat, hash of tags. + # Returns array of id, long, lat, hash of tags, version. - def getpoi(id,timestamp) #:doc: - if timestamp>0 then - n = OldNode.find(id, :conditions=>['UNIX_TIMESTAMP(timestamp)=?',timestamp]) + 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_as_hash] + return [n.id, n.lon, n.lat, n.tags, n.version] else - return [nil, nil, nil, ''] + return [nil, nil, nil, {}, nil] end end # Delete way and all constituent nodes. Also removes from any relations. # 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 + def deleteway(usertoken, changeset_id, way_id) #:doc: + if !getuserid(usertoken) then return -1,"You are not logged in, so the way could not be deleted." end + + way_id = way_id.to_i # 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) + way.delete_with_relations_and_nodes_and_history(changeset_id.to_i) [0, way_id] end @@ -596,6 +636,9 @@ class AmfController < ApplicationController def sendresponse(results) 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) @@ -607,9 +650,9 @@ class AmfController < ApplicationController # ==================================================================== # Alternative SQL queries for getway/whichways - def sql_find_way_ids_in_area(xmin,ymin,xmax,ymax) + def sql_find_ways_in_area(xmin,ymin,xmax,ymax) sql=<<-EOF - SELECT DISTINCT current_way_nodes.id AS wayid + 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 @@ -617,26 +660,34 @@ class AmfController < ApplicationController 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 } + 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) + pois=[] sql=<<-EOF - SELECT current_nodes.id,current_nodes.latitude*0.0000001 AS lat,current_nodes.longitude*0.0000001 AS lon,current_nodes.tags + 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=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'])] } + 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 + 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' @@ -645,20 +696,20 @@ class AmfController < ApplicationController unless way_ids.empty? sql+=<<-EOF UNION - SELECT DISTINCT cr.id AS relid + 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 }.uniq + 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 + 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 @@ -666,7 +717,10 @@ class AmfController < ApplicationController ORDER BY sequence_id EOF ActiveRecord::Base.connection.select_all(sql).each do |row| - nodetags=tagstring_to_hash(row['tags']) + 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 @@ -681,6 +735,9 @@ class AmfController < ApplicationController 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: diff --git a/app/models/way.rb b/app/models/way.rb index da7b8d733..7c29a44b3 100644 --- a/app/models/way.rb +++ b/app/models/way.rb @@ -291,22 +291,24 @@ class Way < ActiveRecord::Base end end - # delete a way and it's nodes that aren't part of other ways, with history + # delete a way and its nodes that aren't part of other ways, with history # FIXME: merge the potlatch code to delete the relations - def delete_with_relations_and_nodes_and_history(user) + # and refactor to use delete_with_history! + def delete_with_relations_and_nodes_and_history(changeset_id) # 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.changeset_id = changeset_id n.visible = false n.save_with_history! end - # FIXME needs more information passed in so that the changeset can be updated - self.user_id = user.id - - self.delete_with_history(user) + self.changeset_id = changeset_id + self.tags = [] + self.nds = [] + self.visible = false + self.save_with_history! end # Find nodes that belong to this way only diff --git a/config/initializers/potlatch.rb b/config/initializers/potlatch.rb index b98d60e1a..880947148 100644 --- a/config/initializers/potlatch.rb +++ b/config/initializers/potlatch.rb @@ -3,4 +3,4 @@ POTLATCH_PRESETS = Potlatch::Potlatch.get_presets() # Use SQL (faster) or Rails (more elegant) for common Potlatch reads # getway speedup is approximately x2, whichways approximately x7 -POTLATCH_USE_SQL = false +POTLATCH_USE_SQL = true diff --git a/config/routes.rb b/config/routes.rb index 91130b9b0..ddaaa5401 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -61,11 +61,10 @@ 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' - map.connect "api/#{API_VERSION}/amf", :controller =>'amf', :action =>'talk' map.connect "api/#{API_VERSION}/swf/trackpoints", :controller =>'swf', :action =>'trackpoints' # Data browsing diff --git a/lib/potlatch.rb b/lib/potlatch.rb index cf8f5903d..ebafbce00 100644 --- a/lib/potlatch.rb +++ b/lib/potlatch.rb @@ -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/public/potlatch/potlatch.swf b/public/potlatch/potlatch.swf index 0381868f00f36b23c82221c3c73ef4d620ad29da..afef53f8c01e649de32886e084f798ec46887c89 100755 GIT binary patch delta 44980 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 XNV`l9ypdlSn;HBaAD`TJBQO6yxznnE delta 44531 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 From fc99863df9a078f3c29a6e9dadac31ad70061a91 Mon Sep 17 00:00:00 2001 From: Andy Allan Date: Mon, 10 Nov 2008 11:32:17 +0000 Subject: [PATCH 219/381] In YAML yes is a boolean keyword: also fix test that was erroneously asserting success --- test/fixtures/current_node_tags.yml | 24 +++++++++---------- test/fixtures/current_relation_tags.yml | 12 +++++----- test/fixtures/current_way_tags.yml | 12 +++++----- test/fixtures/node_tags.yml | 32 ++++++++++++------------- test/fixtures/relation_tags.yml | 12 +++++----- test/fixtures/way_tags.yml | 12 +++++----- 6 files changed, 52 insertions(+), 52 deletions(-) diff --git a/test/fixtures/current_node_tags.yml b/test/fixtures/current_node_tags.yml index 8a699e9d4..1494daf54 100644 --- a/test/fixtures/current_node_tags.yml +++ b/test/fixtures/current_node_tags.yml @@ -1,29 +1,29 @@ t1: id: 1 - k: testvisible - v: yes + k: 'testvisible' + v: 'yes' t2: id: 2 - k: testused - v: yes + k: 'testused' + v: 'yes' t3: id: 3 - k: test - v: yes + k: 'test' + v: 'yes' t4: id: 4 - k: test - v: yes + k: 'test' + v: 'yes' nv_t1: id: 15 - k: testing - v: added in node version 3 + k: 'testing' + v: 'added in node version 3' nv_t2: id: 15 - k: testing two - v: modified in node version 4 + k: 'testing two' + v: 'modified in node version 4' diff --git a/test/fixtures/current_relation_tags.yml b/test/fixtures/current_relation_tags.yml index aaf06a397..8185d5891 100644 --- a/test/fixtures/current_relation_tags.yml +++ b/test/fixtures/current_relation_tags.yml @@ -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: id: 3 - k: test - v: yes + k: 'test' + v: 'yes' diff --git a/test/fixtures/current_way_tags.yml b/test/fixtures/current_way_tags.yml index 375247ea2..c1ef21d57 100644 --- a/test/fixtures/current_way_tags.yml +++ b/test/fixtures/current_way_tags.yml @@ -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' diff --git a/test/fixtures/node_tags.yml b/test/fixtures/node_tags.yml index c6a31186b..722bc5367 100644 --- a/test/fixtures/node_tags.yml +++ b/test/fixtures/node_tags.yml @@ -1,47 +1,47 @@ t1: id: 1 - k: testvisible - v: yes + k: 'testvisible' + v: 'yes' version: 1 t2: id: 3 - k: test - v: yes + k: 'test' + v: 'yes' version: 1 t3: id: 4 - k: test - v: yes + k: 'test' + v: 'yes' version: 1 nv3_t1: id: 15 - k: testing - v: added in node version 3 + k: 'testing' + v: 'added in node version 3' version: 3 nv3_t2: id: 15 - k: testing two - v: added in node version 3 + k: 'testing two' + v: 'added in node version 3' version: 3 nv3_t3: id: 15 - k: testing three - v: added in node version 3 + k: 'testing three' + v: 'added in node version 3' version: 3 nv4_t1: id: 15 - k: testing - v: added in node version 3 + k: 'testing' + v: 'added in node version 3' version: 4 nv4_t2: id: 15 - k: testing two - v: modified in node version 4 + k: 'testing two' + v: 'modified in node version 4' version: 4 diff --git a/test/fixtures/relation_tags.yml b/test/fixtures/relation_tags.yml index 39f4bd5de..7e671672d 100644 --- a/test/fixtures/relation_tags.yml +++ b/test/fixtures/relation_tags.yml @@ -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 diff --git a/test/fixtures/way_tags.yml b/test/fixtures/way_tags.yml index 39f4bd5de..7e671672d 100644 --- a/test/fixtures/way_tags.yml +++ b/test/fixtures/way_tags.yml @@ -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 From e1dde5cffc1df439f150067574331d18a3d659c3 Mon Sep 17 00:00:00 2001 From: Andy Allan Date: Mon, 10 Nov 2008 11:39:10 +0000 Subject: [PATCH 220/381] forgot the test change from 11866 --- test/functional/api_controller_test.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/functional/api_controller_test.rb b/test/functional/api_controller_test.rb index 6a0c2e2ac..a8e808716 100644 --- a/test/functional/api_controller_test.rb +++ b/test/functional/api_controller_test.rb @@ -45,7 +45,7 @@ class ApiControllerTest < ActionController::TestCase 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=1]" + assert_select "tag[k='test'][v='yes']" end # Should also test for the ways and relation end From f2093eb4c413770aa010f666f680071c31d77e8f Mon Sep 17 00:00:00 2001 From: Andy Allan Date: Mon, 10 Nov 2008 11:47:20 +0000 Subject: [PATCH 221/381] boolean correctness --- app/controllers/diary_entry_controller.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/diary_entry_controller.rb b/app/controllers/diary_entry_controller.rb index d9f5e4253..60f5211fd 100644 --- a/app/controllers/diary_entry_controller.rb +++ b/app/controllers/diary_entry_controller.rb @@ -70,7 +70,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 @@ -92,7 +92,7 @@ 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" From 0be6dd860f1843681e6380fbe69ca71a6eb01ab7 Mon Sep 17 00:00:00 2001 From: Matt Amos Date: Mon, 10 Nov 2008 15:37:34 +0000 Subject: [PATCH 222/381] Ooops, forgot to commit the routes for the changeset query and include calls. --- config/routes.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/routes.rb b/config/routes.rb index ddaaa5401..a835dc552 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -6,8 +6,10 @@ ActionController::Routing::Routes.draw do |map| 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/include", :controller => 'changeset', :action => 'include', :id => /\d+/ map.connect "api/#{API_VERSION}/changeset/:id", :controller => 'changeset', :action => 'read', :id => /\d+/ 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+/ From e79fd0763854f8cf41aefd7364c6bdf476280811 Mon Sep 17 00:00:00 2001 From: Matt Amos Date: Mon, 10 Nov 2008 15:41:05 +0000 Subject: [PATCH 223/381] Made relations ordered. Added some tests for this. Otherwise interface is unchanged. --- app/controllers/relation_controller.rb | 2 +- app/models/node.rb | 7 ++ app/models/old_relation.rb | 8 +- app/models/old_relation_member.rb | 3 + app/models/relation.rb | 100 +++++++------------- app/models/relation_member.rb | 1 + db/migrate/022_order_relation_members.rb | 33 +++++++ test/functional/relation_controller_test.rb | 100 ++++++++++++++++++++ 8 files changed, 183 insertions(+), 71 deletions(-) create mode 100644 db/migrate/022_order_relation_members.rb diff --git a/app/controllers/relation_controller.rb b/app/controllers/relation_controller.rb index da5129467..cdd1d34d6 100644 --- a/app/controllers/relation_controller.rb +++ b/app/controllers/relation_controller.rb @@ -180,7 +180,7 @@ 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 diff --git a/app/models/node.rb b/app/models/node.rb index 391b50dcd..faba4ed66 100644 --- a/app/models/node.rb +++ b/app/models/node.rb @@ -267,6 +267,13 @@ class Node < ActiveRecord::Base @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. diff --git a/app/models/old_relation.rb b/app/models/old_relation.rb index 491b444a6..ffddc7945 100644 --- a/app/models/old_relation.rb +++ b/app/models/old_relation.rb @@ -36,14 +36,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 @@ -51,7 +49,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 diff --git a/app/models/old_relation_member.rb b/app/models/old_relation_member.rb index d8b685854..f0294d339 100644 --- a/app/models/old_relation_member.rb +++ b/app/models/old_relation_member.rb @@ -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 diff --git a/app/models/relation.rb b/app/models/relation.rb index 1bbb1d8e9..b94aef9ae 100644 --- a/app/models/relation.rb +++ b/app/models/relation.rb @@ -9,7 +9,7 @@ class Relation < ActiveRecord::Base 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 @@ -243,33 +243,33 @@ class Relation < ActiveRecord::Base # 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 = self.members_as_hash + 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 - # i'd love to rely on rails' dirty handling here, but the - # relation members are always dirty because of the member_class - # handling. - if members[key] != old_member.member_role - old_member.member_role = members[key] - changed_members << key - old_member.save! - end members.delete key - else changed_members << key - RelationMember.delete_all ['id = ? and member_id = ? and member_type = ?', self.id, old_member.member_id, old_member.member_type] end end # any remaining members must be new additions changed_members += members.keys - members.each do |k,v| + + # 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 = k[1]; - mem.member_id = k[0]; - mem.member_role = v; + mem.id = [self.id, i] + mem.member_type = m[0] + mem.member_id = m[1] + mem.member_role = m[2] mem.save! end @@ -364,67 +364,37 @@ class Relation < ActiveRecord::Base 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.. + # 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} - nodes = Hash.new - ways = Hash.new - relations = Hash.new + elements = { :node => Hash.new, :way => Hash.new, :relation => Hash.new } self.members.each do |m| - if (m[0] == "node") - n = Node.find(:first, :conditions => ["id = ?", m[1]]) - unless n and n.visible + # 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 - 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 - end - else - return false + hash[m[1]] = true end end + return true rescue return false end - ## - # members in a hash table [id,type] => role - def members_as_hash - h = Hash.new - members.each do |m| - # should be: h[[m.id, m.type]] = m.role, but someone prefers arrays - h[[m[1], m[0]]] = m[2] - end - return h - end - # Temporary method to match interface to nodes def tags_as_hash return self.tags diff --git a/app/models/relation_member.rb b/app/models/relation_member.rb index 9ff4f46f3..08bb988ee 100644 --- a/app/models/relation_member.rb +++ b/app/models/relation_member.rb @@ -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 diff --git a/db/migrate/022_order_relation_members.rb b/db/migrate/022_order_relation_members.rb new file mode 100644 index 000000000..e3820d4e6 --- /dev/null +++ b/db/migrate/022_order_relation_members.rb @@ -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. + ActiveRecord::Base.connection().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. + ActiveRecord::Base.connection().execute("alter table relation_members drop primary key, add primary key (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) + ActiveRecord::Base.connection().execute("update current_relation_members set sequence_id = mod(member_id, 16384)") + ActiveRecord::Base.connection().execute("alter table current_relation_members drop primary key, add primary key (id, member_type, member_id, member_role, sequence_id)") + end + + def self.down + ActiveRecord::Base.connection().execute("alter table current_relation_members drop primary key, add primary key (id, member_type, member_id, member_role)") + remove_column :relation_members, :sequence_id + + ActiveRecord::Base.connection().execute("alter table relation_members drop primary key, add primary key (id, version, member_type, member_id, member_role)") + remove_column :current_relation_members, :sequence_id + end +end diff --git a/test/functional/relation_controller_test.rb b/test/functional/relation_controller_test.rb index 4b265b503..d44490036 100644 --- a/test/functional/relation_controller_test.rb +++ b/test/functional/relation_controller_test.rb @@ -335,6 +335,106 @@ class RelationControllerTest < ActionController::TestCase end end + ## + # check that relations are ordered + def test_relation_member_ordering + basic_authorization("test@openstreetmap.org", "test"); + + doc_str = < + + + + + + + +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 + 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+. From 9f64b0848f01fb849849380ff32756228a35eca6 Mon Sep 17 00:00:00 2001 From: Andy Allan Date: Mon, 10 Nov 2008 17:10:55 +0000 Subject: [PATCH 224/381] nil protection - mysql however has a default for enums, which is why it wasn't a problem before --- app/models/relation_member.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/relation_member.rb b/app/models/relation_member.rb index 08bb988ee..1cb6d7131 100644 --- a/app/models/relation_member.rb +++ b/app/models/relation_member.rb @@ -10,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 if !self.member_type.nil? end def before_save From d717f2d02a9f767237d103dc206e30e3a4a8c056 Mon Sep 17 00:00:00 2001 From: Andy Allan Date: Mon, 10 Nov 2008 19:14:00 +0000 Subject: [PATCH 225/381] Postgres adapter, and make migration 022 db-agnostic --- db/migrate/022_order_relation_members.rb | 12 ++--- lib/migrate.rb | 60 +++++++++++++++++++++++- 2 files changed, 64 insertions(+), 8 deletions(-) diff --git a/db/migrate/022_order_relation_members.rb b/db/migrate/022_order_relation_members.rb index e3820d4e6..5500edfcf 100644 --- a/db/migrate/022_order_relation_members.rb +++ b/db/migrate/022_order_relation_members.rb @@ -8,26 +8,26 @@ class OrderRelationMembers < ActiveRecord::Migration # 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. - ActiveRecord::Base.connection().execute("update relation_members set sequence_id = mod(member_id, 16384)") + 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. - ActiveRecord::Base.connection().execute("alter table relation_members drop primary key, add primary key (id, version, member_type, member_id, member_role, sequence_id)") + 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) - ActiveRecord::Base.connection().execute("update current_relation_members set sequence_id = mod(member_id, 16384)") - ActiveRecord::Base.connection().execute("alter table current_relation_members drop primary key, add primary key (id, member_type, member_id, member_role, sequence_id)") + 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 - ActiveRecord::Base.connection().execute("alter table current_relation_members drop primary key, add primary key (id, member_type, member_id, member_role)") + alter_primary_key("current_relation_members", [:id, :member_type, :member_id, :member_role]) remove_column :relation_members, :sequence_id - ActiveRecord::Base.connection().execute("alter table relation_members drop primary key, add primary key (id, version, member_type, member_id, member_role)") + 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/lib/migrate.rb b/lib/migrate.rb index 38f8db6b3..3b1e46fb8 100644 --- a/lib/migrate.rb +++ b/lib/migrate.rb @@ -38,7 +38,9 @@ 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 @@ -78,7 +80,7 @@ module ActiveRecord def innodb_option return "ENGINE=InnoDB" end - + def change_engine (table_name, engine) execute "ALTER TABLE #{table_name} ENGINE = #{engine}" end @@ -90,6 +92,60 @@ module ActiveRecord 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 From f3e382732ace00c7e1893784e2207d9522cc883c Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Mon, 10 Nov 2008 19:21:59 +0000 Subject: [PATCH 226/381] slight more work on the diary test --- test/functional/diary_entry_controller_test.rb | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/functional/diary_entry_controller_test.rb b/test/functional/diary_entry_controller_test.rb index a8d0adf6d..31c62fe9f 100644 --- a/test/functional/diary_entry_controller_test.rb +++ b/test/functional/diary_entry_controller_test.rb @@ -15,6 +15,13 @@ class DiaryEntryControllerTest < ActionController::TestCase get :new assert_response 302 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 + # can't really redirect to the #follow_redirect # Now login From 995c9df1958a9f2333748179fe276bc9886b8a86 Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Tue, 11 Nov 2008 18:07:59 +0000 Subject: [PATCH 227/381] more diary entry tests, and a minor fix to the relation member that makes the code a bit more readable. --- app/models/relation_member.rb | 2 +- .../functional/diary_entry_controller_test.rb | 30 ++++------ test/integration/user_diaries_test.rb | 57 ++++++++++++------- 3 files changed, 48 insertions(+), 41 deletions(-) diff --git a/app/models/relation_member.rb b/app/models/relation_member.rb index 1cb6d7131..f3033d1c6 100644 --- a/app/models/relation_member.rb +++ b/app/models/relation_member.rb @@ -10,7 +10,7 @@ class RelationMember < ActiveRecord::Base end def after_initialize - self[:member_class] = self.member_type.capitalize if !self.member_type.nil? + self[:member_class] = self.member_type.capitalize unless self.member_type.nil? end def before_save diff --git a/test/functional/diary_entry_controller_test.rb b/test/functional/diary_entry_controller_test.rb index 31c62fe9f..ca9ce4c36 100644 --- a/test/functional/diary_entry_controller_test.rb +++ b/test/functional/diary_entry_controller_test.rb @@ -17,28 +17,20 @@ class DiaryEntryControllerTest < ActionController::TestCase 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 - - # can't really redirect to the - #follow_redirect - # Now login - #post :login, :user_email => "test@openstreetmap.org", :user_password => "test" - - #get :controller => :users, :action => :new - #assert_response :success + #print @response.body + #print @response.to_yaml - #assert_select "html" do - # assert_select "body" do - # assert_select "div#content" do - # assert_select "form" do - # assert_select "input[id=diary_entry_title]" - # end - # end - # end - #end + assert_select "html" do + assert_select "body" do + assert_select "div#content" do + assert_select "form" do + assert_select "input[id=diary_entry_title]" + end + end + end + end end diff --git a/test/integration/user_diaries_test.rb b/test/integration/user_diaries_test.rb index 7ec743804..02908e18d 100644 --- a/test/integration/user_diaries_test.rb +++ b/test/integration/user_diaries_test.rb @@ -1,28 +1,43 @@ -require 'test_helper' +require File.dirname(__FILE__) + '/../test_helper' class UserDiariesTest < ActionController::IntegrationTest fixtures :users, :diary_entries def test_showing_create_diary_entry - get '/user/test/diary/new' - assert_response 302 - assert_redirected_to :controller => :user, :action => "login", :referer => "/user/test/diary/new" - #follow_redirect - # Now login - #post :login, :user_email => "test@openstreetmap.org", :user_password => "test" - # - #get :controller => :users, :action => :new - #assert_response :success - #print @response.to_yaml - #assert_select "html" do - # assert_select "body" do - # assert_select "div#content" do - # assert_select "form" do - # assert_select "input[id=diary_entry_title]" - # end - # end - # end - #end - + 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 + + assert_select "html:root" do + assert_select "body" do + assert_select "div#content" do + assert_select "h1", "New diary entry" + assert_select "form" do + assert_select "input[id=diary_entry_title]" + end + end + end + end end end From 22f3d7ce2ebd1ebeb54929ac8334988d195aaa1b Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Tue, 11 Nov 2008 18:33:08 +0000 Subject: [PATCH 228/381] more diary entry test --- test/functional/diary_entry_controller_test.rb | 11 ++++++----- test/integration/user_diaries_test.rb | 9 ++++++++- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/test/functional/diary_entry_controller_test.rb b/test/functional/diary_entry_controller_test.rb index ca9ce4c36..1dc9026bb 100644 --- a/test/functional/diary_entry_controller_test.rb +++ b/test/functional/diary_entry_controller_test.rb @@ -1,5 +1,4 @@ require File.dirname(__FILE__) + '/../test_helper' -require 'app/controllers/user_controller.rb' class DiaryEntryControllerTest < ActionController::TestCase fixtures :users, :diary_entries, :diary_comments @@ -20,13 +19,15 @@ class DiaryEntryControllerTest < ActionController::TestCase get(:new, nil, {'user' => users(:normal_user).id}) assert_response :success #print @response.body - + #print @response.to_yaml - assert_select "html" do + assert_select "html:root", :count => 1 do assert_select "body" do assert_select "div#content" do - assert_select "form" do - assert_select "input[id=diary_entry_title]" + assert_select "h1", "New diary entry" + assert_select "form[action='/diary_entry/new']" do + assert_select "input[id=diary_entry_title][name='diary_entry[title]']" + assert_select "textarea#diary_entry_body[name='diary_entry[body]']" end end end diff --git a/test/integration/user_diaries_test.rb b/test/integration/user_diaries_test.rb index 02908e18d..2e7a01030 100644 --- a/test/integration/user_diaries_test.rb +++ b/test/integration/user_diaries_test.rb @@ -29,15 +29,22 @@ class UserDiariesTest < ActionController::IntegrationTest #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" do + 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 From ad9e48120aaf33e5170c70e7f2f07148cee5ee74 Mon Sep 17 00:00:00 2001 From: Frederik Ramm Date: Wed, 12 Nov 2008 00:11:57 +0000 Subject: [PATCH 229/381] replaced "wtf does this do" comment by something explaining what it does. --- app/controllers/node_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/node_controller.rb b/app/controllers/node_controller.rb index 9763be5d4..c03f3c4fb 100644 --- a/app/controllers/node_controller.rb +++ b/app/controllers/node_controller.rb @@ -84,7 +84,7 @@ class NodeController < ApplicationController 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 } From 797cbaab1ef4b842f3b57bb46c29dea61b4c7e3a Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Wed, 12 Nov 2008 18:48:58 +0000 Subject: [PATCH 230/381] Covering the case of an entry not being found. More diary entry tests. --- app/controllers/diary_entry_controller.rb | 2 + app/views/diary_entry/no_such_entry.rhtml | 2 + config/potlatch/presets.txt | 1 + .../functional/diary_entry_controller_test.rb | 61 ++++++++++++++++--- 4 files changed, 57 insertions(+), 9 deletions(-) create mode 100644 app/views/diary_entry/no_such_entry.rhtml diff --git a/app/controllers/diary_entry_controller.rb b/app/controllers/diary_entry_controller.rb index 60f5211fd..bd08bcd97 100644 --- a/app/controllers/diary_entry_controller.rb +++ b/app/controllers/diary_entry_controller.rb @@ -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 diff --git a/app/views/diary_entry/no_such_entry.rhtml b/app/views/diary_entry/no_such_entry.rhtml new file mode 100644 index 000000000..1ebcf269c --- /dev/null +++ b/app/views/diary_entry/no_such_entry.rhtml @@ -0,0 +1,2 @@ +

      No entry with the id: <%= h(params[:id]) %>

      +

      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.

      diff --git a/config/potlatch/presets.txt b/config/potlatch/presets.txt index 464204edb..1917b5a6c 100644 --- a/config/potlatch/presets.txt +++ b/config/potlatch/presets.txt @@ -36,6 +36,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= diff --git a/test/functional/diary_entry_controller_test.rb b/test/functional/diary_entry_controller_test.rb index 1dc9026bb..cb7877336 100644 --- a/test/functional/diary_entry_controller_test.rb +++ b/test/functional/diary_entry_controller_test.rb @@ -10,9 +10,9 @@ class DiaryEntryControllerTest < ActionController::TestCase @request.env["RAW_POST_DATA"] = c.to_s end - def test_showing_create_diary_entry + def test_showing_new_diary_entry get :new - assert_response 302 + 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 @@ -22,12 +22,20 @@ class DiaryEntryControllerTest < ActionController::TestCase #print @response.to_yaml assert_select "html:root", :count => 1 do - assert_select "body" do - assert_select "div#content" do - assert_select "h1", "New diary entry" - assert_select "form[action='/diary_entry/new']" do - assert_select "input[id=diary_entry_title][name='diary_entry[title]']" - assert_select "textarea#diary_entry_body[name='diary_entry[body]']" + 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 @@ -36,8 +44,43 @@ class DiaryEntryControllerTest < ActionController::TestCase 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 :not_authorized + 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 + 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 + end + end + end + + #print @response.body end def test_editing_creating_diary_comment From bdd5c317a7a765a653d98baa062e192ddf615804 Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Thu, 13 Nov 2008 17:20:40 +0000 Subject: [PATCH 231/381] Adding some more diary entry tests. Making the RSS feed links more portable, by using the SERVER_URL constant. Fixing the validation for the length of the language of diary entries until the i18n branch is integrated. --- app/controllers/diary_entry_controller.rb | 5 +- app/models/diary_entry.rb | 2 +- .../functional/diary_entry_controller_test.rb | 79 ++++++++++++++++--- 3 files changed, 73 insertions(+), 13 deletions(-) diff --git a/app/controllers/diary_entry_controller.rb b/app/controllers/diary_entry_controller.rb index bd08bcd97..3592ccb4f 100644 --- a/app/controllers/diary_entry_controller.rb +++ b/app/controllers/diary_entry_controller.rb @@ -86,7 +86,7 @@ class DiaryEntryController < ApplicationController @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 @@ -98,7 +98,7 @@ class DiaryEntryController < ApplicationController :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 @@ -109,6 +109,7 @@ class DiaryEntryController < ApplicationController 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] diff --git a/app/models/diary_entry.rb b/app/models/diary_entry.rb index c20788fbb..4b2058b9d 100644 --- a/app/models/diary_entry.rb +++ b/app/models/diary_entry.rb @@ -6,7 +6,7 @@ class DiaryEntry < ActiveRecord::Base validates_presence_of :title, :body validates_length_of :title, :within => 1..255 - validates_length_of :language, :within => 2..3 + 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 diff --git a/test/functional/diary_entry_controller_test.rb b/test/functional/diary_entry_controller_test.rb index cb7877336..c0bd4b9ff 100644 --- a/test/functional/diary_entry_controller_test.rb +++ b/test/functional/diary_entry_controller_test.rb @@ -2,14 +2,7 @@ require File.dirname(__FILE__) + '/../test_helper' class DiaryEntryControllerTest < ActionController::TestCase fixtures :users, :diary_entries, :diary_comments - 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 - def test_showing_new_diary_entry get :new assert_response :redirect @@ -65,7 +58,8 @@ class DiaryEntryControllerTest < ActionController::TestCase end end - # Now pass the id, and check that you can edit it + # 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 @@ -75,12 +69,77 @@ class DiaryEntryControllerTest < ActionController::TestCase 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 + 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 From c8b70c1c7279b4ddaf4910c77b6dbc237c0c67e1 Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Fri, 14 Nov 2008 02:06:04 +0000 Subject: [PATCH 232/381] Adding some more unit test stubs that need filling out. Adding changeset tags fixture. Indentation fix in way model. --- app/models/way.rb | 2 +- test/fixtures/changeset_tags.yml | 4 ++++ test/unit/changeset_tag_test.rb | 11 +++++++++++ test/unit/changeset_test.rb | 11 +++++++++++ test/unit/way_node_test.rb | 12 ++++++++++++ test/unit/way_tag_test.rb | 11 +++++++++++ test/unit/way_test.rb | 24 ++++++++++++++++++++++++ 7 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 test/fixtures/changeset_tags.yml create mode 100644 test/unit/changeset_tag_test.rb create mode 100644 test/unit/changeset_test.rb create mode 100644 test/unit/way_node_test.rb create mode 100644 test/unit/way_tag_test.rb diff --git a/app/models/way.rb b/app/models/way.rb index 7c29a44b3..0c27e5460 100644 --- a/app/models/way.rb +++ b/app/models/way.rb @@ -27,7 +27,7 @@ class Way < ActiveRecord::Base doc = p.parse doc.find('//osm/way').each do |pt| - return Way.from_xml_node(pt, create) + return Way.from_xml_node(pt, create) end rescue return nil diff --git a/test/fixtures/changeset_tags.yml b/test/fixtures/changeset_tags.yml new file mode 100644 index 000000000..34d2bf4e6 --- /dev/null +++ b/test/fixtures/changeset_tags.yml @@ -0,0 +1,4 @@ +changeset_1_tag_1: + id: 1 + k: created_by + v: test suite yml diff --git a/test/unit/changeset_tag_test.rb b/test/unit/changeset_tag_test.rb new file mode 100644 index 000000000..466ee0405 --- /dev/null +++ b/test/unit/changeset_tag_test.rb @@ -0,0 +1,11 @@ +require File.dirname(__FILE__) + '/../test_helper' + +class ChangesetTagTest < Test::Unit::TestCase + fixtures :changeset_tags + + + def test_changeset_tags_count + assert_equal 1, ChangesetTag.count + end + +end diff --git a/test/unit/changeset_test.rb b/test/unit/changeset_test.rb new file mode 100644 index 000000000..ee9d0925a --- /dev/null +++ b/test/unit/changeset_test.rb @@ -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/way_node_test.rb b/test/unit/way_node_test.rb new file mode 100644 index 000000000..b931d3c04 --- /dev/null +++ b/test/unit/way_node_test.rb @@ -0,0 +1,12 @@ +require File.dirname(__FILE__) + '/../test_helper' + +class WayNodeTest < Test::Unit::TestCase + fixtures :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 index 000000000..099d29da3 --- /dev/null +++ b/test/unit/way_tag_test.rb @@ -0,0 +1,11 @@ +require File.dirname(__FILE__) + '/../test_helper' + +class WayTagTest < Test::Unit::TestCase + fixtures :way_tags + + + def test_way_tag_count + assert_equal 3, WayTag.count + end + +end diff --git a/test/unit/way_test.rb b/test/unit/way_test.rb index cd565fd27..584a30daa 100644 --- a/test/unit/way_test.rb +++ b/test/unit/way_test.rb @@ -3,6 +3,13 @@ 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, @@ -13,4 +20,21 @@ class WayTest < Test::Unit::TestCase 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 From 57e2d5a8f5adc18df966ca1801e42d3cfe01d16e Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Fri, 14 Nov 2008 16:26:56 +0000 Subject: [PATCH 233/381] Unit tests work again. Fixed the set fixture class that I should. Adding 2 length checks to the user preference controller. --- test/unit/user_preference_test.rb | 28 ++++++++++++++++++++++++++++ test/unit/way_node_test.rb | 2 +- test/unit/way_tag_test.rb | 4 ++-- 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/test/unit/user_preference_test.rb b/test/unit/user_preference_test.rb index d591db69d..2118fcd39 100644 --- a/test/unit/user_preference_test.rb +++ b/test/unit/user_preference_test.rb @@ -22,5 +22,33 @@ class UserPreferenceTest < ActiveSupport::TestCase 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 diff --git a/test/unit/way_node_test.rb b/test/unit/way_node_test.rb index b931d3c04..1871eae9d 100644 --- a/test/unit/way_node_test.rb +++ b/test/unit/way_node_test.rb @@ -1,7 +1,7 @@ require File.dirname(__FILE__) + '/../test_helper' class WayNodeTest < Test::Unit::TestCase - fixtures :way_nodes + fixtures :way_nodes, :current_way_nodes set_fixture_class :way_nodes=>OldWayNode set_fixture_class :current_way_nodes=>WayNode diff --git a/test/unit/way_tag_test.rb b/test/unit/way_tag_test.rb index 099d29da3..cd296749e 100644 --- a/test/unit/way_tag_test.rb +++ b/test/unit/way_tag_test.rb @@ -1,8 +1,8 @@ require File.dirname(__FILE__) + '/../test_helper' class WayTagTest < Test::Unit::TestCase - fixtures :way_tags - + fixtures :current_way_tags + set_fixture_class :current_way_tags => WayTag def test_way_tag_count assert_equal 3, WayTag.count From 6bca4b66d14f01f716af0f11ddb96c275a3ad0dd Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Fri, 14 Nov 2008 17:28:16 +0000 Subject: [PATCH 234/381] More unit test stubs --- test/fixtures/current_relation_tags.yml | 2 +- test/unit/node_tag_test.rb | 11 +++++++++++ test/unit/relation_member_test.rb | 11 +++++++++++ test/unit/relation_tag_test.rb | 11 +++++++++++ test/unit/relation_test.rb | 11 +++++++++++ test/unit/trace_test.rb | 11 +++++++++++ test/unit/tracepoint_test.rb | 11 +++++++++++ test/unit/tracetag_test.rb | 11 +++++++++++ test/unit/user_token_test.rb | 10 ++++++++++ 9 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 test/unit/node_tag_test.rb create mode 100644 test/unit/relation_member_test.rb create mode 100644 test/unit/relation_tag_test.rb create mode 100644 test/unit/relation_test.rb create mode 100644 test/unit/trace_test.rb create mode 100644 test/unit/tracepoint_test.rb create mode 100644 test/unit/tracetag_test.rb create mode 100644 test/unit/user_token_test.rb diff --git a/test/fixtures/current_relation_tags.yml b/test/fixtures/current_relation_tags.yml index 8185d5891..d2755bdfd 100644 --- a/test/fixtures/current_relation_tags.yml +++ b/test/fixtures/current_relation_tags.yml @@ -8,7 +8,7 @@ t2: k: 'test' v: 'yes' -t2: +t3: id: 3 k: 'test' v: 'yes' diff --git a/test/unit/node_tag_test.rb b/test/unit/node_tag_test.rb new file mode 100644 index 000000000..5e4d47953 --- /dev/null +++ b/test/unit/node_tag_test.rb @@ -0,0 +1,11 @@ +require File.dirname(__FILE__) + '/../test_helper' + +class NodeTagTest < Test::Unit::TestCase + fixtures :current_node_tags + set_fixture_class :current_node_tags => NodeTag + + def test_node_tag_count + assert_equal 6, NodeTag.count + end + +end diff --git a/test/unit/relation_member_test.rb b/test/unit/relation_member_test.rb new file mode 100644 index 000000000..d67ac3462 --- /dev/null +++ b/test/unit/relation_member_test.rb @@ -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 index 000000000..38c8af22b --- /dev/null +++ b/test/unit/relation_tag_test.rb @@ -0,0 +1,11 @@ +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 + +end diff --git a/test/unit/relation_test.rb b/test/unit/relation_test.rb new file mode 100644 index 000000000..b5b6391ab --- /dev/null +++ b/test/unit/relation_test.rb @@ -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 index 000000000..706455aa2 --- /dev/null +++ b/test/unit/trace_test.rb @@ -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 index 000000000..5d4100530 --- /dev/null +++ b/test/unit/tracepoint_test.rb @@ -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 index 000000000..4eaf41ed8 --- /dev/null +++ b/test/unit/tracetag_test.rb @@ -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 diff --git a/test/unit/user_token_test.rb b/test/unit/user_token_test.rb new file mode 100644 index 000000000..2bc1a2dce --- /dev/null +++ b/test/unit/user_token_test.rb @@ -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 From 0a57413d3e5d3734f2f3d83df00abd861d80aee2 Mon Sep 17 00:00:00 2001 From: Richard Fairhurst Date: Fri, 14 Nov 2008 17:36:38 +0000 Subject: [PATCH 235/381] finished Rails-friendly amf_controller. Note that this requires Tom's patched composite_primary_keys to work unless you tell Potlatch to use SQL reads --- app/controllers/amf_controller.rb | 51 +++++++++++++++++++++++------- public/potlatch/potlatch.swf | Bin 167436 -> 167475 bytes 2 files changed, 40 insertions(+), 11 deletions(-) diff --git a/app/controllers/amf_controller.rb b/app/controllers/amf_controller.rb index 4034ce10d..1fa433c9e 100644 --- a/app/controllers/amf_controller.rb +++ b/app/controllers/amf_controller.rb @@ -14,6 +14,12 @@ # 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 # @@ -72,6 +78,7 @@ class AmfController < ApplicationController 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 @@ -159,19 +166,20 @@ class AmfController < ApplicationController begin check_boundaries(xmin, ymin, xmax, ymax) rescue Exception => err - # FIXME: report an error rather than just return an empty result - return [[],[],[]] + return [-2,"Sorry - I can't get the map for that area."] end 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) - relation_ids = sql_find_relations_in_area_and_ways(xmin, ymin, xmax, ymax, ways.collect {|x| x[0]}) + 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) # ** include causes problems - way_ids = nodes_in_area.collect { |node| node.way_ids }.flatten.uniq - # ** get versions + nodes_in_area = Node.find_by_area(ymin, xmin, ymax, xmax, :conditions => ["current_nodes.visible = ?", true], :include => :ways) + ways = nodes_in_area.collect { |node| + node.ways.collect { |w| [w.id,w.version] }.flatten + }.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? } @@ -179,11 +187,11 @@ class AmfController < ApplicationController # 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(way_ids, :conditions => {:visible => true}) - relation_ids = relations.collect { |relation| relation.id }.uniq + Relation.find_for_ways(ways.collect { |w| w[0] }, :conditions => {:visible => true}) + relations = relations.collect { |relation| [relation.id,relation.version] }.uniq end - [ways, points, relation_ids] + [0,ways, points, relations] end # Find deleted ways in current bounding box (similar to whichways, but ways @@ -310,6 +318,27 @@ class AmfController < ApplicationController end end + # 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, @@ -613,10 +642,10 @@ class AmfController < ApplicationController end # Authenticate token - # (could be removed if no-one uses the username+password form) + # (can also be of form user:pass) def getuserid(token) #:doc: - if (token =~ /^(.+)\+(.+)$/) then + if (token =~ /^(.+)\:(.+)$/) then user = User.authenticate(:username => $1, :password => $2) else user = User.authenticate(:token => token) diff --git a/public/potlatch/potlatch.swf b/public/potlatch/potlatch.swf index afef53f8c01e649de32886e084f798ec46887c89..d26ff6e0d5d6c5abb5dcb6ef0958488f08b92a98 100755 GIT binary patch delta 70 zcmV-M0J;B+oC>p?3I#@2QwTG$1%oO9ezS=x`DFpClY@}r0mzePktzbv1(yr40Y!t8 ck++kP0hR;;?*X?fmH|cq0;d4Cke2~`_Pv4|OaK4? delta 94 zcmdlyhpT4}7pq%%FbB^@)@Cin@XeiCKT{dgCpS-c#Mm-9ZK4)a4{P(aiQBJDWSq{z y#KW{be;T73qx=zuX-o_ptTNM>892=u{id-paK Date: Sat, 15 Nov 2008 01:11:27 +0000 Subject: [PATCH 236/381] Freezing composite primary key so that we can apply TomH's patch to it, and that way Potlatch will work in Rails mode without a problem. Please test. --- config/initializers/potlatch.rb | 2 +- .../composite_primary_keys-1.1.0/History.txt | 148 ++++++ .../composite_primary_keys-1.1.0/Manifest.txt | 121 +++++ .../composite_primary_keys-1.1.0/README.txt | 41 ++ .../README_DB2.txt | 33 ++ .../composite_primary_keys-1.1.0/Rakefile | 65 +++ .../gems/composite_primary_keys-1.1.0/init.rb | 2 + .../composite_primary_keys-1.1.0/install.rb | 30 ++ .../lib/adapter_helper/base.rb | 63 +++ .../lib/adapter_helper/mysql.rb | 13 + .../lib/adapter_helper/oracle.rb | 12 + .../lib/adapter_helper/postgresql.rb | 13 + .../lib/adapter_helper/sqlite3.rb | 13 + .../lib/composite_primary_keys.rb | 55 +++ .../association_preload.rb | 236 ++++++++++ .../composite_primary_keys/associations.rb | 428 ++++++++++++++++++ .../attribute_methods.rb | 84 ++++ .../lib/composite_primary_keys/base.rb | 337 ++++++++++++++ .../composite_primary_keys/calculations.rb | 68 +++ .../composite_arrays.rb | 30 ++ .../connection_adapters/ibm_db_adapter.rb | 21 + .../connection_adapters/oracle_adapter.rb | 15 + .../connection_adapters/postgresql_adapter.rb | 53 +++ .../connection_adapters/sqlite3_adapter.rb | 15 + .../lib/composite_primary_keys/fixtures.rb | 8 + .../lib/composite_primary_keys/migration.rb | 20 + .../lib/composite_primary_keys/reflection.rb | 19 + .../lib/composite_primary_keys/version.rb | 8 + .../composite_primary_keys-1.1.0/loader.rb | 24 + .../local/database_connections.rb.sample | 10 + .../local/paths.rb.sample | 2 + .../local/tasks.rb.sample | 2 + .../scripts/console.rb | 48 ++ .../scripts/txt2html | 67 +++ .../scripts/txt2js | 59 +++ .../tasks/activerecord_selection.rake | 43 ++ .../tasks/databases.rake | 12 + .../tasks/databases/mysql.rake | 30 ++ .../tasks/databases/oracle.rake | 25 + .../tasks/databases/postgresql.rake | 26 ++ .../tasks/databases/sqlite3.rake | 28 ++ .../tasks/deployment.rake | 22 + .../tasks/local_setup.rake | 13 + .../tasks/website.rake | 18 + .../test/README_tests.txt | 67 +++ .../test/abstract_unit.rb | 94 ++++ .../connections/native_ibm_db/connection.rb | 23 + .../connections/native_mysql/connection.rb | 13 + .../connections/native_oracle/connection.rb | 14 + .../native_postgresql/connection.rb | 9 + .../connections/native_sqlite/connection.rb | 9 + .../test/fixtures/article.rb | 5 + .../test/fixtures/articles.yml | 6 + .../test/fixtures/comment.rb | 6 + .../test/fixtures/comments.yml | 16 + .../db_definitions/db2-create-tables.sql | 113 +++++ .../db_definitions/db2-drop-tables.sql | 16 + .../test/fixtures/db_definitions/mysql.sql | 174 +++++++ .../fixtures/db_definitions/oracle.drop.sql | 39 ++ .../test/fixtures/db_definitions/oracle.sql | 188 ++++++++ .../fixtures/db_definitions/postgresql.sql | 199 ++++++++ .../test/fixtures/db_definitions/sqlite.sql | 160 +++++++ .../test/fixtures/department.rb | 5 + .../test/fixtures/departments.yml | 3 + .../test/fixtures/employee.rb | 4 + .../test/fixtures/employees.yml | 9 + .../test/fixtures/group.rb | 3 + .../test/fixtures/groups.yml | 3 + .../test/fixtures/hack.rb | 6 + .../test/fixtures/hacks.yml | 2 + .../test/fixtures/membership.rb | 7 + .../test/fixtures/membership_status.rb | 3 + .../test/fixtures/membership_statuses.yml | 10 + .../test/fixtures/memberships.yml | 6 + .../test/fixtures/product.rb | 7 + .../test/fixtures/product_tariff.rb | 5 + .../test/fixtures/product_tariffs.yml | 12 + .../test/fixtures/products.yml | 6 + .../test/fixtures/reading.rb | 4 + .../test/fixtures/readings.yml | 10 + .../test/fixtures/reference_code.rb | 7 + .../test/fixtures/reference_codes.yml | 28 ++ .../test/fixtures/reference_type.rb | 7 + .../test/fixtures/reference_types.yml | 9 + .../test/fixtures/street.rb | 3 + .../test/fixtures/streets.yml | 15 + .../test/fixtures/suburb.rb | 6 + .../test/fixtures/suburbs.yml | 9 + .../test/fixtures/tariff.rb | 6 + .../test/fixtures/tariffs.yml | 13 + .../test/fixtures/user.rb | 10 + .../test/fixtures/users.yml | 6 + .../test/hash_tricks.rb | 34 ++ .../test/plugins/pagination.rb | 405 +++++++++++++++++ .../test/plugins/pagination_helper.rb | 135 ++++++ .../test/test_associations.rb | 160 +++++++ .../test/test_attribute_methods.rb | 22 + .../test/test_attributes.rb | 84 ++++ .../test/test_clone.rb | 34 ++ .../test/test_composite_arrays.rb | 51 +++ .../test/test_create.rb | 68 +++ .../test/test_delete.rb | 96 ++++ .../test/test_dummy.rb | 28 ++ .../test/test_find.rb | 73 +++ .../test/test_ids.rb | 97 ++++ .../test/test_miscellaneous.rb | 39 ++ .../test/test_pagination.rb | 38 ++ .../test/test_polymorphic.rb | 31 ++ .../test/test_santiago.rb | 27 ++ .../test/test_tutorial_examle.rb | 26 ++ .../test/test_update.rb | 40 ++ .../composite_primary_keys-1.1.0/tmp/test.db | Bin 0 -> 46080 bytes .../website/index.html | 199 ++++++++ .../website/index.txt | 159 +++++++ .../javascripts/rounded_corners_lite.inc.js | 285 ++++++++++++ .../website/stylesheets/screen.css | 126 ++++++ .../website/template.js | 3 + .../website/template.rhtml | 53 +++ .../website/version-raw.js | 3 + .../website/version-raw.txt | 2 + .../website/version.js | 4 + .../website/version.txt | 3 + 122 files changed, 6081 insertions(+), 1 deletion(-) create mode 100644 vendor/gems/composite_primary_keys-1.1.0/History.txt create mode 100644 vendor/gems/composite_primary_keys-1.1.0/Manifest.txt create mode 100644 vendor/gems/composite_primary_keys-1.1.0/README.txt create mode 100644 vendor/gems/composite_primary_keys-1.1.0/README_DB2.txt create mode 100644 vendor/gems/composite_primary_keys-1.1.0/Rakefile create mode 100644 vendor/gems/composite_primary_keys-1.1.0/init.rb create mode 100644 vendor/gems/composite_primary_keys-1.1.0/install.rb create mode 100644 vendor/gems/composite_primary_keys-1.1.0/lib/adapter_helper/base.rb create mode 100644 vendor/gems/composite_primary_keys-1.1.0/lib/adapter_helper/mysql.rb create mode 100644 vendor/gems/composite_primary_keys-1.1.0/lib/adapter_helper/oracle.rb create mode 100644 vendor/gems/composite_primary_keys-1.1.0/lib/adapter_helper/postgresql.rb create mode 100644 vendor/gems/composite_primary_keys-1.1.0/lib/adapter_helper/sqlite3.rb create mode 100644 vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys.rb create mode 100644 vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/association_preload.rb create mode 100644 vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/associations.rb create mode 100644 vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/attribute_methods.rb create mode 100644 vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/base.rb create mode 100644 vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/calculations.rb create mode 100644 vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/composite_arrays.rb create mode 100644 vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/connection_adapters/ibm_db_adapter.rb create mode 100644 vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/connection_adapters/oracle_adapter.rb create mode 100644 vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/connection_adapters/postgresql_adapter.rb create mode 100644 vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/connection_adapters/sqlite3_adapter.rb create mode 100644 vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/fixtures.rb create mode 100644 vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/migration.rb create mode 100644 vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/reflection.rb create mode 100644 vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/version.rb create mode 100644 vendor/gems/composite_primary_keys-1.1.0/loader.rb create mode 100644 vendor/gems/composite_primary_keys-1.1.0/local/database_connections.rb.sample create mode 100644 vendor/gems/composite_primary_keys-1.1.0/local/paths.rb.sample create mode 100644 vendor/gems/composite_primary_keys-1.1.0/local/tasks.rb.sample create mode 100755 vendor/gems/composite_primary_keys-1.1.0/scripts/console.rb create mode 100644 vendor/gems/composite_primary_keys-1.1.0/scripts/txt2html create mode 100644 vendor/gems/composite_primary_keys-1.1.0/scripts/txt2js create mode 100644 vendor/gems/composite_primary_keys-1.1.0/tasks/activerecord_selection.rake create mode 100644 vendor/gems/composite_primary_keys-1.1.0/tasks/databases.rake create mode 100644 vendor/gems/composite_primary_keys-1.1.0/tasks/databases/mysql.rake create mode 100644 vendor/gems/composite_primary_keys-1.1.0/tasks/databases/oracle.rake create mode 100644 vendor/gems/composite_primary_keys-1.1.0/tasks/databases/postgresql.rake create mode 100644 vendor/gems/composite_primary_keys-1.1.0/tasks/databases/sqlite3.rake create mode 100644 vendor/gems/composite_primary_keys-1.1.0/tasks/deployment.rake create mode 100644 vendor/gems/composite_primary_keys-1.1.0/tasks/local_setup.rake create mode 100644 vendor/gems/composite_primary_keys-1.1.0/tasks/website.rake create mode 100644 vendor/gems/composite_primary_keys-1.1.0/test/README_tests.txt create mode 100644 vendor/gems/composite_primary_keys-1.1.0/test/abstract_unit.rb create mode 100644 vendor/gems/composite_primary_keys-1.1.0/test/connections/native_ibm_db/connection.rb create mode 100644 vendor/gems/composite_primary_keys-1.1.0/test/connections/native_mysql/connection.rb create mode 100644 vendor/gems/composite_primary_keys-1.1.0/test/connections/native_oracle/connection.rb create mode 100644 vendor/gems/composite_primary_keys-1.1.0/test/connections/native_postgresql/connection.rb create mode 100644 vendor/gems/composite_primary_keys-1.1.0/test/connections/native_sqlite/connection.rb create mode 100644 vendor/gems/composite_primary_keys-1.1.0/test/fixtures/article.rb create mode 100644 vendor/gems/composite_primary_keys-1.1.0/test/fixtures/articles.yml create mode 100644 vendor/gems/composite_primary_keys-1.1.0/test/fixtures/comment.rb create mode 100644 vendor/gems/composite_primary_keys-1.1.0/test/fixtures/comments.yml create mode 100644 vendor/gems/composite_primary_keys-1.1.0/test/fixtures/db_definitions/db2-create-tables.sql create mode 100644 vendor/gems/composite_primary_keys-1.1.0/test/fixtures/db_definitions/db2-drop-tables.sql create mode 100644 vendor/gems/composite_primary_keys-1.1.0/test/fixtures/db_definitions/mysql.sql create mode 100644 vendor/gems/composite_primary_keys-1.1.0/test/fixtures/db_definitions/oracle.drop.sql create mode 100644 vendor/gems/composite_primary_keys-1.1.0/test/fixtures/db_definitions/oracle.sql create mode 100644 vendor/gems/composite_primary_keys-1.1.0/test/fixtures/db_definitions/postgresql.sql create mode 100644 vendor/gems/composite_primary_keys-1.1.0/test/fixtures/db_definitions/sqlite.sql create mode 100644 vendor/gems/composite_primary_keys-1.1.0/test/fixtures/department.rb create mode 100644 vendor/gems/composite_primary_keys-1.1.0/test/fixtures/departments.yml create mode 100644 vendor/gems/composite_primary_keys-1.1.0/test/fixtures/employee.rb create mode 100644 vendor/gems/composite_primary_keys-1.1.0/test/fixtures/employees.yml create mode 100644 vendor/gems/composite_primary_keys-1.1.0/test/fixtures/group.rb create mode 100644 vendor/gems/composite_primary_keys-1.1.0/test/fixtures/groups.yml create mode 100644 vendor/gems/composite_primary_keys-1.1.0/test/fixtures/hack.rb create mode 100644 vendor/gems/composite_primary_keys-1.1.0/test/fixtures/hacks.yml create mode 100644 vendor/gems/composite_primary_keys-1.1.0/test/fixtures/membership.rb create mode 100644 vendor/gems/composite_primary_keys-1.1.0/test/fixtures/membership_status.rb create mode 100644 vendor/gems/composite_primary_keys-1.1.0/test/fixtures/membership_statuses.yml create mode 100644 vendor/gems/composite_primary_keys-1.1.0/test/fixtures/memberships.yml create mode 100644 vendor/gems/composite_primary_keys-1.1.0/test/fixtures/product.rb create mode 100644 vendor/gems/composite_primary_keys-1.1.0/test/fixtures/product_tariff.rb create mode 100644 vendor/gems/composite_primary_keys-1.1.0/test/fixtures/product_tariffs.yml create mode 100644 vendor/gems/composite_primary_keys-1.1.0/test/fixtures/products.yml create mode 100644 vendor/gems/composite_primary_keys-1.1.0/test/fixtures/reading.rb create mode 100644 vendor/gems/composite_primary_keys-1.1.0/test/fixtures/readings.yml create mode 100644 vendor/gems/composite_primary_keys-1.1.0/test/fixtures/reference_code.rb create mode 100644 vendor/gems/composite_primary_keys-1.1.0/test/fixtures/reference_codes.yml create mode 100644 vendor/gems/composite_primary_keys-1.1.0/test/fixtures/reference_type.rb create mode 100644 vendor/gems/composite_primary_keys-1.1.0/test/fixtures/reference_types.yml create mode 100644 vendor/gems/composite_primary_keys-1.1.0/test/fixtures/street.rb create mode 100644 vendor/gems/composite_primary_keys-1.1.0/test/fixtures/streets.yml create mode 100644 vendor/gems/composite_primary_keys-1.1.0/test/fixtures/suburb.rb create mode 100644 vendor/gems/composite_primary_keys-1.1.0/test/fixtures/suburbs.yml create mode 100644 vendor/gems/composite_primary_keys-1.1.0/test/fixtures/tariff.rb create mode 100644 vendor/gems/composite_primary_keys-1.1.0/test/fixtures/tariffs.yml create mode 100644 vendor/gems/composite_primary_keys-1.1.0/test/fixtures/user.rb create mode 100644 vendor/gems/composite_primary_keys-1.1.0/test/fixtures/users.yml create mode 100644 vendor/gems/composite_primary_keys-1.1.0/test/hash_tricks.rb create mode 100644 vendor/gems/composite_primary_keys-1.1.0/test/plugins/pagination.rb create mode 100644 vendor/gems/composite_primary_keys-1.1.0/test/plugins/pagination_helper.rb create mode 100644 vendor/gems/composite_primary_keys-1.1.0/test/test_associations.rb create mode 100644 vendor/gems/composite_primary_keys-1.1.0/test/test_attribute_methods.rb create mode 100644 vendor/gems/composite_primary_keys-1.1.0/test/test_attributes.rb create mode 100644 vendor/gems/composite_primary_keys-1.1.0/test/test_clone.rb create mode 100644 vendor/gems/composite_primary_keys-1.1.0/test/test_composite_arrays.rb create mode 100644 vendor/gems/composite_primary_keys-1.1.0/test/test_create.rb create mode 100644 vendor/gems/composite_primary_keys-1.1.0/test/test_delete.rb create mode 100644 vendor/gems/composite_primary_keys-1.1.0/test/test_dummy.rb create mode 100644 vendor/gems/composite_primary_keys-1.1.0/test/test_find.rb create mode 100644 vendor/gems/composite_primary_keys-1.1.0/test/test_ids.rb create mode 100644 vendor/gems/composite_primary_keys-1.1.0/test/test_miscellaneous.rb create mode 100644 vendor/gems/composite_primary_keys-1.1.0/test/test_pagination.rb create mode 100644 vendor/gems/composite_primary_keys-1.1.0/test/test_polymorphic.rb create mode 100644 vendor/gems/composite_primary_keys-1.1.0/test/test_santiago.rb create mode 100644 vendor/gems/composite_primary_keys-1.1.0/test/test_tutorial_examle.rb create mode 100644 vendor/gems/composite_primary_keys-1.1.0/test/test_update.rb create mode 100644 vendor/gems/composite_primary_keys-1.1.0/tmp/test.db create mode 100644 vendor/gems/composite_primary_keys-1.1.0/website/index.html create mode 100644 vendor/gems/composite_primary_keys-1.1.0/website/index.txt create mode 100644 vendor/gems/composite_primary_keys-1.1.0/website/javascripts/rounded_corners_lite.inc.js create mode 100644 vendor/gems/composite_primary_keys-1.1.0/website/stylesheets/screen.css create mode 100644 vendor/gems/composite_primary_keys-1.1.0/website/template.js create mode 100644 vendor/gems/composite_primary_keys-1.1.0/website/template.rhtml create mode 100644 vendor/gems/composite_primary_keys-1.1.0/website/version-raw.js create mode 100644 vendor/gems/composite_primary_keys-1.1.0/website/version-raw.txt create mode 100644 vendor/gems/composite_primary_keys-1.1.0/website/version.js create mode 100644 vendor/gems/composite_primary_keys-1.1.0/website/version.txt diff --git a/config/initializers/potlatch.rb b/config/initializers/potlatch.rb index 880947148..b98d60e1a 100644 --- a/config/initializers/potlatch.rb +++ b/config/initializers/potlatch.rb @@ -3,4 +3,4 @@ POTLATCH_PRESETS = Potlatch::Potlatch.get_presets() # Use SQL (faster) or Rails (more elegant) for common Potlatch reads # getway speedup is approximately x2, whichways approximately x7 -POTLATCH_USE_SQL = true +POTLATCH_USE_SQL = false 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 index 000000000..7016020dc --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/History.txt @@ -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 index 000000000..2ca2fc8da --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/Manifest.txt @@ -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 index 000000000..11daeb922 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/README.txt @@ -0,0 +1,41 @@ += Composite Primary Keys for ActiveRecords + +== Summary + +ActiveRecords/Rails famously doesn't support composite primary keys. +This RubyGem extends the activerecord gem to provide CPK support. + +== Installation + + gem install composite_primary_keys + +== Usage + + require 'composite_primary_keys' + class ProductVariation + set_primary_keys :product_id, :variation_seq + end + + pv = ProductVariation.find(345, 12) + +It even supports composite foreign keys for associations. + +See http://compositekeys.rubyforge.org for more. + +== Running Tests + +See test/README.tests.txt + +== Url + +http://compositekeys.rubyforge.org + +== Questions, Discussion and Contributions + +http://groups.google.com/compositekeys + +== Author + +Written by Dr Nic Williams, drnicwilliams@gmail +Contributions by many! + 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 index 000000000..b69505fdb --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/README_DB2.txt @@ -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 index 000000000..22c1fb664 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/Rakefile @@ -0,0 +1,65 @@ +require 'rubygems' +require 'rake' +require 'rake/clean' +require 'rake/testtask' +require 'rake/rdoctask' +require 'rake/packagetask' +require 'rake/gempackagetask' +require 'rake/contrib/rubyforgepublisher' +require 'fileutils' +require 'hoe' +include FileUtils +require File.join(File.dirname(__FILE__), 'lib', 'composite_primary_keys', 'version') + +AUTHOR = "Dr Nic Williams" +EMAIL = "drnicwilliams@gmail.com" +DESCRIPTION = "Composite key support for ActiveRecords" +GEM_NAME = "composite_primary_keys" # what ppl will type to install your gem +if File.exists?("~/.rubyforge/user-config.yml") + # TODO this should prob go in a local/ file + config = YAML.load(File.read(File.expand_path("~/.rubyforge/user-config.yml"))) + RUBYFORGE_USERNAME = config["username"] +end +RUBYFORGE_PROJECT = "compositekeys" +HOMEPATH = "http://#{RUBYFORGE_PROJECT}.rubyforge.org" + +REV = nil #File.read(".svn/entries")[/committed-rev="(\d+)"/, 1] rescue nil +VERS = ENV['VERSION'] || (CompositePrimaryKeys::VERSION::STRING + (REV ? ".#{REV}" : "")) +CLEAN.include ['**/.*.sw?', '*.gem', '.config','debug.log','*.db','logfile','log/**/*','**/.DS_Store', '.project'] +RDOC_OPTS = ['--quiet', '--title', "newgem documentation", + "--opname", "index.html", + "--line-numbers", + "--main", "README", + "--inline-source"] + +class Hoe + def extra_deps + @extra_deps.reject { |x| Array(x).first == 'hoe' } + end +end + +# Generate all the Rake tasks +# Run 'rake -T' to see list of generated tasks (from gem root directory) +hoe = Hoe.new(GEM_NAME, VERS) do |p| + p.author = AUTHOR + p.description = DESCRIPTION + p.email = EMAIL + p.summary = DESCRIPTION + p.url = HOMEPATH + p.rubyforge_name = RUBYFORGE_PROJECT if RUBYFORGE_PROJECT + p.test_globs = ["test/**/test*.rb"] + p.clean_globs |= CLEAN #An array of file patterns to delete on clean. + + # == Optional + p.changes = p.paragraphs_of("History.txt", 0..1).join("\n\n") + p.extra_deps = [['activerecord', '>= 2.1.2']] #An array of rubygem dependencies. + #p.spec_extras - A hash of extra values to set in the gemspec. +end + +CHANGES = hoe.paragraphs_of('History.txt', 0..1).join("\n\n") +PATH = RUBYFORGE_PROJECT +hoe.remote_rdoc_dir = File.join(PATH.gsub(/^#{RUBYFORGE_PROJECT}\/?/,''), 'rdoc') + +PROJECT_ROOT = File.expand_path(".") + +require 'loader' 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 index 000000000..7ae5e5d4e --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/init.rb @@ -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 index 000000000..5be89cf10 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/install.rb @@ -0,0 +1,30 @@ +require 'rbconfig' +require 'find' +require 'ftools' + +include Config + +# this was adapted from rdoc's install.rb by ways of Log4r + +$sitedir = CONFIG["sitelibdir"] +unless $sitedir + version = CONFIG["MAJOR"] + "." + CONFIG["MINOR"] + $libdir = File.join(CONFIG["libdir"], "ruby", version) + $sitedir = $:.find {|x| x =~ /site_ruby/ } + if !$sitedir + $sitedir = File.join($libdir, "site_ruby") + elsif $sitedir !~ Regexp.quote(version) + $sitedir = File.join($sitedir, version) + end +end + +# the acual gruntwork +Dir.chdir("lib") + +Find.find("composite_primary_keys", "composite_primary_keys.rb") { |f| + if f[-3..-1] == ".rb" + File::install(f, File.join($sitedir, *f.split(/\//)), 0644, true) + else + File::makedirs(File.join($sitedir, *f.split(/\//))) + end +} 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 index 000000000..36ed05a68 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/lib/adapter_helper/base.rb @@ -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 index 000000000..8762e1d73 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/lib/adapter_helper/mysql.rb @@ -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 index 000000000..76a9d19f4 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/lib/adapter_helper/oracle.rb @@ -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 index 000000000..ea2c4bef1 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/lib/adapter_helper/postgresql.rb @@ -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 index 000000000..7a45d9fad --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/lib/adapter_helper/sqlite3.rb @@ -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 index 000000000..99b61407e --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys.rb @@ -0,0 +1,55 @@ +#-- +# Copyright (c) 2006 Nic Williams +# +# 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. +#++ + +$:.unshift(File.dirname(__FILE__)) unless + $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__))) + +unless defined?(ActiveRecord) + begin + require 'active_record' + rescue LoadError + require 'rubygems' + require_gem 'activerecord' + end +end + +require 'composite_primary_keys/fixtures' +require 'composite_primary_keys/composite_arrays' +require 'composite_primary_keys/associations' +require 'composite_primary_keys/association_preload' +require 'composite_primary_keys/reflection' +require 'composite_primary_keys/base' +require 'composite_primary_keys/calculations' +require 'composite_primary_keys/migration' +require 'composite_primary_keys/attribute_methods' + +ActiveRecord::Base.class_eval do + include CompositePrimaryKeys::ActiveRecord::Base +end + +Dir[File.dirname(__FILE__) + '/composite_primary_keys/connection_adapters/*.rb'].each do |adapter| + begin + require adapter.gsub('.rb','') + rescue MissingSourceFile + end +end 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 index 000000000..54e5eeb0e --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/association_preload.rb @@ -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 index 000000000..4ea4a7b02 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/associations.rb @@ -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 index 000000000..a0e3331df --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/attribute_methods.rb @@ -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 index 000000000..42ec475af --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/base.rb @@ -0,0 +1,337 @@ +module CompositePrimaryKeys + module ActiveRecord #:nodoc: + class CompositeKeyError < StandardError #:nodoc: + end + + module Base #:nodoc: + + INVALID_FOR_COMPOSITE_KEYS = 'Not appropriate for composite primary keys' + NOT_IMPLEMENTED_YET = 'Not implemented for composite primary keys yet' + + def self.append_features(base) + super + base.send(:include, InstanceMethods) + base.extend(ClassMethods) + end + + module ClassMethods + def set_primary_keys(*keys) + keys = keys.first if keys.first.is_a?(Array) + keys = keys.map { |k| k.to_sym } + cattr_accessor :primary_keys + self.primary_keys = keys.to_composite_keys + + class_eval <<-EOV + extend CompositeClassMethods + include CompositeInstanceMethods + + include CompositePrimaryKeys::ActiveRecord::Associations + include CompositePrimaryKeys::ActiveRecord::AssociationPreload + include CompositePrimaryKeys::ActiveRecord::Calculations + include CompositePrimaryKeys::ActiveRecord::AttributeMethods + EOV + end + + def composite? + false + end + end + + module InstanceMethods + def composite?; self.class.composite?; end + end + + module CompositeInstanceMethods + + # A model instance's primary keys is always available as model.ids + # whether you name it the default 'id' or set it to something else. + def id + attr_names = self.class.primary_keys + CompositeIds.new(attr_names.map { |attr_name| read_attribute(attr_name) }) + end + alias_method :ids, :id + + def to_param + id.to_s + end + + def id_before_type_cast #:nodoc: + raise CompositeKeyError, CompositePrimaryKeys::ActiveRecord::Base::NOT_IMPLEMENTED_YET + end + + def quoted_id #:nodoc: + [self.class.primary_keys, ids]. + transpose. + map {|attr_name,id| quote_value(id, column_for_attribute(attr_name))}. + to_composite_ids + end + + # Sets the primary ID. + def id=(ids) + ids = ids.split(ID_SEP) if ids.is_a?(String) + ids.flatten! + unless ids.is_a?(Array) and ids.length == self.class.primary_keys.length + raise "#{self.class}.id= requires #{self.class.primary_keys.length} ids" + end + [primary_keys, ids].transpose.each {|key, an_id| write_attribute(key , an_id)} + id + end + + # Returns a clone of the record that hasn't been assigned an id yet and + # is treated as a new record. Note that this is a "shallow" clone: + # it copies the object's attributes only, not its associations. + # The extent of a "deep" clone is application-specific and is therefore + # left to the application to implement according to its need. + def clone + attrs = self.attributes_before_type_cast + self.class.primary_keys.each {|key| attrs.delete(key.to_s)} + self.class.new do |record| + record.send :instance_variable_set, '@attributes', attrs + end + end + + + private + # The xx_without_callbacks methods are overwritten as that is the end of the alias chain + + # Creates a new record with values matching those of the instance attributes. + def create_without_callbacks + unless self.id + raise CompositeKeyError, "Composite keys do not generated ids from sequences, you must provide id values" + end + attributes_minus_pks = attributes_with_quotes(false) + quoted_pk_columns = self.class.primary_key.map { |col| connection.quote_column_name(col) } + cols = quoted_column_names(attributes_minus_pks) << quoted_pk_columns + vals = attributes_minus_pks.values << quoted_id + connection.insert( + "INSERT INTO #{self.class.quoted_table_name} " + + "(#{cols.join(', ')}) " + + "VALUES (#{vals.join(', ')})", + "#{self.class.name} Create", + self.class.primary_key, + self.id + ) + @new_record = false + return true + end + + # Updates the associated record with values matching those of the instance attributes. + def update_without_callbacks + where_clause_terms = [self.class.primary_key, quoted_id].transpose.map do |pair| + "(#{connection.quote_column_name(pair[0])} = #{pair[1]})" + end + where_clause = where_clause_terms.join(" AND ") + connection.update( + "UPDATE #{self.class.quoted_table_name} " + + "SET #{quoted_comma_pair_list(connection, attributes_with_quotes(false))} " + + "WHERE #{where_clause}", + "#{self.class.name} Update" + ) + return true + end + + # Deletes the record in the database and freezes this instance to reflect that no changes should + # be made (since they can't be persisted). + def destroy_without_callbacks + where_clause_terms = [self.class.primary_key, quoted_id].transpose.map do |pair| + "(#{connection.quote_column_name(pair[0])} = #{pair[1]})" + end + where_clause = where_clause_terms.join(" AND ") + unless new_record? + connection.delete( + "DELETE FROM #{self.class.quoted_table_name} " + + "WHERE #{where_clause}", + "#{self.class.name} Destroy" + ) + end + freeze + end + end + + module CompositeClassMethods + def primary_key; primary_keys; end + def primary_key=(keys); primary_keys = keys; end + + def composite? + true + end + + #ids_to_s([[1,2],[7,3]]) -> "(1,2),(7,3)" + #ids_to_s([[1,2],[7,3]], ',', ';') -> "1,2;7,3" + def ids_to_s(many_ids, id_sep = CompositePrimaryKeys::ID_SEP, list_sep = ',', left_bracket = '(', right_bracket = ')') + many_ids.map {|ids| "#{left_bracket}#{ids}#{right_bracket}"}.join(list_sep) + end + + # Creates WHERE condition from list of composited ids + # 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) + # 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) + def composite_where_clause(ids) + 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 + + ids.map do |id_set| + [primary_keys, id_set].transpose.map do |key, id| + "#{table_name}.#{key.to_s}=#{sanitize(id)}" + end.join(" AND ") + end.join(") OR (") + end + + # Returns true if the given +ids+ represents the primary keys of a record in the database, false otherwise. + # Example: + # Person.exists?(5,7) + def exists?(ids) + obj = find(ids) rescue false + !obj.nil? and obj.is_a?(self) + end + + # Deletes the record with the given +ids+ without instantiating an object first, e.g. delete(1,2) + # If an array of ids is provided (e.g. delete([1,2], [3,4]), all of them + # are deleted. + def delete(*ids) + unless ids.is_a?(Array); raise "*ids must be an Array"; end + ids = [ids.to_composite_ids] if not ids.first.is_a?(Array) + where_clause = ids.map do |id_set| + [primary_keys, id_set].transpose.map do |key, id| + "#{quoted_table_name}.#{connection.quote_column_name(key.to_s)}=#{sanitize(id)}" + end.join(" AND ") + end.join(") OR (") + delete_all([ "(#{where_clause})" ]) + end + + # Destroys the record with the given +ids+ by instantiating the object and calling #destroy (all the callbacks are the triggered). + # If an array of ids is provided, all of them are destroyed. + def destroy(*ids) + unless ids.is_a?(Array); raise "*ids must be an Array"; end + if ids.first.is_a?(Array) + ids = ids.map{|compids| compids.to_composite_ids} + else + ids = ids.to_composite_ids + end + ids.first.is_a?(CompositeIds) ? ids.each { |id_set| find(id_set).destroy } : find(ids).destroy + end + + # Returns an array of column objects for the table associated with this class. + # Each column that matches to one of the primary keys has its + # primary attribute set to true + def columns + unless @columns + @columns = connection.columns(table_name, "#{name} Columns") + @columns.each {|column| column.primary = primary_keys.include?(column.name.to_sym)} + end + @columns + end + + ## DEACTIVATED METHODS ## + public + # Lazy-set the sequence name to the connection's default. This method + # is only ever called once since set_sequence_name overrides it. + def sequence_name #:nodoc: + raise CompositeKeyError, CompositePrimaryKeys::ActiveRecord::Base::INVALID_FOR_COMPOSITE_KEYS + end + + def reset_sequence_name #:nodoc: + raise CompositeKeyError, CompositePrimaryKeys::ActiveRecord::Base::INVALID_FOR_COMPOSITE_KEYS + end + + def set_primary_key(value = nil, &block) + raise CompositeKeyError, CompositePrimaryKeys::ActiveRecord::Base::INVALID_FOR_COMPOSITE_KEYS + end + + private + def find_one(id, options) + raise CompositeKeyError, CompositePrimaryKeys::ActiveRecord::Base::INVALID_FOR_COMPOSITE_KEYS + end + + def find_some(ids, options) + raise CompositeKeyError, CompositePrimaryKeys::ActiveRecord::Base::INVALID_FOR_COMPOSITE_KEYS + end + + def find_from_ids(ids, options) + ids = ids.first if ids.last == nil + conditions = " AND (#{sanitize_sql(options[:conditions])})" if options[:conditions] + # if ids is just a flat list, then its size must = primary_key.length (one id per primary key, in order) + # if ids is list of lists, then each inner list must follow rule above + if ids.first.is_a? String + # find '2,1' -> ids = ['2,1'] + # find '2,1;7,3' -> ids = ['2,1;7,3'] + ids = ids.first.split(ID_SET_SEP).map {|id_set| id_set.split(ID_SEP).to_composite_ids} + # find '2,1;7,3' -> ids = [['2','1'],['7','3']], inner [] are CompositeIds + end + ids = [ids.to_composite_ids] if not ids.first.kind_of?(Array) + ids.each do |id_set| + unless id_set.is_a?(Array) + raise "Ids must be in an Array, instead received: #{id_set.inspect}" + end + unless id_set.length == primary_keys.length + raise "#{id_set.inspect}: Incorrect number of primary keys for #{class_name}: #{primary_keys.inspect}" + end + end + + # Let keys = [:a, :b] + # If ids = [[10, 50], [11, 51]], then :conditions => + # "(#{quoted_table_name}.a, #{quoted_table_name}.b) IN ((10, 50), (11, 51))" + + conditions = ids.map do |id_set| + [primary_keys, id_set].transpose.map do |key, id| + col = columns_hash[key.to_s] + val = quote_value(id, col) + "#{quoted_table_name}.#{connection.quote_column_name(key.to_s)}=#{val}" + end.join(" AND ") + end.join(") OR (") + + options.update :conditions => "(#{conditions})" + + result = find_every(options) + + if result.size == ids.size + ids.size == 1 ? result[0] : result + else + raise ::ActiveRecord::RecordNotFound, "Couldn't find all #{name.pluralize} with IDs (#{ids.inspect})#{conditions}" + end + end + end + end + end +end + + +module ActiveRecord + ID_SEP = ',' + ID_SET_SEP = ';' + + class Base + # Allows +attr_name+ to be the list of primary_keys, and returns the id + # of the object + # e.g. @object[@object.class.primary_key] => [1,1] + def [](attr_name) + if attr_name.is_a?(String) and attr_name != attr_name.split(ID_SEP).first + attr_name = attr_name.split(ID_SEP) + end + attr_name.is_a?(Array) ? + attr_name.map {|name| read_attribute(name)} : + read_attribute(attr_name) + end + + # Updates the attribute identified by attr_name with the specified +value+. + # (Alias for the protected write_attribute method). + def []=(attr_name, value) + if attr_name.is_a?(String) and attr_name != attr_name.split(ID_SEP).first + attr_name = attr_name.split(ID_SEP) + end + + if attr_name.is_a? Array + value = value.split(ID_SEP) if value.is_a? String + unless value.length == attr_name.length + raise "Number of attr_names and values do not match" + end + #breakpoint + [attr_name, value].transpose.map {|name,val| write_attribute(name.to_s, val)} + else + write_attribute(attr_name, value) + end + end + end +end 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 index 000000000..44280e1c7 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/calculations.rb @@ -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 index 000000000..030c416f3 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/composite_arrays.rb @@ -0,0 +1,30 @@ +module CompositePrimaryKeys + ID_SEP = ',' + ID_SET_SEP = ';' + + module ArrayExtension + def to_composite_keys + CompositeKeys.new(self) + end + + def to_composite_ids + CompositeIds.new(self) + end + end + + class CompositeArray < Array + def to_s + join(ID_SEP) + end + end + + class CompositeKeys < CompositeArray + + end + + class CompositeIds < CompositeArray + + end +end + +Array.send(:include, CompositePrimaryKeys::ArrayExtension) 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 index 000000000..1ab47179f --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/connection_adapters/ibm_db_adapter.rb @@ -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 index 000000000..af558fa6e --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/connection_adapters/oracle_adapter.rb @@ -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 index 000000000..65fce48f6 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/connection_adapters/postgresql_adapter.rb @@ -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 index 000000000..052757720 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/connection_adapters/sqlite3_adapter.rb @@ -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 index 000000000..7dfaf08fb --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/fixtures.rb @@ -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 index 000000000..2a50404c5 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/migration.rb @@ -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 index 000000000..309baf118 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/reflection.rb @@ -0,0 +1,19 @@ +module ActiveRecord + module Reflection + class AssociationReflection + def primary_key_name + return @primary_key_name if @primary_key_name + case + when macro == :belongs_to + @primary_key_name = options[:foreign_key] || class_name.foreign_key + when options[:as] + @primary_key_name = options[:foreign_key] || "#{options[:as]}_id" + else + @primary_key_name = options[:foreign_key] || active_record.name.foreign_key + end + @primary_key_name = @primary_key_name.to_composite_keys.to_s if @primary_key_name.is_a? Array + @primary_key_name + 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/version.rb b/vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/version.rb new file mode 100644 index 000000000..c6cbeeb4c --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/lib/composite_primary_keys/version.rb @@ -0,0 +1,8 @@ +module CompositePrimaryKeys + module VERSION #:nodoc: + MAJOR = 1 + MINOR = 1 + TINY = 0 + STRING = [MAJOR, MINOR, TINY].join('.') + end +end 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 index 000000000..052c47ce1 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/loader.rb @@ -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 index 000000000..be67edd97 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/local/database_connections.rb.sample @@ -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 index 000000000..65ba16f68 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/local/paths.rb.sample @@ -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 index 000000000..29daf8d6f --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/local/tasks.rb.sample @@ -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 index 000000000..7053e926b --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/scripts/console.rb @@ -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__} " + 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 index 000000000..d5ab2c698 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/scripts/txt2html @@ -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!^
      |
      $!,'') +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=['"]([^'"]+)[^>]*>(.*?)!m){ + ident = syntax_items.length + element, syntax, source = $1, $2, $3 + syntax_items << "<#{element} class=\"syntax\">#{convert_syntax(syntax, source)}" + "syntax-temp-#{ident}" + } + title = RedCloth.new(title_text).to_html.gsub(%r!<.*?>!,'').strip + body = RedCloth.new(body_text).to_html + body.gsub!(%r!(?:
      )?syntax-temp-(\d+)(?:
      )?!){ 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 index 000000000..4a287cad1 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/scripts/txt2js @@ -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!^
      |
      $!,'') +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 index 000000000..44ca4bb24 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/tasks/activerecord_selection.rake @@ -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 index 000000000..0d9151753 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/tasks/databases.rake @@ -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 index 000000000..e05239ee2 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/tasks/databases/mysql.rake @@ -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 index 000000000..4861d0137 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/tasks/databases/oracle.rake @@ -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 index 000000000..13b34e2ed --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/tasks/databases/postgresql.rake @@ -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 index 000000000..9a5579ad8 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/tasks/databases/sqlite3.rake @@ -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 index 000000000..84f143b70 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/tasks/deployment.rake @@ -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 index 000000000..1b8afa8b7 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/tasks/local_setup.rake @@ -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 index 000000000..600f5633f --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/tasks/website.rake @@ -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 index 000000000..66fe21f2e --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/test/README_tests.txt @@ -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_ + 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 index 000000000..f33edfaeb --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/test/abstract_unit.rb @@ -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 index 000000000..7a40f7c3d --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/test/connections/native_ibm_db/connection.rb @@ -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 index 000000000..12dbe4cd3 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/test/connections/native_mysql/connection.rb @@ -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 index 000000000..383538fed --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/test/connections/native_oracle/connection.rb @@ -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 index 000000000..a2d93f92b --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/test/connections/native_postgresql/connection.rb @@ -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 index 000000000..7c6102e0b --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/test/connections/native_sqlite/connection.rb @@ -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 index 000000000..7233f8126 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/article.rb @@ -0,0 +1,5 @@ +class Article < ActiveRecord::Base + has_many :readings + has_many :users, :through => :readings +end + 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 index 000000000..e51060463 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/articles.yml @@ -0,0 +1,6 @@ +first: + id: 1 + name: Article One +second: + id: 2 + 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 index 000000000..857bf70aa --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/comment.rb @@ -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 index 000000000..7f145143a --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/comments.yml @@ -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 index 000000000..edff930de --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/db_definitions/db2-create-tables.sql @@ -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 index 000000000..eb4843358 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/db_definitions/db2-drop-tables.sql @@ -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 index 000000000..e83d3aae5 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/db_definitions/mysql.sql @@ -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 index 000000000..d23e3a341 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/db_definitions/oracle.drop.sql @@ -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 index 000000000..8db0ff208 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/db_definitions/oracle.sql @@ -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 index 000000000..c3ca7881e --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/db_definitions/postgresql.sql @@ -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 index 000000000..fd8c56687 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/db_definitions/sqlite.sql @@ -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 index 000000000..a76eaf3ca --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/department.rb @@ -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 index 000000000..4213244a4 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/departments.yml @@ -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 index 000000000..2b47e0989 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/employee.rb @@ -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 index 000000000..c2efd839a --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/employees.yml @@ -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 index 000000000..889ee2f52 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/group.rb @@ -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 index 000000000..a15185e13 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/groups.yml @@ -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 index 000000000..71d6cac83 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/hack.rb @@ -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 index 000000000..29f67b1f4 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/hacks.yml @@ -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 index 000000000..d5111e964 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/membership.rb @@ -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 index 000000000..54b687c6c --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/membership_status.rb @@ -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 index 000000000..d3f3c3062 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/membership_statuses.yml @@ -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 index 000000000..f6cdc84a3 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/memberships.yml @@ -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 index 000000000..5466dcabe --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/product.rb @@ -0,0 +1,7 @@ +class Product < ActiveRecord::Base + set_primary_keys :id # redundant + has_many :product_tariffs, :foreign_key => :product_id + has_one :product_tariff, :foreign_key => :product_id + + has_many :tariffs, :through => :product_tariffs, :foreign_key => [:tariff_id, :tariff_start_date] +end 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 index 000000000..cbabee7c5 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/product_tariff.rb @@ -0,0 +1,5 @@ +class ProductTariff < ActiveRecord::Base + set_primary_keys :product_id, :tariff_id, :tariff_start_date + belongs_to :product, :foreign_key => :product_id + belongs_to :tariff, :foreign_key => [:tariff_id, :tariff_start_date] +end 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 index 000000000..27a464fb3 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/product_tariffs.yml @@ -0,0 +1,12 @@ +first_flat: + product_id: 1 + tariff_id: 1 + tariff_start_date: <%= Date.today.to_s(:db) %> +first_free: + product_id: 1 + tariff_id: 2 + tariff_start_date: <%= Date.today.to_s(:db) %> +second_free: + product_id: 2 + tariff_id: 2 + tariff_start_date: <%= Date.today.to_s(:db) %> 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 index 000000000..3c38a5ba0 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/products.yml @@ -0,0 +1,6 @@ +first_product: + id: 1 + name: Product One +second_product: + id: 2 + 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 index 000000000..2e8197062 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/reading.rb @@ -0,0 +1,4 @@ +class Reading < ActiveRecord::Base + belongs_to :article + belongs_to :user +end 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 index 000000000..e3afaa9cd --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/readings.yml @@ -0,0 +1,10 @@ +santiago_first: + id: 1 + user_id: 1 + article_id: 1 + rating: 4 +santiago_second: + id: 2 + user_id: 1 + article_id: 2 + 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 index 000000000..594d8d8be --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/reference_code.rb @@ -0,0 +1,7 @@ +class ReferenceCode < ActiveRecord::Base + set_primary_keys :reference_type_id, :reference_code + + belongs_to :reference_type, :foreign_key => "reference_type_id" + + validates_presence_of :reference_code, :code_label, :abbreviation +end 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 index 000000000..397938199 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/reference_codes.yml @@ -0,0 +1,28 @@ +name_prefix_mr: + reference_type_id: 1 + reference_code: 1 + code_label: MR + abbreviation: Mr +name_prefix_mrs: + reference_type_id: 1 + reference_code: 2 + code_label: MRS + abbreviation: Mrs +name_prefix_ms: + reference_type_id: 1 + reference_code: 3 + code_label: MS + abbreviation: Ms + +gender_male: + reference_type_id: 2 + reference_code: 1 + code_label: MALE + abbreviation: Male +gender_female: + reference_type_id: 2 + reference_code: 2 + code_label: FEMALE + abbreviation: Female + + \ 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 index 000000000..5b2b12b4e --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/reference_type.rb @@ -0,0 +1,7 @@ +class ReferenceType < ActiveRecord::Base + set_primary_key :reference_type_id + has_many :reference_codes, :foreign_key => "reference_type_id" + + validates_presence_of :type_label, :abbreviation + validates_uniqueness_of :type_label +end 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 index 000000000..0520ba9f9 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/reference_types.yml @@ -0,0 +1,9 @@ +name_prefix: + reference_type_id: 1 + type_label: NAME_PREFIX + abbreviation: Name Prefix + +gender: + reference_type_id: 2 + type_label: GENDER + abbreviation: Gender 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 index 000000000..de92917d0 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/street.rb @@ -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 index 000000000..38998c469 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/streets.yml @@ -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 index 000000000..93045350e --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/suburb.rb @@ -0,0 +1,6 @@ +class Suburb < ActiveRecord::Base + set_primary_keys :city_id, :suburb_id + has_many :streets, :foreign_key => [:city_id, :suburb_id] + has_many :first_streets, :foreign_key => [:city_id, :suburb_id], + :class_name => 'Street', :conditions => "streets.name = 'First Street'" +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 index 000000000..efae0c0a2 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/suburbs.yml @@ -0,0 +1,9 @@ +first: + city_id: 1 + suburb_id: 1 + name: First Suburb +second: + city_id: 2 + suburb_id: 1 + name: Second Suburb + \ 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 index 000000000..d5cb07da1 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/tariff.rb @@ -0,0 +1,6 @@ +class Tariff < ActiveRecord::Base + set_primary_keys [:tariff_id, :start_date] + has_many :product_tariffs, :foreign_key => [:tariff_id, :tariff_start_date] + has_one :product_tariff, :foreign_key => [:tariff_id, :tariff_start_date] + has_many :products, :through => :product_tariffs, :foreign_key => [:tariff_id, :tariff_start_date] +end 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 index 000000000..7346fc510 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/tariffs.yml @@ -0,0 +1,13 @@ +flat: + tariff_id: 1 + start_date: <%= Date.today.to_s(:db) %> + amount: 50 +free: + tariff_id: 2 + start_date: <%= Date.today.to_s(:db) %> + amount: 0 +flat_future: + tariff_id: 1 + start_date: <%= Date.today.next.to_s(:db) %> + amount: 100 + \ 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 index 000000000..a8487c49f --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/user.rb @@ -0,0 +1,10 @@ +class User < ActiveRecord::Base + has_many :readings + has_many :articles, :through => :readings + has_many :comments, :as => :person + has_many :hacks, :through => :comments, :source => :hack + + def find_custom_articles + articles.find(:all, :conditions => ["name = ?", "Article One"]) + end +end 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 index 000000000..d33a38a4a --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/test/fixtures/users.yml @@ -0,0 +1,6 @@ +santiago: + id: 1 + name: Santiago +drnic: + id: 2 + 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 index 000000000..b37bbbbf1 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/test/hash_tricks.rb @@ -0,0 +1,34 @@ +# From: +# http://www.bigbold.com/snippets/posts/show/2178 +# http://blog.caboo.se/articles/2006/06/11/stupid-hash-tricks +# +# An example utilisation of these methods in a controller is: +# def some_action +# # some script kiddie also passed in :bee, which we don't want tampered with _here_. +# @model = Model.create(params.pass(:foo, :bar)) +# end +class Hash + + # lets through the keys in the argument + # >> {:one => 1, :two => 2, :three => 3}.pass(:one) + # => {:one=>1} + def pass(*keys) + keys = keys.first if keys.first.is_a?(Array) + tmp = self.clone + tmp.delete_if {|k,v| ! keys.include?(k.to_sym) } + tmp.delete_if {|k,v| ! keys.include?(k.to_s) } + tmp + end + + # blocks the keys in the arguments + # >> {:one => 1, :two => 2, :three => 3}.block(:one) + # => {:two=>2, :three=>3} + def block(*keys) + keys = keys.first if keys.first.is_a?(Array) + tmp = self.clone + tmp.delete_if {|k,v| keys.include?(k.to_sym) } + tmp.delete_if {|k,v| keys.include?(k.to_s) } + tmp + end + +end 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 index 000000000..6a3e1a97b --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/test/plugins/pagination.rb @@ -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 @people + # 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 + # @person_pages Paginator instance. The current page is determined + # by the params[:page] 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 @person_pages + # and @people 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 @people 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: + # :singular_name:: the singular name to use, if it can't be inferred by singularizing the collection name + # :class_name:: the class name to use, if it can't be inferred by + # camelizing the singular name + # :per_page:: the maximum number of items to include in a + # single page. Defaults to 10 + # :conditions:: optional conditions passed to Model.find(:all, *params) and + # Model.count + # :order:: optional order parameter passed to Model.find(:all, *params) + # :order_by:: (deprecated, used :order) optional order parameter passed to Model.find(:all, *params) + # :joins:: optional joins parameter passed to Model.find(:all, *params) + # and Model.count + # :join:: (deprecated, used :joins or :include) optional join parameter passed to Model.find(:all, *params) + # and Model.count + # :include:: optional eager loading parameter passed to Model.find(:all, *params) + # and Model.count + # :select:: :select parameter passed to Model.find(:all, *params) + # + # :count:: parameter passed as :select option to Model.count(*params) + # + # :group:: :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 :actions option). + # + # +options+ are the same as PaginationHelper#paginate, with the addition + # of: + # :actions:: 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 index 000000000..069d77566 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/test/plugins/pagination_helper.rb @@ -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 + # :name:: the routing name for this paginator + # (defaults to +page+) + # :prefix:: prefix for pagination links + # (i.e. Older Pages: 1 2 3 4) + # :suffix:: suffix for pagination links + # (i.e. 1 2 3 4 <- Older Pages) + # :window_size:: the number of pages to show around + # the current page (defaults to 2) + # :always_show_anchors:: whether or not the first and last + # pages should always be shown + # (defaults to +true+) + # :link_to_current_page:: whether or not the current page + # should be linked to (defaults to + # +false+) + # :params:: any additional routing parameters + # for page URLs + # + # ==== Examples + # # We'll assume we have a paginator setup in @person_pages... + # + # pagination_links(@person_pages) + # # => 1 2 3 ... 10 + # + # pagination_links(@person_pages, :link_to_current_page => true) + # # => 1 2 3 ... 10 + # + # pagination_links(@person_pages, :always_show_anchors => false) + # # => 1 2 3 + # + # pagination_links(@person_pages, :window_size => 1) + # # => 1 2 ... 10 + # + # pagination_links(@person_pages, :params => { :viewer => "flash" }) + # # => 1 2 3 ... + # # 10 + 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 + # :window_size:: the number of pages to show around + # the current page (defaults to +2+) + # :always_show_anchors:: whether or not the first and last + # pages should always be shown + # (defaults to +true+) + # :link_to_current_page:: 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 index 000000000..78302f86c --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/test/test_associations.rb @@ -0,0 +1,160 @@ +require 'abstract_unit' +require 'fixtures/article' +require 'fixtures/product' +require 'fixtures/tariff' +require 'fixtures/product_tariff' +require 'fixtures/suburb' +require 'fixtures/street' +require 'fixtures/restaurant' +require 'fixtures/dorm' +require 'fixtures/room' +require 'fixtures/room_attribute' +require 'fixtures/room_attribute_assignment' +require 'fixtures/student' +require 'fixtures/room_assignment' +require 'fixtures/user' +require 'fixtures/reading' + +class TestAssociations < Test::Unit::TestCase + fixtures :articles, :products, :tariffs, :product_tariffs, :suburbs, :streets, :restaurants, :restaurants_suburbs, + :dorms, :rooms, :room_attributes, :room_attribute_assignments, :students, :room_assignments, :users, :readings + + def test_has_many_through_with_conditions_when_through_association_is_not_composite + user = User.find(:first) + assert_equal 1, user.articles.find(:all, :conditions => ["articles.name = ?", "Article One"]).size + end + + def test_has_many_through_with_conditions_when_through_association_is_composite + room = Room.find(:first) + assert_equal 0, room.room_attributes.find(:all, :conditions => ["room_attributes.name != ?", "keg"]).size + end + + def test_has_many_through_on_custom_finder_when_through_association_is_composite_finder_when_through_association_is_not_composite + user = User.find(:first) + assert_equal 1, user.find_custom_articles.size + end + + def test_has_many_through_on_custom_finder_when_through_association_is_composite + room = Room.find(:first) + assert_equal 0, room.find_custom_room_attributes.size + end + + def test_count + assert_equal 2, Product.count(:include => :product_tariffs) + assert_equal 3, Tariff.count(:include => :product_tariffs) + assert_equal 2, Tariff.count(:group => :start_date).size + end + + def test_products + assert_not_nil products(:first_product).product_tariffs + assert_equal 2, products(:first_product).product_tariffs.length + assert_not_nil products(:first_product).tariffs + assert_equal 2, products(:first_product).tariffs.length + assert_not_nil products(:first_product).product_tariff + end + + def test_product_tariffs + assert_not_nil product_tariffs(:first_flat).product + assert_not_nil product_tariffs(:first_flat).tariff + assert_equal Product, product_tariffs(:first_flat).product.class + assert_equal Tariff, product_tariffs(:first_flat).tariff.class + end + + def test_tariffs + assert_not_nil tariffs(:flat).product_tariffs + assert_equal 1, tariffs(:flat).product_tariffs.length + assert_not_nil tariffs(:flat).products + assert_equal 1, tariffs(:flat).products.length + assert_not_nil tariffs(:flat).product_tariff + end + + # Its not generating the instances of associated classes from the rows + def test_find_includes_products + assert @products = Product.find(:all, :include => :product_tariffs) + assert_equal 2, @products.length + assert_not_nil @products.first.instance_variable_get('@product_tariffs'), '@product_tariffs not set; should be array' + assert_equal 3, @products.inject(0) {|sum, tariff| sum + tariff.instance_variable_get('@product_tariffs').length}, + "Incorrect number of product_tariffs returned" + end + + def test_find_includes_tariffs + assert @tariffs = Tariff.find(:all, :include => :product_tariffs) + assert_equal 3, @tariffs.length + assert_not_nil @tariffs.first.instance_variable_get('@product_tariffs'), '@product_tariffs not set; should be array' + assert_equal 3, @tariffs.inject(0) {|sum, tariff| sum + tariff.instance_variable_get('@product_tariffs').length}, + "Incorrect number of product_tariffs returnedturned" + end + + def test_find_includes_product + assert @product_tariffs = ProductTariff.find(:all, :include => :product) + assert_equal 3, @product_tariffs.length + assert_not_nil @product_tariffs.first.instance_variable_get('@product'), '@product not set' + end + + def test_find_includes_comp_belongs_to_tariff + assert @product_tariffs = ProductTariff.find(:all, :include => :tariff) + assert_equal 3, @product_tariffs.length + assert_not_nil @product_tariffs.first.instance_variable_get('@tariff'), '@tariff not set' + end + + def test_find_includes_extended + assert @products = Product.find(:all, :include => {:product_tariffs => :tariff}) + assert_equal 3, @products.inject(0) {|sum, product| sum + product.instance_variable_get('@product_tariffs').length}, + "Incorrect number of product_tariffs returned" + + assert @tariffs = Tariff.find(:all, :include => {:product_tariffs => :product}) + assert_equal 3, @tariffs.inject(0) {|sum, tariff| sum + tariff.instance_variable_get('@product_tariffs').length}, + "Incorrect number of product_tariffs returned" + end + + def test_join_where_clause + @product = Product.find(:first, :include => :product_tariffs) + where_clause = @product.product_tariffs.composite_where_clause( + ['foo','bar'], [1,2] + ) + assert_equal('(foo=1 AND bar=2)', where_clause) + end + + def test_has_many_through + @products = Product.find(:all, :include => :tariffs) + assert_equal 3, @products.inject(0) {|sum, product| sum + product.instance_variable_get('@tariffs').length}, + "Incorrect number of tariffs returned" + end + + def test_has_many_through_when_not_pre_loaded + student = Student.find(:first) + rooms = student.rooms + assert_equal 1, rooms.size + assert_equal 1, rooms.first.dorm_id + assert_equal 1, rooms.first.room_id + end + + def test_has_many_through_when_through_association_is_composite + dorm = Dorm.find(:first) + assert_equal 1, dorm.rooms.length + assert_equal 1, dorm.rooms.first.room_attributes.length + assert_equal 'keg', dorm.rooms.first.room_attributes.first.name + end + + def test_associations_with_conditions + @suburb = Suburb.find([2, 1]) + assert_equal 2, @suburb.streets.size + + @suburb = Suburb.find([2, 1]) + assert_equal 1, @suburb.first_streets.size + + @suburb = Suburb.find([2, 1], :include => :streets) + assert_equal 2, @suburb.streets.size + + @suburb = Suburb.find([2, 1], :include => :first_streets) + assert_equal 1, @suburb.first_streets.size + end + + def test_has_and_belongs_to_many + @restaurant = Restaurant.find([1,1]) + assert_equal 2, @restaurant.suburbs.size + + @restaurant = Restaurant.find([1,1], :include => :suburbs) + assert_equal 2, @restaurant.suburbs.size + end +end 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 index 000000000..b020a64ca --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/test/test_attribute_methods.rb @@ -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 index 000000000..750408202 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/test/test_attributes.rb @@ -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 index 000000000..822974430 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/test/test_clone.rb @@ -0,0 +1,34 @@ +require 'abstract_unit' +require 'fixtures/reference_type' +require 'fixtures/reference_code' + +class TestClone < Test::Unit::TestCase + fixtures :reference_types, :reference_codes + + 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_truth + testing_with do + clone = @first.clone + assert_equal @first.attributes.block(@klass.primary_key), clone.attributes + if composite? + @klass.primary_key.each {|key| assert_nil clone[key], "Primary key '#{key}' should be nil"} + else + assert_nil clone[@klass.primary_key], "Sole primary key should be nil" + end + end + end +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 index 000000000..41e21f8f8 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/test/test_composite_arrays.rb @@ -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 index 000000000..dfbc77397 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/test/test_create.rb @@ -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 index 000000000..cd79bbd72 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/test/test_delete.rb @@ -0,0 +1,96 @@ +require 'abstract_unit' +require 'fixtures/reference_type' +require 'fixtures/reference_code' +require 'fixtures/department' +require 'fixtures/employee' + +class TestDelete < Test::Unit::TestCase + fixtures :reference_types, :reference_codes, :departments, :employees + + 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_destroy_one + testing_with do + #assert @first.destroy + assert true + end + end + + def test_destroy_one_via_class + testing_with do + assert @klass.destroy(*@first.id) + end + end + + def test_destroy_one_alone_via_class + testing_with do + assert @klass.destroy(@first.id) + end + end + + def test_delete_one + testing_with do + assert @klass.delete(*@first.id) if composite? + end + end + + def test_delete_one_alone + testing_with do + assert @klass.delete(@first.id) + end + end + + def test_delete_many + testing_with do + to_delete = @klass.find(:all)[0..1] + assert_equal 2, to_delete.length + end + end + + def test_delete_all + testing_with do + @klass.delete_all + end + end + + def test_clear_association + department = Department.find(1,1) + assert_equal 2, department.employees.size, "Before clear employee count should be 2." + department.employees.clear + assert_equal 0, department.employees.size, "After clear employee count should be 0." + department.reload + assert_equal 0, department.employees.size, "After clear and a reload from DB employee count should be 0." + end + + def test_delete_association + department = Department.find(1,1) + assert_equal 2, department.employees.size , "Before delete employee count should be 2." + first_employee = department.employees[0] + department.employees.delete(first_employee) + assert_equal 1, department.employees.size, "After delete employee count should be 1." + department.reload + assert_equal 1, department.employees.size, "After delete and a reload from DB employee count should be 1." + end + + def test_delete_records_for_has_many_association_with_composite_primary_key + reference_type = ReferenceType.find(1) + codes_to_delete = reference_type.reference_codes[0..1] + assert_equal 3, reference_type.reference_codes.size, "Before deleting records reference_code count should be 3." + reference_type.reference_codes.delete_records(codes_to_delete) + reference_type.reload + assert_equal 1, reference_type.reference_codes.size, "After deleting 2 records and a reload from DB reference_code count should be 1." + end +end 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 index 000000000..44386685b --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/test/test_dummy.rb @@ -0,0 +1,28 @@ +require 'abstract_unit' +require 'fixtures/reference_type' +require 'fixtures/reference_code' + +class TestDummy < Test::Unit::TestCase + fixtures :reference_types, :reference_codes + + 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_truth + testing_with do + assert true + end + end +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 index 000000000..a07d30a64 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/test/test_find.rb @@ -0,0 +1,73 @@ +require 'abstract_unit' +require 'fixtures/reference_type' +require 'fixtures/reference_code' + +# Testing the find action on composite ActiveRecords with two primary keys +class TestFind < Test::Unit::TestCase + fixtures :reference_types, :reference_codes + + CLASSES = { + :single => { + :class => ReferenceType, + :primary_keys => [:reference_type_id], + }, + :dual => { + :class => ReferenceCode, + :primary_keys => [:reference_type_id, :reference_code], + }, + :dual_strs => { + :class => ReferenceCode, + :primary_keys => ['reference_type_id', 'reference_code'], + }, + } + + def setup + self.class.classes = CLASSES + end + + def test_find_first + testing_with do + obj = @klass.find(:first) + assert obj + assert_equal @klass, obj.class + end + end + + def test_find + testing_with do + found = @klass.find(*first_id) # e.g. find(1,1) or find 1,1 + assert found + assert_equal @klass, found.class + assert_equal found, @klass.find(found.id) + assert_equal found, @klass.find(found.to_param) + end + end + + def test_find_composite_ids + testing_with do + found = @klass.find(first_id) # e.g. find([1,1].to_composite_ids) + assert found + assert_equal @klass, found.class + assert_equal found, @klass.find(found.id) + assert_equal found, @klass.find(found.to_param) + end + end + + def test_to_param + testing_with do + assert_equal first_id_str, @first.to_param.to_s + end + end + + def things_to_look_at + testing_with do + assert_equal found, @klass.find(found.id.to_s) # fails for 2+ keys + end + end + + def test_not_found + assert_raise(::ActiveRecord::RecordNotFound) do + ReferenceCode.send :find, '999,999' + end + end +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 index 000000000..9ba2d92a7 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/test/test_ids.rb @@ -0,0 +1,97 @@ +require 'abstract_unit' +require 'fixtures/reference_type' +require 'fixtures/reference_code' + +class TestIds < Test::Unit::TestCase + fixtures :reference_types, :reference_codes + + CLASSES = { + :single => { + :class => ReferenceType, + :primary_keys => [:reference_type_id], + }, + :dual => { + :class => ReferenceCode, + :primary_keys => [:reference_type_id, :reference_code], + }, + :dual_strs => { + :class => ReferenceCode, + :primary_keys => ['reference_type_id', 'reference_code'], + }, + } + + def setup + self.class.classes = CLASSES + end + + def test_id + testing_with do + assert_equal @first.id, @first.ids if composite? + end + end + + def test_id_to_s + testing_with do + assert_equal first_id_str, @first.id.to_s + assert_equal first_id_str, "#{@first.id}" + end + end + + def test_ids_to_s + testing_with do + order = @klass.primary_key.is_a?(String) ? @klass.primary_key : @klass.primary_key.join(',') + to_test = @klass.find(:all, :order => order)[0..1].map(&:id) + assert_equal '(1,1),(1,2)', @klass.ids_to_s(to_test) if @key_test == :dual + assert_equal '1,1;1,2', @klass.ids_to_s(to_test, ',', ';', '', '') if @key_test == :dual + end + end + + def test_composite_where_clause + testing_with do + 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' + assert_equal(where, @klass.composite_where_clause([[1, 2], [2, 2]])) if @key_test == :dual + end + end + + def test_set_ids_string + testing_with do + array = @primary_keys.collect {|key| 5} + expected = composite? ? array.to_composite_keys : array.first + @first.id = expected.to_s + assert_equal expected, @first.id + end + end + + def test_set_ids_array + testing_with do + array = @primary_keys.collect {|key| 5} + expected = composite? ? array.to_composite_keys : array.first + @first.id = expected + assert_equal expected, @first.id + end + end + + def test_set_ids_comp + testing_with do + array = @primary_keys.collect {|key| 5} + expected = composite? ? array.to_composite_keys : array.first + @first.id = expected + assert_equal expected, @first.id + end + end + + def test_primary_keys + testing_with do + if composite? + assert_not_nil @klass.primary_keys + assert_equal @primary_keys.map {|key| key.to_sym}, @klass.primary_keys + assert_equal @klass.primary_keys, @klass.primary_key + else + assert_not_nil @klass.primary_key + assert_equal @primary_keys, [@klass.primary_key.to_sym] + end + assert_equal @primary_keys.join(','), @klass.primary_key.to_s + # Need a :primary_keys should be Array with to_s overridden + end + end +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 index 000000000..25f6096fe --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/test/test_miscellaneous.rb @@ -0,0 +1,39 @@ +require 'abstract_unit' +require 'fixtures/reference_type' +require 'fixtures/reference_code' + +class TestMiscellaneous < Test::Unit::TestCase + fixtures :reference_types, :reference_codes, :products + + 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_composite_class + testing_with do + assert_equal composite?, @klass.composite? + end + end + + def test_composite_instance + testing_with do + assert_equal composite?, @first.composite? + end + end + + def test_count + assert_equal 2, Product.count + end + +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 index 000000000..fa19d95a6 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/test/test_pagination.rb @@ -0,0 +1,38 @@ +require 'abstract_unit' +require 'fixtures/reference_type' +require 'fixtures/reference_code' +require 'plugins/pagination' + +class TestPagination < Test::Unit::TestCase + fixtures :reference_types, :reference_codes + + include ActionController::Pagination + DEFAULT_PAGE_SIZE = 2 + + attr_accessor :params + + CLASSES = { + :single => { + :class => ReferenceType, + :primary_keys => :reference_type_id, + :table => :reference_types, + }, + :dual => { + :class => ReferenceCode, + :primary_keys => [:reference_type_id, :reference_code], + :table => :reference_codes, + }, + } + + def setup + self.class.classes = CLASSES + @params = {} + end + + def test_paginate_all + testing_with do + @object_pages, @objects = paginate @klass_info[:table], :per_page => DEFAULT_PAGE_SIZE + assert_equal 2, @objects.length, "Each page should have #{DEFAULT_PAGE_SIZE} items" + end + end +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 index 000000000..a632da977 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/test/test_polymorphic.rb @@ -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 index 000000000..4b5f433e4 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/test/test_santiago.rb @@ -0,0 +1,27 @@ +# Test cases devised by Santiago that broke the Composite Primary Keys +# code at one point in time. But no more!!! + +require 'abstract_unit' +require 'fixtures/user' +require 'fixtures/article' +require 'fixtures/reading' + +class TestSantiago < Test::Unit::TestCase + fixtures :suburbs, :streets, :users, :articles, :readings + + def test_normal_and_composite_associations + assert_not_nil @suburb = Suburb.find(1,1) + assert_equal 1, @suburb.streets.length + + assert_not_nil @street = Street.find(1) + assert_not_nil @street.suburb + end + + def test_single_keys + @santiago = User.find(1) + assert_not_nil @santiago.articles + assert_equal 2, @santiago.articles.length + assert_not_nil @santiago.readings + assert_equal 2, @santiago.readings.length + end +end 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 index 000000000..01f9ec603 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/test/test_tutorial_examle.rb @@ -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 index 000000000..d612c92a8 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/test/test_update.rb @@ -0,0 +1,40 @@ +require 'abstract_unit' +require 'fixtures/reference_type' +require 'fixtures/reference_code' + +class TestUpdate < Test::Unit::TestCase + fixtures :reference_types, :reference_codes + + CLASSES = { + :single => { + :class => ReferenceType, + :primary_keys => :reference_type_id, + :update => { :description => 'RT Desc' }, + }, + :dual => { + :class => ReferenceCode, + :primary_keys => [:reference_type_id, :reference_code], + :update => { :description => 'RT Desc' }, + }, + } + + def setup + self.class.classes = CLASSES + end + + def test_setup + testing_with do + assert_not_nil @klass_info[:update] + end + end + + def test_update_attributes + testing_with do + assert @first.update_attributes(@klass_info[:update]) + assert @first.reload + @klass_info[:update].each_pair do |attr_name, new_value| + assert_equal new_value, @first[attr_name], "Attribute #{attr_name} is incorrect" + end + end + end +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 index 0000000000000000000000000000000000000000..923df5f426a270c16c3189a1cd5f1a438adf3d4e GIT binary patch literal 46080 zcmeHQ&2JmW6`v206ls0gvc0AiMJp?g%}Q!T8AhuXX{y+AlGgH<+%MDxu_D*<#uTaS zF6}sIi>}&x{(~HH=^?rI(nAg{0<;Ja6bKR&g<&8_Yq)9Sx~b!|IrPoUF1bUADn!?) zB)$c?yYpst_PyWC?0md8GYjWWJHBmRuDZ*XZyqCEgi`Vi(4Q@`*$ZdJ-y!@R z{#Urg0}((3wlM-jG(pD3$eG#sxx%>%bElsF-WhAzHqW{CW#^^cEJ3na^6cE1lXDlI zwJRmt73Dv{-yr;N{1*Q`{}tTgfe0W1TM&W1M3U0MK|wnIFEBgO` zgbN;s03xt05g6cIbd=KRsj06_=BFl)O_c_iMwy?U`^N1RPt@4zH;M&Yp5z4Su*(#I!QX*-b7;eRV^uj_u zx=RLMPo|8aA-cLC8TKvLxqR6RFDZ@PkQj2STwy`bm{!1xIQ&A%0^1yU(ldp%T&-37 zs0_)+*Om`0?dtEFFdI=rdWrGBZgQBw?;<@xSJ_vY!@tOXDn2LmV=|RAW`|nA5(-wx zhd13pxm$fb`s1ls2)#q>BIhm*q$bZ0tcbc!5-YUo(zPf=p?^d2^+46|YP@p7tjAuz zZA2upe0rlSA4L>OU(Y*{{5`?(jLJf@5-qQ`SaTP>@Y1WH6B0vIijIF{gA63&Y3;wJ zBzlG?|kWm>E9kNyApmkw_bfo+O_=>NwD0H$X||6lL!i~c_y zWw8!`>i@q*_*?uBXaTrQlOEabbOiP!WZAEe{!P#DkQ6KHOp)TnKh*!fNBCd(dz}s< z%8v-_5Cry!s{b&hrySSw%>~(Q8x)oQ5k?p6Vzp8ViAw)}SJ?kO*del`7>EEO&;o(= z?Eg4_lkhj81@LZ*hmed2AOc$wfkBw>6R+vz(CJ<-h_R|(QU_u74+LdVqd*eH`2Q|` ziST>;Ll^=04gV#7gTKmO;Q_A!84pAN5oi+uV;4V0m+j?67}USwtQ0_Nax&Yc)Yipk5GGgrChyXL%^OCc)GS#awY>WoiB@d0puNS-BuW=Le2w92&)MN}P zIS-*}^3n{J!WbsZ=mD@-IzE^*o*!#v#1`YCkdHZx;L_fdaqJjf1%oXxg>87%oAc|C z4z!T4{!IJeZyKDm>UL;WkC7pjvj1WHBLXXO-EXrX<70>bBCrh*=$2!JF!!&kTMh+^ z^#MfvPx*%ges~}Ph`>%nAPb#x{{ZX%ot^S1U56sjE4%$!N@t6{ zbItCNeSd?|v)3zX{a?yIgZ6)iqJv^10-cC}Z2wc){-2gvz< zDe?j#FVGSC9md$N`7nR(QJ@k`?~|(G)p4mvE!iue>XufIvD>VsIE{IP3eyH9)B&%>o;*FSu`Wp= zz%4Bp;sRuHxr0gLDWjFifQ+z^mzR;?_)y9?a)hqtBokL1zj(#26g;PL)zciN#+2?Q zK`rDP2~=MR3cxN*uSd5UYXnVX(iRvPxWYOljxf-*3=dJ|Crl9`WNw zI-#%AKe1QYyZkjoXp15+^H|cDKhnw?MYAL+n20W zA{X?NrzbUjC27-ES&{wE9T@-TceZH0N45|FMBt$jNHZ7$6bGYeF&d~2rT_n&@XsIm z1sDM$fCy|O1Q=yRm4BT7w~dkwSw#dM9s!*H_wX2E42ZxsMIb)@hx328X(A%qh(MbN zBw + + + + + + Composite Primary Keys + + + + + + +
      + +

      Composite Primary Keys

      +
      + Get Version + 1.1.0 +
      +

      &#x2192; Ruby on Rails

      +

      &#x2192; ActiveRecords

      +

      What

      +

      Ruby on Rails does not support composite primary keys. This free software is an extension
      +to the database layer of Rails – ActiveRecords – to support composite primary keys as transparently as possible.

      +

      Any Ruby script using ActiveRecords can use Composite Primary Keys with this library.

      +

      Installing

      +

      sudo gem install composite_primary_keys

      +

      Rails: Add the following to the bottom of your environment.rb file

      +

      require 'composite_primary_keys'

      +

      Ruby scripts: Add the following to the top of your script

      +

      require 'rubygems'
      +require 'composite_primary_keys'

      +

      The basics

      +

      A model with composite primary keys would look like…

      +

      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

      +

      A model associated with a composite key model would be defined like…

      +

      class MembershipStatus < ActiveRecord::Base
      +  belongs_to :membership, :foreign_key => [:user_id, :group_id]
      +end

      +

      That is, associations can include composite keys too. Nice.

      +

      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.

      +

      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"

      +

      Now we want to be able to find instances using the same syntax we always use for ActiveRecords…

      +

      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"}>

      +

      Using Ruby on Rails? You’ll want to your url_for helpers
      +to convert composite keys into strings and back again…

      +

      Membership.find(:first).to_param # => "1,1"

      +

      And then use the string id within your controller to find the object again

      +

      params[:id] # => '1,1'
      +Membership.find(params[:id])
      +=> <Membership:0x3904288 @attributes={"user_id"=>"1", "group_id"=>"1"}>

      +

      That is, an ActiveRecord supporting composite keys behaves transparently
      +throughout your application. Just like a normal ActiveRecord.

      +

      Other tricks

      +

      Pass a list of composite ids to the #find method

      +

      Membership.find [1,1], [2,1]
      +=> [
      +  <Membership:0x394ade8 @attributes={"user_id"=>"1", "group_id"=>"1"}>, 
      +  <Membership:0x394ada0 @attributes={"user_id"=>"2", "group_id"=>"1"}>
      +]

      +

      Perform #count operations

      +

      MembershipStatus.find(:first).memberships.count # => 1

      +

      Routes with Rails

      +

      From Pete Sumskas:

      +
      +

      I ran into one problem that I didn’t see mentioned on this list
      + 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:
      +
      +

      /controller/show/123000,Bu70

      +

      for a two-field composite PK. The default routing would not match that,
      + so after working out how to do the routing I added:
      +
      +

      map.connect ':controller/:action/:id', :id => /\w+(,\w+)*/

      +
      + to my route.rb file.

      +
      +

      +

      Which databases?

      +

      A suite of unit tests have been run on the following databases supported by ActiveRecord:

      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      DatabaseTest SuccessUser feedback
      mysql YESYES (Yes! or No…)
      sqlite3 YESYES (Yes! or No…)
      postgresqlYESYES (Yes! or No…)
      oracle YESYES (Yes! or No…)
      sqlserver ??? (I can help)??? (Yes! or No…)
      db2 ??? (I can help)??? (Yes! or No…)
      firebird ??? (I can help)??? (Yes! or No…)
      sybase ??? (I can help)??? (Yes! or No…)
      openbase ??? (I can help)??? (Yes! or No…)
      frontbase ??? (I can help)??? (Yes! or No…)
      +

      Dr Nic’s Blog

      +

      http://www.drnicwilliams.com – for future announcements and
      +other stories and things.

      +

      Forum

      +

      http://groups.google.com/group/compositekeys

      +

      How to submit patches

      +

      Read the 8 steps for fixing other people’s code and for section 8b: Submit patch to Google Groups, use the Google Group above.

      +

      The source for this project is available via git. You can browse and/or fork the source, or to clone the project locally:
      +
      +

      git clone git://github.com/drnic/composite_primary_keys.git

      +

      Licence

      +

      This code is free to use under the terms of the MIT licence.

      +

      Contact

      +

      Comments are welcome. Send an email to Dr Nic Williams.

      +

      + Dr Nic, 25th October 2008
      + Theme extended from Paul Battley +

      +
      + + + + + + 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 index 000000000..fd66d978e --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/website/index.txt @@ -0,0 +1,159 @@ +h1. Composite Primary Keys + +h1. → Ruby on Rails + +h1. → 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 + +
      sudo gem install composite_primary_keys
      + +Rails: Add the following to the bottom of your environment.rb file + +
      require 'composite_primary_keys'
      + +Ruby scripts: Add the following to the top of your script + +
      require 'rubygems'
      +require 'composite_primary_keys'
      + +h2. The basics + +A model with composite primary keys would look like... + +
      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
      + +A model associated with a composite key model would be defined like... + +
      class MembershipStatus < ActiveRecord::Base
      +  belongs_to :membership, :foreign_key => [:user_id, :group_id]
      +end
      + +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. + +
      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"
      + +Now we want to be able to find instances using the same syntax we always use for ActiveRecords... + +
      MembershipStatus.find(1)    # single id returns single instance
      +=> "1", "status"=>"Active"}>
      +Membership.find(1,1)  # composite ids returns single instance
      +=> "1", "group_id"=>"1"}>
      + +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... + +
      Membership.find(:first).to_param # => "1,1"
      + +And then use the string id within your controller to find the object again + +
      params[:id] # => '1,1'
      +Membership.find(params[:id])
      +=> "1", "group_id"=>"1"}>
      + +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 #find method + +
      Membership.find [1,1], [2,1]
      +=> [
      +  "1", "group_id"=>"1"}>, 
      +  "2", "group_id"=>"1"}>
      +]
      + +Perform #count operations + +
      MembershipStatus.find(:first).memberships.count # => 1
      + +h3. Routes with Rails + +From Pete Sumskas: + +
      + 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: + +
      /controller/show/123000,Bu70
      + + for a two-field composite PK. The default routing would not match that, + so after working out how to do the routing I added: + +
      map.connect ':controller/:action/:id', :id => /\w+(,\w+)*/
      + + to my route.rb file. + +
      + + + +h2. Which databases? + + +A suite of unit tests have been run on the following databases supported by ActiveRecord: + +|_.Database|_.Test Success|_.User feedback| +|mysql |YES|YES ("Yes!":mailto:compositekeys@googlegroups.com?subject=Mysql+is+working or "No...":mailto:compositekeys@googlegroups.com?subject=Mysql+is+failing)| +|sqlite3 |YES|YES ("Yes!":mailto:compositekeys@googlegroups.com?subject=Sqlite3+is+working or "No...":mailto:compositekeys@googlegroups.com?subject=Sqlite3+is+failing)| +|postgresql|YES|YES ("Yes!":mailto:compositekeys@googlegroups.com?subject=Postgresql+is+working or "No...":mailto:compositekeys@googlegroups.com?subject=Postgresql+is+failing)| +|oracle |YES|YES ("Yes!":mailto:compositekeys@googlegroups.com?subject=Oracle+is+working or "No...":mailto:compositekeys@googlegroups.com?subject=Oracle+is+failing)| +|sqlserver |??? ("I can help":mailto:compositekeys@googlegroups.com?subject=Help+with+SQLServer)|??? ("Yes!":mailto:compositekeys@googlegroups.com?subject=SQLServer+is+working or "No...":mailto:compositekeys@googlegroups.com?subject=SQLServer+is+failing)| +|db2 |??? ("I can help":mailto:compositekeys@googlegroups.com?subject=Help+with+DB2)|??? ("Yes!":mailto:compositekeys@googlegroups.com?subject=DB2+is+working or "No...":mailto:compositekeys@googlegroups.com?subject=DB2+is+failing)| +|firebird |??? ("I can help":mailto:compositekeys@googlegroups.com?subject=Help+with+Firebird)|??? ("Yes!":mailto:compositekeys@googlegroups.com?subject=Firebird+is+working or "No...":mailto:compositekeys@googlegroups.com?subject=Firebird+is+failing)| +|sybase |??? ("I can help":mailto:compositekeys@googlegroups.com?subject=Help+with+Sybase)|??? ("Yes!":mailto:compositekeys@googlegroups.com?subject=Sybase+is+working or "No...":mailto:compositekeys@googlegroups.com?subject=Sybase+is+failing)| +|openbase |??? ("I can help":mailto:compositekeys@googlegroups.com?subject=Help+with+Openbase)|??? ("Yes!":mailto:compositekeys@googlegroups.com?subject=Openbase+is+working or "No...":mailto:compositekeys@googlegroups.com?subject=Openbase+is+failing)| +|frontbase |??? ("I can help":mailto:compositekeys@googlegroups.com?subject=Help+with+Frontbase)|??? ("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: + +
      git clone git://github.com/drnic/composite_primary_keys.git
      + +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 index 000000000..afc3ea327 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/website/javascripts/rounded_corners_lite.inc.js @@ -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 index 000000000..3f2d8f951 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/website/stylesheets/screen.css @@ -0,0 +1,126 @@ +body { + background-color: #2F30EE; + font-family: "Georgia", sans-serif; + font-size: 16px; + line-height: 1.6em; + padding: 1.6em 0 0 0; + color: #eee; +} +h1, h2, h3, h4, h5, h6 { + color: #FFEDFA; +} +h1 { + font-family: sans-serif; + font-weight: normal; + font-size: 4em; + line-height: 0.8em; + letter-spacing: -0.1ex; + margin: 5px; +} +li { + padding: 0; + margin: 0; + list-style-type: square; +} +a { + color: #99f; + font-weight: normal; + text-decoration: underline; +} +blockquote { + font-size: 90%; + font-style: italic; + border-left: 1px solid #eee; + padding-left: 1em; +} +.caps { + font-size: 80%; +} + +#main { + width: 45em; + padding: 0; + margin: 0 auto; +} +.coda { + text-align: right; + color: #77f; + font-size: smaller; +} + +table { + font-size: 90%; + line-height: 1.4em; + color: #ff8; + background-color: #111; + padding: 2px 10px 2px 10px; + border-style: dashed; +} + +th { + color: #fff; +} + +td { + padding: 2px 10px 2px 10px; +} + +.success { + color: #0CC52B; +} + +.failed { + color: #E90A1B; +} + +.unknown { + color: #995000; +} +pre, code { + font-family: monospace; + font-size: 90%; + line-height: 1.4em; + color: #ff8; + background-color: #111; + padding: 2px 10px 2px 10px; +} +.comment { color: #aaa; font-style: italic; } +.keyword { color: #eff; font-weight: bold; } +.punct { color: #eee; font-weight: bold; } +.symbol { color: #0bb; } +.string { color: #6b4; } +.ident { color: #ff8; } +.constant { color: #66f; } +.regex { color: #ec6; } +.number { color: #F99; } +.expr { color: #227; } + +#version { + float: right; + text-align: right; + font-family: sans-serif; + font-weight: normal; + background-color: #ff8; + color: #66f; + padding: 15px 20px 10px 20px; + margin: 0 auto; + margin-top: 15px; + border: 3px solid #66f; +} + +#version .numbers { + display: block; + font-size: 4em; + line-height: 0.8em; + letter-spacing: -0.1ex; +} + +#version a { + text-decoration: none; +} + +.clickable { + cursor: pointer; + cursor: hand; +} + 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 index 000000000..fbaf5a5e8 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/website/template.js @@ -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 index 000000000..3e2c531c0 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/website/template.rhtml @@ -0,0 +1,53 @@ + + + + + + + <%= title %> + + + + + + +
      + +

      <%= title %>

      +
      + Get Version + <%= version %> +
      + <%= body %> +

      + Dr Nic, <%= modified.pretty %>
      + Theme extended from Paul Battley +

      +
      + + + + + + 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 index 000000000..9d2ac788f --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/website/version-raw.js @@ -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 index 000000000..74ca3ac67 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/website/version-raw.txt @@ -0,0 +1,2 @@ +h1. Announcement JS file +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 index 000000000..56921a962 --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/website/version.js @@ -0,0 +1,4 @@ +// Version JS file +var version = "1.1.0"; + +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 index 000000000..d0ac6a7ac --- /dev/null +++ b/vendor/gems/composite_primary_keys-1.1.0/website/version.txt @@ -0,0 +1,3 @@ +h1. Version JS file + +document.write(" - " + version); \ No newline at end of file From 6690a9a09bca0afc3aac41e0d51789e965d9b9e7 Mon Sep 17 00:00:00 2001 From: Richard Fairhurst Date: Sun, 16 Nov 2008 13:30:02 +0000 Subject: [PATCH 237/381] changeset comments and a couple of other small fixes --- app/controllers/amf_controller.rb | 13 ++++++++++--- public/potlatch/potlatch.swf | Bin 167475 -> 168667 bytes 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/app/controllers/amf_controller.rb b/app/controllers/amf_controller.rb index 1fa433c9e..6e1b364d4 100644 --- a/app/controllers/amf_controller.rb +++ b/app/controllers/amf_controller.rb @@ -135,6 +135,14 @@ class AmfController < ApplicationController # close previous changeset and add comment if closeid + cs = Changeset.find(closeid) + cs.open = false + if closecomment.empty? + cs.save! + else + cs.tags['comment']=closecomment + cs.save_with_tags! + end end # open a new changeset @@ -206,14 +214,13 @@ class AmfController < ApplicationController begin check_boundaries(xmin, ymin, xmax, ymax) rescue Exception => err - # FIXME: report an error rather than just return an empty result - return [[]] + return [-2,"Sorry - I can't get the map for that area."] end 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 - [way_ids] + [0,way_ids] end # Get a way including nodes and tags. diff --git a/public/potlatch/potlatch.swf b/public/potlatch/potlatch.swf index d26ff6e0d5d6c5abb5dcb6ef0958488f08b92a98..cf17a3091e4ad07362976700026183defa1f7548 100755 GIT binary patch delta 6076 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 aUNpz*X#<5ggLhQ_8G_9z6+>2=+5ZP@ezO_? delta 5486 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 WkCxGgkLY$(+(L!Pg&S6}&i@Bdt`(^O From e57e2c497c60b8b3e14b6858a6f2f173c4bba33d Mon Sep 17 00:00:00 2001 From: Matt Amos Date: Mon, 17 Nov 2008 11:12:48 +0000 Subject: [PATCH 238/381] Altered tests on AMF controller so that they give failures instead of errors. Return format seems to have changed, so someone who knows more about AMF controller should have a look. --- test/functional/amf_controller_test.rb | 87 +++++++++++++++----------- 1 file changed, 50 insertions(+), 37 deletions(-) diff --git a/test/functional/amf_controller_test.rb b/test/functional/amf_controller_test.rb index 0cf56fca4..9221a293d 100644 --- a/test/functional/amf_controller_test.rb +++ b/test/functional/amf_controller_test.rb @@ -5,6 +5,10 @@ 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 @@ -51,47 +55,37 @@ class AmfControllerTest < ActionController::TestCase # check contents of message map = amf_result "/1" - assert map[0].include?(current_ways(:used_way).id) - assert !map[0].include?(current_ways(:invisible_way).id) + assert_equal 0, map[0] + assert_equal Array, map[1].class + assert map[1].include?(current_ways(:used_way).id) + assert !map[1].include?(current_ways(:invisible_way).id) 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] - amf_content "whichways", "/1", bbox - post :amf_read - assert_response :success - amf_parse_response - - # FIXME: whichways needs to reject large bboxes and the test needs to check for this - map = amf_result "/1" - assert map[0].empty? and map[1].empty? and map[2].empty? - end - - def test_whichways_badlat - bboxes = [[0,0.1,0.1,0], [-0.1,80,0.1,70], [0.24,54.34,0.25,54.33]] - bboxes.each do |bbox| - amf_content "whichways", "/1", bbox - post :amf_read - assert_response :success - amf_parse_response - - # FIXME: whichways needs to reject bboxes with illegal lats and the test needs to check for this - map = amf_result "/1" - assert map[0].empty? and map[1].empty? and map[2].empty? + check_bboxes_are_bad [bbox] do |map| + 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. + def test_whichways_badlat + bboxes = [[0,0.1,0.1,0], [-0.1,80,0.1,70], [0.24,54.34,0.25,54.33]] + check_bboxes_are_bad bboxes do |map| + assert_equal BOUNDARY_ERROR, map, "AMF controller should have returned an error." + end + end + + ## + # same as test_whichways_badlat, but for longitudes def test_whichways_badlon bboxes = [[80,-0.1,70,0.1], [54.34,0.24,54.33,0.25]] - bboxes.each do |bbox| - amf_content "whichways", "/1", bbox - post :amf_read - assert_response :success - amf_parse_response - - # FIXME: whichways needs to reject bboxes with illegal lons and the test needs to check for this - map = amf_result "/1" - assert map[0].empty? and map[1].empty? and map[2].empty? + check_bboxes_are_bad bboxes do |map| + assert_equal BOUNDARY_ERROR, map, "AMF controller should have returned an error." end end @@ -107,9 +101,11 @@ class AmfControllerTest < ActionController::TestCase amf_parse_response # check contents of message - ways = amf_result "/1" - assert ways[0].include?(current_ways(:invisible_way).id) - assert !ways[0].include?(current_ways(:used_way).id) + map = amf_result "/1" + assert_equal 0, map[0] + assert_equal Array, map[1].class + assert map[1].include?(current_ways(:used_way).id) + assert !map[1].include?(current_ways(:invisible_way).id) end def test_whichways_deleted_toobig @@ -119,8 +115,8 @@ class AmfControllerTest < ActionController::TestCase assert_response :success amf_parse_response - ways = amf_result "/1" - assert ways[0].empty? + map = amf_result "/1" + assert_equal BOUNDARY_ERROR, map, "AMF controller should have returned an error." end def test_getrelation @@ -305,4 +301,21 @@ class AmfControllerTest < ActionController::TestCase 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 + end + end end From 98b15bef455de6fcf83fec1e5fdddc244dc1a914 Mon Sep 17 00:00:00 2001 From: Matt Amos Date: Mon, 17 Nov 2008 11:45:50 +0000 Subject: [PATCH 239/381] Implemented changeset tags updating via the update method. --- app/controllers/changeset_controller.rb | 36 ++++++++++++++++++ app/models/changeset.rb | 25 ++++++++++++- config/routes.rb | 3 +- test/functional/changeset_controller_test.rb | 39 ++++++++++++++++++++ 4 files changed, 100 insertions(+), 3 deletions(-) diff --git a/app/controllers/changeset_controller.rb b/app/controllers/changeset_controller.rb index 8c042ef8d..cd49176e6 100644 --- a/app/controllers/changeset_controller.rb +++ b/app/controllers/changeset_controller.rb @@ -257,6 +257,42 @@ class ChangesetController < ApplicationController rescue String => s render :text => s, :content_type => "text/plain", :status => :bad_request 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? + 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 + + #------------------------------------------------------------ + # utility functions below. + #------------------------------------------------------------ ## # merge two conditions diff --git a/app/models/changeset.rb b/app/models/changeset.rb index 4a4d12124..047569dca 100644 --- a/app/models/changeset.rb +++ b/app/models/changeset.rb @@ -104,13 +104,13 @@ class Changeset < ActiveRecord::Base 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 # fixme update modified_at time? # FIXME there is no modified_at time, should it be added self.save! - end - ChangesetTag.transaction do tags = self.tags ChangesetTag.delete_all(['id = ?', self.id]) @@ -168,4 +168,25 @@ class Changeset < ActiveRecord::Base 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 open + raise OSM::APIChangesetAlreadyClosedError + end + + # copy the other's tags + self.tags = other.tags + + save_with_tags! + end end diff --git a/config/routes.rb b/config/routes.rb index a835dc552..88fa0a551 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -7,7 +7,8 @@ ActionController::Routing::Routes.draw do |map| 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/include", :controller => 'changeset', :action => 'include', :id => /\d+/ - map.connect "api/#{API_VERSION}/changeset/:id", :controller => 'changeset', :action => 'read', :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' diff --git a/test/functional/changeset_controller_test.rb b/test/functional/changeset_controller_test.rb index d81bf46b8..1b0c63e2d 100644 --- a/test/functional/changeset_controller_test.rb +++ b/test/functional/changeset_controller_test.rb @@ -584,6 +584,45 @@ EOF # FIXME: write the actual test bit after fixing the fixtures! end + ## + # check updating tags on a changeset + def test_changeset_update + basic_authorization "test@openstreetmap.org", "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 :success + + assert_select "osm>changeset[id=#{changeset.id}]", 1 + assert_select "osm>changeset>tag", 2 + assert_select "osm>changeset>tag[k=testing][v=testing]", 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 + #------------------------------------------------------------ # utility functions #------------------------------------------------------------ From 519d402cb2234844f3dfee62cdcb2f3f44d5d618 Mon Sep 17 00:00:00 2001 From: Matt Amos Date: Mon, 17 Nov 2008 11:59:42 +0000 Subject: [PATCH 240/381] Added tests for changeset close method. --- app/controllers/changeset_controller.rb | 37 +++++++++++--------- test/functional/changeset_controller_test.rb | 16 ++++++++- 2 files changed, 35 insertions(+), 18 deletions(-) diff --git a/app/controllers/changeset_controller.rb b/app/controllers/changeset_controller.rb index cd49176e6..1e6a44189 100644 --- a/app/controllers/changeset_controller.rb +++ b/app/controllers/changeset_controller.rb @@ -38,25 +38,28 @@ class ChangesetController < ApplicationController end end + ## + # marks a changeset as closed. this may be called multiple times + # on the same changeset, so is idempotent. def close - begin - unless request.put? - render :nothing => true, :status => :method_not_allowed - return - end - - changeset = Changeset.find(params[:id]) - - unless @user.id == changeset.user_id - raise OSM::APIUserChangesetMismatchError - end - - changeset.open = false - changeset.save! - render :nothing => true - rescue ActiveRecord::RecordNotFound - render :nothing => true, :status => :not_found + unless request.put? + render :nothing => true, :status => :method_not_allowed + return end + + changeset = Changeset.find(params[:id]) + + unless @user.id == changeset.user_id + raise OSM::APIUserChangesetMismatchError + end + + changeset.open = false + changeset.save! + render :nothing => true + rescue ActiveRecord::RecordNotFound + render :nothing => true, :status => :not_found + rescue OSM::APIError => ex + render ex.render_opts end ## diff --git a/test/functional/changeset_controller_test.rb b/test/functional/changeset_controller_test.rb index 1b0c63e2d..6cbee1eb8 100644 --- a/test/functional/changeset_controller_test.rb +++ b/test/functional/changeset_controller_test.rb @@ -48,8 +48,22 @@ class ChangesetControllerTest < ActionController::TestCase assert_select "osm>changeset[id=#{changeset_id}]", 1 end + ## + # test that the user who opened a change can close it def test_close - # FIXME FIXME FIXME! + 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 end ## From 28839fd1504004bd10f30eeba3d7212c77adfbb4 Mon Sep 17 00:00:00 2001 From: Matt Amos Date: Mon, 17 Nov 2008 14:32:15 +0000 Subject: [PATCH 241/381] Fixed some bugs in changeset query code. Added more test cases. --- app/controllers/application.rb | 11 ++++- app/controllers/changeset_controller.rb | 20 +++++++--- lib/osm.rb | 3 ++ test/fixtures/changesets.yml | 9 ++++- test/functional/changeset_controller_test.rb | 42 +++++++++++++++++++- 5 files changed, 75 insertions(+), 10 deletions(-) diff --git a/app/controllers/application.rb b/app/controllers/application.rb index 579c50e95..f5ea0063d 100644 --- a/app/controllers/application.rb +++ b/app/controllers/application.rb @@ -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 diff --git a/app/controllers/changeset_controller.rb b/app/controllers/changeset_controller.rb index 1e6a44189..5a0be3588 100644 --- a/app/controllers/changeset_controller.rb +++ b/app/controllers/changeset_controller.rb @@ -236,9 +236,9 @@ class ChangesetController < ApplicationController # create the conditions that the user asked for. some or all of # these may be nil. conditions = conditions_bbox(params['bbox']) - cond_merge conditions, conditions_user(params['user']) - cond_merge conditions, conditions_time(params['time']) - cond_merge conditions, conditions_open(params['open']) + 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 @@ -333,7 +333,15 @@ class ChangesetController < ApplicationController def conditions_user(user) unless user.nil? u = User.find(user.to_i) - raise OSM::APINotFoundError unless u.data_public? + # 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 @@ -347,11 +355,11 @@ class ChangesetController < ApplicationController # 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 - from, to = time.split(/,/).collect { |t| Date.parse(t) } + from, to = time.split(/,/).collect { |t| DateTime.parse(t) } return ['created_at > ? and created_at < ?', from, to] else # if there is no comma, assume its a lower limit on time - return ['created_at > ?', Date.parse(time)] + return ['created_at > ?', DateTime.parse(time)] end else return nil diff --git a/lib/osm.rb b/lib/osm.rb index c132ff25c..128e65aac 100644 --- a/lib/osm.rb +++ b/lib/osm.rb @@ -17,6 +17,9 @@ module OSM # Raised when an API object is not found. class APINotFoundError < APIError + def render_opts + { :nothing => true, :status => :not_found } + end end # Raised when a precondition to an API action fails sanity check. diff --git a/test/fixtures/changesets.yml b/test/fixtures/changesets.yml index 2047af8d5..ed0df0228 100644 --- a/test/fixtures/changesets.yml +++ b/test/fixtures/changesets.yml @@ -30,12 +30,17 @@ normal_user_version_change: user_id: 1 created_at: "2008-01-01 00:00:00" open: true + min_lon: <%= 1 * SCALE %> + min_lat: <%= 1 * SCALE %> + max_lon: <%= 4 * SCALE %> + max_lat: <%= 4 * SCALE %> # changeset to contain all the invalid stuff that is in the -# fixtures (nodes outside the world, etc...) +# fixtures (nodes outside the world, etc...), but needs to have +# a valid user... invalid_changeset: id: 5 - user_id: 0 + user_id: 3 created_at: "2008-01-01 00:00:00" open: false \ No newline at end of file diff --git a/test/functional/changeset_controller_test.rb b/test/functional/changeset_controller_test.rb index 6cbee1eb8..2b8fa360a 100644 --- a/test/functional/changeset_controller_test.rb +++ b/test/functional/changeset_controller_test.rb @@ -595,7 +595,37 @@ EOF def test_changeset_by_bbox get :query, :bbox => "-10,-10, 10, 10" assert_response :success, "can't get changesets in bbox" - # FIXME: write the actual test bit after fixing the fixtures! + 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 [2,4,5] + + get :query, :time => '2008-01-01T12:34Z' + assert_response :success, "can't get changesets by time-since with hour" + assert_changesets [2] + + get :query, :time => '2007-12-31T23:59Z,2008-01-01T00:01Z' + assert_response :success, "can't get changesets by time-range" + assert_changesets [4,5] end ## @@ -641,6 +671,16 @@ EOF # 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) From 5254f79c080b398ffe2f2400dea250c6decf5e3b Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Mon, 17 Nov 2008 15:00:19 +0000 Subject: [PATCH 242/381] Verify that the key and value isn't getting mixed up by having them different. verify the error message is the same when returning the user conflict on closing changeset test. --- test/functional/changeset_controller_test.rb | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/functional/changeset_controller_test.rb b/test/functional/changeset_controller_test.rb index 2b8fa360a..d5fc08116 100644 --- a/test/functional/changeset_controller_test.rb +++ b/test/functional/changeset_controller_test.rb @@ -64,6 +64,7 @@ class ChangesetControllerTest < ActionController::TestCase put :close, :id => changesets(:normal_user_first_change).id assert_response :conflict + assert_equal "The user doesn't own that changeset", @response.body end ## @@ -636,8 +637,8 @@ EOF 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_tag['k'] = "tagtesting" + new_tag['v'] = "valuetesting" new_changeset.find("//osm/changeset").first << new_tag content new_changeset @@ -646,7 +647,7 @@ EOF assert_select "osm>changeset[id=#{changeset.id}]", 1 assert_select "osm>changeset>tag", 2 - assert_select "osm>changeset>tag[k=testing][v=testing]", 1 + assert_select "osm>changeset>tag[k=tagtesting][v=valuetesting]", 1 end ## From 495bd7f1f077e5cae4428fab8a780f0f479893d0 Mon Sep 17 00:00:00 2001 From: Matt Amos Date: Mon, 17 Nov 2008 15:30:46 +0000 Subject: [PATCH 243/381] Made user input parsing more robust in changeset query method. Added tests. --- app/controllers/changeset_controller.rb | 23 +++++++++---- lib/osm.rb | 12 +++++++ test/functional/changeset_controller_test.rb | 36 ++++++++++++++++++-- 3 files changed, 62 insertions(+), 9 deletions(-) diff --git a/app/controllers/changeset_controller.rb b/app/controllers/changeset_controller.rb index 5a0be3588..e16a4e9b3 100644 --- a/app/controllers/changeset_controller.rb +++ b/app/controllers/changeset_controller.rb @@ -257,8 +257,6 @@ class ChangesetController < ApplicationController render :nothing => true, :status => :not_found rescue OSM::APIError => ex render ex.render_opts - rescue String => s - render :text => s, :content_type => "text/plain", :status => :bad_request end ## @@ -317,10 +315,10 @@ class ChangesetController < ApplicationController # area restriction. def conditions_bbox(bbox) unless bbox.nil? - raise "Bounding box should be min_lon,min_lat,max_lon,max_lat" unless bbox.count(',') == 3 + 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 "Minimum longitude should be less than maximum." unless bbox[0] <= bbox[2] - raise "Minimum latitude should be less than maximum." unless bbox[1] <= bbox[3] + 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 @@ -332,6 +330,9 @@ class ChangesetController < ApplicationController # 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. @@ -355,7 +356,11 @@ class ChangesetController < ApplicationController # 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 - from, to = time.split(/,/).collect { |t| DateTime.parse(t) } + # 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 ['created_at > ? and created_at < ?', from, to] else # if there is no comma, assume its a lower limit on time @@ -364,8 +369,12 @@ class ChangesetController < ApplicationController 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 ex.message.to_s + raise OSM::APIBadUserInput.new(ex.message.to_s) + rescue RuntimeError => ex + raise OSM::APIBadUserInput.new(ex.message.to_s) end ## diff --git a/lib/osm.rb b/lib/osm.rb index 128e65aac..00215c677 100644 --- a/lib/osm.rb +++ b/lib/osm.rb @@ -141,6 +141,18 @@ module OSM end end + ## + # raised when user input couldn't be parsed + class APIBadUserInput < APIError + def initialize(message) + @message = message + end + + def render_opts + { :text => message, :mime_type => "text/plain", :status => :bad_request } + end + end + # Helper methods for going to/from mercator and lat/lng. class Mercator include Math diff --git a/test/functional/changeset_controller_test.rb b/test/functional/changeset_controller_test.rb index d5fc08116..8ccdec889 100644 --- a/test/functional/changeset_controller_test.rb +++ b/test/functional/changeset_controller_test.rb @@ -592,8 +592,8 @@ EOF end ## - # check searching for changesets by bbox - def test_changeset_by_bbox + # 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] @@ -629,6 +629,38 @@ EOF assert_changesets [4,5] 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 From 9a4ea6bfd09acd76533e29e1d4c22c6037909707 Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Mon, 17 Nov 2008 18:53:09 +0000 Subject: [PATCH 244/381] Adding validations for the length of node tags, and the presence of the id (and version). More tests, including some boiler plate tests. --- app/models/node_tag.rb | 3 ++ app/models/old_node_tag.rb | 2 + test/fixtures/friends.yml | 4 ++ test/unit/current_node_tag_test.rb | 22 ---------- test/unit/diary_comment_test.rb | 11 +++++ test/unit/diary_entry_test.rb | 11 +++++ test/unit/friend_test.rb | 11 +++++ test/unit/node_tag_test.rb | 64 ++++++++++++++++++++++++++++- test/unit/old_node_tag_test.rb | 66 ++++++++++++++++++++++++++++++ test/unit/user_test.rb | 18 ++++---- 10 files changed, 179 insertions(+), 33 deletions(-) create mode 100644 test/fixtures/friends.yml delete mode 100644 test/unit/current_node_tag_test.rb create mode 100644 test/unit/diary_comment_test.rb create mode 100644 test/unit/diary_entry_test.rb create mode 100644 test/unit/friend_test.rb create mode 100644 test/unit/old_node_tag_test.rb diff --git a/app/models/node_tag.rb b/app/models/node_tag.rb index 9795ff493..de3d0629c 100644 --- a/app/models/node_tag.rb +++ b/app/models/node_tag.rb @@ -2,4 +2,7 @@ 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, :within => 0..255, :allow_blank => true end diff --git a/app/models/old_node_tag.rb b/app/models/old_node_tag.rb index 26a6c92b4..ef32a0613 100644 --- a/app/models/old_node_tag.rb +++ b/app/models/old_node_tag.rb @@ -3,5 +3,7 @@ class OldNodeTag < ActiveRecord::Base set_table_name 'node_tags' + validates_presence_of :id, :version + validates_length_of :k, :v, :within => 0..255, :allow_blank => true end diff --git a/test/fixtures/friends.yml b/test/fixtures/friends.yml new file mode 100644 index 000000000..782f1e3d2 --- /dev/null +++ b/test/fixtures/friends.yml @@ -0,0 +1,4 @@ +normal_user_with_second_user: + id: 1 + user_id: 1 + friend_user_id: 2 diff --git a/test/unit/current_node_tag_test.rb b/test/unit/current_node_tag_test.rb deleted file mode 100644 index 143fa2442..000000000 --- a/test/unit/current_node_tag_test.rb +++ /dev/null @@ -1,22 +0,0 @@ -require File.dirname(__FILE__) + '/../test_helper' - -class CurrentNodeTagTest < 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 - -end diff --git a/test/unit/diary_comment_test.rb b/test/unit/diary_comment_test.rb new file mode 100644 index 000000000..d7f30a69c --- /dev/null +++ b/test/unit/diary_comment_test.rb @@ -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 index 000000000..0e10f8a9a --- /dev/null +++ b/test/unit/diary_entry_test.rb @@ -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 index 000000000..fd8b5033f --- /dev/null +++ b/test/unit/friend_test.rb @@ -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 diff --git a/test/unit/node_tag_test.rb b/test/unit/node_tag_test.rb index 5e4d47953..fe6ebb166 100644 --- a/test/unit/node_tag_test.rb +++ b/test/unit/node_tag_test.rb @@ -1,11 +1,71 @@ require File.dirname(__FILE__) + '/../test_helper' class NodeTagTest < Test::Unit::TestCase - fixtures :current_node_tags + fixtures :current_node_tags, :current_nodes + set_fixture_class :current_nodes => Node set_fixture_class :current_node_tags => NodeTag - def test_node_tag_count + 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", "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 = 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 end diff --git a/test/unit/old_node_tag_test.rb b/test/unit/old_node_tag_test.rb new file mode 100644 index 000000000..4a9bcbfcb --- /dev/null +++ b/test/unit/old_node_tag_test.rb @@ -0,0 +1,66 @@ +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 +end diff --git a/test/unit/user_test.rb b/test/unit/user_test.rb index 486344fee..c0df4b716 100644 --- a/test/unit/user_test.rb +++ b/test/unit/user_test.rb @@ -96,7 +96,7 @@ class UserTest < Test::Unit::TestCase end def test_friend_with - assert_equal false, users(:normal_user).is_friends_with?(users(:second_user)) + 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)) @@ -114,15 +114,15 @@ class UserTest < Test::Unit::TestCase end def test_friends_with - # make normal user a friend of second user + # normal user is a friend of second user # it should be a one way friend accossitation - assert_equal 0, Friend.count + assert_equal 1, Friend.count norm = users(:normal_user) sec = users(:second_user) - friend = Friend.new - friend.befriender = norm - friend.befriendee = sec - friend.save + #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 @@ -133,7 +133,7 @@ class UserTest < Test::Unit::TestCase 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 + #Friend.delete(friend) + #assert_equal 0, Friend.count end end From a90be5e69a478e2b49ae676b649a78589f85a50e Mon Sep 17 00:00:00 2001 From: Matt Amos Date: Mon, 17 Nov 2008 19:00:01 +0000 Subject: [PATCH 245/381] Migration to add close-time to changesets. This replaces the boolean 'open' attribute. Added checks to ensure that the maximum lifetime and number of changes in a changeset are enforced. Added some tests. --- app/controllers/changeset_controller.rb | 12 ++-- app/models/changeset.rb | 48 +++++++++++--- app/models/node.rb | 3 + app/models/relation.rb | 3 + app/models/way.rb | 4 ++ db/migrate/023_add_end_time_to_changesets.rb | 34 ++++++++++ test/fixtures/changesets.yml | 16 +++-- test/functional/changeset_controller_test.rb | 66 +++++++++++++++++++- 8 files changed, 166 insertions(+), 20 deletions(-) create mode 100644 db/migrate/023_add_end_time_to_changesets.rb diff --git a/app/controllers/changeset_controller.rb b/app/controllers/changeset_controller.rb index e16a4e9b3..181c827b3 100644 --- a/app/controllers/changeset_controller.rb +++ b/app/controllers/changeset_controller.rb @@ -53,7 +53,11 @@ class ChangesetController < ApplicationController raise OSM::APIUserChangesetMismatchError end - changeset.open = false + # 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.closed_at = DateTime.now + changeset.save! render :nothing => true rescue ActiveRecord::RecordNotFound @@ -361,10 +365,10 @@ class ChangesetController < ApplicationController raise OSM::APIBadUserInput.new("bad time range") if times.size != 2 from, to = times.collect { |t| DateTime.parse(t) } - return ['created_at > ? and created_at < ?', from, to] + return ['closed_at >= ? and created_at <= ?', from, to] else # if there is no comma, assume its a lower limit on time - return ['created_at > ?', DateTime.parse(time)] + return ['closed_at >= ?', DateTime.parse(time)] end else return nil @@ -380,7 +384,7 @@ class ChangesetController < ApplicationController ## # restrict changes to those which are open def conditions_open(open) - return open.nil? ? nil : ['open = ?', true] + return open.nil? ? nil : ['closed_at >= ?', DateTime.now] end end diff --git a/app/models/changeset.rb b/app/models/changeset.rb index 047569dca..f67d2118d 100644 --- a/app/models/changeset.rb +++ b/app/models/changeset.rb @@ -12,17 +12,31 @@ class Changeset < ActiveRecord::Base has_many :old_ways has_many :old_relations - validates_presence_of :user_id, :created_at - validates_inclusion_of :open, :in => [ true, false ] + validates_presence_of :user_id, :created_at, :closed_at # 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. + 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? - return 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 self.from_xml(xml, create=false) @@ -36,6 +50,11 @@ class Changeset < ActiveRecord::Base 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| @@ -78,6 +97,14 @@ class Changeset < ActiveRecord::Base 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 @@ -107,8 +134,14 @@ class Changeset < ActiveRecord::Base # do the changeset update and the changeset tags update in the # same transaction to ensure consistency. Changeset.transaction do - # fixme update modified_at time? - # FIXME there is no modified_at time, should it be added + # 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 @@ -155,7 +188,8 @@ class Changeset < ActiveRecord::Base end el1['created_at'] = self.created_at.xmlschema - el1['open'] = self.open.to_s + 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? @@ -180,7 +214,7 @@ class Changeset < ActiveRecord::Base end # can't change a closed changeset - unless open + unless is_open? raise OSM::APIChangesetAlreadyClosedError end diff --git a/app/models/node.rb b/app/models/node.rb index faba4ed66..e926e06a2 100644 --- a/app/models/node.rb +++ b/app/models/node.rb @@ -141,6 +141,9 @@ class Node < ActiveRecord::Base 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 diff --git a/app/models/relation.rb b/app/models/relation.rb index b94aef9ae..2607e7f2f 100644 --- a/app/models/relation.rb +++ b/app/models/relation.rb @@ -307,6 +307,9 @@ class Relation < ActiveRecord::Base end end + # tell the changeset we updated one element only + changeset.add_changes! 1 + # save the (maybe updated) changeset bounding box changeset.save! end diff --git a/app/models/way.rb b/app/models/way.rb index 0c27e5460..3b3f92432 100644 --- a/app/models/way.rb +++ b/app/models/way.rb @@ -233,6 +233,10 @@ class Way < ActiveRecord::Base # 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 diff --git a/db/migrate/023_add_end_time_to_changesets.rb b/db/migrate/023_add_end_time_to_changesets.rb new file mode 100644 index 000000000..b87ce3fde --- /dev/null +++ b/db/migrate/023_add_end_time_to_changesets.rb @@ -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 diff --git a/test/fixtures/changesets.yml b/test/fixtures/changesets.yml index ed0df0228..defd691d2 100644 --- a/test/fixtures/changesets.yml +++ b/test/fixtures/changesets.yml @@ -7,33 +7,37 @@ normal_user_first_change: id: 1 user_id: 1 created_at: "2007-01-01 00:00:00" - open: true + 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" - open: true + 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" - open: false + 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" - open: true + 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 @@ -42,5 +46,5 @@ invalid_changeset: id: 5 user_id: 3 created_at: "2008-01-01 00:00:00" - open: false - \ No newline at end of file + closed_at: "2008-01-02 00:00:00" + num_changes: 9 diff --git a/test/functional/changeset_controller_test.rb b/test/functional/changeset_controller_test.rb index 8ccdec889..3d7531cfb 100644 --- a/test/functional/changeset_controller_test.rb +++ b/test/functional/changeset_controller_test.rb @@ -618,15 +618,19 @@ EOF get :query, :time => '2007-12-31' assert_response :success, "can't get changesets by time-since" - assert_changesets [2,4,5] + 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 [2] + 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 [4,5] + 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 ## @@ -700,6 +704,62 @@ EOF 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 "" + 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 "" + 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 #------------------------------------------------------------ From 18186794165f47df4c8be61405fd30ac07cb2a53 Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Mon, 17 Nov 2008 19:11:54 +0000 Subject: [PATCH 246/381] Make the dates and times that the server deals with UTC. This is probably required for changeset, as they could auto close instantly when the clocks go forward by one hour. --- config/environment.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/environment.rb b/config/environment.rb index d2ab58ce4..2e5da44e3 100644 --- a/config/environment.rb +++ b/config/environment.rb @@ -91,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 From 96dfe22fb006ec421f6b0bf42ca71f365b1a1e95 Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Tue, 18 Nov 2008 16:21:32 +0000 Subject: [PATCH 247/381] Validations on the way tags, with unit tests --- app/models/node_tag.rb | 2 +- app/models/old_node_tag.rb | 2 +- app/models/old_way_tag.rb | 4 ++ app/models/way_tag.rb | 5 +++ test/unit/node_tag_test.rb | 4 +- test/unit/old_way_tag_test.rb | 76 +++++++++++++++++++++++++++++++++++ test/unit/way_tag_test.rb | 59 +++++++++++++++++++++++++++ 7 files changed, 148 insertions(+), 4 deletions(-) create mode 100644 test/unit/old_way_tag_test.rb diff --git a/app/models/node_tag.rb b/app/models/node_tag.rb index de3d0629c..6b21e4b01 100644 --- a/app/models/node_tag.rb +++ b/app/models/node_tag.rb @@ -4,5 +4,5 @@ class NodeTag < ActiveRecord::Base belongs_to :node, :foreign_key => 'id' validates_presence_of :id - validates_length_of :k, :v, :within => 0..255, :allow_blank => true + validates_length_of :k, :v, :maximum => 255, :allow_blank => true end diff --git a/app/models/old_node_tag.rb b/app/models/old_node_tag.rb index ef32a0613..392e3934e 100644 --- a/app/models/old_node_tag.rb +++ b/app/models/old_node_tag.rb @@ -4,6 +4,6 @@ class OldNodeTag < ActiveRecord::Base set_table_name 'node_tags' validates_presence_of :id, :version - validates_length_of :k, :v, :within => 0..255, :allow_blank => true + validates_length_of :k, :v, :maximum => 255, :allow_blank => true end diff --git a/app/models/old_way_tag.rb b/app/models/old_way_tag.rb index b02fd45b9..547fd177c 100644 --- a/app/models/old_way_tag.rb +++ b/app/models/old_way_tag.rb @@ -3,4 +3,8 @@ class OldWayTag < ActiveRecord::Base set_table_name 'way_tags' + 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 diff --git a/app/models/way_tag.rb b/app/models/way_tag.rb index 4548674d4..fa9b43361 100644 --- a/app/models/way_tag.rb +++ b/app/models/way_tag.rb @@ -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/test/unit/node_tag_test.rb b/test/unit/node_tag_test.rb index fe6ebb166..9d3da03b1 100644 --- a/test/unit/node_tag_test.rb +++ b/test/unit/node_tag_test.rb @@ -46,8 +46,8 @@ class NodeTagTest < Test::Unit::TestCase tag = NodeTag.new tag.id = current_node_tags(:t1).id tag.k = i - tag.v = "v", "Key should be too long" - assert !tag.valid? + tag.v = "v" + assert !tag.valid?, "Key should be too long" assert tag.errors.invalid?(:k) end end diff --git a/test/unit/old_way_tag_test.rb b/test/unit/old_way_tag_test.rb new file mode 100644 index 000000000..8210ef0c4 --- /dev/null +++ b/test/unit/old_way_tag_test.rb @@ -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/way_tag_test.rb b/test/unit/way_tag_test.rb index cd296749e..44b96ffe3 100644 --- a/test/unit/way_tag_test.rb +++ b/test/unit/way_tag_test.rb @@ -8,4 +8,63 @@ class WayTagTest < Test::Unit::TestCase 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 + ["k"*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" + 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_uniquess + 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 From b7b2b502cf2b119208e3c1f79e4a4e38cc0edcc5 Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Tue, 18 Nov 2008 18:50:24 +0000 Subject: [PATCH 248/381] Relation Tag testing. Also sort the belong_to/has_many for user/changeset/old_way. --- app/models/old_way_tag.rb | 2 +- app/models/relation_tag.rb | 4 ++ app/models/user.rb | 1 + test/unit/old_relation_tag_test.rb | 76 ++++++++++++++++++++++++++++++ test/unit/relation_tag_test.rb | 60 +++++++++++++++++++++++ test/unit/way_tag_test.rb | 4 +- 6 files changed, 144 insertions(+), 3 deletions(-) create mode 100644 test/unit/old_relation_tag_test.rb diff --git a/app/models/old_way_tag.rb b/app/models/old_way_tag.rb index 547fd177c..68003cbeb 100644 --- a/app/models/old_way_tag.rb +++ b/app/models/old_way_tag.rb @@ -1,5 +1,5 @@ class OldWayTag < ActiveRecord::Base - belongs_to :user + belongs_to :old_way set_table_name 'way_tags' diff --git a/app/models/relation_tag.rb b/app/models/relation_tag.rb index 939165ebd..812b2ec35 100644 --- a/app/models/relation_tag.rb +++ b/app/models/relation_tag.rb @@ -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 diff --git a/app/models/user.rb b/app/models/user.rb index 80faf68e9..ecf41fd18 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -9,6 +9,7 @@ class User < ActiveRecord::Base 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' diff --git a/test/unit/old_relation_tag_test.rb b/test/unit/old_relation_tag_test.rb new file mode 100644 index 000000000..309e2fe80 --- /dev/null +++ b/test/unit/old_relation_tag_test.rb @@ -0,0 +1,76 @@ +require File.dirname(__FILE__) + '/../test_helper' + +class RelationTagTest < Test::Unit::TestCase + fixtures :relation_tags + set_fixture_class :relation_tags => OldRelationTag + + def test_tag_count + assert_equal 3, OldRlationTag.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/relation_tag_test.rb b/test/unit/relation_tag_test.rb index 38c8af22b..f93e689ca 100644 --- a/test/unit/relation_tag_test.rb +++ b/test/unit/relation_tag_test.rb @@ -8,4 +8,64 @@ class RelationTagTest < Test::Unit::TestCase 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/way_tag_test.rb b/test/unit/way_tag_test.rb index 44b96ffe3..fc1c8efa5 100644 --- a/test/unit/way_tag_test.rb +++ b/test/unit/way_tag_test.rb @@ -42,7 +42,7 @@ class WayTagTest < Test::Unit::TestCase end def test_length_value_invalid - ["k"*256].each do |i| + ["v"*256].each do |i| tag = WayTag.new tag.id = current_way_tags(:t1).id tag.k = "k" @@ -57,7 +57,7 @@ class WayTagTest < Test::Unit::TestCase assert tag.errors.invalid?(:id) end - def test_uniquess + def test_uniquness tag = WayTag.new tag.id = current_way_tags(:t1).id tag.k = current_way_tags(:t1).k From 55d511bf21dfc4e1b7d0666a2a547b1d3c3845b0 Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Tue, 18 Nov 2008 20:22:09 +0000 Subject: [PATCH 249/381] Remember whether the user set the public checkbox on GPX upload as a user preference. Closes #827 --- app/controllers/trace_controller.rb | 17 +++++++++++++++++ app/models/user.rb | 4 ++++ 2 files changed, 21 insertions(+) diff --git a/app/controllers/trace_controller.rb b/app/controllers/trace_controller.rb index d94280a6a..06ae5dc3b 100644 --- a/app/controllers/trace_controller.rb +++ b/app/controllers/trace_controller.rb @@ -79,6 +79,12 @@ class TraceController < ApplicationController def mine if @user + @trace = Trace.new + unless @user.trace_public_default.nil? + @trace.public = true + else + @trace.public = false + end list(@user, "mine") unless @user.nil? else redirect_to :controller => 'user', :action => 'login', :referer => request.request_uri @@ -312,6 +318,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 diff --git a/app/models/user.rb b/app/models/user.rb index ecf41fd18..0eddb259d 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -106,4 +106,8 @@ class User < ActiveRecord::Base return false end + def trace_public_default + return self.preferences.find(:first, :conditions => {:k => "gps.trace.public", :v => "default"}) + end + end From ffbc4d5cb278c1afeafea5cfe904ec48d9f4d237 Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Wed, 19 Nov 2008 17:23:15 +0000 Subject: [PATCH 250/381] More tag validations, and finishing the node/way/relation tests. --- app/models/node_tag.rb | 2 ++ app/models/old_node_tag.rb | 7 ++++--- app/models/old_relation_tag.rb | 7 +++++++ app/models/old_way_tag.rb | 4 ++-- test/unit/node_tag_test.rb | 11 +++++++++++ test/unit/old_node_tag_test.rb | 12 ++++++++++++ test/unit/old_relation_tag_test.rb | 4 ++-- 7 files changed, 40 insertions(+), 7 deletions(-) diff --git a/app/models/node_tag.rb b/app/models/node_tag.rb index 6b21e4b01..494260112 100644 --- a/app/models/node_tag.rb +++ b/app/models/node_tag.rb @@ -5,4 +5,6 @@ class NodeTag < ActiveRecord::Base 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/models/old_node_tag.rb b/app/models/old_node_tag.rb index 392e3934e..3fd4bf86b 100644 --- a/app/models/old_node_tag.rb +++ b/app/models/old_node_tag.rb @@ -1,9 +1,10 @@ class OldNodeTag < ActiveRecord::Base - belongs_to :user - 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 diff --git a/app/models/old_relation_tag.rb b/app/models/old_relation_tag.rb index 7ce6f694e..0fcb11326 100644 --- a/app/models/old_relation_tag.rb +++ b/app/models/old_relation_tag.rb @@ -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 diff --git a/app/models/old_way_tag.rb b/app/models/old_way_tag.rb index 68003cbeb..801532dba 100644 --- a/app/models/old_way_tag.rb +++ b/app/models/old_way_tag.rb @@ -1,8 +1,8 @@ class OldWayTag < ActiveRecord::Base - belongs_to :old_way - 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] diff --git a/test/unit/node_tag_test.rb b/test/unit/node_tag_test.rb index 9d3da03b1..2ff9f9f00 100644 --- a/test/unit/node_tag_test.rb +++ b/test/unit/node_tag_test.rb @@ -68,4 +68,15 @@ class NodeTagTest < Test::Unit::TestCase 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 diff --git a/test/unit/old_node_tag_test.rb b/test/unit/old_node_tag_test.rb index 4a9bcbfcb..497184348 100644 --- a/test/unit/old_node_tag_test.rb +++ b/test/unit/old_node_tag_test.rb @@ -63,4 +63,16 @@ class OldNodeTest < Test::Unit::TestCase 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_relation_tag_test.rb b/test/unit/old_relation_tag_test.rb index 309e2fe80..d651810c0 100644 --- a/test/unit/old_relation_tag_test.rb +++ b/test/unit/old_relation_tag_test.rb @@ -1,11 +1,11 @@ require File.dirname(__FILE__) + '/../test_helper' -class RelationTagTest < Test::Unit::TestCase +class OldRelationTagTest < Test::Unit::TestCase fixtures :relation_tags set_fixture_class :relation_tags => OldRelationTag def test_tag_count - assert_equal 3, OldRlationTag.count + assert_equal 3, OldRelationTag.count end def test_length_key_valid From d6b4b8c8a180d0f54201c457e6b70dfdb2acf3e7 Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Wed, 19 Nov 2008 18:00:52 +0000 Subject: [PATCH 251/381] Changeset tag unit tests --- app/models/changeset_tag.rb | 5 ++- test/unit/changeset_tag_test.rb | 65 +++++++++++++++++++++++++++++++-- test/unit/way_tag_test.rb | 3 +- 3 files changed, 68 insertions(+), 5 deletions(-) diff --git a/app/models/changeset_tag.rb b/app/models/changeset_tag.rb index 6298fbe77..6a414a0fc 100644 --- a/app/models/changeset_tag.rb +++ b/app/models/changeset_tag.rb @@ -1,5 +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 diff --git a/test/unit/changeset_tag_test.rb b/test/unit/changeset_tag_test.rb index 466ee0405..e0201d538 100644 --- a/test/unit/changeset_tag_test.rb +++ b/test/unit/changeset_tag_test.rb @@ -2,10 +2,69 @@ require File.dirname(__FILE__) + '/../test_helper' class ChangesetTagTest < Test::Unit::TestCase fixtures :changeset_tags - - - def test_changeset_tags_count + + 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/way_tag_test.rb b/test/unit/way_tag_test.rb index fc1c8efa5..b1a7d2256 100644 --- a/test/unit/way_tag_test.rb +++ b/test/unit/way_tag_test.rb @@ -48,6 +48,7 @@ class WayTagTest < Test::Unit::TestCase tag.k = "k" tag.v = i assert !tag.valid?, "Value #{i} should be too long" + assert tag.errors.invalid?(:v) end end @@ -57,7 +58,7 @@ class WayTagTest < Test::Unit::TestCase assert tag.errors.invalid?(:id) end - def test_uniquness + def test_uniqueness tag = WayTag.new tag.id = current_way_tags(:t1).id tag.k = current_way_tags(:t1).k From e2c0e51e4545a7e373aad3650048e0a50497e108 Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Thu, 20 Nov 2008 21:23:44 +0000 Subject: [PATCH 252/381] Closes #1350 through updating amf_controller to use the newer closed_at method of determining whether a changeset is closed. Richard please check the rest of the amf controller for more things that may reference the open field that is no longer available. Use is_open? to determine if a changeset is open. Fix indentation in amf controller. Fix minor bug in the links to changesets in the data brower. --- app/controllers/amf_controller.rb | 845 ++++++++++++------------ app/controllers/changeset_controller.rb | 2 +- app/models/changeset.rb | 6 +- app/views/browse/_common_details.rhtml | 2 +- 4 files changed, 429 insertions(+), 426 deletions(-) diff --git a/app/controllers/amf_controller.rb b/app/controllers/amf_controller.rb index 6e1b364d4..589005d3c 100644 --- a/app/controllers/amf_controller.rb +++ b/app/controllers/amf_controller.rb @@ -45,83 +45,84 @@ 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 + 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 + # Parse request - headers=AMF.getint(req) # Read number of 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) # | - header["name"]=value # | - end + 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) + 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 + 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 + 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 + 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) + 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 - 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)) - when 'putpoi'; results[index]=AMF.putdata(index,putpoi(*args)) - when 'startchangeset'; results[index]=AMF.putdata(index,startchangeset(*args)) - end - end + case message + when 'putway'; + r=putway(renumberednodes,*args) + renumberednodes=r[3] + if r[1] != r[2] + 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)) + when 'putpoi'; results[index]=AMF.putdata(index,putpoi(*args)) + when 'startchangeset'; results[index]=AMF.putdata(index,startchangeset(*args)) + end + end sendresponse(results) end @@ -130,35 +131,37 @@ class AmfController < ApplicationController # Start new changeset def startchangeset(usertoken, cstags, closeid, closecomment) - uid = getuserid(usertoken) - if !uid then return -1,"You are not logged in, so Potlatch can't write any changes to the database." end + uid = getuserid(usertoken) + if !uid 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.open = false - if closecomment.empty? - cs.save! - else - cs.tags['comment']=closecomment - cs.save_with_tags! - end - 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 - cs.created_at = Time.now - cs.save_with_tags! - return [0,cs.id] + # 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 @@ -166,8 +169,8 @@ class AmfController < ApplicationController # are IDs only. def whichways(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 # check boundary is sane and area within defined # see /config/application.yml @@ -177,37 +180,37 @@ class AmfController < ApplicationController return [-2,"Sorry - I can't get the map for that area."] end - 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.collect { |node| - node.ways.collect { |w| [w.id,w.version] }.flatten - }.uniq - ways.delete([]) + 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.collect { |node| + node.ways.collect { |w| [w.id,w.version] }.flatten + }.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] } + # 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] } - # find the relations used by those nodes and ways - relations = Relation.find_for_nodes(nodes_in_area.collect { |n| n.id }, :conditions => {:visible => true}) + + # 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 + relations = relations.collect { |relation| [relation.id,relation.version] }.uniq + end - [0,ways, points, relations] + [0,ways, points, relations] 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 # check boundary is sane and area within defined # see /config/application.yml @@ -217,43 +220,43 @@ class AmfController < ApplicationController return [-2,"Sorry - I can't get the map for that area."] end - 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 + 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] + [0,way_ids] end # Get a way including nodes and 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) - 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,[],{}] + 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 - # 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] + [wayid, points, tags, version] end # Get an old version of a way, and all constituent nodes. @@ -272,20 +275,20 @@ class AmfController < ApplicationController # 5. is this the current, visible version? (boolean) def getway_old(id, version) #:doc: - 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 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)] + 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 @@ -294,14 +297,14 @@ class AmfController < ApplicationController def getway_history(wayid) #:doc: begin - history = Way.find(wayid).old_ways.reverse.collect do |old_way| + 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 + 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] + return ['way',wayid,history] rescue ActiveRecord::RecordNotFound return ['way', wayid, []] end @@ -314,12 +317,12 @@ class AmfController < ApplicationController begin 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 + 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] + return ['node',nodeid,history] rescue ActiveRecord::RecordNotFound return ['node', nodeid, []] end @@ -329,21 +332,21 @@ class AmfController < ApplicationController # 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 + 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]) + 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 + gpxs end # Get a relation with all tags and members. @@ -355,33 +358,33 @@ class AmfController < ApplicationController def getrelation(relid) #:doc: begin - rel = Relation.find(relid) + 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] + [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. @@ -391,50 +394,50 @@ class AmfController < ApplicationController # 2. new relation id. def putrelation(renumberednodes, renumberedways, usertoken, changeset, 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 + 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 != 0) + relid = relid.to_i + visible = (visible.to_i != 0) - # create a new relation, or find the existing one - if relid <= 0 - rel = Relation.new - rel.version = 0 - else - rel = Relation.find(relid) - end + # create a new relation, or find the existing one + if relid <= 0 + rel = Relation.new + rel.version = 0 + 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 + # 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 + typedmembers << [m[0], mid, m[2]] + end + end - # assign new contents - rel.members = typedmembers - rel.tags = tags - rel.visible = visible - rel.changeset_id = changeset + # assign new contents + rel.members = typedmembers + rel.tags = tags + rel.visible = visible + rel.changeset_id = changeset - # 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. + # 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 + #if !rel.preconditions_ok? + # return -2, "Relation preconditions failed" + #else + rel.save_with_history! + #end - [0, relid, rel.id] + [0, relid, rel.id] end # Save a way to the database, including all nodes. Any nodes in the previous @@ -449,98 +452,98 @@ class AmfController < ApplicationController 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 + 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 + 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 + 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 + if points.length < 2 then return -2,"Server error - way is only #{points.length} points long." end - # -- Get unique nodes + # -- Get unique nodes - if originalway < 0 - way = Way.new - way.version = 0 # otherwise +=1 breaks - uniques = [] - else - way = Way.find(originalway) - uniques = way.unshared_node_ids - end + if originalway < 0 + way = Way.new + way.version = 0 # otherwise +=1 breaks + uniques = [] + else + way = Way.find(originalway) + uniques = way.unshared_node_ids + end - # -- Compare nodes and save changes to any that have changed + # -- Compare nodes and save changes to any that have changed - nodes = [] + nodes = [] - points.each do |n| - lon = n[0].to_f - lat = n[1].to_f - id = n[2].to_i - savenode = false + 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 - node.version = 0 # otherwise +=1 breaks - savenode = true - else - 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 renumberednodes[id] + id = renumberednodes[id] + elsif id < 0 + # Create new node + node = Node.new + node.version = 0 # otherwise +=1 breaks + savenode = true + else + 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 - node.changeset_id = changeset - node.lat = lat + if savenode + node.changeset_id = changeset + node.lat = lat node.lon = lon - node.tags = n[4] - node.visible = true - node.save_with_history! + node.tags = n[4] + node.visible = true + node.save_with_history! - if id != node.id - renumberednodes[id] = node.id - id = node.id - end - end + if id != node.id + renumberednodes[id] = node.id + id = node.id + end + end - uniques = uniques - [id] - nodes.push(id) - end + uniques = uniques - [id] + nodes.push(id) + end - # -- Delete any unique nodes + # -- Delete any unique nodes - uniques.each do |n| - deleteitemrelations(n, 'node') + uniques.each do |n| + deleteitemrelations(n, 'node') - node = Node.find(n) - node.changeset_id = changeset - node.visible = false - node.save_with_history! - end + node = Node.find(n) + node.changeset_id = changeset + node.visible = false + node.save_with_history! + end - # -- Save revised way + # -- Save revised way - if way.tags!=attributes or way.nds!=nodes or !way.visible? - way.tags = attributes - way.nds = nodes - way.changeset_id = changeset - way.visible = true - way.save_with_history! - end + if way.tags!=attributes or way.nds!=nodes or !way.visible? + way.tags = attributes + way.nds = nodes + way.changeset_id = changeset + way.visible = true + way.save_with_history! + end - [0, originalway, way.id, renumberednodes, way.version] + [0, originalway, way.id, renumberednodes, way.version] end # Save POI to the database. @@ -552,32 +555,32 @@ class AmfController < ApplicationController # 3. version. def putpoi(usertoken, changeset, 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 + 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) + id = id.to_i + visible = (visible.to_i == 1) - if id > 0 then - node = Node.find(id) + 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 - node.version = 0 - end + 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 + node.version = 0 + end - node.changeset_id = changeset - node.lat = lat - node.lon = lon - node.tags = tags - node.visible = visible - node.save_with_history! + node.changeset_id = changeset + node.lat = lat + node.lon = lon + node.tags = tags + node.visible = visible + node.save_with_history! - [0, id, node.id, node.version] + [0, id, node.id, node.version] end # Read POI from database @@ -586,38 +589,38 @@ class AmfController < ApplicationController # 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 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 + 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. # Returns 0 (success), unchanged way id. def deleteway(usertoken, changeset_id, way_id) #:doc: - if !getuserid(usertoken) then return -1,"You are not logged in, so the way could not be deleted." end + if !getuserid(usertoken) then return -1,"You are not logged in, so the way could not be deleted." end - way_id = way_id.to_i + way_id = way_id.to_i - # FIXME: would be good not to make two history entries when removing - # two nodes from the same relation - way = Way.find(way_id) - way.unshared_node_ids.each do |n| - deleteitemrelations(n, 'node') - end - deleteitemrelations(way_id, 'way') + # FIXME: would be good not to make two history entries when removing + # two nodes from the same relation + 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(changeset_id.to_i) + way.delete_with_relations_and_nodes_and_history(changeset_id.to_i) - [0, way_id] + [0, way_id] end @@ -627,59 +630,59 @@ class AmfController < ApplicationController # Remove a node or way from all relations def deleteitemrelations(objid, type) #:doc: - relations = RelationMember.find(:all, + 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 } + rel.save_with_history! + 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 # (can also be of form user:pass) def getuserid(token) #:doc: - if (token =~ /^(.+)\:(.+)$/) then - user = User.authenticate(:username => $1, :password => $2) - else - user = User.authenticate(:token => token) - end + if (token =~ /^(.+)\:(.+)$/) then + user = User.authenticate(:username => $1, :password => $2) + else + user = User.authenticate(:token => token) + end - return user ? user.id : nil; + 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| - # ** 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 - } + 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 @@ -687,88 +690,88 @@ class AmfController < ApplicationController # Alternative SQL queries for getway/whichways 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] } + 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) - pois=[] - sql=<<-EOF + 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 + 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 - 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 + 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,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] } + # ** 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 - 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=TRUE - ORDER BY sequence_id + ORDER BY sequence_id EOF - 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 + 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) @@ -776,7 +779,3 @@ class AmfController < ApplicationController end end -# Local Variables: -# indent-tabs-mode: t -# tab-width: 4 -# End: diff --git a/app/controllers/changeset_controller.rb b/app/controllers/changeset_controller.rb index 181c827b3..ca19fba30 100644 --- a/app/controllers/changeset_controller.rb +++ b/app/controllers/changeset_controller.rb @@ -56,7 +56,7 @@ class ChangesetController < ApplicationController # 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.closed_at = DateTime.now + changeset.set_closed_time_now changeset.save! render :nothing => true diff --git a/app/models/changeset.rb b/app/models/changeset.rb index f67d2118d..a65e3aebc 100644 --- a/app/models/changeset.rb +++ b/app/models/changeset.rb @@ -25,7 +25,7 @@ class Changeset < ActiveRecord::Base MAX_TIME_OPEN = 1 # idle timeout increment, one hour as a rational number of days. - IDLE_TIMEOUT = Rational(1,24) + IDLE_TIMEOUT = 1.hour #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 @@ -39,6 +39,10 @@ class Changeset < ActiveRecord::Base return ((closed_at > DateTime.now) and (num_changes <= MAX_ELEMENTS)) end + def set_closed_time_now + closed_at = DateTime.now + end + def self.from_xml(xml, create=false) begin p = XML::Parser.new diff --git a/app/views/browse/_common_details.rhtml b/app/views/browse/_common_details.rhtml index 1f9f9ffe6..71a9dd314 100644 --- a/app/views/browse/_common_details.rhtml +++ b/app/views/browse/_common_details.rhtml @@ -17,7 +17,7 @@ In changeset: - <%= link_to common_details.changeset_id, :action => :changeset %> + <%= link_to common_details.changeset_id, :action => :changeset, :id => common_details.changeset_id %> <% unless common_details.tags_as_hash.empty? %> From 783528830ad1b0d5f07c8e758747841a0b4e5cc6 Mon Sep 17 00:00:00 2001 From: Matt Amos Date: Fri, 21 Nov 2008 12:53:09 +0000 Subject: [PATCH 253/381] Fixed bug in changeset idle timeout. Fixed another with a spurious require. --- app/controllers/changeset_controller.rb | 1 - app/models/changeset.rb | 7 +- lib/consistency_validations.rb | 4 +- lib/osm.rb | 8 ++- test/functional/changeset_controller_test.rb | 75 +++++++++++++++++++- 5 files changed, 88 insertions(+), 7 deletions(-) diff --git a/app/controllers/changeset_controller.rb b/app/controllers/changeset_controller.rb index ca19fba30..5e538c721 100644 --- a/app/controllers/changeset_controller.rb +++ b/app/controllers/changeset_controller.rb @@ -2,7 +2,6 @@ class ChangesetController < ApplicationController require 'xml/libxml' - require 'diff_reader' before_filter :authorize, :only => [:create, :update, :delete, :upload, :include, :close] before_filter :check_write_availability, :only => [:create, :update, :delete, :upload, :include] diff --git a/app/models/changeset.rb b/app/models/changeset.rb index a65e3aebc..38cd8014f 100644 --- a/app/models/changeset.rb +++ b/app/models/changeset.rb @@ -25,7 +25,10 @@ class Changeset < ActiveRecord::Base MAX_TIME_OPEN = 1 # idle timeout increment, one hour as a rational number of days. - IDLE_TIMEOUT = 1.hour #Rational(1,24) + # 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 @@ -219,7 +222,7 @@ class Changeset < ActiveRecord::Base # can't change a closed changeset unless is_open? - raise OSM::APIChangesetAlreadyClosedError + raise OSM::APIChangesetAlreadyClosedError.new(self) end # copy the other's tags diff --git a/lib/consistency_validations.rb b/lib/consistency_validations.rb index 6e214f902..46fb3c06e 100644 --- a/lib/consistency_validations.rb +++ b/lib/consistency_validations.rb @@ -13,7 +13,7 @@ module ConsistencyValidations elsif new.changeset.user_id != user.id raise OSM::APIUserChangesetMismatchError.new elsif not new.changeset.is_open? - raise OSM::APIChangesetAlreadyClosedError.new + raise OSM::APIChangesetAlreadyClosedError.new(new.changeset) end end @@ -24,7 +24,7 @@ module ConsistencyValidations elsif new.changeset.user_id != user.id raise OSM::APIUserChangesetMismatchError.new elsif not new.changeset.is_open? - raise OSM::APIChangesetAlreadyClosedError.new + raise OSM::APIChangesetAlreadyClosedError.new(new.changeset) end end end diff --git a/lib/osm.rb b/lib/osm.rb index 00215c677..09ded2bd2 100644 --- a/lib/osm.rb +++ b/lib/osm.rb @@ -45,8 +45,14 @@ module OSM # 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 supplied changeset has already been closed", :status => :conflict } + { :text => "The changeset #{@changeset.id} was closed at #{@changeset.closed_at}.", :status => :conflict } end end diff --git a/test/functional/changeset_controller_test.rb b/test/functional/changeset_controller_test.rb index 3d7531cfb..47cd95c24 100644 --- a/test/functional/changeset_controller_test.rb +++ b/test/functional/changeset_controller_test.rb @@ -26,7 +26,19 @@ class ChangesetControllerTest < ActionController::TestCase put :create assert_response :success, "Creation of changeset did not return sucess status" - newid = @response.body + 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 @@ -455,6 +467,67 @@ EOF 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 "" + + "" + + "" + put :create + assert_response :success + changeset_id = @response.body.to_i + + diff = < + + + + + + + + + + + + + + + + + + + + + + + + + +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. From 152cb13a02dfcb2966b8a9b29382f84a99a23a69 Mon Sep 17 00:00:00 2001 From: Harry Wood Date: Mon, 24 Nov 2008 00:40:04 +0000 Subject: [PATCH 254/381] cosmetic tweaks to diary interface. icon for new entries link --- app/views/diary_entry/edit.rhtml | 14 ++++++++----- app/views/diary_entry/list.rhtml | 33 ++++++++++++++++++++----------- public/images/new.png | Bin 0 -> 991 bytes public/stylesheets/site.css | 6 ++++++ 4 files changed, 37 insertions(+), 16 deletions(-) create mode 100644 public/images/new.png diff --git a/app/views/diary_entry/edit.rhtml b/app/views/diary_entry/edit.rhtml index 87f5e9fac..a69f4fd9c 100644 --- a/app/views/diary_entry/edit.rhtml +++ b/app/views/diary_entry/edit.rhtml @@ -5,24 +5,28 @@ <% form_for :diary_entry do |f| %> - + - + - + - - + +
      SubjectSubject: <%= f.text_field :title, :size => 60 %>
      BodyBody: <%= f.text_area :body, :cols => 80 %>
      LocationLocation: Latitude: <%= f.text_field :latitude, :size => 20, :id => "latitude" %> Longitude: <%= f.text_field :longitude, :size => 20, :id => "longitude" %> use map +

      <%= submit_tag 'Save' %> + <%= submit_tag 'Save' %> + <%# TODO: button should say 'publish' or 'save changes' depending on new/edit state %> +
      <% end %> diff --git a/app/views/diary_entry/list.rhtml b/app/views/diary_entry/list.rhtml index 7a2ccf74d..9852313bb 100644 --- a/app/views/diary_entry/list.rhtml +++ b/app/views/diary_entry/list.rhtml @@ -4,29 +4,40 @@ <%= image_tag url_for_file_column(@this_user, "image") %> <% end %> -
      <% 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 %> -

      Recent diary entries:

      -<%= render :partial => 'diary_entry', :collection => @entries %> +<% if @entries.empty? %> +

      No diary 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 %> -| +<% else %> + +

      Recent diary entries:

      +
      + <%= 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 %> + +
      + <% end %> -<%= link_to "Newer Entries", { :page => @entry_pages.current.previous } if @entry_pages.current.previous %> - -
      <%= rss_link_to :action => 'rss' %> <%= auto_discovery_link_tag :atom, :action => 'rss' %> + + +
      +
      \ No newline at end of file diff --git a/public/images/new.png b/public/images/new.png new file mode 100644 index 0000000000000000000000000000000000000000..202e10eca8aff2839d598e91428029a7f44d6d73 GIT binary patch literal 991 zcmV<510ei~P)Px#1ZP1_K>z@;j|==^1poj5AY({UO#lFTCIA3{ga82g0001h=l}q9FaQARU;qF* zm;eA5aGbhPJOBUy32;bRa{vGfh5!Hyh5N2bPDNB8b~7$BH8Ae8p8x;@5lKWrR5(w4lv_v?Q5eTn zv$(|r31e`GqZDk=l`8KGs#K+X`w()T2d0$(=8jQ zPmSK1yhuL_)Q8hECJ`Tgv!iV<(LwaaxZS1hee}4pZ)m9a*_Y3Sy7BRX;qmeOVUa&x z9ts>fOKe{=KNF078(W^6b7Ovfo^TP9kX-j5d72>!MVQkcgJ?bjBX`>75)StS{n>>i z+MJlAmk9!a0F%&LfHEd9?Kqx+)mLi?dv2-9uX~*mB11( ziR_ml%Rk_Clwd*Xz@Oj;EH8;9VQmSEp-%|QFCot!;6j1yB9dezNF*98N0Vsl2_<}% zTr4jOtAbd!h>%c@fM@sDP#~+4M44+OUayzdLLx@t^LE3>pTI9~S0q8KErll`JB}jg z+s`CrB-s|1NRh#wND{TSfvBN0sG?=yo!)Z0ok63Ab$xVu?#lDDR|sRdML#~k|f6^PTmQ(ON=Jb zG!ZO~il`#Wsk{P`o+mD2YN`~R{@j)LlZibYyWIv+;2{W3IGr{)9acE(li;0$5cxj1 zgbw<@6E5)yMn?M3eWxjF&1P%A)z-@-Jnx7kY1T9*&_Kua5iI6W*sRld_x>$tTda|! zw6$v=Sgo_Gv(Lh2v%qRKVb)@V+04Og(!*$&hJl-g$;4rB=nb1BCoxHywzav>V9>*E zPZStvFb9)y21X+j{G@f5qT?Bo=y0Q~G?yePF;S|frRna(#P=1BL>$MMiKD04_4)+p zeBf38?`tK+d5MzbYDLA#W=+HW`o`L)sygM18dY__qNZw~T3PYBN>QO>&^sUcSvS*QR2 N002ovPDHLkV1lVBuzCOh literal 0 HcmV?d00001 diff --git a/public/stylesheets/site.css b/public/stylesheets/site.css index 2bfe21d24..764d8971b 100644 --- a/public/stylesheets/site.css +++ b/public/stylesheets/site.css @@ -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; From 0ff1214f86ac1347bb257abfda70581cc78903dd Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Mon, 24 Nov 2008 18:55:24 +0000 Subject: [PATCH 255/381] Make the role in relations optional, with a test to make sure it is. Also start moving the errors reading the xml to exceptions, thus making it possible to give meaningful error messages, when bad xml is sent (More work is required on this including doing the same for nodes and ways). With the latest gems update it seems that the lib xml handling was broken, using the newer method. Adding the content type for the exceptions. --- app/controllers/relation_controller.rb | 10 ++-- app/models/relation.rb | 18 +++++-- config/environment.rb | 2 +- config/initializers/libxml.rb | 6 ++- lib/osm.rb | 38 +++++++------ test/functional/relation_controller_test.rb | 59 ++++++++++++++++++++- 6 files changed, 106 insertions(+), 27 deletions(-) diff --git a/app/controllers/relation_controller.rb b/app/controllers/relation_controller.rb index cdd1d34d6..575cca419 100644 --- a/app/controllers/relation_controller.rb +++ b/app/controllers/relation_controller.rb @@ -12,12 +12,14 @@ class RelationController < ApplicationController if request.put? relation = Relation.from_xml(request.raw_post, true) - if relation + # 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 :nothing => true, :status => :bad_request - end + #else + # render :text => "Couldn't get turn the input into a relation.", :status => :bad_request + #end else render :nothing => true, :status => :method_not_allowed end diff --git a/app/models/relation.rb b/app/models/relation.rb index 2607e7f2f..2a2ec3dca 100644 --- a/app/models/relation.rb +++ b/app/models/relation.rb @@ -15,6 +15,8 @@ class Relation < 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 + TYPES = ["node", "way", "relation"] + def self.from_xml(xml, create=false) begin p = XML::Parser.new @@ -22,10 +24,11 @@ class Relation < ActiveRecord::Base doc = p.parse doc.find('//osm/relation').each do |pt| - return Relation.from_xml_node(pt, create) + return Relation.from_xml_node(pt, create) end - rescue - return nil + rescue LibXML::XML::Error => ex + #return nil + raise OSM::APIBadXMLError.new("relation", xml, ex.message) end end @@ -53,8 +56,17 @@ class Relation < ActiveRecord::Base 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 diff --git a/config/environment.rb b/config/environment.rb index 2e5da44e3..ffed548bd 100644 --- a/config/environment.rb +++ b/config/environment.rb @@ -46,7 +46,7 @@ Rails::Initializer.run do |config| # 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.8.3', :lib => 'libxml' + config.gem 'libxml-ruby', :version => '0.9.4', :lib => 'libxml' config.gem 'rmagick', :lib => 'RMagick' config.gem 'mysql' diff --git a/config/initializers/libxml.rb b/config/initializers/libxml.rb index 3b5919f0f..ae636a9a3 100644 --- a/config/initializers/libxml.rb +++ b/config/initializers/libxml.rb @@ -1,5 +1,9 @@ # This is required otherwise libxml writes out memory errors to # the standard output and exits uncleanly -LibXML::XML::Parser.register_error_handler do |message| +# 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 diff --git a/lib/osm.rb b/lib/osm.rb index 09ded2bd2..f6646503d 100644 --- a/lib/osm.rb +++ b/lib/osm.rb @@ -11,35 +11,39 @@ module OSM # The base class for API Errors. class APIError < RuntimeError def render_opts - { :text => "", :status => :internal_server_error } + { :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 - { :nothing => true, :status => :not_found } + { :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 => "", :status => :precondition_failed } + { :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 => "", :status => :gone } + { :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 } + { :text => "The user doesn't own that changeset", :status => :conflict, :content_type => "text/plain" } end end @@ -52,14 +56,14 @@ module OSM attr_reader :changeset def render_opts - { :text => "The changeset #{@changeset.id} was closed at #{@changeset.closed_at}.", :status => :conflict } + { :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 } + { :text => "You need to supply a changeset to be able to make a change", :status => :conflict, :content_type => "text/plain" } end end @@ -72,7 +76,7 @@ module OSM def render_opts { :text => "Changeset mismatch: Provided #{@provided} but only " + - "#{@allowed} is allowed.", :status => :conflict } + "#{@allowed} is allowed.", :status => :conflict, :content_type => "text/plain" } end end @@ -85,20 +89,20 @@ module OSM def render_opts { :text => "Unknown action #{@provided}, choices are create, modify, delete.", - :status => :bad_request } + :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) - @model, @xml = model, xml + def initialize(model, xml, message="") + @model, @xml, @message = model, xml, message end def render_opts - { :text => "Cannot parse valid #{@model} from xml string #{@xml}", - :status => :bad_request } + { :text => "Cannot parse valid #{@model} from xml string #{@xml}. #{@message}", + :status => :bad_request, :content_type => "text/plain" } end end @@ -113,7 +117,7 @@ module OSM def render_opts { :text => "Version mismatch: Provided " + provided.to_s + ", server had: " + latest.to_s + " of " + type + " " + id.to_s, - :status => :conflict } + :status => :conflict, :content_type => "text/plain" } end end @@ -128,7 +132,7 @@ module OSM def render_opts { :text => "Element #{@type}/#{@id} has duplicate tags with key #{@tag_key}.", - :status => :bad_request } + :status => :bad_request, :content_type => "text/plain" } end end @@ -143,7 +147,7 @@ module OSM def render_opts { :text => "You tried to add #{provided} nodes to the way, however only #{max} are allowed", - :status => :bad_request } + :status => :bad_request, :content_type => "text/plain" } end end @@ -155,7 +159,7 @@ module OSM end def render_opts - { :text => message, :mime_type => "text/plain", :status => :bad_request } + { :text => @message, :content_type => "text/plain", :status => :bad_request } end end diff --git a/test/functional/relation_controller_test.rb b/test/functional/relation_controller_test.rb index d44490036..b8d15e529 100644 --- a/test/functional/relation_controller_test.rb +++ b/test/functional/relation_controller_test.rb @@ -117,10 +117,45 @@ class RelationControllerTest < ActionController::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 "" + - "" + + "" + + "" + 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 "" + + ""+ "" put :create # hope for success @@ -147,6 +182,7 @@ class RelationControllerTest < ActionController::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 @@ -200,6 +236,27 @@ class RelationControllerTest < ActionController::TestCase "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 "" + + "" + + "" + 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. # ------------------------------------- From ecefee485b8dc033d020bd6b67d9ae4559ec796a Mon Sep 17 00:00:00 2001 From: Richard Fairhurst Date: Tue, 25 Nov 2008 00:15:30 +0000 Subject: [PATCH 256/381] various fixes/enhancements, including a couple to the 0.5 version --- app/controllers/amf_controller.rb | 15 +++++++-------- config/potlatch/autocomplete.txt | 3 ++- config/potlatch/colours.txt | 1 + config/potlatch/presets.txt | 12 ++++++++---- public/potlatch/potlatch.swf | Bin 168667 -> 169553 bytes 5 files changed, 18 insertions(+), 13 deletions(-) diff --git a/app/controllers/amf_controller.rb b/app/controllers/amf_controller.rb index 589005d3c..43bec41a1 100644 --- a/app/controllers/amf_controller.rb +++ b/app/controllers/amf_controller.rb @@ -110,16 +110,15 @@ class AmfController < ApplicationController 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 - results[index]=AMF.putdata(index,r) + when 'putway'; r=putway(renumberednodes,*args) + renumberednodes=r[3] + 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)) - when 'putpoi'; results[index]=AMF.putdata(index,putpoi(*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 diff --git a/config/potlatch/autocomplete.txt b/config/potlatch/autocomplete.txt index 86b6c62b8..ad6cb0154 100755 --- a/config/potlatch/autocomplete.txt +++ b/config/potlatch/autocomplete.txt @@ -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 diff --git a/config/potlatch/colours.txt b/config/potlatch/colours.txt index c54247cb1..ec1c2de03 100644 --- a/config/potlatch/colours.txt +++ b/config/potlatch/colours.txt @@ -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 - - diff --git a/config/potlatch/presets.txt b/config/potlatch/presets.txt index 1917b5a6c..c46f9d12a 100644 --- a/config/potlatch/presets.txt +++ b/config/potlatch/presets.txt @@ -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= diff --git a/public/potlatch/potlatch.swf b/public/potlatch/potlatch.swf index cf17a3091e4ad07362976700026183defa1f7548..3a1b8c4d107ceb7f4a3bd6a4f74317112c1a552f 100755 GIT binary patch literal 169553 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 V?OfmKoI~Ji&AI0-;)GMZ{uh1awQK+Y literal 168667 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 z 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�Y|cnX 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 From 137509534304be853ab01ca554111c524a9a1bca Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Tue, 25 Nov 2008 17:23:47 +0000 Subject: [PATCH 257/381] More model validations. Fixing one test. --- app/models/changeset.rb | 11 +++++++++-- app/models/node.rb | 14 +++++++++----- app/models/old_node.rb | 1 + app/models/old_relation.rb | 2 ++ app/models/old_way.rb | 2 ++ app/models/relation.rb | 8 ++++++++ app/models/way.rb | 11 ++++++++--- test/unit/node_test.rb | 2 +- 8 files changed, 40 insertions(+), 11 deletions(-) diff --git a/app/models/changeset.rb b/app/models/changeset.rb index 38cd8014f..5baee2d59 100644 --- a/app/models/changeset.rb +++ b/app/models/changeset.rb @@ -12,8 +12,15 @@ class Changeset < ActiveRecord::Base has_many :old_ways has_many :old_relations - validates_presence_of :user_id, :created_at, :closed_at - + 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, :greater_than_or_equal_to => 1 + 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 diff --git a/app/models/node.rb b/app/models/node.rb index e926e06a2..f2fe341ae 100644 --- a/app/models/node.rb +++ b/app/models/node.rb @@ -6,11 +6,6 @@ class Node < ActiveRecord::Base set_table_name 'current_nodes' - validates_presence_of :changeset_id, :timestamp - validates_inclusion_of :visible, :in => [ true, false ] - validates_numericality_of :latitude, :longitude - validate :validate_position - belongs_to :changeset has_many :old_nodes, :foreign_key => :id @@ -26,6 +21,15 @@ class Node < 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 :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? diff --git a/app/models/old_node.rb b/app/models/old_node.rb index badcd74a2..be115c53e 100644 --- a/app/models/old_node.rb +++ b/app/models/old_node.rb @@ -8,6 +8,7 @@ class OldNode < ActiveRecord::Base validates_inclusion_of :visible, :in => [ true, false ] validates_numericality_of :latitude, :longitude validate :validate_position + validates_associated :changeset belongs_to :changeset diff --git a/app/models/old_relation.rb b/app/models/old_relation.rb index ffddc7945..e2a650511 100644 --- a/app/models/old_relation.rb +++ b/app/models/old_relation.rb @@ -4,6 +4,8 @@ class OldRelation < ActiveRecord::Base set_table_name 'relations' belongs_to :changeset + + validates_associated :changeset def self.from_relation(relation) old_relation = OldRelation.new diff --git a/app/models/old_way.rb b/app/models/old_way.rb index ce856e208..da9cf0697 100644 --- a/app/models/old_way.rb +++ b/app/models/old_way.rb @@ -5,6 +5,8 @@ class OldWay < ActiveRecord::Base belongs_to :changeset + validates_associated :changeset + def self.from_way(way) old_way = OldWay.new old_way.visible = way.visible diff --git a/app/models/relation.rb b/app/models/relation.rb index 2a2ec3dca..19548f20c 100644 --- a/app/models/relation.rb +++ b/app/models/relation.rb @@ -15,6 +15,14 @@ class Relation < 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 :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) diff --git a/app/models/way.rb b/app/models/way.rb index 3b3f92432..e2e1ae302 100644 --- a/app/models/way.rb +++ b/app/models/way.rb @@ -4,9 +4,6 @@ class Way < ActiveRecord::Base include ConsistencyValidations set_table_name 'current_ways' - - validates_presence_of :changeset_id, :timestamp - validates_inclusion_of :visible, :in => [ true, false ] belongs_to :changeset @@ -20,6 +17,14 @@ 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 diff --git a/test/unit/node_test.rb b/test/unit/node_test.rb index 2c6515cb7..a2c8d7fb6 100644 --- a/test/unit/node_test.rb +++ b/test/unit/node_test.rb @@ -79,7 +79,7 @@ class NodeTest < Test::Unit::TestCase def test_create node_template = Node.new(:latitude => 12.3456, :longitude => 65.4321, - :changeset_id => changesets(:normal_user_first_change), + :changeset_id => changesets(:normal_user_first_change).id, :visible => 1, :version => 1) assert node_template.save_with_history! From 8348493ccb02fd0bb3f46733d8176b503f9f9883 Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Tue, 25 Nov 2008 18:59:35 +0000 Subject: [PATCH 258/381] Some improvements to the error messages that are returned by the api. --- app/controllers/changeset_controller.rb | 1 + app/controllers/relation_controller.rb | 8 ++++---- app/models/relation.rb | 19 ++++++++++--------- test/functional/relation_controller_test.rb | 3 ++- 4 files changed, 17 insertions(+), 14 deletions(-) diff --git a/app/controllers/changeset_controller.rb b/app/controllers/changeset_controller.rb index 5e538c721..d7764d995 100644 --- a/app/controllers/changeset_controller.rb +++ b/app/controllers/changeset_controller.rb @@ -294,6 +294,7 @@ class ChangesetController < ApplicationController render ex.render_opts end +private #------------------------------------------------------------ # utility functions below. #------------------------------------------------------------ diff --git a/app/controllers/relation_controller.rb b/app/controllers/relation_controller.rb index 575cca419..93573b95f 100644 --- a/app/controllers/relation_controller.rb +++ b/app/controllers/relation_controller.rb @@ -64,7 +64,6 @@ class RelationController < ApplicationController end def delete -#XXX check if member somewhere! begin relation = Relation.find(params[:id]) new_relation = Relation.from_xml(request.raw_post) @@ -143,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 @@ -167,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 diff --git a/app/models/relation.rb b/app/models/relation.rb index 19548f20c..0159c60b8 100644 --- a/app/models/relation.rb +++ b/app/models/relation.rb @@ -35,7 +35,6 @@ class Relation < ActiveRecord::Base return Relation.from_xml_node(pt, create) end rescue LibXML::XML::Error => ex - #return nil raise OSM::APIBadXMLError.new("relation", xml, ex.message) end end @@ -47,16 +46,18 @@ class Relation < ActiveRecord::Base relation.id = pt['id'].to_i end - relation.version = pt['version'] + raise OSM::APIBadXMLError.new("relation", pt, "You are missing the required changeset in the relation") if pt['changeset'].nil? relation.changeset_id = pt['changeset'] if create relation.timestamp = Time.now relation.visible = true + relation.version = 0 else if pt['timestamp'] relation.timestamp = Time.parse(pt['timestamp']) end + relation.version = pt['version'] end pt.find('tag').each do |tag| @@ -348,15 +349,15 @@ class Relation < ActiveRecord::Base def delete_with_history!(new_relation, user) if self.visible 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 - else - self.changeset_id = new_relation.changeset_id - self.tags = {} - self.members = [] - self.visible = false - save_with_history! + 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! else raise OSM::APIAlreadyDeletedError.new end diff --git a/test/functional/relation_controller_test.rb b/test/functional/relation_controller_test.rb index b8d15e529..4ace316a4 100644 --- a/test/functional/relation_controller_test.rb +++ b/test/functional/relation_controller_test.rb @@ -276,7 +276,8 @@ class RelationControllerTest < ActionController::TestCase # try to delete without specifying a changeset content "" delete :delete, :id => current_relations(:visible_relation).id - assert_response :conflict + 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, From a4e5e8437f8c1836c6389f7aa49cc3884f3a7cf7 Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Wed, 26 Nov 2008 11:36:10 +0000 Subject: [PATCH 259/381] Renaming the include action to expand_bbox in the changeset controller. --- app/controllers/changeset_controller.rb | 2 +- config/routes.rb | 2 +- test/functional/changeset_controller_test.rb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/controllers/changeset_controller.rb b/app/controllers/changeset_controller.rb index d7764d995..fad797cdc 100644 --- a/app/controllers/changeset_controller.rb +++ b/app/controllers/changeset_controller.rb @@ -70,7 +70,7 @@ class ChangesetController < ApplicationController # 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 include + def expand_bbox # only allow POST requests, because although this method is # idempotent, there is no "document" to PUT really... if request.post? diff --git a/config/routes.rb b/config/routes.rb index 88fa0a551..b99dfd2ca 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -6,7 +6,7 @@ ActionController::Routing::Routes.draw do |map| 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/include", :controller => 'changeset', :action => 'include', :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+/ diff --git a/test/functional/changeset_controller_test.rb b/test/functional/changeset_controller_test.rb index 47cd95c24..2ff6bee33 100644 --- a/test/functional/changeset_controller_test.rb +++ b/test/functional/changeset_controller_test.rb @@ -851,7 +851,7 @@ EOF # call the include method and assert properties of the bbox def check_after_include(changeset_id, lon, lat, bbox) content "" - post :include, :id => changeset_id + post :expand_bbox, :id => changeset_id assert_response :success, "Setting include of changeset failed: #{@response.body}" # check exactly one changeset From 1ffb5c1502d870a1d1c46648ea0c165e25dfbef9 Mon Sep 17 00:00:00 2001 From: Matt Amos Date: Wed, 26 Nov 2008 12:56:42 +0000 Subject: [PATCH 260/381] Moved changeset consistency checks to library code. --- app/controllers/changeset_controller.rb | 27 +++++++++---------------- lib/consistency_validations.rb | 15 ++++++++++++++ 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/app/controllers/changeset_controller.rb b/app/controllers/changeset_controller.rb index fad797cdc..58bcd1020 100644 --- a/app/controllers/changeset_controller.rb +++ b/app/controllers/changeset_controller.rb @@ -11,6 +11,9 @@ class ChangesetController < ApplicationController # 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? @@ -46,12 +49,9 @@ class ChangesetController < ApplicationController return end - changeset = Changeset.find(params[:id]) - - unless @user.id == changeset.user_id - raise OSM::APIUserChangesetMismatchError - 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. @@ -75,12 +75,7 @@ class ChangesetController < ApplicationController # idempotent, there is no "document" to PUT really... if request.post? cs = Changeset.find(params[:id]) - - # check user credentials - only the user who opened a changeset - # may alter it. - unless @user.id == cs.user_id - raise OSM::APIUserChangesetMismatchError - end + check_changeset_consistency(cs, @user) # keep an array of lons and lats lon = Array.new @@ -142,12 +137,7 @@ class ChangesetController < ApplicationController end changeset = Changeset.find(params[:id]) - - # access control - only the user who created a changeset may - # upload to it. - unless @user.id == changeset.user_id - raise OSM::APIUserChangesetMismatchError - end + check_changeset_consistency(changeset, @user) diff_reader = DiffReader.new(request.raw_post, changeset) Changeset.transaction do @@ -281,6 +271,7 @@ class ChangesetController < ApplicationController 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 diff --git a/lib/consistency_validations.rb b/lib/consistency_validations.rb index 46fb3c06e..4f3881542 100644 --- a/lib/consistency_validations.rb +++ b/lib/consistency_validations.rb @@ -27,4 +27,19 @@ module ConsistencyValidations 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 From 299b6715221fe2e6d31af5f5711656c8f6e40da0 Mon Sep 17 00:00:00 2001 From: Matt Amos Date: Wed, 26 Nov 2008 14:49:56 +0000 Subject: [PATCH 261/381] Moved transaction boundary to cover used-by tests on deletion so that the database can help prevent race conditions. --- app/models/node.rb | 15 ++++++++++----- app/models/relation.rb | 11 ++++++++--- app/models/way.rb | 13 +++++++++---- 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/app/models/node.rb b/app/models/node.rb index f2fe341ae..e1ad818dd 100644 --- a/app/models/node.rb +++ b/app/models/node.rb @@ -155,7 +155,14 @@ class Node < ActiveRecord::Base # Should probably be renamed delete_from to come in line with update def delete_with_history!(new_node, user) - if self.visible + 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 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 @@ -164,14 +171,12 @@ class Node < ActiveRecord::Base 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 - else - raise OSM::APIAlreadyDeletedError.new end end diff --git a/app/models/relation.rb b/app/models/relation.rb index 0159c60b8..0bdacb5ec 100644 --- a/app/models/relation.rb +++ b/app/models/relation.rb @@ -347,7 +347,14 @@ class Relation < ActiveRecord::Base end def delete_with_history!(new_relation, user) - if self.visible + 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 ]) @@ -358,8 +365,6 @@ class Relation < ActiveRecord::Base self.members = [] self.visible = false save_with_history! - else - raise OSM::APIAlreadyDeletedError.new end end diff --git a/app/models/way.rb b/app/models/way.rb index e2e1ae302..2071fd559 100644 --- a/app/models/way.rb +++ b/app/models/way.rb @@ -283,8 +283,15 @@ class Way < ActiveRecord::Base end def delete_with_history!(new_way, user) - check_consistency(self, new_way, user) - if self.visible + 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 = ? AND member_type='way' and member_id=? ", true, self.id]) raise OSM::APIPreconditionFailedError @@ -295,8 +302,6 @@ class Way < ActiveRecord::Base self.visible = false self.save_with_history! end - else - raise OSM::APIAlreadyDeletedError end end From 89bd962ff0d7d96bda9f8148a0103224752fbd2e Mon Sep 17 00:00:00 2001 From: Matt Amos Date: Wed, 26 Nov 2008 15:03:50 +0000 Subject: [PATCH 262/381] Adding test to check that users can't update changesets that they don't own. --- test/functional/changeset_controller_test.rb | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/test/functional/changeset_controller_test.rb b/test/functional/changeset_controller_test.rb index 2ff6bee33..59c92e19b 100644 --- a/test/functional/changeset_controller_test.rb +++ b/test/functional/changeset_controller_test.rb @@ -741,16 +741,25 @@ EOF ## # check updating tags on a changeset def test_changeset_update - basic_authorization "test@openstreetmap.org", "test" - 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 From 2ed84e26cdeaaa20b8f607624d65b7891fcf1fed Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Thu, 27 Nov 2008 17:06:43 +0000 Subject: [PATCH 263/381] Adding pagination to the changesets, thus allowing you to browe large changesets, without MySQL complaining about memory. Probably needs some fine tuning. You currently can only page one of nodes, ways, and relations, without manually setting the relations. Fixing deprection notice for the classic_pagination plugin. --- app/controllers/browse_controller.rb | 3 ++ app/helpers/browse_helper.rb | 3 ++ app/views/browse/_changeset_details.rhtml | 35 +++++++++++++------ app/views/browse/_paging_nav.rhtml | 15 ++++++++ .../classic_pagination/lib/pagination.rb | 4 +-- 5 files changed, 47 insertions(+), 13 deletions(-) create mode 100644 app/views/browse/_paging_nav.rhtml diff --git a/app/controllers/browse_controller.rb b/app/controllers/browse_controller.rb index 2c6c3dc5f..60f580963 100644 --- a/app/controllers/browse_controller.rb +++ b/app/controllers/browse_controller.rb @@ -116,6 +116,9 @@ class BrowseController < ApplicationController 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 }] ) diff --git a/app/helpers/browse_helper.rb b/app/helpers/browse_helper.rb index c86ad5b71..34302a8af 100644 --- a/app/helpers/browse_helper.rb +++ b/app/helpers/browse_helper.rb @@ -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/views/browse/_changeset_details.rhtml b/app/views/browse/_changeset_details.rhtml index 27335dd88..765fe4778 100644 --- a/app/views/browse/_changeset_details.rhtml +++ b/app/views/browse/_changeset_details.rhtml @@ -5,6 +5,11 @@ <%= h(changeset_details.created_at) %> + + Closed at: + <%= h(changeset_details.closed_at) %> + + <% if changeset_details.user.data_public? %> Belongs to: @@ -21,27 +26,33 @@ - <% end %> + <% else %> + + Tags + There are no tags for this changeset + + <% end %> - <% unless changeset_details.old_nodes.empty? %> + <% unless @nodes.empty? %> - Has the following nodes: + Has the following <%= @node_pages.item_count %> nodes: - <% changeset_details.old_nodes.each do |node| %> + <% @nodes.each do |node| %> <% end %>
      <%= link_to "Node #{node.id.to_s}, version #{node.version.to_s}", :action => "node", :id => node.id.to_s %>
      + <%= render :partial => 'paging_nav', :locals => { :pages => @node_pages, :page_param => "node_page"} %> <% end %> - <% unless changeset_details.old_ways.empty? %> + <% unless @ways.empty? %> - Has the following ways: + Has the following <%= @way_pages.item_count %> ways: - <% changeset_details.old_ways.each do |way| %> + <% @ways.each do |way| %> <% end %> <%= @@ -49,20 +60,22 @@ %>
      <%= link_to "Way #{way.id.to_s}, version #{way.version.to_s}", :action => "way", :id => way.id.to_s %>
      - + + <%= render :partial => 'paging_nav', :locals => { :pages => @way_pages, :page_param => "way_page" } %> <% end %> - <% unless changeset_details.old_relations.empty? %> + <% unless @relations.empty? %> - Has the following relations: + Has the following <%= @relation_pages.item_count %> relations: - <% changeset_details.old_relations.each do |relation| %> + <% @relations.each do |relation| %> <% end %>
      <%= link_to "Relation #{relation.id.to_s}, version #{relation.version.to_s}", :action => "relation", :id => relation.id.to_s %>
      + <%= render :partial => 'paging_nav', :locals => { :pages => @relation_pages, :page_param => "relation_page" } %> <% end %> diff --git a/app/views/browse/_paging_nav.rhtml b/app/views/browse/_paging_nav.rhtml new file mode 100644 index 000000000..fcfbb05dc --- /dev/null +++ b/app/views/browse/_paging_nav.rhtml @@ -0,0 +1,15 @@ + +<% 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 %> + + diff --git a/vendor/plugins/classic_pagination/lib/pagination.rb b/vendor/plugins/classic_pagination/lib/pagination.rb index b6e9cf4bc..6a3e1a97b 100644 --- a/vendor/plugins/classic_pagination/lib/pagination.rb +++ b/vendor/plugins/classic_pagination/lib/pagination.rb @@ -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 From c3785ff278f605fc6c98ac0bdcd3dc3c79de05a4 Mon Sep 17 00:00:00 2001 From: Matt Amos Date: Fri, 28 Nov 2008 12:33:18 +0000 Subject: [PATCH 264/381] Changed duplicate tags test to test the correct code path - was previously failing in libxml, rather than the duplicate tags check. --- test/functional/way_controller_test.rb | 30 ++++++-------------------- 1 file changed, 7 insertions(+), 23 deletions(-) diff --git a/test/functional/way_controller_test.rb b/test/functional/way_controller_test.rb index be4c41a39..fcd1557d1 100644 --- a/test/functional/way_controller_test.rb +++ b/test/functional/way_controller_test.rb @@ -249,10 +249,6 @@ class WayControllerTest < ActionController::TestCase # 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. - # - # NOTE: I'm not sure this test is working correctly, as a lot of the tag - # keys seem to come out as "addr��housenumber". It might be something to - # do with Ruby's unicode handling...? def test_invalid_duplicate_tags # setup auth basic_authorization(users(:normal_user).email, "test") @@ -260,26 +256,14 @@ class WayControllerTest < ActionController::TestCase # add the tag into the existing xml way_str = "" way_str << "" + way_str << "" + way_str << ""; - # all of these keys have the same unicode decoding, but are binary - # not equal. libxml should make these identical as it decodes the - # XML document... - [ "addr\xc0\xbahousenumber", - "addr\xe0\x80\xbahousenumber", - "addr\xf0\x80\x80\xbahousenumber" ].each do |key| - # copy the XML doc to add the tags - way_str_copy = way_str.clone - - # add all new tags to the way - way_str_copy << "" - way_str_copy << ""; - - # try and upload it - content way_str_copy - put :create - assert_response :bad_request, - "adding new duplicate tags to a way should fail with 'bad request'" - end + # 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'" end ## From 6e98e324e58f67b62c6abf343d60ae5e6f22f9eb Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Fri, 28 Nov 2008 17:38:23 +0000 Subject: [PATCH 265/381] better handling of duplicate tags. Extra validation in the tests. --- app/models/node.rb | 6 +++--- app/models/relation.rb | 2 +- app/models/way.rb | 6 +++--- test/functional/node_controller_test.rb | 3 ++- test/functional/way_controller_test.rb | 3 +++ 5 files changed, 12 insertions(+), 8 deletions(-) diff --git a/app/models/node.rb b/app/models/node.rb index e1ad818dd..b2650b61d 100644 --- a/app/models/node.rb +++ b/app/models/node.rb @@ -71,8 +71,8 @@ class Node < ActiveRecord::Base doc.find('//osm/node').each do |pt| return Node.from_xml_node(pt, create) end - rescue - return nil + rescue LibXML::XML::Error => ex + raise OSM::APIBadXMLError.new("node", xml, ex.message) end end @@ -274,7 +274,7 @@ class Node < ActiveRecord::Base # duplicate tags are now forbidden, so we can't allow values # in the hash to be overwritten. - raise OSM::APIDuplicateTagsError.new if @tags.include? k + raise OSM::APIDuplicateTagsError.new("node", self.id, k) if @tags.include? k @tags[k] = v end diff --git a/app/models/relation.rb b/app/models/relation.rb index 0bdacb5ec..ba27e9d7d 100644 --- a/app/models/relation.rb +++ b/app/models/relation.rb @@ -209,7 +209,7 @@ class Relation < ActiveRecord::Base # duplicate tags are now forbidden, so we can't allow values # in the hash to be overwritten. - raise OSM::APIDuplicateTagsError.new if @tags.include? k + raise OSM::APIDuplicateTagsError.new("relation", self.id, k) if @tags.include? k @tags[k] = v end diff --git a/app/models/way.rb b/app/models/way.rb index 2071fd559..d4bca19aa 100644 --- a/app/models/way.rb +++ b/app/models/way.rb @@ -34,8 +34,8 @@ class Way < ActiveRecord::Base doc.find('//osm/way').each do |pt| return Way.from_xml_node(pt, create) end - rescue - return nil + rescue LibXML::XML::Error => ex + raise OSM::APIBadXMLError.new("relation", xml, ex.message) end end @@ -182,7 +182,7 @@ class Way < ActiveRecord::Base # duplicate tags are now forbidden, so we can't allow values # in the hash to be overwritten. - raise OSM::APIDuplicateTagsError.new if @tags.include? k + raise OSM::APIDuplicateTagsError.new("way", self.id, k) if @tags.include? k @tags[k] = v end diff --git a/test/functional/node_controller_test.rb b/test/functional/node_controller_test.rb index 9e5621f8b..9d7f48ca4 100644 --- a/test/functional/node_controller_test.rb +++ b/test/functional/node_controller_test.rb @@ -194,7 +194,8 @@ class NodeControllerTest < ActionController::TestCase 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'" + "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 diff --git a/test/functional/way_controller_test.rb b/test/functional/way_controller_test.rb index fcd1557d1..40ac0bd71 100644 --- a/test/functional/way_controller_test.rb +++ b/test/functional/way_controller_test.rb @@ -219,6 +219,7 @@ class WayControllerTest < ActionController::TestCase 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 ## @@ -243,6 +244,7 @@ class WayControllerTest < ActionController::TestCase 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 ## @@ -264,6 +266,7 @@ class WayControllerTest < ActionController::TestCase 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 ## From e989b1a880f79fd1835a44d16de115a778f890af Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Mon, 1 Dec 2008 18:32:54 +0000 Subject: [PATCH 266/381] ensure that uploads that don't supply a lat and lon for a node. Adding related test and fixing other tests. --- app/models/node.rb | 4 ++- app/models/way.rb | 2 +- test/functional/changeset_controller_test.rb | 2 +- test/functional/node_controller_test.rb | 28 +++++++++++++++++++- 4 files changed, 32 insertions(+), 4 deletions(-) diff --git a/app/models/node.rb b/app/models/node.rb index b2650b61d..e90c32950 100644 --- a/app/models/node.rb +++ b/app/models/node.rb @@ -79,11 +79,13 @@ class Node < ActiveRecord::Base 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 node.changeset_id = pt['changeset'].to_i - return nil unless node.in_world? + raise OSM::APIBadUserInput.new("The node is outside this world") unless node.in_world? # version must be present unless creating return nil unless create or not pt['version'].nil? diff --git a/app/models/way.rb b/app/models/way.rb index d4bca19aa..d6aa12af1 100644 --- a/app/models/way.rb +++ b/app/models/way.rb @@ -35,7 +35,7 @@ class Way < ActiveRecord::Base return Way.from_xml_node(pt, create) end rescue LibXML::XML::Error => ex - raise OSM::APIBadXMLError.new("relation", xml, ex.message) + raise OSM::APIBadXMLError.new("way", xml, ex.message) end end diff --git a/test/functional/changeset_controller_test.rb b/test/functional/changeset_controller_test.rb index 59c92e19b..e8648e5c3 100644 --- a/test/functional/changeset_controller_test.rb +++ b/test/functional/changeset_controller_test.rb @@ -310,7 +310,7 @@ EOF - + diff --git a/test/functional/node_controller_test.rb b/test/functional/node_controller_test.rb index 9d7f48ca4..2289953fe 100644 --- a/test/functional/node_controller_test.rb +++ b/test/functional/node_controller_test.rb @@ -6,7 +6,7 @@ class NodeControllerTest < ActionController::TestCase def test_create # cannot read password from fixture as it is stored as MD5 digest - basic_authorization(users(:normal_user).email, "test"); + basic_authorization(users(:normal_user).email, "test") # create a node with random lat/lon lat = rand(100)-50 + rand @@ -30,6 +30,32 @@ class NodeControllerTest < ActionController::TestCase 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("") + 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 . lat missing', @response.body + + # test that the upload is rejected when no lon is supplied + # create a minimal xml file + content("") + 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 . lon missing', @response.body + + end + def test_read # check that a visible node is returned properly get :read, :id => current_nodes(:visible_node).id From bbbf2569865bad3198c05587083e670bdba8b592 Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Mon, 1 Dec 2008 18:47:03 +0000 Subject: [PATCH 267/381] Actually set the instance var when closing the changeset --- app/models/changeset.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/changeset.rb b/app/models/changeset.rb index 5baee2d59..9dc60de48 100644 --- a/app/models/changeset.rb +++ b/app/models/changeset.rb @@ -50,7 +50,7 @@ class Changeset < ActiveRecord::Base end def set_closed_time_now - closed_at = DateTime.now + self.closed_at = DateTime.now end def self.from_xml(xml, create=false) From 073c866032becb668c2b7c3b7883b7601f282c0b Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Tue, 2 Dec 2008 18:44:50 +0000 Subject: [PATCH 268/381] adding link to download the full changeset xml --- app/controllers/changeset_controller.rb | 3 +++ app/views/browse/changeset.rhtml | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/controllers/changeset_controller.rb b/app/controllers/changeset_controller.rb index 58bcd1020..ff956f2ac 100644 --- a/app/controllers/changeset_controller.rb +++ b/app/controllers/changeset_controller.rb @@ -31,6 +31,9 @@ class ChangesetController < ApplicationController 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]) diff --git a/app/views/browse/changeset.rhtml b/app/views/browse/changeset.rhtml index 9345eb087..57e39db99 100644 --- a/app/views/browse/changeset.rhtml +++ b/app/views/browse/changeset.rhtml @@ -11,7 +11,8 @@ <%= render :partial => "changeset_details", :object => @changeset %>


      - <%= link_to "Download XML", :controller => "changeset", :action => "read" %> + <%= link_to "Download Changeset XML", :controller => "changeset", :action => "read" %> | + <%= link_to "Download osmChange XML", :controller => "changeset", :action => "download" %> From 1a7ab3570ce850d7f541e2c709ad98986c7b35ef Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Wed, 3 Dec 2008 12:56:02 +0000 Subject: [PATCH 269/381] Deal with active record not found exceptions when creating message. Some tidying of html. For some reason in rails 2.1.2 having a comment at the end of a line, in the view, means that the closing tag needs to be on the next line. --- app/controllers/message_controller.rb | 12 +++++++++--- app/views/message/_message_summary.rhtml | 9 +++++---- app/views/message/_sent_message_summary.rhtml | 9 +++++---- app/views/message/new.rhtml | 7 ++----- 4 files changed, 21 insertions(+), 16 deletions(-) diff --git a/app/controllers/message_controller.rb b/app/controllers/message_controller.rb index fc7a9101b..d2fa9bd5f 100644 --- a/app/controllers/message_controller.rb +++ b/app/controllers/message_controller.rb @@ -7,11 +7,13 @@ class MessageController < ApplicationController # 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 @@ -23,6 +25,8 @@ 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. @@ -33,7 +37,7 @@ 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 @@ -43,7 +47,7 @@ class MessageController < ApplicationController @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. @@ -82,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 diff --git a/app/views/message/_message_summary.rhtml b/app/views/message/_message_summary.rhtml index 6d45d33dd..263e30e64 100644 --- a/app/views/message/_message_summary.rhtml +++ b/app/views/message/_message_summary.rhtml @@ -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 +%> "> - <%= link_to h(message_summary.sender.display_name), :controller => 'user', :action => message_summary.sender.display_name %> - <%= link_to h(message_summary.title), :controller => 'message', :action => 'read', :message_id => message_summary.id %> - <%= message_summary.sent_on %> + <%= link_to h(message_summary.sender.display_name), :controller => 'user', :action => message_summary.sender.display_name %> + <%= link_to h(message_summary.title), :controller => 'message', :action => 'read', :message_id => message_summary.id %> + <%= message_summary.sent_on %> <% if message_summary.message_read? %> <%= button_to 'Mark as unread', :controller => 'message', :action => 'mark', :message_id => message_summary.id, :mark => 'unread' %> <% else %> diff --git a/app/views/message/_sent_message_summary.rhtml b/app/views/message/_sent_message_summary.rhtml index f0d87aa27..91fafe901 100644 --- a/app/views/message/_sent_message_summary.rhtml +++ b/app/views/message/_sent_message_summary.rhtml @@ -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 +%> - <%= link_to h(sent_message_summary.recipient.display_name), :controller => 'user', :action => sent_message_summary.recipient.display_name %> - <%= link_to h(sent_message_summary.title), :controller => 'message', :action => 'read', :message_id => sent_message_summary.id %> - <%= sent_message_summary.sent_on %> + <%= link_to h(sent_message_summary.recipient.display_name), :controller => 'user', :action => sent_message_summary.recipient.display_name %> + <%= link_to h(sent_message_summary.title), :controller => 'message', :action => 'read', :message_id => sent_message_summary.id %> + <%= sent_message_summary.sent_on %> diff --git a/app/views/message/new.rhtml b/app/views/message/new.rhtml index d7bb18f8e..17f3588bb 100644 --- a/app/views/message/new.rhtml +++ b/app/views/message/new.rhtml @@ -1,7 +1,4 @@ -<% user_id = params[:user_id] || @user_id %> -<% display_name = User.find_by_id(user_id).display_name %> - -

      Send a new message to <%= h(display_name) %>

      +

      Send a new message to <%= h(@to_user.display_name) %>

      <% if params[:display_name] %>

      Writing a new message to <%= h(params[:display_name]) %>

      @@ -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| %> From 40adafa69895980a23a1266102827f33baf5ad23 Mon Sep 17 00:00:00 2001 From: Matt Amos Date: Wed, 3 Dec 2008 18:24:00 +0000 Subject: [PATCH 270/381] Adding missing fixtures to test helper. --- test/test_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_helper.rb b/test/test_helper.rb index e38f74af5..ea6e91abe 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -28,7 +28,7 @@ class Test::Unit::TestCase # Load standard fixtures needed to test API methods def self.api_fixtures #print "setting up the api_fixtures" - fixtures :users, :changesets + fixtures :users, :changesets, :changeset_tags fixtures :current_nodes, :nodes set_fixture_class :current_nodes => Node From d9c6758ee6184692fd4219befe13adf6dfa6e32b Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Thu, 4 Dec 2008 14:36:21 +0000 Subject: [PATCH 271/381] turning the session off for the changeset controller, to come in line with the other api controllers --- app/controllers/changeset_controller.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/controllers/changeset_controller.rb b/app/controllers/changeset_controller.rb index ff956f2ac..f7f4dc9f0 100644 --- a/app/controllers/changeset_controller.rb +++ b/app/controllers/changeset_controller.rb @@ -3,6 +3,7 @@ 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] From e9527b363da441337ac8c44c9d3d3feada3eaa9c Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Thu, 4 Dec 2008 17:30:17 +0000 Subject: [PATCH 272/381] Refactoring the amf controller, and all models so that save_with_history! is now private. Everyone should use create_with_history, delete_with_history, and update_from instead. Also gets the amf controller to do version checks in the node/way/relation models. Needs checked. This breaks Potlatch until #1376 is fixed. --- app/controllers/amf_controller.rb | 169 ++++++++++++++--------- app/models/changeset.rb | 4 +- app/models/node.rb | 69 +++++----- app/models/relation.rb | 222 +++++++++++++++--------------- app/models/way.rb | 104 +++++++------- test/unit/node_test.rb | 7 +- 6 files changed, 314 insertions(+), 261 deletions(-) diff --git a/app/controllers/amf_controller.rb b/app/controllers/amf_controller.rb index 43bec41a1..e9bcb281b 100644 --- a/app/controllers/amf_controller.rb +++ b/app/controllers/amf_controller.rb @@ -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. @@ -130,8 +130,8 @@ class AmfController < ApplicationController # Start new changeset def startchangeset(usertoken, cstags, closeid, closecomment) - uid = getuserid(usertoken) - if !uid then return -1,"You are not logged in, so Potlatch can't write any changes to the database." end + 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 @@ -392,20 +392,19 @@ class AmfController < ApplicationController # 1. original relation id (unchanged), # 2. new relation id. - def putrelation(renumberednodes, renumberedways, usertoken, changeset, 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 + 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 - rel = Relation.new - rel.version = 0 - else - rel = Relation.find(relid) + 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 = [] @@ -421,22 +420,38 @@ class AmfController < ApplicationController end # assign new contents - rel.members = typedmembers - rel.tags = tags - rel.visible = visible - rel.changeset_id = changeset + new_relation.members = typedmembers + new_relation.tags = tags + new_relation.visible = visible + new_relation.changeset_id = changeset + new_relation.version = version - # 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] + 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 + return [-1, "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 @@ -453,8 +468,8 @@ class AmfController < ApplicationController # -- 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 + user = getuserid(usertoken) + if !user then return -1,"You are not logged in, so the way could not be saved." end originalway = originalway.to_i @@ -467,14 +482,13 @@ class AmfController < ApplicationController # -- Get unique nodes - if originalway < 0 - way = Way.new - way.version = 0 # otherwise +=1 breaks + 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 @@ -484,16 +498,19 @@ class AmfController < ApplicationController 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] - elsif id < 0 + elsif id <= 0 # Create new node - node = Node.new - node.version = 0 # otherwise +=1 breaks 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') @@ -504,12 +521,19 @@ class AmfController < ApplicationController end if savenode - node.changeset_id = changeset - node.lat = lat - node.lon = lon - node.tags = n[4] - node.visible = true - node.save_with_history! + 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 @@ -527,19 +551,21 @@ class AmfController < ApplicationController deleteitemrelations(n, 'node') node = Node.find(n) - node.changeset_id = changeset - node.visible = false - node.save_with_history! + 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? - way.tags = attributes - way.nds = nodes - way.changeset_id = changeset - way.visible = true - way.save_with_history! + 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] @@ -553,9 +579,9 @@ class AmfController < ApplicationController # 2. new node id, # 3. version. - def putpoi(usertoken, changeset, 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 + 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) @@ -567,19 +593,32 @@ class AmfController < ApplicationController 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 + new_node.visible = visible + 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 - node = Node.new - node.version = 0 + # We're deleting the node + node.delete_with_history!(new_node, user) end - node.changeset_id = changeset - node.lat = lat - node.lon = lon - node.tags = tags - node.visible = visible - node.save_with_history! - - [0, id, node.id, node.version] + if id <= 0 + return [0, id, new_node.id, new_node.version] + else + return [0, id, node.id, node.version] + end end # Read POI from database @@ -652,14 +691,20 @@ class AmfController < ApplicationController # Authenticate token # (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 getuserid(token) #:doc: + 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 diff --git a/app/models/changeset.rb b/app/models/changeset.rb index 9dc60de48..6bc553a78 100644 --- a/app/models/changeset.rb +++ b/app/models/changeset.rb @@ -50,7 +50,9 @@ class Changeset < ActiveRecord::Base end def set_closed_time_now - self.closed_at = DateTime.now + unless is_open? + self.closed_at = DateTime.now + end end def self.from_xml(xml, create=false) diff --git a/app/models/node.rb b/app/models/node.rb index e90c32950..5f4f7af6a 100644 --- a/app/models/node.rb +++ b/app/models/node.rb @@ -88,7 +88,7 @@ class Node < ActiveRecord::Base raise OSM::APIBadUserInput.new("The node is outside this world") unless node.in_world? # version must be present unless creating - return nil unless create or not pt['version'].nil? + 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 unless create @@ -119,42 +119,12 @@ class Node < ActiveRecord::Base end ## - # the bounding box around a node + # the bounding box around a node, which is used for determining the changeset's + # bounding box def bbox [ longitude, latitude, longitude, latitude ] end - 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 - end - # Should probably be renamed delete_from to come in line with update def delete_with_history!(new_node, user) unless self.visible @@ -294,5 +264,38 @@ class Node < ActiveRecord::Base 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 + end + end diff --git a/app/models/relation.rb b/app/models/relation.rb index ba27e9d7d..94ef9d42a 100644 --- a/app/models/relation.rb +++ b/app/models/relation.rb @@ -214,6 +214,118 @@ class Relation < ActiveRecord::Base @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 @@ -336,114 +448,4 @@ class Relation < ActiveRecord::Base end 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 - end diff --git a/app/models/way.rb b/app/models/way.rb index d6aa12af1..ac4461f8e 100644 --- a/app/models/way.rb +++ b/app/models/way.rb @@ -196,56 +196,6 @@ class Way < ActiveRecord::Base [ lons.min, lats.min, lons.max, lats.max ] end - 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 - def update_from(new_way, user) check_consistency(self, new_way, user) if !new_way.preconditions_ok? @@ -300,7 +250,7 @@ class Way < ActiveRecord::Base self.tags = [] self.nds = [] self.visible = false - self.save_with_history! + save_with_history! end end end @@ -358,4 +308,56 @@ class Way < ActiveRecord::Base 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 diff --git a/test/unit/node_test.rb b/test/unit/node_test.rb index a2c8d7fb6..13dea88da 100644 --- a/test/unit/node_test.rb +++ b/test/unit/node_test.rb @@ -82,7 +82,7 @@ class NodeTest < Test::Unit::TestCase :changeset_id => changesets(:normal_user_first_change).id, :visible => 1, :version => 1) - assert node_template.save_with_history! + assert node_template.create_with_history(users(:normal_user)) node = Node.find(node_template.id) assert_not_nil node @@ -114,7 +114,7 @@ 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! + assert node_template.update_from(old_node_template, users(:normal_user)) node = Node.find(node_template.id) assert_not_nil node @@ -145,8 +145,7 @@ class NodeTest < Test::Unit::TestCase 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 From ec97c63e5c9fa166f0dbd3b0f4bf515dcc747873 Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Fri, 5 Dec 2008 11:08:58 +0000 Subject: [PATCH 273/381] Fix for the refactoring commited yesterday, based on review by Dave Stubbs --- app/controllers/amf_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/amf_controller.rb b/app/controllers/amf_controller.rb index e9bcb281b..738aa443a 100644 --- a/app/controllers/amf_controller.rb +++ b/app/controllers/amf_controller.rb @@ -468,7 +468,7 @@ class AmfController < ApplicationController # -- Initialise and carry out checks - user = getuserid(usertoken) + 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 From c0b4c1b9059890e1fb683a9822b966d4efd2698c Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Mon, 8 Dec 2008 14:22:34 +0000 Subject: [PATCH 274/381] Adding some amf create poi tests. Minor bugfix tweaks to the amf controller --- app/controllers/amf_controller.rb | 6 +- app/models/node.rb | 1 + test/functional/amf_controller_test.rb | 113 +++++++++++++++++++++++++ 3 files changed, 118 insertions(+), 2 deletions(-) diff --git a/app/controllers/amf_controller.rb b/app/controllers/amf_controller.rb index 738aa443a..9c9f85e1d 100644 --- a/app/controllers/amf_controller.rb +++ b/app/controllers/amf_controller.rb @@ -506,7 +506,8 @@ class AmfController < ApplicationController if renumberednodes[id] id = renumberednodes[id] - elsif id <= 0 + end + if id <= 0 # Create new node savenode = true else @@ -573,7 +574,7 @@ class AmfController < ApplicationController # 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, @@ -597,6 +598,7 @@ class AmfController < ApplicationController # We always need a new node, based on the data that has been sent to us new_node = Node.new + new_node.id = id new_node.changeset_id = changeset new_node.version = version new_node.lat = lat diff --git a/app/models/node.rb b/app/models/node.rb index 5f4f7af6a..ce2bd5c74 100644 --- a/app/models/node.rb +++ b/app/models/node.rb @@ -173,6 +173,7 @@ class Node < ActiveRecord::Base def create_with_history(user) check_create_consistency(self, user) + self.id = nil self.version = 0 self.visible = true diff --git a/test/functional/amf_controller_test.rb b/test/functional/amf_controller_test.rb index 9221a293d..75060b4e2 100644 --- a/test/functional/amf_controller_test.rb +++ b/test/functional/amf_controller_test.rb @@ -239,6 +239,119 @@ class AmfControllerTest < ActionController::TestCase 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.count, "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.count, "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.count, "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.count, "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.count, "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.count, "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 From 2e41c354794b183f24e5c5745f8e408ad9a69bc8 Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Mon, 8 Dec 2008 15:30:19 +0000 Subject: [PATCH 275/381] Adding the ability to see the min/max lat/lon for a changeset in the data browser. Adding file for the nice message that should show when a user is missing in the message controller. --- app/views/browse/_changeset_details.rhtml | 19 +++++++++++++++++++ app/views/message/no_such_user.rhtml | 2 ++ 2 files changed, 21 insertions(+) create mode 100644 app/views/message/no_such_user.rhtml diff --git a/app/views/browse/_changeset_details.rhtml b/app/views/browse/_changeset_details.rhtml index 765fe4778..2b711611b 100644 --- a/app/views/browse/_changeset_details.rhtml +++ b/app/views/browse/_changeset_details.rhtml @@ -33,6 +33,25 @@ <% 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? %> + + + + <% else %> +
      Subject
      No bounding box has been stored for this changeset.
      + + + + + + + + + + +
      Max Latitude: <%= changeset_details.max_lat/GeoRecord::SCALE.to_f -%>
      Min Longitude: <%= changeset_details.min_lon/GeoRecord::SCALE.to_f -%>Max Longitude: <%= changeset_details.max_lon/GeoRecord::SCALE.to_f -%>
      Min Latitude: <%= changeset_details.min_lon/GeoRecord::SCALE.to_f -%>
      + <% end %> + <% unless @nodes.empty? %> Has the following <%= @node_pages.item_count %> nodes: diff --git a/app/views/message/no_such_user.rhtml b/app/views/message/no_such_user.rhtml new file mode 100644 index 000000000..c18733af6 --- /dev/null +++ b/app/views/message/no_such_user.rhtml @@ -0,0 +1,2 @@ +

      No such user or message

      +

      Sorry there is no user or message with that name or id

      From 59c69a642ff64111caf8ff4c1dd6e25e7d430b4e Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Mon, 8 Dec 2008 15:32:29 +0000 Subject: [PATCH 276/381] the user id may be negative, so that osmosis can create changesets for users that are anonymous, which it uses a negative id for, as a special id. --- app/models/changeset.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/changeset.rb b/app/models/changeset.rb index 6bc553a78..3e0ba9f8e 100644 --- a/app/models/changeset.rb +++ b/app/models/changeset.rb @@ -17,7 +17,7 @@ class Changeset < ActiveRecord::Base 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, :greater_than_or_equal_to => 1 + 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 From 67157f69f3b3fb3933207016b2d9566ababf1310 Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Mon, 8 Dec 2008 15:47:29 +0000 Subject: [PATCH 277/381] count -> size for compatibility with ruby 1.8.6 --- test/functional/amf_controller_test.rb | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test/functional/amf_controller_test.rb b/test/functional/amf_controller_test.rb index 75060b4e2..b15ae857a 100644 --- a/test/functional/amf_controller_test.rb +++ b/test/functional/amf_controller_test.rb @@ -15,7 +15,7 @@ class AmfControllerTest < ActionController::TestCase amf_content "getway", "/1", [id] post :amf_read assert_response :success - amf_parse_response + amf_parse_response assert_equal amf_result("/1")[0], id end @@ -299,15 +299,15 @@ class AmfControllerTest < ActionController::TestCase 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.count, "There seems to be a tag that has been added to the node" + 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.count, "There should only be one historic node created" + 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.count, "There seems to be a tag that have been attached to this node" + 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" #### @@ -338,16 +338,16 @@ class AmfControllerTest < ActionController::TestCase 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.count, "There seems to be a tag that has been added to the node" + 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.count, "There should only be one historic node created" + 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.count, "There seems to be a tag that have been attached to this node" + 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" From b7335940e21bfe4fc9ac688130687398e677cfae Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Tue, 9 Dec 2008 18:44:11 +0000 Subject: [PATCH 278/381] Some improvements for the XML parsing, as there are somethings that we don't need, like timestamp and visibility, thus hopefully bringing a speedup in the xml parsing. Some more AMF controller fixes, plenty more to go though. --- app/controllers/amf_controller.rb | 64 +++++++++++++++++++++++-------- app/models/node.rb | 12 ++---- app/models/relation.rb | 2 + app/models/way.rb | 8 +++- 4 files changed, 62 insertions(+), 24 deletions(-) diff --git a/app/controllers/amf_controller.rb b/app/controllers/amf_controller.rb index 9c9f85e1d..39de7e671 100644 --- a/app/controllers/amf_controller.rb +++ b/app/controllers/amf_controller.rb @@ -446,7 +446,10 @@ class AmfController < ApplicationController rescue OSM::APIChangesetAlreadyClosedError => ex return [-1, "The changeset #{ex.changeset.id} was closed at #{ex.changeset.closed_at}"] rescue OSM::APIVersionMismatchError => ex - return [-1, "You have taken too long to edit, please reload the area"] + # 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 @@ -570,6 +573,18 @@ class AmfController < ApplicationController 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::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. @@ -621,6 +636,18 @@ class AmfController < ApplicationController 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 @@ -645,21 +672,23 @@ class AmfController < ApplicationController # Delete way and all constituent nodes. Also removes from any relations. # Returns 0 (success), unchanged way id. - def deleteway(usertoken, changeset_id, way_id) #:doc: - if !getuserid(usertoken) then return -1,"You are not logged in, so the way could not be deleted." end + def deleteway(usertoken, changeset_id, way_id, version_id) #:doc: + user = getuser(usertoken) + if 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 - 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') - # FIXME: would be good not to make two history entries when removing - # two nodes from the same relation - way = Way.find(way_id) - way.unshared_node_ids.each do |n| - deleteitemrelations(n, 'node') + way.delete_with_relations_and_nodes_and_history(changeset_id.to_i) end - deleteitemrelations(way_id, 'way') - - way.delete_with_relations_and_nodes_and_history(changeset_id.to_i) - [0, way_id] end @@ -668,7 +697,7 @@ class AmfController < ApplicationController # Support functions # Remove a node or way from all relations - + # FIXME needs version, changeset, and user def deleteitemrelations(objid, type) #:doc: relations = RelationMember.find(:all, :conditions => ['member_type = ? and member_id = ?', type, objid], @@ -676,7 +705,12 @@ class AmfController < ApplicationController relations.each do |rel| rel.members.delete_if { |x| x[0] == type and x[1] == objid } - rel.save_with_history! + # FIXME need to create the new node/way based on the type. + 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 diff --git a/app/models/node.rb b/app/models/node.rb index ce2bd5c74..f2ad3a78a 100644 --- a/app/models/node.rb +++ b/app/models/node.rb @@ -83,6 +83,7 @@ class Node < ActiveRecord::Base 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 raise OSM::APIBadUserInput.new("The node is outside this world") unless node.in_world? @@ -99,15 +100,10 @@ class Node < ActiveRecord::Base # visible if it says it is, or as the default if the attribute # is missing. - node.visible = pt['visible'].nil? or pt['visible'] == 'true' + # 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' - if create - node.timestamp = Time.now - else - if pt['timestamp'] - node.timestamp = Time.parse(pt['timestamp']) - end - end + # We don't care about the time, as it is explicitly set on create/update/delete tags = [] diff --git a/app/models/relation.rb b/app/models/relation.rb index 94ef9d42a..6be106159 100644 --- a/app/models/relation.rb +++ b/app/models/relation.rb @@ -49,6 +49,8 @@ class Relation < ActiveRecord::Base 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 diff --git a/app/models/way.rb b/app/models/way.rb index ac4461f8e..92b69ed7d 100644 --- a/app/models/way.rb +++ b/app/models/way.rb @@ -47,8 +47,10 @@ class Way < ActiveRecord::Base 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 @@ -244,7 +246,7 @@ class Way < ActiveRecord::Base 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 = ? AND member_type='way' and member_id=? ", true, self.id]) - raise OSM::APIPreconditionFailedError + raise OSM::APIPreconditionFailedError.new("You need to make sure that this way is not a member of a relation.") else self.changeset_id = new_way.changeset_id self.tags = [] @@ -259,12 +261,15 @@ class Way < ActiveRecord::Base # FIXME: merge the potlatch code to delete the relations # and refactor to use delete_with_history! + # This really needs the ids and versions of the nodes/relations to be passed in too + # so that we can do the version checking before the delete def delete_with_relations_and_nodes_and_history(changeset_id) # delete the nodes not used by other ways self.unshared_node_ids.each do |node_id| n = Node.find(node_id) n.changeset_id = changeset_id n.visible = false + # FIXME next line is bad n.save_with_history! end @@ -272,6 +277,7 @@ class Way < ActiveRecord::Base self.tags = [] self.nds = [] self.visible = false + # FIXME next line is bad self.save_with_history! end From 3ed9ac21b506100d253d62ba70e84b3060263014 Mon Sep 17 00:00:00 2001 From: Matt Amos Date: Wed, 10 Dec 2008 13:23:21 +0000 Subject: [PATCH 279/381] Some fixes to the rails part of the AMF controller and associated test. --- app/controllers/amf_controller.rb | 46 +++++++++++-------- test/functional/amf_controller_test.rb | 61 +++++++++++++++++--------- 2 files changed, 68 insertions(+), 39 deletions(-) diff --git a/app/controllers/amf_controller.rb b/app/controllers/amf_controller.rb index 39de7e671..45dbea4db 100644 --- a/app/controllers/amf_controller.rb +++ b/app/controllers/amf_controller.rb @@ -163,21 +163,27 @@ class AmfController < ApplicationController 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, ...]], ...], + # [[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 # 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 + check_boundaries(xmin, ymin, xmax, ymax) if POTLATCH_USE_SQL then ways = sql_find_ways_in_area(xmin, ymin, xmax, ymax) @@ -186,8 +192,9 @@ class AmfController < ApplicationController 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.collect { |node| - node.ways.collect { |w| [w.id,w.version] }.flatten + 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([]) @@ -202,6 +209,9 @@ class AmfController < ApplicationController 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 @@ -313,18 +323,16 @@ class AmfController < ApplicationController # an array of previous versions. def getnode_history(nodeid) #:doc: - begin - 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, []] + 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 # Find GPS traces with specified name/id. diff --git a/test/functional/amf_controller_test.rb b/test/functional/amf_controller_test.rb index b15ae857a..eb7fd1487 100644 --- a/test/functional/amf_controller_test.rb +++ b/test/functional/amf_controller_test.rb @@ -55,17 +55,22 @@ class AmfControllerTest < ActionController::TestCase # check contents of message map = amf_result "/1" - assert_equal 0, map[0] - assert_equal Array, map[1].class - assert map[1].include?(current_ways(:used_way).id) - assert !map[1].include?(current_ways(:invisible_way).id) + 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! + 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| + check_bboxes_are_bad [bbox] do |map,bbox| assert_equal BOUNDARY_ERROR, map, "AMF controller should have returned an error." end end @@ -73,19 +78,23 @@ class AmfControllerTest < ActionController::TestCase ## # 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.34,0.25,54.33]] - check_bboxes_are_bad bboxes do |map| - assert_equal BOUNDARY_ERROR, map, "AMF controller should have returned an error." + 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.34,0.24,54.33,0.25]] - check_bboxes_are_bad bboxes do |map| - assert_equal BOUNDARY_ERROR, map, "AMF controller should have returned an error." + 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 @@ -102,10 +111,14 @@ class AmfControllerTest < ActionController::TestCase # check contents of message map = amf_result "/1" - assert_equal 0, map[0] - assert_equal Array, map[1].class - assert map[1].include?(current_ways(:used_way).id) - assert !map[1].include?(current_ways(:invisible_way).id) + 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 @@ -220,10 +233,18 @@ class AmfControllerTest < ActionController::TestCase history = amf_result("/1") # ['node',nodeid,history] - assert_equal history[0], 'node' - assert_equal history[1], latest.id - assert_equal history[2].first[0], latest.timestamp.to_i - assert_equal history[2].last[0], nodes(:node_with_versions_v1).timestamp.to_i + 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 @@ -428,7 +449,7 @@ class AmfControllerTest < ActionController::TestCase # pass the response back to the caller's block to be tested # against what the caller expected. map = amf_result "/1" - yield map + yield map, bbox end end end From aeaf2d2d20ddec901c5e509be678d8c7583ce788 Mon Sep 17 00:00:00 2001 From: Matt Amos Date: Wed, 10 Dec 2008 14:31:07 +0000 Subject: [PATCH 280/381] Added missing version to the pure rails AMF controller. More stringent tests on whichways. --- app/controllers/amf_controller.rb | 4 ++-- test/functional/amf_controller_test.rb | 32 ++++++++++++++++++++++++-- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/app/controllers/amf_controller.rb b/app/controllers/amf_controller.rb index 45dbea4db..2b9a756d8 100644 --- a/app/controllers/amf_controller.rb +++ b/app/controllers/amf_controller.rb @@ -171,7 +171,7 @@ class AmfController < ApplicationController # return is of the form: # [error_code, # [[way_id, way_version], ...], - # [[node_id, lat, lon, [tags, ...]], ...], + # [[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 @@ -200,7 +200,7 @@ class AmfController < ApplicationController # 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] } + 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}) + diff --git a/test/functional/amf_controller_test.rb b/test/functional/amf_controller_test.rb index eb7fd1487..b1b2212e7 100644 --- a/test/functional/amf_controller_test.rb +++ b/test/functional/amf_controller_test.rb @@ -55,8 +55,36 @@ class AmfControllerTest < ActionController::TestCase # 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' + 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] } From eafcf91ceb1b5f293a38455a2025ffda06112b8f Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Wed, 10 Dec 2008 18:07:30 +0000 Subject: [PATCH 281/381] some further progress to amf controller checking the version numbers. --- app/controllers/amf_controller.rb | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/app/controllers/amf_controller.rb b/app/controllers/amf_controller.rb index 2b9a756d8..0dca302ed 100644 --- a/app/controllers/amf_controller.rb +++ b/app/controllers/amf_controller.rb @@ -678,9 +678,16 @@ class AmfController < ApplicationController 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, changeset_id, way_id, version_id) #:doc: + def deleteway(usertoken, changeset_id, way_id, version_id, node_id_version) #:doc: user = getuser(usertoken) if 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. @@ -706,14 +713,14 @@ class AmfController < ApplicationController # Remove a node or way from all relations # FIXME needs version, changeset, and user - def deleteitemrelations(objid, type) #:doc: + 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 } - # FIXME need to create the new node/way based on the type. + # FIXME need to create the new relation new_rel = Relation.new new_rel.version = version new_rel.members = members From 8bf0a3197c53b8671f4dcf65855e38d2a2143035 Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Thu, 11 Dec 2008 10:38:36 +0000 Subject: [PATCH 282/381] some more progress towards making amf_controller do version checking of nodes before deleting them --- app/controllers/amf_controller.rb | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/app/controllers/amf_controller.rb b/app/controllers/amf_controller.rb index 0dca302ed..14ad2147a 100644 --- a/app/controllers/amf_controller.rb +++ b/app/controllers/amf_controller.rb @@ -702,7 +702,17 @@ class AmfController < ApplicationController end deleteitemrelations(way_id, 'way') - way.delete_with_relations_and_nodes_and_history(changeset_id.to_i) + + #way.delete_with_relations_and_nodes_and_history(changeset_id.to_i) + way.unshared_node_ids.each do |node_id| + # delete the node + node = Node.find(node_id) + delete_node = Node.new + delete_node.id = node_id + delete_node.version = node_id_version[node_id] + node.delete_with_history!(delete_node, user) + end + # delete the way end [0, way_id] end @@ -711,6 +721,11 @@ class AmfController < ApplicationController # ==================================================================== # 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 # FIXME needs version, changeset, and user def deleteitemrelations(objid, type, version) #:doc: From 814822120a6becb634a22e872094b8fa16c3ea4f Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Thu, 11 Dec 2008 15:05:53 +0000 Subject: [PATCH 283/381] Don't give an error when you access the login page when you are logged in. Instead take you to the page in the referer or the home page. --- app/controllers/user_controller.rb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/controllers/user_controller.rb b/app/controllers/user_controller.rb index b9ed54096..825c92635 100644 --- a/app/controllers/user_controller.rb +++ b/app/controllers/user_controller.rb @@ -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] From b051dd11afd42e2ae6fc8ec99982ddfc26788fe8 Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Thu, 11 Dec 2008 17:22:15 +0000 Subject: [PATCH 284/381] Making amf controller act more like the xml apis --- app/controllers/amf_controller.rb | 34 +++++++++++++++++++++---------- app/models/way.rb | 26 +---------------------- 2 files changed, 24 insertions(+), 36 deletions(-) diff --git a/app/controllers/amf_controller.rb b/app/controllers/amf_controller.rb index 14ad2147a..d040a51bb 100644 --- a/app/controllers/amf_controller.rb +++ b/app/controllers/amf_controller.rb @@ -621,13 +621,11 @@ class AmfController < ApplicationController # We always need a new node, based on the data that has been sent to us new_node = Node.new - new_node.id = id new_node.changeset_id = changeset new_node.version = version new_node.lat = lat new_node.lon = lon new_node.tags = tags - new_node.visible = visible if id <= 0 # We're creating the node new_node.create_with_history(user) @@ -647,7 +645,7 @@ class AmfController < ApplicationController 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 + # 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"] @@ -687,9 +685,9 @@ class AmfController < ApplicationController # of the nodes have been changed by someone else then, there is a problem! # Returns 0 (success), unchanged way id. - def deleteway(usertoken, changeset_id, way_id, version_id, node_id_version) #:doc: + def deleteway(usertoken, changeset_id, way_id, way_version, node_id_version) #:doc: user = getuser(usertoken) - if user then return -1,"You are not logged in, so the way could not be deleted." end + 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 @@ -697,24 +695,38 @@ class AmfController < ApplicationController # 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') + #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) - way.unshared_node_ids.each do |node_id| + old_way.unshared_node_ids.each do |node_id| # delete the node node = Node.find(node_id) delete_node = Node.new - delete_node.id = node_id 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 diff --git a/app/models/way.rb b/app/models/way.rb index 92b69ed7d..86b25e08e 100644 --- a/app/models/way.rb +++ b/app/models/way.rb @@ -228,7 +228,7 @@ class Way < ActiveRecord::Base 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 @@ -257,30 +257,6 @@ class Way < ActiveRecord::Base end end - # delete a way and its nodes that aren't part of other ways, with history - - # FIXME: merge the potlatch code to delete the relations - # and refactor to use delete_with_history! - # This really needs the ids and versions of the nodes/relations to be passed in too - # so that we can do the version checking before the delete - def delete_with_relations_and_nodes_and_history(changeset_id) - # delete the nodes not used by other ways - self.unshared_node_ids.each do |node_id| - n = Node.find(node_id) - n.changeset_id = changeset_id - n.visible = false - # FIXME next line is bad - n.save_with_history! - end - - self.changeset_id = changeset_id - self.tags = [] - self.nds = [] - self.visible = false - # FIXME next line is bad - self.save_with_history! - end - # Find nodes that belong to this way only def unshared_node_ids node_ids = self.nodes.collect { |node| node.id } From 541c24b23d09917dc841c51ea6225c76bc074c0b Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Fri, 12 Dec 2008 11:24:40 +0000 Subject: [PATCH 285/381] capture the too many way nodes error, and give the user an error message --- app/controllers/amf_controller.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/controllers/amf_controller.rb b/app/controllers/amf_controller.rb index d040a51bb..706180557 100644 --- a/app/controllers/amf_controller.rb +++ b/app/controllers/amf_controller.rb @@ -588,6 +588,8 @@ class AmfController < ApplicationController # 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 From c76e60f052051a35fe7e9044969337cd88e5b1fd Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Fri, 12 Dec 2008 18:54:03 +0000 Subject: [PATCH 286/381] Improving the not found handling of preferences. adding a user preference test. adding some utility methods to the test helper --- app/controllers/user_preference_controller.rb | 72 +++++++++---------- test/functional/node_controller_test.rb | 1 - .../user_preference_controller_test.rb | 24 ++++++- test/test_helper.rb | 8 +++ 4 files changed, 62 insertions(+), 43 deletions(-) diff --git a/app/controllers/user_preference_controller.rb b/app/controllers/user_preference_controller.rb index 559479929..3b56c257b 100644 --- a/app/controllers/user_preference_controller.rb +++ b/app/controllers/user_preference_controller.rb @@ -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 + p = XML::Parser.new + p.string = request.raw_post + doc = p.parse - prefs = [] + prefs = [] - keyhash = {} + keyhash = {} - doc.find('//preferences/preference').each do |pt| - pref = UserPreference.new + doc.find('//preferences/preference').each do |pt| + pref = UserPreference.new - unless keyhash[pt['k']].nil? # already have that key - render :text => 'OH NOES! CAN HAS UNIQUE KEYS?', :status => :not_acceptable - return - end - - keyhash[pt['k']] = 1 - - pref.k = pt['k'] - pref.v = pt['v'] - pref.user_id = @user.id - prefs << pref + unless keyhash[pt['k']].nil? # already have that key + render :text => 'OH NOES! CAN HAS UNIQUE KEYS?', :status => :not_acceptable end - if prefs.size > 150 - render :text => 'Too many preferences', :status => :request_entity_too_large - return - end + keyhash[pt['k']] = 1 - # kill the existing ones - UserPreference.delete_all(['user_id = ?', @user.id]) - - # save the new ones - prefs.each do |pref| - pref.save! - end - - rescue Exception => ex - render :text => 'OH NOES! FAIL!: ' + ex.to_s, :status => :internal_server_error - return + pref.k = pt['k'] + pref.v = pt['v'] + pref.user_id = @user.id + prefs << pref end + 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 diff --git a/test/functional/node_controller_test.rb b/test/functional/node_controller_test.rb index 2289953fe..bc9ffa489 100644 --- a/test/functional/node_controller_test.rb +++ b/test/functional/node_controller_test.rb @@ -1,5 +1,4 @@ require File.dirname(__FILE__) + '/../test_helper' -require 'node_controller' class NodeControllerTest < ActionController::TestCase api_fixtures diff --git a/test/functional/user_preference_controller_test.rb b/test/functional/user_preference_controller_test.rb index 7ff64b30e..ff44c1e36 100644 --- a/test/functional/user_preference_controller_test.rb +++ b/test/functional/user_preference_controller_test.rb @@ -1,8 +1,26 @@ 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 + print @response.body + 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 diff --git a/test/test_helper.rb b/test/test_helper.rb index ea6e91abe..88a6fbe4a 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -106,5 +106,13 @@ class Test::Unit::TestCase 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... end From 850bd333eb6b53dc76ccf64218012b110396af57 Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Fri, 12 Dec 2008 18:56:22 +0000 Subject: [PATCH 287/381] removing debug statement --- test/functional/user_preference_controller_test.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/test/functional/user_preference_controller_test.rb b/test/functional/user_preference_controller_test.rb index ff44c1e36..714f45c5d 100644 --- a/test/functional/user_preference_controller_test.rb +++ b/test/functional/user_preference_controller_test.rb @@ -13,7 +13,6 @@ class UserPreferenceControllerTest < ActionController::TestCase get :read assert_response :success - print @response.body assert_select "osm:root" do assert_select "preferences", :count => 1 do assert_select "preference", :count => 2 From fac8b5d28aa81f2233c7233b6876ad31d86be28b Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Fri, 12 Dec 2008 19:04:28 +0000 Subject: [PATCH 288/381] Log the request on a few requests when there is a bad request, probably should do this in a few more places. This makes it easier to see why we are getting errors/bad requests. --- app/controllers/way_controller.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/controllers/way_controller.rb b/app/controllers/way_controller.rb index 5b0a632f7..80c75d91c 100644 --- a/app/controllers/way_controller.rb +++ b/app/controllers/way_controller.rb @@ -22,6 +22,7 @@ class WayController < ApplicationController render :nothing => true, :status => :method_not_allowed end rescue OSM::APIError => ex + logger.warn request.raw_post render ex.render_opts end end @@ -56,6 +57,7 @@ class WayController < ApplicationController 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 From eb9138813c3784343c7e2478b94eaee494b51421 Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Sun, 14 Dec 2008 23:16:27 +0000 Subject: [PATCH 289/381] Return, rather than allow a second render to happen. Closes #1407 --- app/controllers/user_controller.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/controllers/user_controller.rb b/app/controllers/user_controller.rb index 825c92635..7ebe6b6b6 100644 --- a/app/controllers/user_controller.rb +++ b/app/controllers/user_controller.rb @@ -124,6 +124,7 @@ class UserController < ApplicationController else redirect_to :controller => 'site', :action => 'index' end + return end @title = 'login' if params[:user] From e5674abd2c9b0359b06c281804f7286e22b9a258 Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Mon, 15 Dec 2008 18:57:12 +0000 Subject: [PATCH 290/381] bug fix. Adding some transactions to amf controller, and adding the missing version to putway method signature. --- app/controllers/amf_controller.rb | 284 +++++++++++++++--------------- 1 file changed, 144 insertions(+), 140 deletions(-) diff --git a/app/controllers/amf_controller.rb b/app/controllers/amf_controller.rb index 706180557..b18ebe346 100644 --- a/app/controllers/amf_controller.rb +++ b/app/controllers/amf_controller.rb @@ -407,44 +407,46 @@ class AmfController < ApplicationController 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' + Relation.transaction do + # create a new relation, or find the existing one + if relid > 0 + relation = Relation.find(relid) end - if mid - typedmembers << [m[0], mid, m[2]] + # 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 - 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 + # 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 + # 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 + end # transaction if id <= 0 return [0, relid, new_relation.id, new_relation.version] @@ -475,7 +477,7 @@ class AmfController < ApplicationController # 3. hash of renumbered nodes (old id=>new id), # 4. version - def putway(renumberednodes, usertoken, changeset, originalway, points, attributes) #:doc: + def putway(renumberednodes, usertoken, changeset, version, originalway, points, attributes) #:doc: # -- Initialise and carry out checks @@ -493,92 +495,92 @@ class AmfController < ApplicationController # -- 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 + Way.transaction do + if originalway <= 0 + uniques = [] 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 + way = Way.find(originalway) + uniques = way.unshared_node_ids 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) - 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 + + # -- 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.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) + 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 + end # transaction [0, originalway, way.id, renumberednodes, way.version] rescue OSM::APIChangesetAlreadyClosedError => ex @@ -612,32 +614,34 @@ class AmfController < ApplicationController id = id.to_i visible = (visible.to_i == 1) - if id > 0 then - node = Node.find(id) + Node.transaction do + 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') + 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 - end - # We always need a new node, based on the data that has been sent to us - new_node = Node.new + # 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 + 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 + end # transaction if id <= 0 return [0, id, new_node.id, new_node.version] From b1445ab023010af959758fffb8705c377c5cfffa Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Mon, 15 Dec 2008 19:20:24 +0000 Subject: [PATCH 291/381] Fix the transactions, need to set anything that will be returned to nil outside the transaction, otherwise it won't be available after the end of the transaction, when it is returned. --- app/controllers/amf_controller.rb | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/app/controllers/amf_controller.rb b/app/controllers/amf_controller.rb index b18ebe346..6f22cdcfa 100644 --- a/app/controllers/amf_controller.rb +++ b/app/controllers/amf_controller.rb @@ -407,6 +407,8 @@ class AmfController < ApplicationController relid = relid.to_i visible = (visible.to_i != 0) + new_relation = nil + relation = nil Relation.transaction do # create a new relation, or find the existing one if relid > 0 @@ -495,6 +497,8 @@ class AmfController < ApplicationController # -- Get unique nodes + new_way = nil + way= nil Way.transaction do if originalway <= 0 uniques = [] @@ -613,7 +617,8 @@ class AmfController < ApplicationController id = id.to_i visible = (visible.to_i == 1) - + node = nil + new_node = nil Node.transaction do if id > 0 then node = Node.find(id) @@ -641,13 +646,13 @@ class AmfController < ApplicationController # We're deleting the node node.delete_with_history!(new_node, user) end - end # transaction + end # transaction if id <= 0 return [0, id, new_node.id, new_node.version] else return [0, id, node.id, node.version] - end + end rescue OSM::APIChangesetAlreadyClosedError => ex return [-1, "The changeset #{ex.changeset.id} was closed at #{ex.changeset.closed_at}"] rescue OSM::APIVersionMismatchError => ex @@ -694,9 +699,10 @@ class AmfController < ApplicationController 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 + + way_id = way_id.to_i # 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 @@ -719,7 +725,7 @@ class AmfController < ApplicationController delete_way = Way.new delete_way.version = way_version old_way.delete_with_history!(delete_way, user) - end + end # transaction [0, way_id] rescue OSM::APIChangesetAlreadyClosedError => ex return [-1, "The changeset #{ex.changeset.id} was closed at #{ex.changeset.closed_at}"] @@ -746,6 +752,8 @@ class AmfController < ApplicationController # Remove a node or way from all relations # FIXME needs version, changeset, and user + # Fixme make sure this doesn't depend on anything and delete this, as potlatch + # itself should remove the relations first def deleteitemrelations(objid, type, version) #:doc: relations = RelationMember.find(:all, :conditions => ['member_type = ? and member_id = ?', type, objid], From 548265372eab9430f901ed9b278ab74c7bea0619 Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Mon, 15 Dec 2008 19:27:52 +0000 Subject: [PATCH 292/381] Some extra stubs in the amf controller test --- test/functional/amf_controller_test.rb | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/test/functional/amf_controller_test.rb b/test/functional/amf_controller_test.rb index b1b2212e7..b71c680e3 100644 --- a/test/functional/amf_controller_test.rb +++ b/test/functional/amf_controller_test.rb @@ -399,7 +399,22 @@ class AmfControllerTest < ActionController::TestCase 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 + + def test_putpoi_delete_valid + + end + + def test_putpoi_delete_already_deleted + + end + + def test_putpoi_delete_not_found + + end + + def test_putpoi_invalid_latlon + end # ************************************************************ From e198ec6252aaacbece6c3d3a6e7957588b619b8a Mon Sep 17 00:00:00 2001 From: Thomas Wood Date: Mon, 15 Dec 2008 23:24:09 +0000 Subject: [PATCH 293/381] Add validations for diary entry lat/lons and tests for diary entry validations. Closes #1395 --- app/models/diary_entry.rb | 6 ++++-- test/unit/diary_entry_test.rb | 22 ++++++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/app/models/diary_entry.rb b/app/models/diary_entry.rb index 4b2058b9d..8063372da 100644 --- a/app/models/diary_entry.rb +++ b/app/models/diary_entry.rb @@ -7,7 +7,9 @@ class DiaryEntry < ActiveRecord::Base 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_numericality_of :latitude, :allow_nil => true, + :greater_than_or_equal_to => -90, :less_than_or_equal_to => 90 + validates_numericality_of :longitude, :allow_nil => true, + :greater_than_or_equal_to => -180, :less_than_or_equal_to => 180 validates_associated :user end diff --git a/test/unit/diary_entry_test.rb b/test/unit/diary_entry_test.rb index 0e10f8a9a..6b2800d18 100644 --- a/test/unit/diary_entry_test.rb +++ b/test/unit/diary_entry_test.rb @@ -8,4 +8,26 @@ class DiaryEntryTest < Test::Unit::TestCase assert_equal 2, DiaryEntry.count end + def test_diary_entry_validations + diary_entry_valid({}) + diary_entry_valid({:title => ''}, false) + diary_entry_valid({:title => 'a'*255}) + diary_entry_valid({:title => 'a'*256}, false) + diary_entry_valid({:body => ''}, false) + diary_entry_valid({:latitude => 90}) + diary_entry_valid({:latitude => 90.00001}, false) + diary_entry_valid({:latitude => -90}) + diary_entry_valid({:latitude => -90.00001}, false) + diary_entry_valid({:longitude => 180}) + diary_entry_valid({:longitude => 180.00001}, false) + diary_entry_valid({:longitude => -180}) + diary_entry_valid({:longitude => -180.00001}, false) + end + + def diary_entry_valid(attrs, result = true) + entry = diary_entries(:normal_user_entry_1).clone + entry.attributes = attrs + assert_equal result, entry.valid? + end + end From 40647c53dff5f04f0ecfd4db891c4d3bf76b4a8d Mon Sep 17 00:00:00 2001 From: Richard Fairhurst Date: Tue, 16 Dec 2008 00:23:11 +0000 Subject: [PATCH 294/381] yet more rewriting of putway... --- app/controllers/amf_controller.rb | 129 +++++++++++++----------------- 1 file changed, 56 insertions(+), 73 deletions(-) diff --git a/app/controllers/amf_controller.rb b/app/controllers/amf_controller.rb index 6f22cdcfa..ea58c2866 100644 --- a/app/controllers/amf_controller.rb +++ b/app/controllers/amf_controller.rb @@ -137,7 +137,9 @@ class AmfController < ApplicationController if closeid cs = Changeset.find(closeid) cs.set_closed_time_now - if closecomment.empty? + if cs.user_id!=user.id + return -2,"You cannot close that changeset because you're not the person who opened it." + elsif closecomment.empty? cs.save! else cs.tags['comment']=closecomment @@ -148,8 +150,8 @@ class AmfController < ApplicationController # 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.user_id = user.id + # smsm1 doesn't like the next two lines and thinks they 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! @@ -259,7 +261,7 @@ class AmfController < ApplicationController points = way.nodes.collect do |node| nodetags=node.tags nodetags.delete('created_by') - [node.lon, node.lat, node.id, nodetags] + [node.lon, node.lat, node.id, nodetags, node.version] end tags = way.tags version = way.version @@ -472,35 +474,39 @@ class AmfController < ApplicationController # Save a way to the database, including all nodes. Any nodes in the previous # version and no longer used are deleted. # + # Parameters: + # 0. hash of renumbered nodes + # 1. current user token (for authentication) + # 2. current changeset + # 3. new way version + # 4. way ID + # 5. list of nodes in way + # 6. hash of way tags + # 7. array of nodes to change (each one is [lon,lat,id,version,tags]) + # # Returns: # 0. '0' (code for success), # 1. original way id (unchanged), # 2. new way id, # 3. hash of renumbered nodes (old id=>new id), - # 4. version + # 4. way version, + # 5. hash of node versions (node=>version) - def putway(renumberednodes, usertoken, changeset, version, originalway, points, attributes) #:doc: + def putway(renumberednodes, usertoken, changeset, version, originalway, pointlist, attributes, nodes) #:doc: - # -- Initialise and carry out checks + # -- Initialise user = getuser(usertoken) if !user then return -1,"You are not logged in, so the way could not be saved." end + if pointlist.length < 2 then return -2,"Server error - way is only #{points.length} points long." 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 - - new_way = nil - way= nil Way.transaction do + + # -- Get unique nodes + if originalway <= 0 + way = nil uniques = [] else way = Way.find(originalway) @@ -508,65 +514,42 @@ class AmfController < ApplicationController end new_way = Way.new - # -- Compare nodes and save changes to any that have changed + #Ê-- Update each changed node - nodes = [] + nodeversions = {} + nodes.each do |a| + lon = a[0].to_f + lat = a[1].to_f + id = a[2].to_i + version = a[3].to_i + if id == 0 then return -2,"Server error - node with id 0 found in way #{originalway}." end + if lat== 90 then return -2,"Server error - node with latitude -90 found in way #{originalway}." end + if renumberednodes[id] then id = renumberednodes[id] end - 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 + node = Node.new + node.changeset_id = changeset + node.lat = lat + node.lon = lon + node.tags = a[4] + node.tags.delete('created_by') + node.version = version if id <= 0 - # Create new node - savenode = true + # We're creating the node + node.create_with_history(user) + renumberednodes[id] = node.id + nodeversions[id] = node.version 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 + # We're updating an existing node + previous=Node.find(id) + previous.update_from(node, user) + nodeversions[id] = previous.version end - - if savenode - new_node.changeset_id = changeset - new_node.lat = lat - new_node.lon = lon - new_node.tags = n[4] - 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') + # -- Delete any unique nodes no longer used + uniques=uniques-pointlist + uniques.each do |n| node = Node.find(n) new_node = Node.new new_node.changeset_id = changeset @@ -579,21 +562,21 @@ class AmfController < ApplicationController 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.nds = pointlist new_way.changeset_id = changeset new_way.version = version way.update_from(new_way, user) end end # transaction - [0, originalway, way.id, renumberednodes, way.version] + [0, originalway, way.id, renumberednodes, way.version, nodeversions] 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"] + return [-3, "Sorry, someone else has changed this way since you started editing - 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 From ff01f76293c8e2c95d8d13b289ca112c449cfe44 Mon Sep 17 00:00:00 2001 From: Richard Fairhurst Date: Tue, 16 Dec 2008 00:40:04 +0000 Subject: [PATCH 295/381] bit more putway stuff --- app/controllers/amf_controller.rb | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/app/controllers/amf_controller.rb b/app/controllers/amf_controller.rb index ea58c2866..2bd7a4108 100644 --- a/app/controllers/amf_controller.rb +++ b/app/controllers/amf_controller.rb @@ -506,13 +506,11 @@ class AmfController < ApplicationController # -- Get unique nodes if originalway <= 0 - way = nil uniques = [] else way = Way.find(originalway) uniques = way.unshared_node_ids end - new_way = Way.new #Ê-- Update each changed node @@ -559,31 +557,34 @@ class AmfController < ApplicationController # -- 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 = pointlist - new_way.changeset_id = changeset - new_way.version = version + new_way = Way.new + new_way.tags = attributes + new_way.nds = pointlist + new_way.changeset_id = changeset + new_way.version = version + if originalway <= 0 + new_way.create_with_history(user) + way=new_way # so we can get way.id and way.version + elsif way.tags!=attributes or way.nds!=pointlist or !way.visible? way.update_from(new_way, user) end end # transaction [0, originalway, way.id, renumberednodes, way.version, nodeversions] rescue OSM::APIChangesetAlreadyClosedError => ex - return [-1, "The changeset #{ex.changeset.id} was closed at #{ex.changeset.closed_at}"] + return [-2, "Sorry, your changeset #{ex.changeset.id} has been 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, "Sorry, someone else has changed this way since you started editing - 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."] + return [-1, "You have tried to upload a really long way with #{ex.provided} points: only #{ex.max} are allowed."] rescue OSM::APIAlreadyDeletedError => ex - return [-1, "The object has already been deleted"] + 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 :-()"] + return [-2, "Something really bad happened :-(."] end # Save POI to the database. From d6465302066e4d16ef43c0df1f289c040269ce78 Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Tue, 16 Dec 2008 16:40:59 +0000 Subject: [PATCH 296/381] Fix UTF-8 encoding error char in source file that RichardF introduced --- app/controllers/amf_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/amf_controller.rb b/app/controllers/amf_controller.rb index 2bd7a4108..936dcbe1b 100644 --- a/app/controllers/amf_controller.rb +++ b/app/controllers/amf_controller.rb @@ -512,7 +512,7 @@ class AmfController < ApplicationController uniques = way.unshared_node_ids end - #Ê-- Update each changed node + #-- Update each changed node nodeversions = {} nodes.each do |a| From b823e9fccca6072d92cc4f1ba0d1850488a5f3c4 Mon Sep 17 00:00:00 2001 From: Richard Fairhurst Date: Wed, 17 Dec 2008 00:30:29 +0000 Subject: [PATCH 297/381] some more putway fixes - this actually creates new ways with 0.6 Potlatch now --- app/controllers/amf_controller.rb | 33 +++++++++++++----------------- public/potlatch/potlatch.swf | Bin 169553 -> 170070 bytes 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/app/controllers/amf_controller.rb b/app/controllers/amf_controller.rb index 936dcbe1b..b6e3bf351 100644 --- a/app/controllers/amf_controller.rb +++ b/app/controllers/amf_controller.rb @@ -130,7 +130,7 @@ class AmfController < ApplicationController # Start new changeset def startchangeset(usertoken, cstags, closeid, closecomment) - user = getuserid(usertoken) + user = getuser(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 @@ -153,7 +153,7 @@ class AmfController < ApplicationController cs.user_id = user.id # smsm1 doesn't like the next two lines and thinks they need to be abstracted to the model more/better cs.created_at = Time.now - cs.closed_at = Time.new + Changeset::IDLE_TIMEOUT + cs.closed_at = cs.created_at + Changeset::IDLE_TIMEOUT cs.save_with_tags! return [0,cs.id] end @@ -341,17 +341,17 @@ class AmfController < ApplicationController # Returns array listing GPXs, each one comprising id, name and description. def findgpx(searchterm, usertoken) - uid = getuserid(usertoken) + user = getuser(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] ) + gpx = Trace.find(searchterm.to_i, :conditions => ["visible=? AND (public=? OR user_id=?)",true,true,user.id] ) 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| + Trace.find(:all, :limit => 21, :conditions => ["visible=? AND (public=? OR user_id=?) AND MATCH(name) AGAINST (?)",true,true,user.id,searchterm] ).each do |gpx| gpxs.push([gpx.id, gpx.name, gpx.description]) end end @@ -403,7 +403,7 @@ class AmfController < ApplicationController # 2. new relation id. def putrelation(renumberednodes, renumberedways, usertoken, changeset, version, relid, tags, members, visible) #:doc: - user = getuserid(usertoken) + user = getuser(usertoken) if !user then return -1,"You are not logged in, so the relation could not be saved." end relid = relid.to_i @@ -475,7 +475,7 @@ class AmfController < ApplicationController # version and no longer used are deleted. # # Parameters: - # 0. hash of renumbered nodes + # 0. hash of renumbered nodes (added by amf_controller) # 1. current user token (for authentication) # 2. current changeset # 3. new way version @@ -501,6 +501,10 @@ class AmfController < ApplicationController if pointlist.length < 2 then return -2,"Server error - way is only #{points.length} points long." end originalway = originalway.to_i + pointlist.collect! {|a| a.to_i } + + way=nil # this is returned, so scope it outside the transaction + nodeversions = {} Way.transaction do # -- Get unique nodes @@ -514,7 +518,6 @@ class AmfController < ApplicationController #-- Update each changed node - nodeversions = {} nodes.each do |a| lon = a[0].to_f lat = a[1].to_f @@ -557,6 +560,9 @@ class AmfController < ApplicationController # -- Save revised way + pointlist.collect! {|a| + renumberednodes[a] ? renumberednodes[a]:a + } # renumber nodes new_way = Way.new new_way.tags = attributes new_way.nds = pointlist @@ -778,17 +784,6 @@ class AmfController < ApplicationController 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) - end # Send AMF response diff --git a/public/potlatch/potlatch.swf b/public/potlatch/potlatch.swf index 3a1b8c4d107ceb7f4a3bd6a4f74317112c1a552f..21442cb121255fa2978680c0a957d04e82e970e7 100755 GIT binary patch literal 170070 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�ZL+TU+!Yy(67V3P6w{ifIf{M&h0p7Zyu_ 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 V?OfmKoI~Ji&AI0-;)GMZ{uh1awQK+Y From 2b8bde9055f0cfdf7d261382cd40980d72bae188 Mon Sep 17 00:00:00 2001 From: Richard Fairhurst Date: Thu, 18 Dec 2008 00:32:02 +0000 Subject: [PATCH 298/381] Potlatch for API 0.6 now saving ways happily with node versioning... I think --- app/controllers/amf_controller.rb | 8 ++++---- app/models/changeset.rb | 4 ++-- public/potlatch/potlatch.swf | Bin 170070 -> 170224 bytes 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/controllers/amf_controller.rb b/app/controllers/amf_controller.rb index b6e3bf351..80472fe25 100644 --- a/app/controllers/amf_controller.rb +++ b/app/controllers/amf_controller.rb @@ -458,12 +458,12 @@ class AmfController < ApplicationController 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}"] + 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"] + 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 @@ -538,12 +538,12 @@ class AmfController < ApplicationController # We're creating the node node.create_with_history(user) renumberednodes[id] = node.id - nodeversions[id] = node.version + nodeversions[node.id] = node.version else # We're updating an existing node previous=Node.find(id) previous.update_from(node, user) - nodeversions[id] = previous.version + nodeversions[previous.id] = previous.version end end diff --git a/app/models/changeset.rb b/app/models/changeset.rb index 3e0ba9f8e..0b214fe72 100644 --- a/app/models/changeset.rb +++ b/app/models/changeset.rb @@ -29,13 +29,13 @@ class Changeset < ActiveRecord::Base # 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 + MAX_TIME_OPEN = 1.day # 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) + IDLE_TIMEOUT = 1.hour # 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 diff --git a/public/potlatch/potlatch.swf b/public/potlatch/potlatch.swf index 21442cb121255fa2978680c0a957d04e82e970e7..6bc751f6ce548c21f487acd35cf48f65dd65bff9 100755 GIT binary patch literal 170224 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 literal 170070 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�ZL+TU+!Yy(67V3P6w{ifIf{M&h0p7Zyu_ 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 Date: Thu, 18 Dec 2008 00:36:01 +0000 Subject: [PATCH 299/381] remove debug box --- public/potlatch/potlatch.swf | Bin 170224 -> 170159 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/public/potlatch/potlatch.swf b/public/potlatch/potlatch.swf index 6bc751f6ce548c21f487acd35cf48f65dd65bff9..9405b3b7d749944067aae371bbd9676b07780145 100755 GIT binary patch literal 170159 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 From 4e8ed960a7aaffca8feb7a676d2f858c8aa4ba00 Mon Sep 17 00:00:00 2001 From: Richard Fairhurst Date: Thu, 18 Dec 2008 09:20:58 +0000 Subject: [PATCH 300/381] Relations support --- public/potlatch/potlatch.swf | Bin 170159 -> 170218 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/public/potlatch/potlatch.swf b/public/potlatch/potlatch.swf index 9405b3b7d749944067aae371bbd9676b07780145..5a9b7572a5864593c1c41cc0398e8d5c3da963b3 100755 GIT binary patch delta 90 zcmV-g0Hy!0u?p(33I#@2QwZv@1%oOAHEx57Dz}O%0dI5yDgu{G)&VI33jvdVltq_o wp8<)N>$(97lV6zz0sog6kO67|w3kAl0cMvN-2p8E00Nf|kpU&Q@1Oy|_r#_k=>Px# delta 83 zcmV-Z0IdJ&u?nxT3I#@2QwXoI1%oOA`fY=XDz}O%0dI5y?*W(lo&hNV-IuY_0Y;Z= pp8<)NHIM;K0jHO6paEu=DU1Obm;9gs3IPb05Rm~Rw<@6l!1r)yAZGvo From cc90867183367d7758a11da3f2058117c7529891 Mon Sep 17 00:00:00 2001 From: Richard Fairhurst Date: Tue, 23 Dec 2008 13:06:07 +0000 Subject: [PATCH 301/381] view changesets by user --- app/controllers/changeset_controller.rb | 20 ++++++++++- app/views/changeset/_changeset.rhtml | 34 +++++++++++++++++++ .../changeset/_changeset_paging_nav.rhtml | 12 +++++++ app/views/changeset/list.rhtml | 15 ++++++++ app/views/user/view.rhtml | 2 ++ config/routes.rb | 1 + 6 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 app/views/changeset/_changeset.rhtml create mode 100644 app/views/changeset/_changeset_paging_nav.rhtml create mode 100644 app/views/changeset/list.rhtml diff --git a/app/controllers/changeset_controller.rb b/app/controllers/changeset_controller.rb index f7f4dc9f0..3e1870b70 100644 --- a/app/controllers/changeset_controller.rb +++ b/app/controllers/changeset_controller.rb @@ -1,9 +1,12 @@ # The ChangesetController is the RESTful interface to Changeset objects class ChangesetController < ApplicationController + layout 'site' require 'xml/libxml' - session :off +# session :off +# FIXME is this required? + before_filter :authorize_web, :only => [:list] 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] @@ -289,6 +292,21 @@ class ChangesetController < ApplicationController render ex.render_opts end + ## + # list edits belonging to a user + def list + user = User.find(:first, :conditions => [ "visible = ? and display_name = ?", true, params[:display_name]]) + @edit_pages, @edits = paginate(:changesets, + :include => [:user, :changeset_tags], + :conditions => ["changesets.user_id = ? AND min_lat IS NOT NULL", user.id], + :order => "changesets.created_at DESC", + :per_page => 20) + + @action = 'list' + @display_name = user.display_name + # FIXME needs rescues in here + end + private #------------------------------------------------------------ # utility functions below. diff --git a/app/views/changeset/_changeset.rhtml b/app/views/changeset/_changeset.rhtml new file mode 100644 index 000000000..83b0b0ce8 --- /dev/null +++ b/app/views/changeset/_changeset.rhtml @@ -0,0 +1,34 @@ + + <% cl = cycle('table0', 'table1') %> + + + #<%= changeset.id %> + + + <% if changeset.closed_at > DateTime.now %> (still editing) + <% else %><%= changeset.closed_at.strftime("%d %b %Y %H:%M") %><% end %> + + + <% if changeset.tags['comment'] %> + <%= changeset.tags['comment'] %> + <% else %> + (none) + <% end %> + + + <% if changeset.min_lat.nil? %> + (no edits) + <% else + lat1 = changeset.min_lat/GeoRecord::SCALE.to_f + lat2 = changeset.max_lat/GeoRecord::SCALE.to_f + lon1 = changeset.min_lon/GeoRecord::SCALE.to_f + lon2 = changeset.min_lon/GeoRecord::SCALE.to_f + %> + (<%= format("%0.3f",lat1) -%>,<%= format("%0.3f",lon1) -%>) to + (<%= format("%0.3f",lat2) -%>,<%= format("%0.3f",lon2) -%>) + <% end %> + + + <%= link_to 'more', {:controller => 'browse', :action => 'changeset', :id => changeset.id}, {:title => 'View changeset details'} %> + + diff --git a/app/views/changeset/_changeset_paging_nav.rhtml b/app/views/changeset/_changeset_paging_nav.rhtml new file mode 100644 index 000000000..df84a3930 --- /dev/null +++ b/app/views/changeset/_changeset_paging_nav.rhtml @@ -0,0 +1,12 @@ +<% current_page = @edit_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 changeset on page + %>-<%= current_page.last_item %><% +end %> +of <%= @edit_pages.item_count %>) + +<% if @edit_pages.page_count > 1 %> + | <%= pagination_links_each(@edit_pages, {}) { |n| link_to(n, :display_name => @display_name, :page => n) } %> +<% end %> diff --git a/app/views/changeset/list.rhtml b/app/views/changeset/list.rhtml new file mode 100644 index 000000000..cea156320 --- /dev/null +++ b/app/views/changeset/list.rhtml @@ -0,0 +1,15 @@ +

      Edits by <%= link_to(@display_name, {:controller=>'user', :action=>'view', :display_name=>@display_name}) %>

      +<%= render :partial => 'changeset_paging_nav' %> + + + + + + + + + + <%= render :partial => 'changeset', :collection => @edits unless @edits.nil? %> +
      IDSaved atCommentArea
      + +<%= render :partial => 'changeset_paging_nav' %> diff --git a/app/views/user/view.rhtml b/app/views/user/view.rhtml index 66a7426f5..4f2a168a1 100644 --- a/app/views/user/view.rhtml +++ b/app/views/user/view.rhtml @@ -5,12 +5,14 @@ <%= 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 edits', :controller => 'changeset', :action => 'list', :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 %> <%= 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 'edits', :controller => 'changeset', :action => 'list', :display_name => @this_user.display_name %> | <%= link_to 'traces', :controller => 'trace', :action => 'view', :display_name => @this_user.display_name %> | <% if @user and @user.is_friends_with?(@this_user) %> <%= link_to 'remove as friend', :controller => 'user', :action => 'remove_friend', :display_name => @this_user.display_name %> diff --git a/config/routes.rb b/config/routes.rb index b99dfd2ca..4431c4765 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -137,6 +137,7 @@ ActionController::Routing::Routes.draw do |map| # user pages map.connect '/user/:display_name', :controller => 'user', :action => 'view' + map.connect '/user/:display_name/edits', :controller => 'changeset', :action => 'list' map.connect '/user/:display_name/make_friend', :controller => 'user', :action => 'make_friend' map.connect '/user/:display_name/remove_friend', :controller => 'user', :action => 'remove_friend' map.connect '/user/:display_name/diary', :controller => 'diary_entry', :action => 'list' From 9fa1f4b0131fc9c0ba0b261fda191eab2792d90e Mon Sep 17 00:00:00 2001 From: Richard Fairhurst Date: Tue, 23 Dec 2008 13:09:36 +0000 Subject: [PATCH 302/381] session management on only for :list --- app/controllers/changeset_controller.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/controllers/changeset_controller.rb b/app/controllers/changeset_controller.rb index 3e1870b70..c820aa705 100644 --- a/app/controllers/changeset_controller.rb +++ b/app/controllers/changeset_controller.rb @@ -4,8 +4,7 @@ class ChangesetController < ApplicationController layout 'site' require 'xml/libxml' -# session :off -# FIXME is this required? + session :off, :except => [:list] before_filter :authorize_web, :only => [:list] before_filter :authorize, :only => [:create, :update, :delete, :upload, :include, :close] before_filter :check_write_availability, :only => [:create, :update, :delete, :upload, :include] From 60834f33f9f38f94fd824ea8cd0a3fe210fbf86b Mon Sep 17 00:00:00 2001 From: Thomas Wood Date: Tue, 23 Dec 2008 15:08:18 +0000 Subject: [PATCH 303/381] Pass the to_user to the view. Tests may follow. Closes #1394 --- app/controllers/message_controller.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/controllers/message_controller.rb b/app/controllers/message_controller.rb index d2fa9bd5f..c039fe636 100644 --- a/app/controllers/message_controller.rb +++ b/app/controllers/message_controller.rb @@ -35,6 +35,7 @@ class MessageController < ApplicationController @body = "On #{message.sent_on} #{message.sender.display_name} wrote:\n\n#{message.body.gsub(/^/, '> ')}" @title = "Re: #{message.title.sub(/^Re:\s*/, '')}" @user_id = message.from_user_id + @to_user = User.find(message.to_user_id) render :action => 'new' rescue ActiveRecord::RecordNotFound render :action => 'no_such_user', :status => :not_found From 62b6d159672ad29344c7b7074a37d23b16dbb5d9 Mon Sep 17 00:00:00 2001 From: Thomas Wood Date: Tue, 23 Dec 2008 15:47:06 +0000 Subject: [PATCH 304/381] Make browse controller index show recently closed changesets, rather than recently changed nodes. --- app/controllers/browse_controller.rb | 2 +- app/views/browse/index.rhtml | 18 +++++++++------ test/functional/browse_controller_test.rb | 27 ++++++++++++++--------- 3 files changed, 29 insertions(+), 18 deletions(-) diff --git a/app/controllers/browse_controller.rb b/app/controllers/browse_controller.rb index 60f580963..2e4af03d8 100644 --- a/app/controllers/browse_controller.rb +++ b/app/controllers/browse_controller.rb @@ -8,7 +8,7 @@ class BrowseController < ApplicationController end def index - @nodes = Node.find(:all, :order => "timestamp DESC", :limit=> 20) + @changesets = Changeset.find(:all, :order => "closed_at DESC", :limit=> 20) end def relation diff --git a/app/views/browse/index.rhtml b/app/views/browse/index.rhtml index e9d830a10..fa7e13ac0 100644 --- a/app/views/browse/index.rhtml +++ b/app/views/browse/index.rhtml @@ -1,12 +1,16 @@ -

      <%= @nodes.length %> Recently Changed Nodes

      +

      <%= @changesets.length %> Recently Closed Changesets

        -<% @nodes.each do |node| - name = node.tags_as_hash['name'].to_s - if name.length == 0: - name = "(No name)" +<% @changesets.each do |changeset| + if changeset.user.data_public? + user = changeset.user.display_name + else + user = "(anonymous)" end - name = "#{name} - #{node.id} (#{node.version})" + + cmt = changeset.tags_as_hash['comment'].to_s + cmt = "(no comment)" if cmt.length == 0 + text = "#{changeset.id} by #{user} - #{cmt}" %> -
      • <%= link_to h(name), :action => "node", :id => node.id %>
      • +
      • <%= link_to h(text), :action => "changeset", :id => changeset.id %>
      • <% end %>
      diff --git a/test/functional/browse_controller_test.rb b/test/functional/browse_controller_test.rb index 65e851011..a36fa3703 100644 --- a/test/functional/browse_controller_test.rb +++ b/test/functional/browse_controller_test.rb @@ -18,20 +18,27 @@ class BrowseControllerTest < ActionController::TestCase end - # This should display the last 20 nodes that were edited. + # This should display the last 20 changesets closed. def test_index - @nodes = Node.find(:all, :order => "timestamp DESC", :limit => 20) - assert @nodes.size <= 20 + @changesets = Changeset.find(:all, :order => "closed_at DESC", :limit=> 20) + assert @changesets.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})" + # Now check that all 20 (or however many were returned) changesets are in the html + assert_select "h2", :text => "#{@changesets.size} Recently Closed Changesets", :count => 1 + assert_select "ul[id='recently_changed'] li a", :count => @changesets.size + @changesets.each do |changeset| + if changeset.user.data_public? + user = changeset.user.display_name + else + user = "(anonymous)" + end + + cmt = changeset.tags_as_hash['comment'].to_s + cmt = "(no comment)" if cmt.length == 0 + text = "#{changeset.id} by #{user} - #{cmt}" + assert_select "ul[id='recently_changed'] li a[href=/browse/changeset/#{changeset.id}]", :text => text end end From d6a1ccf6b42b3eaaffe9fa417eeb4e6f56b5cdac Mon Sep 17 00:00:00 2001 From: Matt Amos Date: Tue, 23 Dec 2008 15:59:56 +0000 Subject: [PATCH 305/381] removed comments which apparently no-one bothers to read anyway. since no-one reads the comments, changed changeset model to use Time instead of DateTime. will probably have to change it back later, when someone else who doesn't read comments comes along. --- app/models/changeset.rb | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/app/models/changeset.rb b/app/models/changeset.rb index 0b214fe72..7b90659d8 100644 --- a/app/models/changeset.rb +++ b/app/models/changeset.rb @@ -27,15 +27,11 @@ class Changeset < ActiveRecord::Base # 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)). + # maximum time a changeset is allowed to be open for. MAX_TIME_OPEN = 1.day - # 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 = 1.hour # Rational(1,24) + # idle timeout increment, one hour seems reasonable. + IDLE_TIMEOUT = 1.hour # Use a method like this, so that we can easily change how we # determine whether a changeset is open, without breaking code in at @@ -46,12 +42,12 @@ class Changeset < ActiveRecord::Base # 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)) + return ((closed_at > Time.now) and (num_changes <= MAX_ELEMENTS)) end def set_closed_time_now unless is_open? - self.closed_at = DateTime.now + self.closed_at = Time.now end end @@ -156,7 +152,7 @@ class Changeset < ActiveRecord::Base 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 + self.closed_at = Time.now + IDLE_TIMEOUT end self.save! From 8fe53e482332342dc3823dcde0717d34932bcb3c Mon Sep 17 00:00:00 2001 From: Thomas Wood Date: Wed, 31 Dec 2008 16:14:39 +0000 Subject: [PATCH 306/381] Only show changesets that are actually closed in the index view. --- app/controllers/browse_controller.rb | 2 +- test/functional/browse_controller_test.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/browse_controller.rb b/app/controllers/browse_controller.rb index 2e4af03d8..237c57ab2 100644 --- a/app/controllers/browse_controller.rb +++ b/app/controllers/browse_controller.rb @@ -8,7 +8,7 @@ class BrowseController < ApplicationController end def index - @changesets = Changeset.find(:all, :order => "closed_at DESC", :limit=> 20) + @changesets = Changeset.find(:all, :order => "closed_at DESC", :conditions => ['closed_at < ?', DateTime.now], :limit=> 20) end def relation diff --git a/test/functional/browse_controller_test.rb b/test/functional/browse_controller_test.rb index a36fa3703..c4d7a5ae4 100644 --- a/test/functional/browse_controller_test.rb +++ b/test/functional/browse_controller_test.rb @@ -20,7 +20,7 @@ class BrowseControllerTest < ActionController::TestCase # This should display the last 20 changesets closed. def test_index - @changesets = Changeset.find(:all, :order => "closed_at DESC", :limit=> 20) + @changesets = Changeset.find(:all, :order => "closed_at DESC", :conditions => ['closed_at < ?', DateTime.now], :limit=> 20) assert @changesets.size <= 20 get :index assert_response :success From 898beb7b7f693cc21b329dab9a22268e3df77446 Mon Sep 17 00:00:00 2001 From: Thomas Wood Date: Wed, 31 Dec 2008 16:33:02 +0000 Subject: [PATCH 307/381] DateTime => Time for consistency in changeset code, ok by shaun. --- app/controllers/browse_controller.rb | 2 +- app/controllers/changeset_controller.rb | 6 +++--- app/views/changeset/_changeset.rhtml | 2 +- test/fixtures/changesets.yml | 6 +++--- test/functional/browse_controller_test.rb | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app/controllers/browse_controller.rb b/app/controllers/browse_controller.rb index 237c57ab2..151a7e11e 100644 --- a/app/controllers/browse_controller.rb +++ b/app/controllers/browse_controller.rb @@ -8,7 +8,7 @@ class BrowseController < ApplicationController end def index - @changesets = Changeset.find(:all, :order => "closed_at DESC", :conditions => ['closed_at < ?', DateTime.now], :limit=> 20) + @changesets = Changeset.find(:all, :order => "closed_at DESC", :conditions => ['closed_at < ?', Time.now], :limit=> 20) end def relation diff --git a/app/controllers/changeset_controller.rb b/app/controllers/changeset_controller.rb index c820aa705..8f882e80e 100644 --- a/app/controllers/changeset_controller.rb +++ b/app/controllers/changeset_controller.rb @@ -376,11 +376,11 @@ private times = time.split(/,/) raise OSM::APIBadUserInput.new("bad time range") if times.size != 2 - from, to = times.collect { |t| DateTime.parse(t) } + from, to = times.collect { |t| Time.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)] + return ['closed_at >= ?', Time.parse(time)] end else return nil @@ -396,7 +396,7 @@ private ## # restrict changes to those which are open def conditions_open(open) - return open.nil? ? nil : ['closed_at >= ?', DateTime.now] + return open.nil? ? nil : ['closed_at >= ?', Time.now] end end diff --git a/app/views/changeset/_changeset.rhtml b/app/views/changeset/_changeset.rhtml index 83b0b0ce8..e9c07f94f 100644 --- a/app/views/changeset/_changeset.rhtml +++ b/app/views/changeset/_changeset.rhtml @@ -5,7 +5,7 @@ #<%= changeset.id %> - <% if changeset.closed_at > DateTime.now %> (still editing) + <% if changeset.closed_at > Time.now %> (still editing) <% else %><%= changeset.closed_at.strftime("%d %b %Y %H:%M") %><% end %> diff --git a/test/fixtures/changesets.yml b/test/fixtures/changesets.yml index defd691d2..9e559729f 100644 --- a/test/fixtures/changesets.yml +++ b/test/fixtures/changesets.yml @@ -7,7 +7,7 @@ normal_user_first_change: id: 1 user_id: 1 created_at: "2007-01-01 00:00:00" - closed_at: <%= DateTime.now + Rational(1,24) %> + closed_at: <%= Time.now + 1.hour %> min_lon: <%= 1 * SCALE %> min_lat: <%= 1 * SCALE %> max_lon: <%= 5 * SCALE %> @@ -18,7 +18,7 @@ second_user_first_change: id: 2 user_id: 2 created_at: "2008-05-01 01:23:45" - closed_at: <%= DateTime.now + Rational(1,24) %> + closed_at: <%= Time.now + 1.hour %> num_changes: 0 normal_user_closed_change: @@ -32,7 +32,7 @@ normal_user_version_change: id: 4 user_id: 1 created_at: "2008-01-01 00:00:00" - closed_at: <%= DateTime.now + Rational(1,24) %> + closed_at: <%= Time.now + 1.hour %> min_lon: <%= 1 * SCALE %> min_lat: <%= 1 * SCALE %> max_lon: <%= 4 * SCALE %> diff --git a/test/functional/browse_controller_test.rb b/test/functional/browse_controller_test.rb index c4d7a5ae4..7be3f8497 100644 --- a/test/functional/browse_controller_test.rb +++ b/test/functional/browse_controller_test.rb @@ -20,7 +20,7 @@ class BrowseControllerTest < ActionController::TestCase # This should display the last 20 changesets closed. def test_index - @changesets = Changeset.find(:all, :order => "closed_at DESC", :conditions => ['closed_at < ?', DateTime.now], :limit=> 20) + @changesets = Changeset.find(:all, :order => "closed_at DESC", :conditions => ['closed_at < ?', Time.now], :limit=> 20) assert @changesets.size <= 20 get :index assert_response :success From 747d6f1d11037d79946396270f59fc9aa11cc629 Mon Sep 17 00:00:00 2001 From: Thomas Wood Date: Wed, 31 Dec 2008 16:43:44 +0000 Subject: [PATCH 308/381] Revert last commit - too many test failures --- app/controllers/browse_controller.rb | 2 +- app/controllers/changeset_controller.rb | 6 +++--- app/views/changeset/_changeset.rhtml | 2 +- test/fixtures/changesets.yml | 6 +++--- test/functional/browse_controller_test.rb | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app/controllers/browse_controller.rb b/app/controllers/browse_controller.rb index 151a7e11e..237c57ab2 100644 --- a/app/controllers/browse_controller.rb +++ b/app/controllers/browse_controller.rb @@ -8,7 +8,7 @@ class BrowseController < ApplicationController end def index - @changesets = Changeset.find(:all, :order => "closed_at DESC", :conditions => ['closed_at < ?', Time.now], :limit=> 20) + @changesets = Changeset.find(:all, :order => "closed_at DESC", :conditions => ['closed_at < ?', DateTime.now], :limit=> 20) end def relation diff --git a/app/controllers/changeset_controller.rb b/app/controllers/changeset_controller.rb index 8f882e80e..c820aa705 100644 --- a/app/controllers/changeset_controller.rb +++ b/app/controllers/changeset_controller.rb @@ -376,11 +376,11 @@ private times = time.split(/,/) raise OSM::APIBadUserInput.new("bad time range") if times.size != 2 - from, to = times.collect { |t| Time.parse(t) } + 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 >= ?', Time.parse(time)] + return ['closed_at >= ?', DateTime.parse(time)] end else return nil @@ -396,7 +396,7 @@ private ## # restrict changes to those which are open def conditions_open(open) - return open.nil? ? nil : ['closed_at >= ?', Time.now] + return open.nil? ? nil : ['closed_at >= ?', DateTime.now] end end diff --git a/app/views/changeset/_changeset.rhtml b/app/views/changeset/_changeset.rhtml index e9c07f94f..83b0b0ce8 100644 --- a/app/views/changeset/_changeset.rhtml +++ b/app/views/changeset/_changeset.rhtml @@ -5,7 +5,7 @@ #<%= changeset.id %> - <% if changeset.closed_at > Time.now %> (still editing) + <% if changeset.closed_at > DateTime.now %> (still editing) <% else %><%= changeset.closed_at.strftime("%d %b %Y %H:%M") %><% end %> diff --git a/test/fixtures/changesets.yml b/test/fixtures/changesets.yml index 9e559729f..defd691d2 100644 --- a/test/fixtures/changesets.yml +++ b/test/fixtures/changesets.yml @@ -7,7 +7,7 @@ normal_user_first_change: id: 1 user_id: 1 created_at: "2007-01-01 00:00:00" - closed_at: <%= Time.now + 1.hour %> + closed_at: <%= DateTime.now + Rational(1,24) %> min_lon: <%= 1 * SCALE %> min_lat: <%= 1 * SCALE %> max_lon: <%= 5 * SCALE %> @@ -18,7 +18,7 @@ second_user_first_change: id: 2 user_id: 2 created_at: "2008-05-01 01:23:45" - closed_at: <%= Time.now + 1.hour %> + closed_at: <%= DateTime.now + Rational(1,24) %> num_changes: 0 normal_user_closed_change: @@ -32,7 +32,7 @@ normal_user_version_change: id: 4 user_id: 1 created_at: "2008-01-01 00:00:00" - closed_at: <%= Time.now + 1.hour %> + closed_at: <%= DateTime.now + Rational(1,24) %> min_lon: <%= 1 * SCALE %> min_lat: <%= 1 * SCALE %> max_lon: <%= 4 * SCALE %> diff --git a/test/functional/browse_controller_test.rb b/test/functional/browse_controller_test.rb index 7be3f8497..c4d7a5ae4 100644 --- a/test/functional/browse_controller_test.rb +++ b/test/functional/browse_controller_test.rb @@ -20,7 +20,7 @@ class BrowseControllerTest < ActionController::TestCase # This should display the last 20 changesets closed. def test_index - @changesets = Changeset.find(:all, :order => "closed_at DESC", :conditions => ['closed_at < ?', Time.now], :limit=> 20) + @changesets = Changeset.find(:all, :order => "closed_at DESC", :conditions => ['closed_at < ?', DateTime.now], :limit=> 20) assert @changesets.size <= 20 get :index assert_response :success From 8e394b1625b9da053c3dc22ffcc868bcfe15c8ad Mon Sep 17 00:00:00 2001 From: Richard Fairhurst Date: Sun, 11 Jan 2009 17:09:43 +0000 Subject: [PATCH 309/381] small versioning fix (plus history rework in progress) --- app/controllers/amf_controller.rb | 43 ++++++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/app/controllers/amf_controller.rb b/app/controllers/amf_controller.rb index 80472fe25..ecc291e69 100644 --- a/app/controllers/amf_controller.rb +++ b/app/controllers/amf_controller.rb @@ -284,7 +284,13 @@ class AmfController < ApplicationController # 3. hash of tags, # 4. version, # 5. is this the current, visible version? (boolean) - + # + # *** FIXME: + # Should work by timestamp, not version (so that we can recover versions when + # a node has been changed, but not the enclosing way) + # Use strptime (http://www.ruby-doc.org/core/classes/DateTime.html) to + # to turn string back into timestamp. + def getway_old(id, version) #:doc: if version < 0 old_way = OldWay.find(:first, :conditions => ['visible = ? AND id = ?', true, id], :order => 'version DESC') @@ -305,8 +311,39 @@ class AmfController < ApplicationController # Find history of a way. Returns 'way', id, and # an array of previous versions. + # + # *** FIXME: + # Should look for changes in constituent nodes as well, + # and return timestamps. + # Heuristic: Find all nodes that have ever been part of the way; + # get a list of their revision dates; add revision dates of the way; + # sort and collapse list (to within 2 seconds); trim all dates before the + # start dateÊof the way. def getway_history(wayid) #:doc: + + # Find list of revision dates for way and all constituent nodes + revdates=[] + Way.find(wayid).old_ways.collect do |a| + revdates.push(a.timestamp) + a.nds.each do |n| + Node.find(n).old_nodes.collect do |o| + revdates.push(o.timestamp) + end + end + end + waycreated=revdates[0] + revdates.uniq! + revdates.sort! + + # Remove any dates (from nodes) before first revision date of way + revdates.delete_if { |d| dversion) - def putway(renumberednodes, usertoken, changeset, version, originalway, pointlist, attributes, nodes) #:doc: + def putway(renumberednodes, usertoken, changeset, wayversion, originalway, pointlist, attributes, nodes) #:doc: # -- Initialise @@ -567,7 +604,7 @@ class AmfController < ApplicationController new_way.tags = attributes new_way.nds = pointlist new_way.changeset_id = changeset - new_way.version = version + new_way.version = wayversion if originalway <= 0 new_way.create_with_history(user) way=new_way # so we can get way.id and way.version From d3dc9733760e0ca127abee57cfe1d017429e423e Mon Sep 17 00:00:00 2001 From: Shaun McDonald Date: Wed, 14 Jan 2009 00:17:42 +0000 Subject: [PATCH 310/381] Move the ActiveRecord::RecordNotFound to the block, so that it catches the not found in line 327, when a nonexistant way id is searched for. --- app/controllers/amf_controller.rb | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/app/controllers/amf_controller.rb b/app/controllers/amf_controller.rb index ecc291e69..4103ceda0 100644 --- a/app/controllers/amf_controller.rb +++ b/app/controllers/amf_controller.rb @@ -344,18 +344,16 @@ class AmfController < ApplicationController RAILS_DEFAULT_LOGGER.info("** revision dates: #{revdates.inspect}") RAILS_DEFAULT_LOGGER.info("** range: #{revdates[-1]-revdates[0]}") - 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, []] + 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 # Find history of a node. Returns 'node', id, and From 9bb9de0fbe307304ef4d0ea0dbeaac7972b71cb2 Mon Sep 17 00:00:00 2001 From: Thomas Wood Date: Fri, 23 Jan 2009 01:19:45 +0000 Subject: [PATCH 311/381] Require auth on calls to /trace/create, and pass through to form if called without params. Adjust validations on traces to prevent dual error messages on description (validates_presence_of catches the empty string removed from length validation) More changes to come for api_create References #1510 --- app/controllers/trace_controller.rb | 40 +++++++++++++++-------------- app/models/trace.rb | 4 +-- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/app/controllers/trace_controller.rb b/app/controllers/trace_controller.rb index 022c304fb..47041b491 100644 --- a/app/controllers/trace_controller.rb +++ b/app/controllers/trace_controller.rb @@ -1,8 +1,8 @@ class TraceController < ApplicationController layout 'site' - before_filter :authorize_web - before_filter :require_user, :only => [:mine, :edit, :delete, :make_public] + before_filter :authorize_web + before_filter :require_user, :only => [:mine, :create, :edit, :delete, :make_public] before_filter :authorize, :only => [:api_details, :api_data, :api_create] before_filter :check_database_availability, :except => [:api_details, :api_data, :api_create] before_filter :check_read_availability, :only => [:api_details, :api_data, :api_create] @@ -99,26 +99,28 @@ class TraceController < ApplicationController end def create - logger.info(params[:trace][:gpx_file].class.name) - if params[:trace][:gpx_file].respond_to?(:read) - do_create(params[:trace][:gpx_file], params[:trace][:tagstring], - params[:trace][:description], params[:trace][:public]) + if params[:trace] + logger.info(params[:trace][:gpx_file].class.name) + if params[:trace][:gpx_file].respond_to?(:read) + do_create(params[:trace][:gpx_file], params[:trace][:tagstring], + params[:trace][:description], params[:trace][:public]) - if @trace.id - logger.info("id is #{@trace.id}") - flash[:notice] = "Your GPX file has been uploaded and is awaiting insertion in to the database. This will usually happen within half an hour, and an email will be sent to you on completion." + if @trace.id + logger.info("id is #{@trace.id}") + flash[:notice] = "Your GPX file has been uploaded and is awaiting insertion in to the database. This will usually happen within half an hour, and an email will be sent to you on completion." - redirect_to :action => 'mine' + redirect_to :action => 'mine' + end + else + @trace = Trace.new({:name => "Dummy", + :tagstring => params[:trace][:tagstring], + :description => params[:trace][:description], + :public => params[:trace][:public], + :inserted => false, :user => @user, + :timestamp => Time.now}) + @trace.valid? + @trace.errors.add(:gpx_file, "can't be blank") end - else - @trace = Trace.new({:name => "Dummy", - :tagstring => params[:trace][:tagstring], - :description => params[:trace][:description], - :public => params[:trace][:public], - :inserted => false, :user => @user, - :timestamp => Time.now}) - @trace.valid? - @trace.errors.add(:gpx_file, "can't be blank") end end diff --git a/app/models/trace.rb b/app/models/trace.rb index 1b44e2187..03dbeb0b3 100644 --- a/app/models/trace.rb +++ b/app/models/trace.rb @@ -3,8 +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_length_of :name, :maximum => 255 + validates_length_of :description, :maximum => 255 # validates_numericality_of :latitude, :longitude validates_inclusion_of :public, :inserted, :in => [ true, false] From 1522ed275c3efceb9629438e0272f2f2d4adf5ba Mon Sep 17 00:00:00 2001 From: Thomas Wood Date: Fri, 23 Jan 2009 19:14:21 +0000 Subject: [PATCH 312/381] Set params to sane defaults if unset by client. Reject creates with no file with 400, model validations will also reject with 400 (as was existing but broken behaviour) Closes #1510 --- app/controllers/trace_controller.rb | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/app/controllers/trace_controller.rb b/app/controllers/trace_controller.rb index 47041b491..6528dffde 100644 --- a/app/controllers/trace_controller.rb +++ b/app/controllers/trace_controller.rb @@ -279,12 +279,20 @@ class TraceController < ApplicationController def api_create if request.post? - do_create(params[:file], params[:tags], params[:description], params[:public]) + tags = params[:tags] || "" + description = params[:description] || "" + pub = params[:public] || false + + if params[:file].respond_to?(:read) + do_create(params[:file], tags, description, pub) - if @trace.id - render :text => @trace.id.to_s, :content_type => "text/plain" - elsif @trace.valid? - render :nothing => true, :status => :internal_server_error + if @trace.id + render :text => @trace.id.to_s, :content_type => "text/plain" + elsif @trace.valid? + render :nothing => true, :status => :internal_server_error + else + render :nothing => true, :status => :bad_request + end else render :nothing => true, :status => :bad_request end From 2746a2e103410102f5bb62bf52a988a74c38e28f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dirk=20St=C3=B6cker?= Date: Thu, 29 Jan 2009 14:19:39 +0000 Subject: [PATCH 313/381] fixed typo --- app/views/user/confirm_email.rhtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/user/confirm_email.rhtml b/app/views/user/confirm_email.rhtml index 785297ec6..4be2b19ba 100644 --- a/app/views/user/confirm_email.rhtml +++ b/app/views/user/confirm_email.rhtml @@ -4,5 +4,5 @@
      - +
      From 71938aef1db11e3c7aaac0123c08f5d40c788190 Mon Sep 17 00:00:00 2001 From: Thomas Wood Date: Sat, 31 Jan 2009 13:12:19 +0000 Subject: [PATCH 314/381] Copying across fix for #1547 from trunk --- public/javascripts/map.js | 1 + 1 file changed, 1 insertion(+) diff --git a/public/javascripts/map.js b/public/javascripts/map.js index fcf34336f..608f4c264 100644 --- a/public/javascripts/map.js +++ b/public/javascripts/map.js @@ -134,6 +134,7 @@ function getMapCenter(center, zoom) { } function setMapCenter(center, zoom) { + zoom = parseInt(zoom); var numzoom = map.getNumZoomLevels(); if (zoom >= numzoom) zoom = numzoom - 1; map.setCenter(center.clone().transform(epsg4326, map.getProjectionObject()), zoom); From 509832c5b36384ebac5ef89f99472f569f78dd34 Mon Sep 17 00:00:00 2001 From: Frederik Ramm Date: Tue, 3 Feb 2009 00:57:19 +0000 Subject: [PATCH 315/381] splitting and joining tags is gone for good in 0.6 --- lib/tags.rb | 42 ------------------------------------------ 1 file changed, 42 deletions(-) delete mode 100644 lib/tags.rb diff --git a/lib/tags.rb b/lib/tags.rb deleted file mode 100644 index 2095b09af..000000000 --- a/lib/tags.rb +++ /dev/null @@ -1,42 +0,0 @@ -module Tags - def self.join(tags) - joined = tags.collect { |k,v| "#{escape_string(k)}=#{escape_string(v)}" }.join(';') - joined = '' if joined.nil? - return joined - end - - def self.escape_string(tag) - return tag.gsub(/[;=\\]/) { |v| escape_char(v) } - end - - def self.escape_char(v) - case v - when ';' then return '\\s' - when '=' then return '\\e' - end - return '\\\\' - end - - def self.split(tags) - tags.split(';').each do |tag| - key,val = tag.split('=').collect { |s| s.strip } - key = '' if key.nil? - val = '' if val.nil? - if key != '' && val != '' - yield unescape_string(key),unescape_string(val) - end - end - end - - def self.unescape_string(tag) - return tag.gsub(/\\[se\\]/) { |v| unescape_char(v) } - end - - def self.unescape_char(v) - case v - when '\\s' then return ';' - when '\\e' then return '=' - end - return '\\' - end -end From 76708eefcffb6b451ce9c275db4bcaf23690f69d Mon Sep 17 00:00:00 2001 From: Matt Amos Date: Sat, 7 Feb 2009 17:45:27 +0000 Subject: [PATCH 316/381] Fixed bug in changeset closing and querying where the number of elements exceeded the maximum. Added a fixture for this. --- app/controllers/changeset_controller.rb | 8 ++++++- app/models/changeset.rb | 2 +- test/fixtures/changesets.yml | 14 ++++++++++++ test/functional/changeset_controller_test.rb | 23 +++++++++++++++----- test/unit/changeset_test.rb | 2 +- 5 files changed, 40 insertions(+), 9 deletions(-) diff --git a/app/controllers/changeset_controller.rb b/app/controllers/changeset_controller.rb index c820aa705..b2ff42711 100644 --- a/app/controllers/changeset_controller.rb +++ b/app/controllers/changeset_controller.rb @@ -395,8 +395,14 @@ private ## # restrict changes to those which are open + # + # at the moment this code assumes we're only interested in open + # changesets and gives no facility to query closed changesets. this + # would be reasonably simple to implement if anyone actually wants + # it? def conditions_open(open) - return open.nil? ? nil : ['closed_at >= ?', DateTime.now] + return open.nil? ? nil : ['closed_at >= ? and num_changes <= ?', + DateTime.now, Changeset::MAX_ELEMENTS] end end diff --git a/app/models/changeset.rb b/app/models/changeset.rb index 7b90659d8..31d927e9a 100644 --- a/app/models/changeset.rb +++ b/app/models/changeset.rb @@ -46,7 +46,7 @@ class Changeset < ActiveRecord::Base end def set_closed_time_now - unless is_open? + if is_open? self.closed_at = Time.now end end diff --git a/test/fixtures/changesets.yml b/test/fixtures/changesets.yml index defd691d2..9fe5bc6d8 100644 --- a/test/fixtures/changesets.yml +++ b/test/fixtures/changesets.yml @@ -48,3 +48,17 @@ invalid_changeset: created_at: "2008-01-01 00:00:00" closed_at: "2008-01-02 00:00:00" num_changes: 9 + +# changeset which still has time remaining, but has been closed +# by containing too many elements. +too_many_elements_changeset: + id: 6 + 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: <%= Changeset::MAX_ELEMENTS + 1 %> + diff --git a/test/functional/changeset_controller_test.rb b/test/functional/changeset_controller_test.rb index e8648e5c3..524fad91b 100644 --- a/test/functional/changeset_controller_test.rb +++ b/test/functional/changeset_controller_test.rb @@ -65,8 +65,14 @@ class ChangesetControllerTest < ActionController::TestCase def test_close basic_authorization "test@openstreetmap.org", "test" - put :close, :id => changesets(:normal_user_first_change).id + cs_id = changesets(:normal_user_first_change).id + put :close, :id => cs_id assert_response :success + + # test that it really is closed now + cs = Changeset.find(cs_id) + assert(!cs.is_open?, + "changeset should be closed now (#{cs.closed_at} > #{Time.now}.") end ## @@ -669,7 +675,7 @@ EOF def test_query get :query, :bbox => "-10,-10, 10, 10" assert_response :success, "can't get changesets in bbox" - assert_changesets [1,4] + assert_changesets [1,4,6] get :query, :bbox => "4.5,4.5,4.6,4.6" assert_response :success, "can't get changesets in bbox" @@ -683,7 +689,7 @@ EOF 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] + assert_changesets [1,3,4,6] get :query, :user => users(:normal_user).id, :open => true assert_response :success, "can't get changesets by user and open" @@ -691,15 +697,15 @@ EOF get :query, :time => '2007-12-31' assert_response :success, "can't get changesets by time-since" - assert_changesets [1,2,4,5] + assert_changesets [1,2,4,5,6] 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] + assert_changesets [1,2,4,5,6] 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] + assert_changesets [1,4,5,6] get :query, :open => 'true' assert_response :success, "can't get changesets by open-ness" @@ -840,6 +846,11 @@ EOF changeset = Changeset.find(cs_id) assert_equal Changeset::MAX_ELEMENTS + 1, changeset.num_changes + + # check that the changeset is now closed as well + assert(!changeset.is_open?, + "changeset should have been auto-closed by exceeding " + + "element limit.") end #------------------------------------------------------------ diff --git a/test/unit/changeset_test.rb b/test/unit/changeset_test.rb index ee9d0925a..4550ffba5 100644 --- a/test/unit/changeset_test.rb +++ b/test/unit/changeset_test.rb @@ -5,7 +5,7 @@ class ChangesetTest < Test::Unit::TestCase def test_changeset_count - assert_equal 5, Changeset.count + assert_equal 6, Changeset.count end end From c54471ee33f79b7119eb284b03a845813a058876 Mon Sep 17 00:00:00 2001 From: Matt Amos Date: Sun, 8 Feb 2009 08:28:01 +0000 Subject: [PATCH 317/381] Fix for #1565. Added a couple of tests and fixed another issue with re-used placeholder IDs. --- lib/diff_reader.rb | 77 +++++++++++--- test/functional/changeset_controller_test.rb | 101 +++++++++++++++++++ 2 files changed, 162 insertions(+), 16 deletions(-) diff --git a/lib/diff_reader.rb b/lib/diff_reader.rb index 6a053e4ad..3b13e9462 100644 --- a/lib/diff_reader.rb +++ b/lib/diff_reader.rb @@ -22,6 +22,18 @@ class DiffReader @changeset = changeset end + ## + # Reads the next element from the XML document. Checks the return value + # and throws an exception if an error occurred. + def read_or_die + # NOTE: XML::Reader#read returns 0 for EOF and -1 for error. + # we allow an EOF because we are expecting this to always happen + # at the end of a document. + if @reader.read < 0 + raise APIBadUserInput.new("Unexpected end of XML document.") + end + end + ## # An element-block mapping for using the LibXML reader interface. # @@ -29,15 +41,25 @@ class DiffReader # 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 + # if the start element is empty then don't do any processing, as + # there won't be any child elements to process! + unless @reader.empty_element? + # read the first element + read_or_die + + begin + # because we read elements in DOM-style to reuse their DOM + # parsing code, we don't always read an element on each pass + # as the call to @reader.next in the innermost loop will take + # care of that for us. + if @reader.node_type == 1 # element + yield @reader.name + else + read_or_die + end + end while @reader.node_type != 15 # end element end + read_or_die end ## @@ -75,14 +97,18 @@ class DiffReader # an exception subclassing OSM::APIError will be thrown. def commit + # data structure used for mapping placeholder IDs to real IDs node_ids, way_ids, rel_ids = {}, {}, {} ids = { :node => node_ids, :way => way_ids, :relation => rel_ids} + # take the first element and check that it is an osmChange element + @reader.read + raise APIBadUserInput.new("Document element should be 'osmChange'.") if @reader.name != 'osmChange' + result = OSM::API.new.get_xml_doc result.root.name = "diffResult" - # loop at the top level, within the element (although we - # don't actually check this...) + # loop at the top level, within the element with_element do |action_name| if action_name == 'create' # create a new element. this code is agnostic of the element type @@ -96,6 +122,11 @@ class DiffReader placeholder_id = xml['id'].to_i raise OSM::APIBadXMLError.new(model, xml) if placeholder_id.nil? + # check if the placeholder ID has been given before and throw + # an exception if it has - we can't create the same element twice. + model_sym = model.to_s.downcase.to_sym + raise OSM::APIBadUserInput.new("Placeholder IDs must be unique for created elements.") if ids[model_sym].include? placeholder_id + # some elements may have placeholders for other elements in the # diff, so we must fix these before saving the element. new.fix_placeholders!(ids) @@ -104,7 +135,7 @@ class DiffReader new.create_with_history(@changeset.user) # save placeholder => allocated ID map - ids[model.to_s.downcase.to_sym][placeholder_id] = new.id + ids[model_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 @@ -122,15 +153,22 @@ class DiffReader new = model.from_xml_node(xml, false) check(model, xml, new) + # if the ID is a placeholder then map it to the real ID + model_sym = model.to_s.downcase.to_sym + is_placeholder = ids[model_sym].include? new.id + id = is_placeholder ? ids[model_sym][new.id] : new.id + # and the old one from the database - old = model.find(new.id) + old = model.find(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 + # oh, the irony... the "new" element actually contains the "old" ID + # a better name would have been client/server, but anyway... + xml_result["old_id"] = new.id.to_s + xml_result["new_id"] = 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 @@ -144,7 +182,12 @@ class DiffReader new = model.from_xml_node(xml, false) check(model, xml, new) - old = model.find(new.id) + # if the ID is a placeholder then map it to the real ID + model_sym = model.to_s.downcase.to_sym + is_placeholder = ids[model_sym].include? new.id + id = is_placeholder ? ids[model_sym][new.id] : new.id + + old = model.find(id) # can a delete have placeholders under any circumstances? # if a way is modified, then deleted is that a valid diff? @@ -152,7 +195,9 @@ class DiffReader old.delete_with_history!(new, @changeset.user) xml_result = XML::Node.new model.to_s.downcase - xml_result["old_id"] = old.id.to_s + # oh, the irony... the "new" element actually contains the "old" ID + # a better name would have been client/server, but anyway... + xml_result["old_id"] = new.id.to_s result.root << xml_result end diff --git a/test/functional/changeset_controller_test.rb b/test/functional/changeset_controller_test.rb index 524fad91b..4669df07d 100644 --- a/test/functional/changeset_controller_test.rb +++ b/test/functional/changeset_controller_test.rb @@ -205,6 +205,11 @@ EOF assert_response :success, "can't upload a deletion diff to changeset: #{@response.body}" + # check the response is well-formed + assert_select "diffResult>node", 1 + assert_select "diffResult>way", 1 + assert_select "diffResult>relation", 2 + # 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 @@ -367,6 +372,11 @@ EOF post :upload, :id => 1 assert_response :success, "can't upload multiple versions of an element in a diff: #{@response.body}" + + # check the response is well-formed. its counter-intuitive, but the + # API will return multiple elements with the same ID and different + # version numbers for each change we made. + assert_select "diffResult>node", 8 end ## @@ -429,6 +439,97 @@ EOF assert_equal @response.body, "Unknown action ping, choices are create, modify, delete." end + ## + # upload a valid changeset which has a mixture of whitespace + # to check a bug reported by ivansanchez (#1565). + def test_upload_whitespace_valid + basic_authorization "test@openstreetmap.org", "test" + + diff = < + + + + + + + +EOF + + # upload it + content diff + post :upload, :id => 1 + assert_response :success, + "can't upload a valid diff with whitespace variations to changeset: #{@response.body}" + + # check the response is well-formed + assert_select "diffResult>node", 2 + assert_select "diffResult>relation", 1 + + # check that the changes made it into the database + assert_equal 1, Node.find(1).tags.size, "node 1 should now have one tag" + assert_equal 0, Relation.find(1).tags.size, "relation 1 should now have no tags" + end + + ## + # upload a valid changeset which has a mixture of whitespace + # to check a bug reported by ivansanchez. + def test_upload_reuse_placeholder_valid + basic_authorization "test@openstreetmap.org", "test" + + diff = < + + + + + + + + + + + + +EOF + + # upload it + content diff + post :upload, :id => 1 + assert_response :success, + "can't upload a valid diff with re-used placeholders to changeset: #{@response.body}" + + # check the response is well-formed + assert_select "diffResult>node", 3 + assert_select "diffResult>node[old_id=-1]", 3 + end + + ## + # test what happens if a diff upload re-uses placeholder IDs in an + # illegal way. + def test_upload_placeholder_invalid + basic_authorization "test@openstreetmap.org", "test" + + diff = < + + + + + + +EOF + + # upload it + content diff + post :upload, :id => 1 + assert_response :bad_request, + "shouldn't be able to re-use placeholder IDs" + end + ## # when we make some simple changes we get the same changes back from the # diff download. From 12b779f0b4ad730d42f3042da5da387bc288f929 Mon Sep 17 00:00:00 2001 From: Matt Amos Date: Sun, 8 Feb 2009 16:33:48 +0000 Subject: [PATCH 318/381] Test and fix for issue #1568. Wasn't testing for end element in the right place. --- lib/diff_reader.rb | 4 ++-- test/functional/changeset_controller_test.rb | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/lib/diff_reader.rb b/lib/diff_reader.rb index 3b13e9462..452d73dab 100644 --- a/lib/diff_reader.rb +++ b/lib/diff_reader.rb @@ -47,7 +47,7 @@ class DiffReader # read the first element read_or_die - begin + while @reader.node_type != 15 do # end element # because we read elements in DOM-style to reuse their DOM # parsing code, we don't always read an element on each pass # as the call to @reader.next in the innermost loop will take @@ -57,7 +57,7 @@ class DiffReader else read_or_die end - end while @reader.node_type != 15 # end element + end end read_or_die end diff --git a/test/functional/changeset_controller_test.rb b/test/functional/changeset_controller_test.rb index 4669df07d..b5d65d46d 100644 --- a/test/functional/changeset_controller_test.rb +++ b/test/functional/changeset_controller_test.rb @@ -530,6 +530,24 @@ EOF "shouldn't be able to re-use placeholder IDs" end + ## + # test for more issues in #1568 + def test_upload_empty_invalid + basic_authorization "test@openstreetmap.org", "test" + + [ "", + "", + "", + "" + ].each do |diff| + # upload it + content diff + post :upload, :id => 1 + assert_response(:success, "should be able to upload " + + "empty changeset: " + diff) + end + end + ## # when we make some simple changes we get the same changes back from the # diff download. From 5143fbcd5bbaf59e498fff4ac7dd797342a69a04 Mon Sep 17 00:00:00 2001 From: Matt Amos Date: Sun, 8 Feb 2009 16:54:28 +0000 Subject: [PATCH 319/381] Test and fix for #1567, for teh betterer osmChange compliance. --- lib/diff_reader.rb | 19 +++++++++++----- test/functional/changeset_controller_test.rb | 23 ++++++++++++++++++++ 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/lib/diff_reader.rb b/lib/diff_reader.rb index 452d73dab..37de8ea59 100644 --- a/lib/diff_reader.rb +++ b/lib/diff_reader.rb @@ -179,14 +179,23 @@ class DiffReader # 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) + # delete doesn't have to contain a full payload, according to + # the wiki docs, so we just extract the things we need. + new_id = xml['id'].to_i + raise API::APIBadXMLError.new(model, xml, "ID attribute is required") if new_id.nil? # if the ID is a placeholder then map it to the real ID model_sym = model.to_s.downcase.to_sym - is_placeholder = ids[model_sym].include? new.id - id = is_placeholder ? ids[model_sym][new.id] : new.id + is_placeholder = ids[model_sym].include? new_id + id = is_placeholder ? ids[model_sym][new_id] : new_id + # build the "new" element by modifying the existing one + new = model.find(id) + new.changeset_id = xml['changeset'].to_i + new.version = xml['version'].to_i + check(model, xml, new) + + # fetch the matching old element from the DB old = model.find(id) # can a delete have placeholders under any circumstances? @@ -197,7 +206,7 @@ class DiffReader xml_result = XML::Node.new model.to_s.downcase # oh, the irony... the "new" element actually contains the "old" ID # a better name would have been client/server, but anyway... - xml_result["old_id"] = new.id.to_s + xml_result["old_id"] = new_id.to_s result.root << xml_result end diff --git a/test/functional/changeset_controller_test.rb b/test/functional/changeset_controller_test.rb index b5d65d46d..edc2aee93 100644 --- a/test/functional/changeset_controller_test.rb +++ b/test/functional/changeset_controller_test.rb @@ -217,6 +217,29 @@ EOF assert_equal false, Relation.find(current_relations(:used_relation).id).visible end + ## + # test uploading a delete with no lat/lon, as they are optional in + # the osmChange spec. + def test_upload_nolatlon_delete + basic_authorization "test@openstreetmap.org", "test" + + node = current_nodes(:visible_node) + cs = changesets(:normal_user_first_change) + diff = "" + + # upload it + content diff + post :upload, :id => cs.id + assert_response :success, + "can't upload a deletion diff to changeset: #{@response.body}" + + # check the response is well-formed + assert_select "diffResult>node", 1 + + # check that everything was deleted + assert_equal false, Node.find(node.id).visible + end + ## # test that deleting stuff in a transaction doesn't bypass the checks # to ensure that used elements are not deleted. From e827a0460795bc076c115f941d074dca67decaa1 Mon Sep 17 00:00:00 2001 From: Richard Fairhurst Date: Wed, 25 Feb 2009 01:14:19 +0000 Subject: [PATCH 320/381] further work on 0.6 history (not quite complete yet) --- app/controllers/amf_controller.rb | 191 +++++++++++++++--------------- app/models/old_way.rb | 4 +- config/potlatch/autocomplete.txt | 25 +++- config/potlatch/presets.txt | 173 +++++++++++++++++++++++---- lib/potlatch.rb | 4 +- public/potlatch/potlatch.swf | Bin 170218 -> 173474 bytes 6 files changed, 271 insertions(+), 126 deletions(-) diff --git a/app/controllers/amf_controller.rb b/app/controllers/amf_controller.rb index 4103ceda0..69222fc53 100644 --- a/app/controllers/amf_controller.rb +++ b/app/controllers/amf_controller.rb @@ -27,7 +27,10 @@ # return(-1,"message") <-- just puts up a dialogue # return(-2,"message") <-- also asks the user to e-mail me # -# To write to the Rails log, use RAILS_DEFAULT_LOGGER.info("message"). +# To write to the Rails log, use logger.info("message"). + +# Remaining issues: +# * version conflict when POIs and ways are reverted class AmfController < ApplicationController require 'stringio' @@ -75,7 +78,7 @@ class AmfController < ApplicationController 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_old'; results[index]=AMF.putdata(index,getway_old(args[0].to_i,args[1])) 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)) @@ -180,8 +183,9 @@ class AmfController < ApplicationController # 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 + enlarge = [(xmax-xmin)/8,0.01].min + xmin -= enlarge; ymin -= enlarge + xmax += enlarge; ymax += enlarge # check boundary is sane and area within defined # see /config/application.yml @@ -202,7 +206,7 @@ class AmfController < ApplicationController # 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] } + points = nodes_not_used_in_area.collect { |n| [n.id, n.lon, n.lat, n.tags, n.version] }.uniq # find the relations used by those nodes and ways relations = Relation.find_for_nodes(nodes_in_area.collect { |n| n.id }, :conditions => {:visible => true}) + @@ -210,7 +214,7 @@ class AmfController < ApplicationController relations = relations.collect { |relation| [relation.id,relation.version] }.uniq end - [0,ways, points, relations] + [0, ways, points, relations] rescue Exception => err [-2,"Sorry - I can't get the map for that area."] @@ -220,8 +224,9 @@ class AmfController < ApplicationController # 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 + enlarge = [(xmax-xmin)/8,0.01].min + xmin -= enlarge; ymin -= enlarge + xmax += enlarge; ymax += enlarge # check boundary is sane and area within defined # see /config/application.yml @@ -284,23 +289,24 @@ class AmfController < ApplicationController # 3. hash of tags, # 4. version, # 5. is this the current, visible version? (boolean) - # - # *** FIXME: - # Should work by timestamp, not version (so that we can recover versions when - # a node has been changed, but not the enclosing way) - # Use strptime (http://www.ruby-doc.org/core/classes/DateTime.html) to - # to turn string back into timestamp. - def getway_old(id, version) #:doc: - if version < 0 + def getway_old(id, timestamp) #:doc: + if timestamp == '' + # undelete 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? + # revert + timestamp = DateTime.strptime(timestamp, "%d %b %Y, %H:%M:%S") + old_way = OldWay.find(:first, :conditions => ['id = ? AND timestamp <= ?', id, timestamp], :order => 'timestamp DESC') + points = old_way.get_nodes_revert(timestamp) unless old_way.nil? + if !old_way.visible + return [-1, "Sorry, the way was deleted at that time - please revert to a previous version."] + end end if old_way.nil? + # *** FIXME: shouldn't this be returning an error? return [-1, id, [], {}, -1,0] else curway=Way.find(id) @@ -309,67 +315,68 @@ class AmfController < ApplicationController end end - # Find history of a way. Returns 'way', id, and - # an array of previous versions. + # Find history of a way. + # Returns 'way', id, and an array of previous versions: + # - formerly [old_way.version, old_way.timestamp.strftime("%d %b %Y, %H:%M"), old_way.visible ? 1 : 0, user, uid] + # - now [timestamp,user,uid] # - # *** FIXME: - # Should look for changes in constituent nodes as well, - # and return timestamps. - # Heuristic: Find all nodes that have ever been part of the way; + # Heuristic: Find all nodes that have ever been part of the way; # get a list of their revision dates; add revision dates of the way; # sort and collapse list (to within 2 seconds); trim all dates before the - # start dateÊof the way. + # start date of the way. def getway_history(wayid) #:doc: - # Find list of revision dates for way and all constituent nodes - revdates=[] - Way.find(wayid).old_ways.collect do |a| - revdates.push(a.timestamp) - a.nds.each do |n| - Node.find(n).old_nodes.collect do |o| - revdates.push(o.timestamp) + begin + # Find list of revision dates for way and all constituent nodes + revdates=[] + revusers={} + Way.find(wayid).old_ways.collect do |a| + revdates.push(a.timestamp) + unless revusers.has_key?(a.timestamp.to_i) then revusers[a.timestamp.to_i]=change_user(a) end + a.nds.each do |n| + Node.find(n).old_nodes.collect do |o| + revdates.push(o.timestamp) + unless revusers.has_key?(o.timestamp.to_i) then revusers[o.timestamp.to_i]=change_user(o) end + end end end + waycreated=revdates[0] + revdates.uniq! + revdates.sort! + revdates.reverse! + + # Remove any dates (from nodes) before first revision date of way + revdates.delete_if { |d| d0 then - n = OldNode.find(id, :conditions=>['version=?',version]) - else + def getpoi(id,timestamp) #:doc: + if timestamp == '' then n = Node.find(id) + else + n = OldNode.find(id, :conditions=>['timestamp=?',DateTime.strptime(timestamp, "%d %b %Y, %H:%M:%S")]) end if n @@ -729,27 +737,29 @@ RAILS_DEFAULT_LOGGER.info("** range: #{revdates[-1]-revdates[0]}") # Need a transaction so that if one item fails to delete, the whole delete fails. Way.transaction do + # delete the way + old_way = Way.find(way_id) + delete_way = Way.new + delete_way.version = way_version + delete_way.changeset_id = changeset_id + old_way.delete_with_history!(delete_way, user) + # 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] + delete_node.changeset_id = changeset_id + delete_node.version = node_id_version[node_id.to_s] 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 # transaction [0, way_id] rescue OSM::APIChangesetAlreadyClosedError => ex @@ -770,11 +780,6 @@ RAILS_DEFAULT_LOGGER.info("** range: #{revdates[-1]-revdates[0]}") # ==================================================================== # 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 # FIXME needs version, changeset, and user # Fixme make sure this doesn't depend on anything and delete this, as potlatch diff --git a/app/models/old_way.rb b/app/models/old_way.rb index da9cf0697..df2ab3fa4 100644 --- a/app/models/old_way.rb +++ b/app/models/old_way.rb @@ -134,10 +134,10 @@ class OldWay < ActiveRecord::Base points end - def get_nodes_revert + def get_nodes_revert(timestamp) points=[] self.nds.each do |n| - oldnode=OldNode.find(:first, :conditions=>['id=? AND timestamp<=?',n,self.timestamp], :order=>"timestamp DESC") + oldnode=OldNode.find(:first, :conditions=>['id=? AND timestamp<=?',n,timestamp], :order=>"timestamp DESC") curnode=Node.find(n) id=n; v=curnode.visible ? 1 : 0 if oldnode.lat!=curnode.lat or oldnode.lon!=curnode.lon or oldnode.tags!=curnode.tags then diff --git a/config/potlatch/autocomplete.txt b/config/potlatch/autocomplete.txt index ad6cb0154..a3c80c7da 100755 --- a/config/potlatch/autocomplete.txt +++ b/config/potlatch/autocomplete.txt @@ -82,12 +82,31 @@ is_in/way - note/point - note/POI - note/way - -source/point - -source/POI - -source/way - +source/point survey,Yahoo,NPE,local_knowledge,GPS,cadastre +source/POI survey,Yahoo,NPE,local_knowledge,GPS,cadastre +source/way survey,Yahoo,NPE,local_knowledge,GPS,cadastre postal_code/point - postal_code/POI - postal_code/way - description/point - description/POI - description/way - +addr:housenumber/point - +addr:street/point - +addr:full/point - +addr:postcode/point - +addr:city/point - +addr:country/point - +addr:housenumber/POI - +addr:street/POI - +addr:full/POI - +addr:postcode/POI - +addr:city/POI - +addr:country/POI - +addr:housenumber/way - +addr:street/way - +addr:full/way - +addr:postcode/way - +addr:city/way - +addr:country/way - +addr:interpolation/way even,odd,all,alphabetic diff --git a/config/potlatch/presets.txt b/config/potlatch/presets.txt index c46f9d12a..6be23b993 100644 --- a/config/potlatch/presets.txt +++ b/config/potlatch/presets.txt @@ -12,17 +12,16 @@ way/footway 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 +paved track: highway=track,foot=,surface=paved +gravel track: highway=track,foot=,surface=gravel +dirt track: highway=track,foot=,surface=dirt +grass track: highway=track,foot=,surface=grass way/cycleway -cycle lane: highway=cycleway,cycleway=lane,ncn_ref= -cycle track: highway=cycleway,cycleway=track,ncn_ref= -cycle lane (NCN): highway=cycleway,cycleway=lane,name=(type name here),ncn_ref=(type route number) -cycle track (NCN): highway=cycleway,cycleway=track,name=(type name here),ncn_ref=(type route number) +cycle track: highway=cycleway,ncn_ref=,rcn_ref=,lcn_ref= +cycle track (national route): highway=cycleway,ncn_ref=(type route number) +cycle track (regional route): highway=cycleway,rcn_ref=(type route number) +cycle track (local route): highway=cycleway,lcn_ref=(type route number) way/waterway canal: waterway=canal,name=(type name here) @@ -42,9 +41,57 @@ disused railway tracks: railway=disused course of old railway: railway=abandoned railway platform: railway=platform +way/tourism +archaeological: place=,tourism=,historic=archaeological_site,name=(type name here) +attraction: place=,tourism=attraction,historic=,amenity=,name=(type name here) +campsite: place=,tourism=camp_site,historic=,amenity=,name=(type name here) +caravan site: place=,tourism=camp_site,historic=,amenity=,name=(type name here) +castle: place=,tourism=,historic=castle,name=(type name here) +hotel: place=,tourism=hotel,historic=,amenity=,name=(type name here),operator=(type chain here) +museum: place=,tourism=museum,historic=,amenity=,name=(type name here) +ruins: place=,tourism=,historic=ruins,name=(type name here) + +way/recreation +golf course: landuse=,leisure=golf_course +pitch: landuse=,leisure=pitch, sport=(type sport here) +playground: landuse=,leisure=playground +recreation ground: landuse=recreation_ground,leisure= +sports centre: landuse=,leisure=sports_centre +stadium: landuse=,leisure=stadium + +way/utility +college: place=,tourism=,amenity=college,name=(type name here) +school: place=,tourism=,amenity=school,name=(type name here) +hospital: place=,tourism=,amenity=hospital,name=(type name here) +library: place=,tourism=,amenity=library,name=(type name here) +university: place=,tourism=,amenity=university,name=(type name here) + way/natural -lake: natural=water,landuse= -forest: landuse=forest,natural= +coastline: natural=coastline,landuse=,leisure= +fell: natural=fell,landuse=,leisure= +heath: natural=heath,landuse=,leisure= +lake: natural=water,landuse=,leisure= +forest: landuse=forest,natural=,leisure= +marsh: natural=marsh,landuse=,leisure= +nature reserve: leisure=nature_reserve,landuse=,natural= +scree: natural=scree,landuse=,leisure= +woodland: natural=wood,landuse=,leisure= + +way/landuse +allotments: landuse=allotments,leisure= +building site: landuse=construction,leisure= +commercial: landuse=commercial,leisure= +common: landuse=,leisure=common +farm: landuse=farm,leisure= +farmyard: landuse=farmyard,leisure= +industry: landuse=industrial,leisure= +landfill site: landuse=landfill,leisure= +park: leisure=park,landuse= +quarry: landuse=quarry,leisure= +reservoir: landuse=reservoir,leisure= +residential: landuse=residential,leisure= +retail: landuse=retail,leisure= +village green: landuse=village_green,leisure= point/road mini roundabout: place=,highway=mini_roundabout @@ -57,20 +104,25 @@ stile: place=,highway=stile cattle grid: place=,highway=cattle_grid point/cycleway -gate: place=,highway=gate +bike park: place=,highway=,amenity=bicycle_parking,capacity=(type number of spaces) +gate: place=,highway=gate,amenity=,capacity= point/waterway -lock gate: place=,waterway=lock_gate -weir: place=,waterway=weir -aqueduct: place=,waterway=aqueduct -winding hole: place=,waterway=turning_point -mooring: place=,waterway=mooring +lock: place=,waterway=,lock=yes,name=(type name here) +single lockgate: place=,waterway=lock_gate,lock= +weir: place=,waterway=weir,lock= +aqueduct: place=,waterway=aqueduct,lock= +winding hole: place=,waterway=turning_point,lock= +mooring: place=,waterway=mooring,lock= point/railway station: place=,railway=station,name=(type name here) viaduct: place=,railway=viaduct level crossing: place=,railway=crossing +point/landmark +pylon: man_made=,power=tower + point/natural peak: place=,natural=peak @@ -78,8 +130,13 @@ POI/road car park: place=,amenity=parking petrol station: place=,amenity=fuel +POI/footway +bench: amenity=bench + POI/cycleway -bike park: place=,amenity=bicycle_parking +bike park: place=,shop=,amenity=bicycle_parking,capacity=(type number of spaces) +bike rental: place=,amenity=bicycle_rental,capacity=(type number of bikes) +bike shop: place=,shop=bicycle POI/place city: place=city,name=(type name here),is_in=(type region or county) @@ -89,14 +146,78 @@ village: place=village,name=(type name here),is_in=(type region or county) hamlet: place=hamlet,name=(type name here),is_in=(type region or county) POI/tourism -attraction: place=,tourism=attraction,amenity=,religion=,denomination= -church: place=,tourism=,amenity=place_of_worship,name=(type name here),religion=christian,denomination=(type denomination here) -hotel: place=,tourism=hotel,amenity=,religion=,denomination= -other religious: place=,tourism=,amenity=place_of_worship,name=(type name here),religion=(type religion),denomination= -post box: place=,amenity=post_box,tourism=,name=,religion=,denomination= -post office: place=,amenity=post_office,tourism=,name=,religion=,denomination= -pub: place=,tourism=,amenity=pub,name=(type name here),religion=,denomination= -school: place=,tourism=,amenity=school,name=(type name here),religion=,denomination= +archaeological: place=,tourism=,historic=archaeological_site,name=(type name here) +artwork: place=,tourism=artwork,historic=,amenity= +attraction: place=,tourism=attraction,historic=,amenity=,name=(type name here) +cafe: place=,tourism=,historic=,amenity=cafe,name=(type name here) +campsite: place=,tourism=camp_site,historic=,amenity=,name=(type name here) +caravan site: place=,tourism=camp_site,historic=,amenity=,name=(type name here) +castle: place=,tourism=,historic=castle,name=(type name here) +cinema: place=,tourism=,historic=,amenity=cinema,name=(type name here),operator=(type chain here) +fast food: place=,tourism=,historic=,amenity=fast_food,name=(type name here) +guesthouse: place=,tourism=guest_house,historic=,amenity=,name=(type name here) +hostel: place=,tourism=hostel,historic=,amenity=,name=(type name here),operator=(type chain here) +hotel: place=,tourism=hotel,historic=,amenity=,name=(type name here),operator=(type chain here) +monument: place=,tourism=,historic=monument,name=(type name here) +museum: place=,tourism=museum,historic=,amenity=,name=(type name here) +picnic site: place=,tourism=picnic_site,historic= +pub: place=,tourism=,historic=,amenity=pub,name=(type name here) +restaurant: place=,tourism=,historic=,amenity=restaurant,name=(type name here) +ruins: place=,tourism=,historic=ruins,name=(type name here) +viewpoint: place=,tourism=viewpoint,historic= + +POI/landmark +church: man_made=,amenity=place_of_worship,name=(type name here),religion=christian,denomination=(type denomination here),power= +other religious: man_made=,amenity=place_of_worship,name=(type name here),religion=(type religion),denomination=,power= +lighthouse: man_made=lighthouse,power=,amenity=,name=,religion=,denomination= +pylon: man_made=,power=tower,amenity=,name=,religion=,denomination= +windmill: man_made=windmill,power=,amenity=,name=,religion=,denomination= + +POI/recreation +golf course: leisure=golf_course +pitch: leisure=pitch, sport=(type sport here) +playground: leisure=playground +recreation ground: landuse=recreation_ground,leisure= +sports centre: leisure=sports_centre +stadium: leisure=stadium + +POI/shop +bank: amenity=bank,shop=,operator=(type bank name) +bike shop: amenity=,shop=bicycle,name=(type name here),operator=(type chain here) +bookshop: amenity=,shop=books,name=(type name here),operator=(type chain here) +butchers: amenity=,shop=butcher,name=(type name here),operator=(type chain here) +chemists: amenity=,shop=chemist,name=(type name here),operator=(type chain here) +convenience store: amenity=,shop=convenience,operator=(type chain here) +department store: amenity=,shop=department_store,operator=(type chain here) +DIY: amenity=,shop=doityourself,operator=(type chain here) +garden centre: amenity=,shop=garden_centre,name=(type name here),operator=(type chain here) +laundry: amenity=,shop=laundry,name=(type name here),operator=(type chain here) +off-licence: amenity=,shop=alcohol,name=(type name here),operator=(type chain here) +outdoor: amenity=,shop=outdoor,name=(type name here),operator=(type chain here) +pharmacy: amenity=pharmacy,shop=,name=(type name here),operator=(type chain here) +post office: amenity=post_office,shop=,name=(type name here) +supermarket: amenity=,shop=supermarket,operator=(type chain here) + +POI/utility +college: place=,tourism=,amenity=college,name=(type name here) +post box: place=,amenity=post_box,tourism=,name=,ref=(type code here) +recycling: place=,amenity=recycling,tourism=,name=,ref=(type code here) +school: place=,tourism=,amenity=school,name=(type name here) +surgery: place=,tourism=,amenity=doctors,name=(type name here) +hospital: place=,tourism=,amenity=hospital,name=(type name here) +library: place=,tourism=,amenity=library,name=(type name here) +phone box: place=,tourism=,amenity=telephone,name=(type name here) +toilets: place=,tourism=,amenity=toilets,name=(type name here) +university: place=,tourism=,amenity=university,name=(type name here) POI/natural peak: place=,natural=peak + +point/address +address: addr:housenumber=(type house number),addr:street=(type street name),addr:postcode=(type postcode),addr:city=(type town name) + +POI/address +address: addr:housenumber=(type house number),addr:street=(type street name),addr:postcode=(type postcode),addr:city=(type town name) + +way/address +address: addr:housenumber=(type house number),addr:street=(type street name),addr:interpolation=(type pattern of house numbers),addr:postcode=(type postcode),addr:city=(type town name) diff --git a/lib/potlatch.rb b/lib/potlatch.rb index ebafbce00..cfb602817 100644 --- a/lib/potlatch.rb +++ b/lib/potlatch.rb @@ -147,7 +147,7 @@ module Potlatch presetcategory=$2 presetmenus[presettype].push(presetcategory) presetnames[presettype][presetcategory]=["(no preset)"] - elsif (t=~/^(.+):\s?(.+)$/) then + elsif (t=~/^([\w\s]+):\s?(.+)$/) then pre=$1; kv=$2 presetnames[presettype][presetcategory].push(pre) presets[pre]={} @@ -191,7 +191,7 @@ module Potlatch File.open("#{RAILS_ROOT}/config/potlatch/autocomplete.txt") do |file| file.each_line {|line| t=line.chomp - if (t=~/^(\w+)\/(\w+)\s+(.+)$/) then + if (t=~/^([\w:]+)\/(\w+)\s+(.+)$/) then tag=$1; type=$2; values=$3 if values=='-' then autotags[type][tag]=[] else autotags[type][tag]=values.split(',').sort.reverse end diff --git a/public/potlatch/potlatch.swf b/public/potlatch/potlatch.swf index 5a9b7572a5864593c1c41cc0398e8d5c3da963b3..2d237cd2236db223f83ee7b205ecfab07e42f4de 100755 GIT binary patch literal 173474 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
      -<% if params['mlon'] and params['mlat'] %> -<% marker = true %> -<% mlon = h(params['mlon']) %> -<% mlat = h(params['mlat']) %> -<% end %> +<% +if params['mlon'] and params['mlat'] + marker = true + mlon = h(params['mlon']) + mlat = h(params['mlat']) +end -<% if params['minlon'] and params['minlat'] and params['maxlon'] and params['maxlat'] %> -<% bbox = true %> -<% minlon = h(params['minlon']) %> -<% minlat = h(params['minlat']) %> -<% maxlon = h(params['maxlon']) %> -<% maxlat = h(params['maxlat']) %> -<% end %> +if params['minlon'] and params['minlat'] and params['maxlon'] and params['maxlat'] + bbox = true + minlon = h(params['minlon']) + minlat = h(params['minlat']) + maxlon = h(params['maxlon']) + maxlat = h(params['maxlat']) +end -<% if params['lon'] and params['lat'] %> -<% lon = h(params['lon']) %> -<% lat = h(params['lat']) %> -<% zoom = h(params['zoom'] || '5') %> -<% layers = h(params['layers']) %> -<% elsif params['mlon'] and params['mlat'] %> -<% lon = h(params['mlon']) %> -<% lat = h(params['mlat']) %> -<% zoom = h(params['zoom'] || '12') %> -<% layers = h(params['layers']) %> -<% elsif cookies.key?("_osm_location") %> -<% lon,lat,zoom,layers = cookies["_osm_location"].split("|") %> -<% elsif @user and !@user.home_lon.nil? and !@user.home_lat.nil? %> -<% lon = @user.home_lon %> -<% lat = @user.home_lat %> -<% zoom = '10' %> -<% else %> -<% session[:location] = OSM::IPLocation(request.env['REMOTE_ADDR']) unless session[:location] %> -<% if session[:location] %> -<% bbox = true %> -<% minlon = session[:location][:minlon] %> -<% minlat = session[:location][:minlat] %> -<% maxlon = session[:location][:maxlon] %> -<% maxlat = session[:location][:maxlat] %> -<% else %> -<% lon = '-0.1' %> -<% lat = '51.5' %> -<% zoom = h(params['zoom'] || '5') %> -<% end %> -<% layers = h(params['layers']) %> -<% end %> +# Decide on a lat lon to initialise the map with. Various ways of doing this +if params['lon'] and params['lat'] + lon = h(params['lon']) + lat = h(params['lat']) + zoom = h(params['zoom'] || '5') + layers = h(params['layers']) + +elsif params['mlon'] and params['mlat'] + lon = h(params['mlon']) + lat = h(params['mlat']) + zoom = h(params['zoom'] || '12') + layers = h(params['layers']) + +elsif cookies.key?("_osm_location") + lon,lat,zoom,layers = cookies["_osm_location"].split("|") + +elsif @user and !@user.home_lon.nil? and !@user.home_lat.nil? + lon = @user.home_lon + lat = @user.home_lat + zoom = '10' +else + session[:location] = OSM::IPLocation(request.env['REMOTE_ADDR']) unless session[:location] + + if session[:location] + bbox = true + minlon = session[:location][:minlon] + minlat = session[:location][:minlat] + maxlon = session[:location][:maxlon] + maxlat = session[:location][:maxlat] + else + lon = '-0.1' + lat = '51.5' + zoom = h(params['zoom'] || '5') + end + layers = h(params['layers']) +end +%> <%= javascript_include_tag '/openlayers/OpenLayers.js' %> <%= javascript_include_tag '/openlayers/OpenStreetMap.js' %> From afe8dd51ece21325eea8a108515b9426764de5b2 Mon Sep 17 00:00:00 2001 From: Harry Wood Date: Mon, 23 Mar 2009 16:39:59 +0000 Subject: [PATCH 328/381] Add 'history' tab + Move 'recent changes' changeset list to re-use the same _changeset.rhtml partial --- app/controllers/browse_controller.rb | 4 +- app/controllers/changeset_controller.rb | 88 ++++++++++++++++--- app/controllers/export_controller.rb | 6 ++ app/views/browse/index.rhtml | 16 ---- app/views/changeset/_changeset.rhtml | 12 ++- .../changeset/_changeset_paging_nav.rhtml | 12 ++- app/views/changeset/list.rhtml | 12 ++- app/views/changeset/list_bbox.rhtml | 50 +++++++++++ app/views/changeset/list_user.rhtml | 15 ++++ app/views/layouts/site.rhtml | 3 + app/views/site/index.rhtml | 3 +- app/views/user/view.rhtml | 2 +- 12 files changed, 182 insertions(+), 41 deletions(-) delete mode 100644 app/views/browse/index.rhtml create mode 100644 app/views/changeset/list_bbox.rhtml create mode 100644 app/views/changeset/list_user.rhtml diff --git a/app/controllers/browse_controller.rb b/app/controllers/browse_controller.rb index 237c57ab2..10145a50a 100644 --- a/app/controllers/browse_controller.rb +++ b/app/controllers/browse_controller.rb @@ -7,9 +7,7 @@ class BrowseController < ApplicationController def start end - def index - @changesets = Changeset.find(:all, :order => "closed_at DESC", :conditions => ['closed_at < ?', DateTime.now], :limit=> 20) - end + def relation begin diff --git a/app/controllers/changeset_controller.rb b/app/controllers/changeset_controller.rb index b2ff42711..ebad59aa0 100644 --- a/app/controllers/changeset_controller.rb +++ b/app/controllers/changeset_controller.rb @@ -238,6 +238,7 @@ class ChangesetController < ApplicationController conditions = cond_merge conditions, conditions_user(params['user']) conditions = cond_merge conditions, conditions_time(params['time']) conditions = cond_merge conditions, conditions_open(params['open']) + conditions = cond_merge conditions, conditions_closed(params['closed']) # create the results document results = OSM::API.new.get_xml_doc @@ -291,21 +292,71 @@ class ChangesetController < ApplicationController render ex.render_opts end + + ## - # list edits belonging to a user + # list edits (open changesets) in reverse chronological order def list + conditions = conditions_nonempty + + + # @changesets = Changeset.find(:all, :order => "closed_at DESC", :conditions => ['closed_at < ?', DateTime.now], :limit=> 20) + + + #@edit_pages, @edits = paginate(:changesets, + # :include => [:user, :changeset_tags], + # :conditions => conditions, + # :order => "changesets.created_at DESC", + # :per_page => 20) + # + + @edits = Changeset.find(:all, + :order => "changesets.created_at DESC", + :conditions => conditions, + :limit => 20) + + end + + ## + # list edits (changesets) belonging to a user + def list_user + #find user by display name user = User.find(:first, :conditions => [ "visible = ? and display_name = ?", true, params[:display_name]]) + + conditions = conditions_user(user.id); + conditions = cond_merge conditions, conditions_nonempty @edit_pages, @edits = paginate(:changesets, :include => [:user, :changeset_tags], - :conditions => ["changesets.user_id = ? AND min_lat IS NOT NULL", user.id], + :conditions => conditions, :order => "changesets.created_at DESC", :per_page => 20) - @action = 'list' @display_name = user.display_name # FIXME needs rescues in here end - + + ## + # list changesets in a bbox + def list_bbox + # support 'bbox' param or alternatively 'minlon', 'minlat' etc + if params['bbox'] + bbox = params['bbox'] + elsif params['minlon'] and params['minlat'] and params['maxlon'] and params['maxlat'] + bbox = params['minlon'] + ',' + params['minlat'] + ',' + params['maxlon'] + ',' + params['maxlat'] + end + + conditions = conditions_bbox(bbox); + conditions = cond_merge conditions, conditions_nonempty + + @edit_pages, @edits = paginate(:changesets, + :include => [:user, :changeset_tags], + :conditions => conditions, + :order => "changesets.created_at DESC", + :per_page => 20) + + @bbox = sanitise_boundaries(bbox.split(/,/)) unless bbox==nil + end + private #------------------------------------------------------------ # utility functions below. @@ -317,7 +368,7 @@ private if a and b a_str = a.shift b_str = b.shift - return [ a_str + " and " + b_str ] + a + b + return [ a_str + " AND " + b_str ] + a + b elsif a return a else b @@ -366,7 +417,7 @@ private end ## - # restrict changes to those during a particular time period + # restrict changes to those closed 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 @@ -394,15 +445,28 @@ private end ## - # restrict changes to those which are open - # - # at the moment this code assumes we're only interested in open - # changesets and gives no facility to query closed changesets. this - # would be reasonably simple to implement if anyone actually wants - # it? + # return changesets which are open (haven't been closed yet) + # we do this by seeing if the 'closed at' time is in the future. Also if we've + # hit the maximum number of changes then it counts as no longer open. + # if parameter 'open' is nill then open and closed changsets are returned def conditions_open(open) return open.nil? ? nil : ['closed_at >= ? and num_changes <= ?', DateTime.now, Changeset::MAX_ELEMENTS] end + + ## + # query changesets which are closed + # ('closed at' time has passed or changes limit is hit) + def conditions_closed(closed) + return closed.nil? ? nil : ['closed_at < ? and num_changes > ?', + DateTime.now, Changeset::MAX_ELEMENTS] + end + ## + # eliminate empty changesets (where the bbox has not been set) + # this should be applied to all changeset list displays + def conditions_nonempty() + return ['min_lat IS NOT NULL'] + end + end diff --git a/app/controllers/export_controller.rb b/app/controllers/export_controller.rb index a773d4b72..754cc4b82 100644 --- a/app/controllers/export_controller.rb +++ b/app/controllers/export_controller.rb @@ -2,18 +2,24 @@ class ExportController < ApplicationController def start end + #When the user clicks 'Export' we redirect to a URL which generates the export download def finish bbox = BoundingBox.new(params[:minlon], params[:minlat], params[:maxlon], params[:maxlat]) format = params[:format] if format == "osm" + #redirect to API map get redirect_to "http://api.openstreetmap.org/api/#{API_VERSION}/map?bbox=#{bbox}" + elsif format == "mapnik" + #redirect to a special 'export' cgi script format = params[:mapnik_format] scale = params[:mapnik_scale] redirect_to "http://tile.openstreetmap.org/cgi-bin/export?bbox=#{bbox}&scale=#{scale}&format=#{format}" + elsif format == "osmarender" + #redirect to the t@h 'MapOf' service format = params[:osmarender_format] zoom = params[:osmarender_zoom].to_i width = bbox.slippy_width(zoom).to_i diff --git a/app/views/browse/index.rhtml b/app/views/browse/index.rhtml deleted file mode 100644 index fa7e13ac0..000000000 --- a/app/views/browse/index.rhtml +++ /dev/null @@ -1,16 +0,0 @@ -

      <%= @changesets.length %> Recently Closed Changesets

      -
        -<% @changesets.each do |changeset| - if changeset.user.data_public? - user = changeset.user.display_name - else - user = "(anonymous)" - end - - cmt = changeset.tags_as_hash['comment'].to_s - cmt = "(no comment)" if cmt.length == 0 - text = "#{changeset.id} by #{user} - #{cmt}" -%> -
      • <%= link_to h(text), :action => "changeset", :id => changeset.id %>
      • -<% end %> -
      diff --git a/app/views/changeset/_changeset.rhtml b/app/views/changeset/_changeset.rhtml index 69d8933f6..67f3e72bb 100644 --- a/app/views/changeset/_changeset.rhtml +++ b/app/views/changeset/_changeset.rhtml @@ -8,9 +8,19 @@ <% if changeset.closed_at > DateTime.now %> (still editing) <% else %><%= changeset.closed_at.strftime("%d %b %Y %H:%M") %><% end %> + + <%if showusername==true %> + + <% if changeset.user.data_public? %> + <%= link_to h(changeset.user.display_name), :controller => "user", :action => "view", :display_name => changeset.user.display_name %> + <% else %> + annon + <% end %> + <% end %> + <% if changeset.tags['comment'] %> - <%= changeset.tags['comment'] %> + <%= h(changeset.tags['comment']) %> <% else %> (none) <% end %> diff --git a/app/views/changeset/_changeset_paging_nav.rhtml b/app/views/changeset/_changeset_paging_nav.rhtml index df84a3930..7be15305d 100644 --- a/app/views/changeset/_changeset_paging_nav.rhtml +++ b/app/views/changeset/_changeset_paging_nav.rhtml @@ -7,6 +7,12 @@ if (current_page.first_item < current_page.last_item) # if more than 1 changeset end %> of <%= @edit_pages.item_count %>) -<% if @edit_pages.page_count > 1 %> - | <%= pagination_links_each(@edit_pages, {}) { |n| link_to(n, :display_name => @display_name, :page => n) } %> -<% end %> +<% +if @edit_pages.page_count > 1 + bboxparam = h(params['bbox']) + bboxparam = nil if bboxparam=="" +%> + | <%= pagination_links_each(@edit_pages, {}) { |n| link_to(n, :display_name => @display_name, :bbox => bboxparam , :page => n) } %> +<% +end +%> diff --git a/app/views/changeset/list.rhtml b/app/views/changeset/list.rhtml index cea156320..0edd951ce 100644 --- a/app/views/changeset/list.rhtml +++ b/app/views/changeset/list.rhtml @@ -1,15 +1,19 @@ -

      Edits by <%= link_to(@display_name, {:controller=>'user', :action=>'view', :display_name=>@display_name}) %>

      -<%= render :partial => 'changeset_paging_nav' %> +

      Recent Changes

      +Recently closed changesets: + - <%= render :partial => 'changeset', :collection => @edits unless @edits.nil? %> + <%= render :partial => 'changeset', :locals => {:showusername => true}, :collection => @edits unless @edits.nil? %>
      ID Saved atUser Comment Area
      -<%= render :partial => 'changeset_paging_nav' %> +

      +For more changesets, select a user and view their edits, or see the editing 'history' of a specific area. +

      +
      diff --git a/app/views/changeset/list_bbox.rhtml b/app/views/changeset/list_bbox.rhtml new file mode 100644 index 000000000..f1d8633f0 --- /dev/null +++ b/app/views/changeset/list_bbox.rhtml @@ -0,0 +1,50 @@ +

      History

      +<% +if @bbox!=nil + lon1 = @bbox[0] + lat1 = @bbox[1] + lon2 = @bbox[2] + lat2 = @bbox[3] + + %> +

      +Changsets within the area: +(<%= format("%0.3f",lat1) -%>,<%= format("%0.3f",lon1) -%>) to +(<%= format("%0.3f",lat2) -%>,<%= format("%0.3f",lon2) -%>) +

      + +<% if @edits.nil? or @edits.empty? %> +

      No changesets

      +<% else %> + +<%= render :partial => 'changeset_paging_nav' %> + + + + + + + + + + + <%= render :partial => 'changeset', :locals => {:showusername => true}, :collection => @edits unless @edits.nil? %> +
      IDSaved atUserCommentArea
      + +<%= render :partial => 'changeset_paging_nav' %> + +<% + end + +else + #bbox is nil. happens if the user surfs to this page directly. +%> + +

      No area specified

      +

      First use the view tab to pan and zoom to an area of interest, then click the history tab

      + +<% +end +%> +
      +
      diff --git a/app/views/changeset/list_user.rhtml b/app/views/changeset/list_user.rhtml new file mode 100644 index 000000000..a2096adef --- /dev/null +++ b/app/views/changeset/list_user.rhtml @@ -0,0 +1,15 @@ +

      Edits by <%= link_to(@display_name, {:controller=>'user', :action=>'view', :display_name=>@display_name}) %>

      +<%= render :partial => 'changeset_paging_nav' %> + + + + + + + + + + <%= render :partial => 'changeset', :locals => {:showusername => false}, :collection => @edits unless @edits.nil? %> +
      IDSaved atCommentArea
      + +<%= render :partial => 'changeset_paging_nav' %> diff --git a/app/views/layouts/site.rhtml b/app/views/layouts/site.rhtml index cc8f7192d..49c0ae376 100644 --- a/app/views/layouts/site.rhtml +++ b/app/views/layouts/site.rhtml @@ -38,16 +38,19 @@ <% viewclass = '' editclass = '' + historyclass = '' exportclass = '' traceclass = '' viewclass = 'active' if params['controller'] == 'site' and params['action'] == 'index' editclass = 'active' if params['controller'] == 'site' and params['action'] == 'edit' + historyclass = 'active' if params['controller'] == 'changeset' and params['action'] == 'list_bbox' exportclass = 'active' if params['controller'] == 'site' and params['action'] == 'export' traceclass = 'active' if params['controller'] == 'trace' diaryclass = 'active' if params['controller'] == 'diary_entry' %>
    • <%= link_to 'View', {:controller => 'site', :action => 'index'}, {:id => 'viewanchor', :title => 'view maps', :class => viewclass} %>
    • <%= link_to 'Edit', {:controller => 'site', :action => 'edit'}, {:id => 'editanchor', :title => 'edit maps', :class => editclass} %>
    • +
    • <%= link_to 'History', {:controller => 'history' }, {:id => 'historyanchor', :title => 'changeset history', :class => historyclass} %>
    • <% if params['controller'] == 'site' and (params['action'] == 'index' or params['action'] == 'export') %>
    • <%= link_to_remote 'Export', {:url => {:controller => 'export', :action => 'start'}}, {:id => 'exportanchor', :title => 'export map data', :class => exportclass, :href => url_for(:controller => 'site', :action => 'export')} %>
    • <% else %> diff --git a/app/views/site/index.rhtml b/app/views/site/index.rhtml index 99024f176..35509f4e0 100644 --- a/app/views/site/index.rhtml +++ b/app/views/site/index.rhtml @@ -164,8 +164,9 @@ end var lonlat = getMapCenter(); var zoom = map.getZoom(); var layers = getMapLayers(); + var extents = getMapExtent(); - updatelinks(lonlat.lon, lonlat.lat, zoom, layers); + updatelinks(lonlat.lon, lonlat.lat, zoom, layers, extents); document.cookie = "_osm_location=" + lonlat.lon + "|" + lonlat.lat + "|" + zoom + "|" + layers; } diff --git a/app/views/user/view.rhtml b/app/views/user/view.rhtml index 4f2a168a1..d12198ca8 100644 --- a/app/views/user/view.rhtml +++ b/app/views/user/view.rhtml @@ -5,7 +5,7 @@ <%= 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 edits', :controller => 'changeset', :action => 'list', :display_name => @user.display_name %> +| <%= link_to 'my edits', :controller => 'changeset', :action => 'list_user', :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 %> From d3c60874092c63b493818f227f31027327b595e3 Mon Sep 17 00:00:00 2001 From: Harry Wood Date: Mon, 23 Mar 2009 16:41:18 +0000 Subject: [PATCH 329/381] js changes for 'history' tab --- config/routes.rb | 11 +++++++---- public/javascripts/map.js | 2 +- public/javascripts/site.js | 27 ++++++++++++++++++++++++++- 3 files changed, 34 insertions(+), 6 deletions(-) diff --git a/config/routes.rb b/config/routes.rb index 4431c4765..fcf22bced 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -71,7 +71,7 @@ ActionController::Routing::Routes.draw do |map| map.connect "api/#{API_VERSION}/swf/trackpoints", :controller =>'swf', :action =>'trackpoints' # Data browsing - map.connect '/browse', :controller => 'browse', :action => 'index' + map.connect '/browse', :controller => 'changeset', :action => 'list' map.connect '/browse/start', :controller => 'browse', :action => 'start' map.connect '/browse/way/:id', :controller => 'browse', :action => 'way', :id => /\d+/ map.connect '/browse/way/:id/history', :controller => 'browse', :action => 'way_history', :id => /\d+/ @@ -80,11 +80,12 @@ ActionController::Routing::Routes.draw do |map| 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+/ + map.connect '/browse/changesets', :controller => 'changeset', :action => 'list' # web site - map.connect '/', :controller => 'site', :action => 'index' map.connect '/edit', :controller => 'site', :action => 'edit' + map.connect '/history', :controller => 'changeset', :action => 'list_bbox' map.connect '/export', :controller => 'site', :action => 'export' map.connect '/login', :controller => 'user', :action => 'login' map.connect '/logout', :controller => 'user', :action => 'logout' @@ -101,6 +102,7 @@ ActionController::Routing::Routes.draw do |map| map.connect '/index.html', :controller => 'site', :action => 'index' map.connect '/edit.html', :controller => 'site', :action => 'edit' + map.connect '/history.html', :controller => 'changeset', :action => 'list_bbox' map.connect '/export.html', :controller => 'site', :action => 'export' map.connect '/search.html', :controller => 'way_tag', :action => 'search' map.connect '/login.html', :controller => 'user', :action => 'login' @@ -137,7 +139,7 @@ ActionController::Routing::Routes.draw do |map| # user pages map.connect '/user/:display_name', :controller => 'user', :action => 'view' - map.connect '/user/:display_name/edits', :controller => 'changeset', :action => 'list' + map.connect '/user/:display_name/edits', :controller => 'changeset', :action => 'list_user' map.connect '/user/:display_name/make_friend', :controller => 'user', :action => 'make_friend' map.connect '/user/:display_name/remove_friend', :controller => 'user', :action => 'remove_friend' map.connect '/user/:display_name/diary', :controller => 'diary_entry', :action => 'list' @@ -152,7 +154,8 @@ ActionController::Routing::Routes.draw do |map| map.connect '/diary/rss', :controller => 'diary_entry', :action => 'rss' map.connect '/diary/:language', :controller => 'diary_entry', :action => 'list' map.connect '/diary/:language/rss', :controller => 'diary_entry', :action => 'rss' - + + # test pages map.connect '/test/populate/:table/:from/:count', :controller => 'test', :action => 'populate' map.connect '/test/populate/:table/:count', :controller => 'test', :action => 'populate', :from => 1 diff --git a/public/javascripts/map.js b/public/javascripts/map.js index 608f4c264..a25564f0d 100644 --- a/public/javascripts/map.js +++ b/public/javascripts/map.js @@ -144,7 +144,7 @@ function setMapExtent(extent) { map.zoomToExtent(extent.clone().transform(epsg4326, map.getProjectionObject())); } -function getMapExtent(extent) { +function getMapExtent() { return map.getExtent().clone().transform(map.getProjectionObject(), epsg4326); } diff --git a/public/javascripts/site.js b/public/javascripts/site.js index 95c0b938e..d381c0231 100644 --- a/public/javascripts/site.js +++ b/public/javascripts/site.js @@ -1,4 +1,6 @@ -function updatelinks(lon,lat,zoom,layers) { +//Called as the user scrolls/zooms around. +//Maniplate hrefs of the view tab and various other links +function updatelinks(lon,lat,zoom,layers,extents) { var decimals = Math.pow(10, Math.floor(zoom/3)); var node; @@ -55,6 +57,29 @@ function updatelinks(lon,lat,zoom,layers) { node.style.fontStyle = 'italic'; } } + + node = document.getElementById("historyanchor"); + if (node) { + if (zoom >= 11) { + var args = new Object(); + //conjure a bounding box centred at the lat/lon. + //TODO: feed actual bounds of the window through to here somehow. + minlon = extents.left; + minlat = extents.bottom; + maxlon = extents.right; + maxlat = extents.top; + minlon = Math.round(minlon * decimals) / decimals; + minlat = Math.round(minlat * decimals) / decimals; + maxlon = Math.round(maxlon * decimals) / decimals; + maxlat = Math.round(maxlat * decimals) / decimals; + args.bbox = minlon + "," + minlat + "," + maxlon + "," + maxlat; + node.href = setArgs("history/", args); + node.style.fontStyle = 'normal'; + } else { + node.href = 'javascript:alert("zoom in to see editing history");'; + node.style.fontStyle = 'italic'; + } + } } function getArgs(url) { From 677f391ec1e49b58136717bbd45fdee00008855a Mon Sep 17 00:00:00 2001 From: Thomas Wood Date: Mon, 23 Mar 2009 18:23:37 +0000 Subject: [PATCH 330/381] Fix various auth bugs with the changeset listing stuff, catch exception (but not yet handled well) for when edits not public. --- app/controllers/changeset_controller.rb | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/app/controllers/changeset_controller.rb b/app/controllers/changeset_controller.rb index ebad59aa0..bb628d48e 100644 --- a/app/controllers/changeset_controller.rb +++ b/app/controllers/changeset_controller.rb @@ -4,8 +4,8 @@ class ChangesetController < ApplicationController layout 'site' require 'xml/libxml' - session :off, :except => [:list] - before_filter :authorize_web, :only => [:list] + session :off, :except => [:list, :list_user, :list_bbox] + before_filter :authorize_web, :only => [:list, :list_user, :list_bbox] 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] @@ -323,7 +323,12 @@ class ChangesetController < ApplicationController #find user by display name user = User.find(:first, :conditions => [ "visible = ? and display_name = ?", true, params[:display_name]]) - conditions = conditions_user(user.id); + conditions = nil + begin + conditions = conditions_user(user.id); + rescue OSM::APINotFoundError + + end conditions = cond_merge conditions, conditions_nonempty @edit_pages, @edits = paginate(:changesets, :include => [:user, :changeset_tags], From 7d3a5899c9ef20b9a67eda05c1b1c167b6677bdd Mon Sep 17 00:00:00 2001 From: Thomas Wood Date: Mon, 23 Mar 2009 19:40:33 +0000 Subject: [PATCH 331/381] More fixes to the changeset viewer, including logged-in-ness on per-user edit lists, fix XSS potential on usernames here, and general tidyups. --- app/controllers/changeset_controller.rb | 34 +++++++++++++------------ app/views/changeset/_changeset.rhtml | 14 +++++----- app/views/changeset/list_user.rhtml | 11 +++++--- 3 files changed, 32 insertions(+), 27 deletions(-) diff --git a/app/controllers/changeset_controller.rb b/app/controllers/changeset_controller.rb index bb628d48e..b905ae663 100644 --- a/app/controllers/changeset_controller.rb +++ b/app/controllers/changeset_controller.rb @@ -320,24 +320,26 @@ class ChangesetController < ApplicationController ## # list edits (changesets) belonging to a user def list_user - #find user by display name - user = User.find(:first, :conditions => [ "visible = ? and display_name = ?", true, params[:display_name]]) + user = User.find_by_display_name(params[:display_name], :conditions => {:visible => true}) - conditions = nil - begin - conditions = conditions_user(user.id); - rescue OSM::APINotFoundError - + if user + @display_name = user.display_name + if not user.data_public? and @user != user + @edits = nil + render + else + conditions = cond_merge conditions, ['user_id = ?', user.id] + conditions = cond_merge conditions, conditions_nonempty + @edit_pages, @edits = paginate(:changesets, + :include => [:user, :changeset_tags], + :conditions => conditions, + :order => "changesets.created_at DESC", + :per_page => 20) + end + else + @not_found_user = params[:display_name] + render :template => 'user/no_such_user', :status => :not_found end - conditions = cond_merge conditions, conditions_nonempty - @edit_pages, @edits = paginate(:changesets, - :include => [:user, :changeset_tags], - :conditions => conditions, - :order => "changesets.created_at DESC", - :per_page => 20) - - @display_name = user.display_name - # FIXME needs rescues in here end ## diff --git a/app/views/changeset/_changeset.rhtml b/app/views/changeset/_changeset.rhtml index 67f3e72bb..7e5aeed88 100644 --- a/app/views/changeset/_changeset.rhtml +++ b/app/views/changeset/_changeset.rhtml @@ -9,13 +9,13 @@ <% else %><%= changeset.closed_at.strftime("%d %b %Y %H:%M") %><% end %> - <%if showusername==true %> - - <% if changeset.user.data_public? %> - <%= link_to h(changeset.user.display_name), :controller => "user", :action => "view", :display_name => changeset.user.display_name %> - <% else %> - annon - <% end %> + <%if showusername %> + + <% if changeset.user.data_public? %> + <%= link_to h(changeset.user.display_name), :controller => "user", :action => "view", :display_name => changeset.user.display_name %> + <% else %> + Anonymous + <% end %> <% end %> diff --git a/app/views/changeset/list_user.rhtml b/app/views/changeset/list_user.rhtml index a2096adef..2f3ca04ce 100644 --- a/app/views/changeset/list_user.rhtml +++ b/app/views/changeset/list_user.rhtml @@ -1,6 +1,9 @@ -

      Edits by <%= link_to(@display_name, {:controller=>'user', :action=>'view', :display_name=>@display_name}) %>

      -<%= render :partial => 'changeset_paging_nav' %> +

      Edits by <%= link_to(h(@display_name), {:controller=>'user', :action=>'view', :display_name=>@display_name}) %>

      +<% if not @edits or @edits.empty? %> +

      No visible edits by <%= h(@display_name) %>.

      +<% else %> +<%= render :partial => 'changeset_paging_nav' %> @@ -9,7 +12,7 @@ - <%= render :partial => 'changeset', :locals => {:showusername => false}, :collection => @edits unless @edits.nil? %> + <%= render :partial => 'changeset', :locals => {:showusername => false}, :collection => @edits %>
      IDArea
      - <%= render :partial => 'changeset_paging_nav' %> +<% end %> From 8fb97a3337a8dbf688edc78dc37862c8e02d8cd9 Mon Sep 17 00:00:00 2001 From: Thomas Wood Date: Mon, 23 Mar 2009 19:56:01 +0000 Subject: [PATCH 332/381] Rather than referring how to view a bbox history, just redirect to the global history. --- app/controllers/changeset_controller.rb | 4 +++- app/views/changeset/list.rhtml | 4 ++-- app/views/changeset/list_bbox.rhtml | 14 +------------- 3 files changed, 6 insertions(+), 16 deletions(-) diff --git a/app/controllers/changeset_controller.rb b/app/controllers/changeset_controller.rb index b905ae663..9b09549d0 100644 --- a/app/controllers/changeset_controller.rb +++ b/app/controllers/changeset_controller.rb @@ -348,8 +348,10 @@ class ChangesetController < ApplicationController # support 'bbox' param or alternatively 'minlon', 'minlat' etc if params['bbox'] bbox = params['bbox'] - elsif params['minlon'] and params['minlat'] and params['maxlon'] and params['maxlat'] + elsif params['minlon'] and params['minlat'] and params['maxlon'] and params['maxlat'] bbox = params['minlon'] + ',' + params['minlat'] + ',' + params['maxlon'] + ',' + params['maxlat'] + else + redirect_to :action => 'list' end conditions = conditions_bbox(bbox); diff --git a/app/views/changeset/list.rhtml b/app/views/changeset/list.rhtml index 0edd951ce..aa0d92758 100644 --- a/app/views/changeset/list.rhtml +++ b/app/views/changeset/list.rhtml @@ -1,6 +1,6 @@

      Recent Changes

      +

      Recently closed changesets:

      -Recently closed changesets: @@ -16,4 +16,4 @@ Recently closed changesets:

      For more changesets, select a user and view their edits, or see the editing 'history' of a specific area.

      -
      + diff --git a/app/views/changeset/list_bbox.rhtml b/app/views/changeset/list_bbox.rhtml index f1d8633f0..4f90d185b 100644 --- a/app/views/changeset/list_bbox.rhtml +++ b/app/views/changeset/list_bbox.rhtml @@ -33,18 +33,6 @@ Changsets within the area: <%= render :partial => 'changeset_paging_nav' %> -<% - end - -else - #bbox is nil. happens if the user surfs to this page directly. -%> - -

      No area specified

      -

      First use the view tab to pan and zoom to an area of interest, then click the history tab

      - -<% -end -%> +<% end %>

      From d841d3d1352b31e7a2196b07f4ae335e6bca8380 Mon Sep 17 00:00:00 2001 From: Thomas Wood Date: Mon, 23 Mar 2009 21:35:06 +0000 Subject: [PATCH 333/381] <%if showusername %> @@ -16,6 +18,7 @@ <% else %> Anonymous <% end %> + <% end %> diff --git a/test/functional/browse_controller_test.rb b/test/functional/browse_controller_test.rb index c4d7a5ae4..c5a333c0d 100644 --- a/test/functional/browse_controller_test.rb +++ b/test/functional/browse_controller_test.rb @@ -18,30 +18,6 @@ class BrowseControllerTest < ActionController::TestCase end - # This should display the last 20 changesets closed. - def test_index - @changesets = Changeset.find(:all, :order => "closed_at DESC", :conditions => ['closed_at < ?', DateTime.now], :limit=> 20) - assert @changesets.size <= 20 - get :index - assert_response :success - assert_template "index" - # Now check that all 20 (or however many were returned) changesets are in the html - assert_select "h2", :text => "#{@changesets.size} Recently Closed Changesets", :count => 1 - assert_select "ul[id='recently_changed'] li a", :count => @changesets.size - @changesets.each do |changeset| - if changeset.user.data_public? - user = changeset.user.display_name - else - user = "(anonymous)" - end - - cmt = changeset.tags_as_hash['comment'].to_s - cmt = "(no comment)" if cmt.length == 0 - text = "#{changeset.id} by #{user} - #{cmt}" - assert_select "ul[id='recently_changed'] li a[href=/browse/changeset/#{changeset.id}]", :text => text - end - end - # Test reading a relation def test_read_relation diff --git a/test/functional/changeset_controller_test.rb b/test/functional/changeset_controller_test.rb index edc2aee93..591241aa0 100644 --- a/test/functional/changeset_controller_test.rb +++ b/test/functional/changeset_controller_test.rb @@ -995,6 +995,21 @@ EOF "element limit.") end + # This should display the last 20 changesets closed. + def test_list + @changesets = Changeset.find(:all, :order => "created_at DESC", :conditions => ['min_lat IS NOT NULL'], :limit=> 20) + assert @changesets.size <= 20 + get :list + assert_response :success + assert_template "list" + # Now check that all 20 (or however many were returned) changesets are in the html + assert_select "h1", :text => "Recent Changes", :count => 1 + assert_select "table[id='keyvalue'] tr", :count => @changesets.size + 1 + @changesets.each do |changeset| + # FIXME this test needs rewriting - test for table contents + end + end + #------------------------------------------------------------ # utility functions #------------------------------------------------------------ From 6167bb650286e30cf9e1beed107b794c03bb3909 Mon Sep 17 00:00:00 2001 From: Thomas Wood Date: Mon, 23 Mar 2009 21:52:32 +0000 Subject: [PATCH 334/381] Shut up libxml's "Must specify a string with one or more characters" error on relation and way models. --- app/models/relation.rb | 2 +- app/models/way.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/relation.rb b/app/models/relation.rb index 64af4ecc1..c3769b14d 100644 --- a/app/models/relation.rb +++ b/app/models/relation.rb @@ -33,7 +33,7 @@ class Relation < ActiveRecord::Base doc.find('//osm/relation').each do |pt| return Relation.from_xml_node(pt, create) end - rescue LibXML::XML::Error => ex + rescue LibXML::XML::Error, ArgumentError => ex raise OSM::APIBadXMLError.new("relation", xml, ex.message) end end diff --git a/app/models/way.rb b/app/models/way.rb index dbc1197a9..88bbaf6c5 100644 --- a/app/models/way.rb +++ b/app/models/way.rb @@ -33,7 +33,7 @@ class Way < ActiveRecord::Base doc.find('//osm/way').each do |pt| return Way.from_xml_node(pt, create) end - rescue LibXML::XML::Error => ex + rescue LibXML::XML::Error, ArgumentError => ex raise OSM::APIBadXMLError.new("way", xml, ex.message) end end From 0f3e5a28c04a1a7899a39d594eb05a2f48d5db6d Mon Sep 17 00:00:00 2001 From: Harry Wood Date: Tue, 24 Mar 2009 11:23:56 +0000 Subject: [PATCH 335/381] show a 'no bbox specified' message for the moment at least. Due to inconsistent location detecting across tabs, the history tab often gets nil bbox (Need to fix for all tabs) + other history tab tweaks --- app/controllers/changeset_controller.rb | 5 +++-- app/views/changeset/list_bbox.rhtml | 16 ++++++++++++++-- public/javascripts/site.js | 3 +-- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/app/controllers/changeset_controller.rb b/app/controllers/changeset_controller.rb index 9b09549d0..d69e7f4c1 100644 --- a/app/controllers/changeset_controller.rb +++ b/app/controllers/changeset_controller.rb @@ -349,9 +349,10 @@ class ChangesetController < ApplicationController if params['bbox'] bbox = params['bbox'] elsif params['minlon'] and params['minlat'] and params['maxlon'] and params['maxlat'] - bbox = params['minlon'] + ',' + params['minlat'] + ',' + params['maxlon'] + ',' + params['maxlat'] + bbox = h(params['minlon']) + ',' + h(params['minlat']) + ',' + h(params['maxlon']) + ',' + h(params['maxlat']) else - redirect_to :action => 'list' + #TODO: fix bugs in location determination for history tab (and other tabs) then uncomment this redirect + #redirect_to :action => 'list' end conditions = conditions_bbox(bbox); diff --git a/app/views/changeset/list_bbox.rhtml b/app/views/changeset/list_bbox.rhtml index 4f90d185b..63e54e11d 100644 --- a/app/views/changeset/list_bbox.rhtml +++ b/app/views/changeset/list_bbox.rhtml @@ -8,7 +8,7 @@ if @bbox!=nil %>

      -Changsets within the area: +Changesets within the area: (<%= format("%0.3f",lat1) -%>,<%= format("%0.3f",lon1) -%>) to (<%= format("%0.3f",lat2) -%>,<%= format("%0.3f",lon2) -%>)

      @@ -32,7 +32,19 @@ Changsets within the area:
      ID tags must be closed, move browse index test over to changeset, lazy fix for new format --- app/views/changeset/_changeset.rhtml | 6 +++++ test/functional/browse_controller_test.rb | 24 -------------------- test/functional/changeset_controller_test.rb | 15 ++++++++++++ 3 files changed, 21 insertions(+), 24 deletions(-) diff --git a/app/views/changeset/_changeset.rhtml b/app/views/changeset/_changeset.rhtml index 7e5aeed88..049be5d33 100644 --- a/app/views/changeset/_changeset.rhtml +++ b/app/views/changeset/_changeset.rhtml @@ -3,10 +3,12 @@ #<%= changeset.id %> + <% if changeset.closed_at > DateTime.now %> (still editing) <% else %><%= changeset.closed_at.strftime("%d %b %Y %H:%M") %><% end %> + @@ -24,6 +27,7 @@ <% else %> (none) <% end %> + <% if changeset.min_lat.nil? %> @@ -37,8 +41,10 @@ (<%= format("%0.3f",lat1) -%>,<%= format("%0.3f",lon1) -%>) to (<%= format("%0.3f",lat2) -%>,<%= format("%0.3f",lon2) -%>) <% end %> + <%= link_to 'more', {:controller => 'browse', :action => 'changeset', :id => changeset.id}, {:title => 'View changeset details'} %> +
      <%= render :partial => 'changeset_paging_nav' %> +<% + end -<% end %> +else + #bbox is nil. happens if the user surfs to this page directly. +%> + +

      No area specified

      +

      First use the view tab to pan and zoom to an area of interest, then click the history tab.

      +

      Alternatively view all <%= link_to("recent changes", :controller => "browse", :action => "changesets") %>

      + +<% +end +%>

      diff --git a/public/javascripts/site.js b/public/javascripts/site.js index d381c0231..06b4152df 100644 --- a/public/javascripts/site.js +++ b/public/javascripts/site.js @@ -62,8 +62,7 @@ function updatelinks(lon,lat,zoom,layers,extents) { if (node) { if (zoom >= 11) { var args = new Object(); - //conjure a bounding box centred at the lat/lon. - //TODO: feed actual bounds of the window through to here somehow. + //set bbox param from 'extents' object minlon = extents.left; minlat = extents.bottom; maxlon = extents.right; From 568c05c548773a222e6204e091342db6e76b62b3 Mon Sep 17 00:00:00 2001 From: Matt Amos Date: Wed, 25 Mar 2009 17:54:21 +0000 Subject: [PATCH 336/381] Fixing failing potlatch tests. Someone who knows what the potlatch API is actually supposed to be doing should check these... --- app/controllers/amf_controller.rb | 19 +++++--- test/fixtures/current_ways.yml | 2 +- test/functional/amf_controller_test.rb | 63 ++++++++++++++++++++------ 3 files changed, 64 insertions(+), 20 deletions(-) diff --git a/app/controllers/amf_controller.rb b/app/controllers/amf_controller.rb index 331487a93..b4cbaeaf0 100644 --- a/app/controllers/amf_controller.rb +++ b/app/controllers/amf_controller.rb @@ -298,12 +298,19 @@ class AmfController < ApplicationController 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 - # revert - timestamp = DateTime.strptime(timestamp, "%d %b %Y, %H:%M:%S") - old_way = OldWay.find(:first, :conditions => ['id = ? AND timestamp <= ?', id, timestamp], :order => 'timestamp DESC') - points = old_way.get_nodes_revert(timestamp) unless old_way.nil? - if !old_way.visible - return [-1, "Sorry, the way was deleted at that time - please revert to a previous version."] + begin + # revert + timestamp = DateTime.strptime(timestamp.to_s, "%d %b %Y, %H:%M:%S") + old_way = OldWay.find(:first, :conditions => ['id = ? AND timestamp <= ?', id, timestamp], :order => 'timestamp DESC') + unless old_way.nil? + points = old_way.get_nodes_revert(timestamp) + if !old_way.visible + return [-1, "Sorry, the way was deleted at that time - please revert to a previous version."] + end + end + rescue ArgumentError + # thrown by date parsing method. leave old_way as nil for + # the superb error handler below. end end diff --git a/test/fixtures/current_ways.yml b/test/fixtures/current_ways.yml index 44a54caac..b4b037a61 100644 --- a/test/fixtures/current_ways.yml +++ b/test/fixtures/current_ways.yml @@ -22,6 +22,6 @@ used_way: way_with_versions: id: 4 changeset_id: 4 - timestamp: 2008-01-01 00:01:00 + 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 index b71c680e3..08f3effe9 100644 --- a/test/functional/amf_controller_test.rb +++ b/test/functional/amf_controller_test.rb @@ -194,26 +194,56 @@ class AmfControllerTest < ActionController::TestCase def test_getway_old # try to get the last visible version (specified by <0) (should be current version) latest = current_ways(:way_with_versions) + # NOTE: looks from the API changes that this now expects a timestamp + # instead of a version number... # 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] + { latest => '', + v1 => v1.timestamp.strftime("%d %b %Y, %H:%M:%S") + }.each do |way, t| + amf_content "getway_old", "/1", [way.id, t] + post :amf_read + assert_response :success + amf_parse_response + returned_way = amf_result("/1") + assert_equal way.id, returned_way[1] + # API returns the *latest* version, even for old ways... + assert_equal latest.version, returned_way[4] + end + end + + ## + # test that the server doesn't fall over when rubbish is passed + # into the method args. + def test_getway_old_invalid + way_id = current_ways(:way_with_versions).id + { "foo" => "bar", + way_id => "not a date", + way_id => "2009-03-25 00:00:00", # <- wrong format + way_id => "0 Jan 2009 00:00:00", # <- invalid date + -1 => "1 Jan 2009 00:00:00" # <- invalid ID + }.each do |id, t| + amf_content "getway_old", "/1", [id, t] 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 + assert returned_way[2].empty? + assert returned_way[3].empty? + assert returned_way[4] < 0 end end def test_getway_old_nonexistent # try to get the last version+10 (shoudn't exist) - latest = current_ways(:way_with_versions) + v1 = ways(:way_with_versions_v1) # 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] + [[nil, ''], + [nil, '1 Jan 1970, 00:00:00'], + [v1, (v1.timestamp - 10).strftime("%d %b %Y, %H:%M:%S")] + ].each do |way, t| + amf_content "getway_old", "/1", [way.nil? ? 0 : way.id, t] post :amf_read assert_response :success amf_parse_response @@ -226,6 +256,8 @@ class AmfControllerTest < ActionController::TestCase def test_getway_history latest = current_ways(:way_with_versions) + oldest = ways(:way_with_versions_v1) + amf_content "getway_history", "/1", [latest.id] post :amf_read assert_response :success @@ -233,10 +265,12 @@ class AmfControllerTest < ActionController::TestCase 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 + assert_equal 'way', history[0] + assert_equal latest.id, history[1] + # for some reason undocumented, the potlatch API now prefers dates + # over version numbers. presumably no-one edits concurrently any more? + assert_equal latest.timestamp.strftime("%d %b %Y, %H:%M:%S"), history[2].first[0] + assert_equal oldest.timestamp.strftime("%d %b %Y, %H:%M:%S"), history[2].last[0] end def test_getway_history_nonexistent @@ -268,10 +302,13 @@ class AmfControllerTest < ActionController::TestCase # 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, + # NOTE2: wow - this is the second time this has changed in the + # API and the tests are being patched up. + assert_equal history[2].first[0], + latest.timestamp.strftime("%d %b %Y, %H:%M:%S"), 'first part of third element should be the latest version' assert_equal history[2].last[0], - nodes(:node_with_versions_v1).version, + nodes(:node_with_versions_v1).timestamp.strftime("%d %b %Y, %H:%M:%S"), 'second part of third element should be the initial version' end From 663416ae0bdca040f23cf867c43b94d0c1e2b45b Mon Sep 17 00:00:00 2001 From: Harry Wood Date: Thu, 26 Mar 2009 23:43:34 +0000 Subject: [PATCH 337/381] fix 'edits' link. action was renamed 'list_user' --- app/views/user/view.rhtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/user/view.rhtml b/app/views/user/view.rhtml index d12198ca8..a62be6a55 100644 --- a/app/views/user/view.rhtml +++ b/app/views/user/view.rhtml @@ -12,7 +12,7 @@ <%= 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 'edits', :controller => 'changeset', :action => 'list', :display_name => @this_user.display_name %> +| <%= link_to 'edits', :controller => 'changeset', :action => 'list_user', :display_name => @this_user.display_name %> | <%= link_to 'traces', :controller => 'trace', :action => 'view', :display_name => @this_user.display_name %> | <% if @user and @user.is_friends_with?(@this_user) %> <%= link_to 'remove as friend', :controller => 'user', :action => 'remove_friend', :display_name => @this_user.display_name %> From be75a1504f3c9a49b6cbf4e83c35339f45390628 Mon Sep 17 00:00:00 2001 From: Matt Amos Date: Fri, 27 Mar 2009 12:22:23 +0000 Subject: [PATCH 338/381] Fixed a couple of bugs with changeset bounding box calculation when modifying or deleting. --- app/models/node.rb | 9 ++- app/models/relation.rb | 5 ++ app/models/way.rb | 23 ++++-- test/functional/changeset_controller_test.rb | 81 +++++++++++++++++++- 4 files changed, 107 insertions(+), 11 deletions(-) diff --git a/app/models/node.rb b/app/models/node.rb index d3e0a7e8d..05aae0896 100644 --- a/app/models/node.rb +++ b/app/models/node.rb @@ -150,17 +150,20 @@ class Node < ActiveRecord::Base def update_from(new_node, user) check_consistency(self, new_node, user) - # update changeset with *old* position first + # update changeset first + self.changeset_id = new_node.changeset_id + self.changeset = new_node.changeset + + # update changeset bbox 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 + # update changeset bbox with *new* position changeset.update_bbox!(bbox); save_with_history! diff --git a/app/models/relation.rb b/app/models/relation.rb index c3769b14d..4b5d9e32b 100644 --- a/app/models/relation.rb +++ b/app/models/relation.rb @@ -253,6 +253,7 @@ class Relation < ActiveRecord::Base raise OSM::APIPreconditionFailedError.new end self.changeset_id = new_relation.changeset_id + self.changeset = new_relation.changeset self.tags = new_relation.tags self.members = new_relation.members self.visible = true @@ -372,6 +373,10 @@ class Relation < ActiveRecord::Base tag.id = self.id tag.save! end + + # reload, so that all of the members are accessible in their + # new state. + self.reload # same pattern as before, but this time we're collecting the # changed members in an array, as the bounding box updates for diff --git a/app/models/way.rb b/app/models/way.rb index 88bbaf6c5..94a6fa754 100644 --- a/app/models/way.rb +++ b/app/models/way.rb @@ -202,7 +202,9 @@ class Way < ActiveRecord::Base if !new_way.preconditions_ok? raise OSM::APIPreconditionFailedError.new end + self.changeset_id = new_way.changeset_id + self.changeset = new_way.changeset self.tags = new_way.tags self.nds = new_way.nds self.visible = true @@ -248,6 +250,8 @@ class Way < ActiveRecord::Base raise OSM::APIPreconditionFailedError.new("You need to make sure that this way is not a member of a relation.") else self.changeset_id = new_way.changeset_id + self.changeset = new_way.changeset + self.tags = [] self.nds = [] self.visible = false @@ -294,11 +298,12 @@ class Way < ActiveRecord::Base 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 + # update the bounding box, 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? + # bbox. we use a copy of the changeset so that it isn't reloaded + # later in the save. + cs = self.changeset + cs.update_bbox!(bbox) unless nodes.empty? Way.transaction do self.version += 1 @@ -330,14 +335,18 @@ class Way < ActiveRecord::Base old_way.timestamp = t old_way.save_with_dependencies! + # reload the way so that the nodes array points to the correct + # new set of nodes. + self.reload + # 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? + cs.update_bbox!(bbox) unless nodes.empty? # tell the changeset we updated one element only - changeset.add_changes! 1 + cs.add_changes! 1 - changeset.save! + cs.save! end end diff --git a/test/functional/changeset_controller_test.rb b/test/functional/changeset_controller_test.rb index 591241aa0..4c98fb36d 100644 --- a/test/functional/changeset_controller_test.rb +++ b/test/functional/changeset_controller_test.rb @@ -553,6 +553,84 @@ EOF "shouldn't be able to re-use placeholder IDs" end + ## + # test what happens if a diff is uploaded containing only a node + # move. + def test_upload_node_move + basic_authorization "test@openstreetmap.org", "test" + + content "" + + "" + + "" + put :create + assert_response :success + changeset_id = @response.body.to_i + + old_node = current_nodes(:visible_node) + + diff = XML::Document.new + diff.root = XML::Node.new "osmChange" + modify = XML::Node.new "modify" + xml_old_node = old_node.to_xml_node + xml_old_node["lat"] = (2.0).to_s + xml_old_node["lon"] = (2.0).to_s + xml_old_node["changeset"] = changeset_id.to_s + modify << xml_old_node + diff.root << modify + + # upload it + content diff + post :upload, :id => changeset_id + assert_response :success, + "diff should have uploaded OK" + + # check the bbox + changeset = Changeset.find(changeset_id) + assert_equal 1*SCALE, changeset.min_lon, "min_lon should be 1 degree" + assert_equal 2*SCALE, changeset.max_lon, "max_lon should be 2 degrees" + assert_equal 1*SCALE, changeset.min_lat, "min_lat should be 1 degree" + assert_equal 2*SCALE, changeset.max_lat, "max_lat should be 2 degrees" + end + + ## + # test what happens if a diff is uploaded adding a node to a way. + def test_upload_way_extend + basic_authorization "test@openstreetmap.org", "test" + + content "" + + "" + + "" + put :create + assert_response :success + changeset_id = @response.body.to_i + + old_way = current_ways(:visible_way) + + diff = XML::Document.new + diff.root = XML::Node.new "osmChange" + modify = XML::Node.new "modify" + xml_old_way = old_way.to_xml_node + nd_ref = XML::Node.new "nd" + nd_ref["ref"] = current_nodes(:visible_node).id.to_s + xml_old_way << nd_ref + xml_old_way["changeset"] = changeset_id.to_s + modify << xml_old_way + diff.root << modify + + # upload it + content diff + post :upload, :id => changeset_id + assert_response :success, + "diff should have uploaded OK" + + # check the bbox + changeset = Changeset.find(changeset_id) + assert_equal 1*SCALE, changeset.min_lon, "min_lon should be 1 degree" + assert_equal 3*SCALE, changeset.max_lon, "max_lon should be 3 degrees" + assert_equal 1*SCALE, changeset.min_lat, "min_lat should be 1 degree" + assert_equal 3*SCALE, changeset.max_lat, "max_lat should be 3 degrees" + end + ## # test for more issues in #1568 def test_upload_empty_invalid @@ -773,7 +851,7 @@ EOF assert_select "osm>changeset[min_lat=1.0]", 1 assert_select "osm>changeset[max_lat=2.0]", 1 - # add (delete) a way to it + # add (delete) a way to it, which contains a point at (3,3) with_controller(WayController.new) do content update_changeset(current_ways(:visible_way).to_xml, changeset_id) @@ -784,6 +862,7 @@ EOF # 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." + # note that the 3.1 here is because of the bbox overexpansion 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 From f492a1b709b74f0bb36821864090657f6026f9f2 Mon Sep 17 00:00:00 2001 From: Matt Amos Date: Sat, 28 Mar 2009 20:18:26 +0000 Subject: [PATCH 339/381] Fixed unit tests and fixture loading for postgres. The foreign keys were playing havoc with the tests when only some fixtures were loaded. --- test/functional/changeset_controller_test.rb | 14 +++++ test/test_helper.rb | 55 ++++++++++++-------- test/unit/changeset_tag_test.rb | 2 +- test/unit/changeset_test.rb | 3 +- test/unit/diary_comment_test.rb | 2 +- test/unit/diary_entry_test.rb | 2 +- test/unit/friend_test.rb | 4 +- test/unit/message_test.rb | 3 +- test/unit/node_tag_test.rb | 4 +- test/unit/node_test.rb | 8 +-- test/unit/old_node_tag_test.rb | 6 +-- test/unit/old_node_test.rb | 8 +-- test/unit/old_relation_tag_test.rb | 3 +- test/unit/old_way_tag_test.rb | 3 +- test/unit/relation_member_test.rb | 3 +- test/unit/relation_tag_test.rb | 3 +- test/unit/relation_test.rb | 3 +- test/unit/trace_test.rb | 3 +- test/unit/tracepoint_test.rb | 3 +- test/unit/tracetag_test.rb | 3 +- test/unit/user_preference_test.rb | 3 +- test/unit/user_test.rb | 5 +- test/unit/user_token_test.rb | 5 +- test/unit/way_node_test.rb | 4 +- test/unit/way_tag_test.rb | 3 +- test/unit/way_test.rb | 1 - 26 files changed, 79 insertions(+), 77 deletions(-) diff --git a/test/functional/changeset_controller_test.rb b/test/functional/changeset_controller_test.rb index 4c98fb36d..7faa56028 100644 --- a/test/functional/changeset_controller_test.rb +++ b/test/functional/changeset_controller_test.rb @@ -240,6 +240,20 @@ EOF assert_equal false, Node.find(node.id).visible end + def test_repeated_changeset_create + 30.times do + basic_authorization "test@openstreetmap.org", "test" + + # create a temporary changeset + content "" + + "" + + "" + put :create + assert_response :success + changeset_id = @response.body.to_i + end + end + ## # test that deleting stuff in a transaction doesn't bypass the checks # to ensure that used elements are not deleted. diff --git a/test/test_helper.rb b/test/test_helper.rb index 88a6fbe4a..8c270c682 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -25,43 +25,52 @@ class Test::Unit::TestCase # then set this back to true. self.use_instantiated_fixtures = false + # Load standard fixtures needed to test API methods def self.api_fixtures #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 + 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 + fixtures :current_ways + set_fixture_class :current_ways => 'Way' - fixtures :ways, :way_nodes, :way_tags - set_fixture_class :ways => OldWay - set_fixture_class :way_nodes => OldWayNode - set_fixture_class :way_tags => OldWayTag + fixtures :current_way_nodes, :current_way_tags + set_fixture_class :current_way_nodes => 'WayNode' + set_fixture_class :current_way_tags => 'WayTag' - 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 + fixtures :ways + set_fixture_class :ways => 'OldWay' - fixtures :relations, :relation_members, :relation_tags - set_fixture_class :relations => OldRelation - set_fixture_class :relation_members => OldRelationMember - set_fixture_class :relation_tags => OldRelationTag + fixtures :way_nodes, :way_tags + set_fixture_class :way_nodes => 'OldWayNode' + set_fixture_class :way_tags => 'OldWayTag' + + fixtures :current_relations + set_fixture_class :current_relations => 'Relation' + + fixtures :current_relation_members, :current_relation_tags + set_fixture_class :current_relation_members => 'RelationMember' + set_fixture_class :current_relation_tags => 'RelationTag' + + fixtures :relations + set_fixture_class :relations => 'OldRelation' + + fixtures :relation_members, :relation_tags + 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 + set_fixture_class :gpx_files => 'Trace' + set_fixture_class :gps_points => 'Tracepoint' + set_fixture_class :gpx_file_tags => 'Tracetag' end ## diff --git a/test/unit/changeset_tag_test.rb b/test/unit/changeset_tag_test.rb index e0201d538..c89f426cc 100644 --- a/test/unit/changeset_tag_test.rb +++ b/test/unit/changeset_tag_test.rb @@ -1,7 +1,7 @@ require File.dirname(__FILE__) + '/../test_helper' class ChangesetTagTest < Test::Unit::TestCase - fixtures :changeset_tags + api_fixtures def test_changeset_tag_count assert_equal 1, ChangesetTag.count diff --git a/test/unit/changeset_test.rb b/test/unit/changeset_test.rb index 4550ffba5..448289aaf 100644 --- a/test/unit/changeset_test.rb +++ b/test/unit/changeset_test.rb @@ -1,8 +1,7 @@ require File.dirname(__FILE__) + '/../test_helper' class ChangesetTest < Test::Unit::TestCase - fixtures :changesets - + api_fixtures def test_changeset_count assert_equal 6, Changeset.count diff --git a/test/unit/diary_comment_test.rb b/test/unit/diary_comment_test.rb index d7f30a69c..8e2bbf32c 100644 --- a/test/unit/diary_comment_test.rb +++ b/test/unit/diary_comment_test.rb @@ -1,9 +1,9 @@ require File.dirname(__FILE__) + '/../test_helper' class DiaryCommentTest < Test::Unit::TestCase + api_fixtures fixtures :diary_comments - def test_diary_comment_count assert_equal 1, DiaryComment.count end diff --git a/test/unit/diary_entry_test.rb b/test/unit/diary_entry_test.rb index 6b2800d18..e28e03a10 100644 --- a/test/unit/diary_entry_test.rb +++ b/test/unit/diary_entry_test.rb @@ -1,9 +1,9 @@ require File.dirname(__FILE__) + '/../test_helper' class DiaryEntryTest < Test::Unit::TestCase + api_fixtures fixtures :diary_entries - def test_diary_entry_count assert_equal 2, DiaryEntry.count end diff --git a/test/unit/friend_test.rb b/test/unit/friend_test.rb index fd8b5033f..8c93146ae 100644 --- a/test/unit/friend_test.rb +++ b/test/unit/friend_test.rb @@ -1,9 +1,9 @@ require File.dirname(__FILE__) + '/../test_helper' class FriendTest < Test::Unit::TestCase + api_fixtures fixtures :friends - - + def test_friend_count assert_equal 1, Friend.count end diff --git a/test/unit/message_test.rb b/test/unit/message_test.rb index 3b83bf95a..a732aa586 100644 --- a/test/unit/message_test.rb +++ b/test/unit/message_test.rb @@ -1,7 +1,8 @@ require File.dirname(__FILE__) + '/../test_helper' class MessageTest < Test::Unit::TestCase - fixtures :messages, :users + api_fixtures + fixtures :messages EURO = "\xe2\x82\xac" #euro symbol diff --git a/test/unit/node_tag_test.rb b/test/unit/node_tag_test.rb index 2ff9f9f00..bd7d9f6e3 100644 --- a/test/unit/node_tag_test.rb +++ b/test/unit/node_tag_test.rb @@ -1,9 +1,7 @@ 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 + api_fixtures def test_tag_count assert_equal 6, NodeTag.count diff --git a/test/unit/node_test.rb b/test/unit/node_test.rb index 13dea88da..51cf82d12 100644 --- a/test/unit/node_test.rb +++ b/test/unit/node_test.rb @@ -1,12 +1,8 @@ require File.dirname(__FILE__) + '/../test_helper' class NodeTest < Test::Unit::TestCase - 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 - + api_fixtures + def test_node_too_far_north invalid_node_test(:node_too_far_north) end diff --git a/test/unit/old_node_tag_test.rb b/test/unit/old_node_tag_test.rb index 497184348..969677653 100644 --- a/test/unit/old_node_tag_test.rb +++ b/test/unit/old_node_tag_test.rb @@ -1,10 +1,8 @@ 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 - + api_fixtures + def test_old_node_tag_count assert_equal 8, OldNodeTag.count, "Unexpected number of fixtures loaded." end diff --git a/test/unit/old_node_test.rb b/test/unit/old_node_test.rb index bdd685388..4915e40b6 100644 --- a/test/unit/old_node_test.rb +++ b/test/unit/old_node_test.rb @@ -1,12 +1,8 @@ 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 - + api_fixtures + def test_node_too_far_north invalid_node_test(:node_too_far_north) end diff --git a/test/unit/old_relation_tag_test.rb b/test/unit/old_relation_tag_test.rb index d651810c0..aee2901cd 100644 --- a/test/unit/old_relation_tag_test.rb +++ b/test/unit/old_relation_tag_test.rb @@ -1,8 +1,7 @@ require File.dirname(__FILE__) + '/../test_helper' class OldRelationTagTest < Test::Unit::TestCase - fixtures :relation_tags - set_fixture_class :relation_tags => OldRelationTag + api_fixtures def test_tag_count assert_equal 3, OldRelationTag.count diff --git a/test/unit/old_way_tag_test.rb b/test/unit/old_way_tag_test.rb index 8210ef0c4..b2724cb01 100644 --- a/test/unit/old_way_tag_test.rb +++ b/test/unit/old_way_tag_test.rb @@ -1,8 +1,7 @@ require File.dirname(__FILE__) + '/../test_helper' class WayTagTest < Test::Unit::TestCase - fixtures :way_tags - set_fixture_class :way_tags => OldWayTag + api_fixtures def test_tag_count assert_equal 3, OldWayTag.count diff --git a/test/unit/relation_member_test.rb b/test/unit/relation_member_test.rb index d67ac3462..f0590ef71 100644 --- a/test/unit/relation_member_test.rb +++ b/test/unit/relation_member_test.rb @@ -1,8 +1,7 @@ require File.dirname(__FILE__) + '/../test_helper' class RelationMemberTest < Test::Unit::TestCase - fixtures :current_relation_members - set_fixture_class :current_relation_members => RelationMember + api_fixtures def test_relation_member_count assert_equal 5, RelationMember.count diff --git a/test/unit/relation_tag_test.rb b/test/unit/relation_tag_test.rb index f93e689ca..5c008fc34 100644 --- a/test/unit/relation_tag_test.rb +++ b/test/unit/relation_tag_test.rb @@ -1,8 +1,7 @@ require File.dirname(__FILE__) + '/../test_helper' class RelationTagTest < Test::Unit::TestCase - fixtures :current_relation_tags - set_fixture_class :current_relation_tags => RelationTag + api_fixtures def test_relation_tag_count assert_equal 3, RelationTag.count diff --git a/test/unit/relation_test.rb b/test/unit/relation_test.rb index b5b6391ab..36aad7c25 100644 --- a/test/unit/relation_test.rb +++ b/test/unit/relation_test.rb @@ -1,8 +1,7 @@ require File.dirname(__FILE__) + '/../test_helper' class RelationTest < Test::Unit::TestCase - fixtures :current_relations - set_fixture_class :current_relations => Relation + api_fixtures def test_relation_count assert_equal 3, Relation.count diff --git a/test/unit/trace_test.rb b/test/unit/trace_test.rb index 706455aa2..638003962 100644 --- a/test/unit/trace_test.rb +++ b/test/unit/trace_test.rb @@ -1,8 +1,7 @@ require File.dirname(__FILE__) + '/../test_helper' class TraceTest < Test::Unit::TestCase - fixtures :gpx_files - set_fixture_class :gpx_files => Trace + api_fixtures def test_trace_count assert_equal 1, Trace.count diff --git a/test/unit/tracepoint_test.rb b/test/unit/tracepoint_test.rb index 5d4100530..80534c2cb 100644 --- a/test/unit/tracepoint_test.rb +++ b/test/unit/tracepoint_test.rb @@ -1,8 +1,7 @@ require File.dirname(__FILE__) + '/../test_helper' class TracepointTest < Test::Unit::TestCase - fixtures :gps_points - set_fixture_class :gps_points => Tracepoint + api_fixtures def test_tracepoint_count assert_equal 1, Tracepoint.count diff --git a/test/unit/tracetag_test.rb b/test/unit/tracetag_test.rb index 4eaf41ed8..c61913ff0 100644 --- a/test/unit/tracetag_test.rb +++ b/test/unit/tracetag_test.rb @@ -1,8 +1,7 @@ require File.dirname(__FILE__) + '/../test_helper' class TracetagTest < Test::Unit::TestCase - fixtures :gpx_file_tags - set_fixture_class :gpx_file_tags => Tracetag + api_fixtures def test_tracetag_count assert_equal 1, Tracetag.count diff --git a/test/unit/user_preference_test.rb b/test/unit/user_preference_test.rb index 2118fcd39..df9e1e9b2 100644 --- a/test/unit/user_preference_test.rb +++ b/test/unit/user_preference_test.rb @@ -1,7 +1,8 @@ require File.dirname(__FILE__) + '/../test_helper' class UserPreferenceTest < ActiveSupport::TestCase - fixtures :users, :user_preferences + api_fixtures + fixtures :user_preferences # This checks to make sure that there are two user preferences # stored in the test database. diff --git a/test/unit/user_test.rb b/test/unit/user_test.rb index c0df4b716..f87b5b6fc 100644 --- a/test/unit/user_test.rb +++ b/test/unit/user_test.rb @@ -1,8 +1,9 @@ require File.dirname(__FILE__) + '/../test_helper' class UserTest < Test::Unit::TestCase - fixtures :users - + api_fixtures + fixtures :friends + def test_invalid_with_empty_attributes user = User.new assert !user.valid? diff --git a/test/unit/user_token_test.rb b/test/unit/user_token_test.rb index 2bc1a2dce..83a36f8d9 100644 --- a/test/unit/user_token_test.rb +++ b/test/unit/user_token_test.rb @@ -1,8 +1,9 @@ require File.dirname(__FILE__) + '/../test_helper' class UserTokenTest < Test::Unit::TestCase - fixtures :users - + api_fixtures + fixtures :user_tokens + def test_user_token_count assert_equal 0, UserToken.count end diff --git a/test/unit/way_node_test.rb b/test/unit/way_node_test.rb index 1871eae9d..acc62d330 100644 --- a/test/unit/way_node_test.rb +++ b/test/unit/way_node_test.rb @@ -1,9 +1,7 @@ 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 + api_fixtures def test_way_nodes_count assert_equal 4, WayNode.count diff --git a/test/unit/way_tag_test.rb b/test/unit/way_tag_test.rb index b1a7d2256..018263da1 100644 --- a/test/unit/way_tag_test.rb +++ b/test/unit/way_tag_test.rb @@ -1,8 +1,7 @@ require File.dirname(__FILE__) + '/../test_helper' class WayTagTest < Test::Unit::TestCase - fixtures :current_way_tags - set_fixture_class :current_way_tags => WayTag + api_fixtures def test_way_tag_count assert_equal 3, WayTag.count diff --git a/test/unit/way_test.rb b/test/unit/way_test.rb index 584a30daa..55ab092ea 100644 --- a/test/unit/way_test.rb +++ b/test/unit/way_test.rb @@ -3,7 +3,6 @@ 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 From 8140c993139247fb13e3827b30851d0b774c0f32 Mon Sep 17 00:00:00 2001 From: Matt Amos Date: Sun, 29 Mar 2009 00:56:08 +0000 Subject: [PATCH 340/381] Fix for correct boolean handling in query. --- app/models/diary_entry.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/diary_entry.rb b/app/models/diary_entry.rb index 8063372da..46d96fec7 100644 --- a/app/models/diary_entry.rb +++ b/app/models/diary_entry.rb @@ -1,7 +1,7 @@ class DiaryEntry < ActiveRecord::Base belongs_to :user has_many :diary_comments, :include => :user, - :conditions => "users.visible = 1", + :conditions => ["users.visible = ?", true], :order => "diary_comments.id" validates_presence_of :title, :body From afcb345014d98914cd4f5d6b14ca2e30feac194c Mon Sep 17 00:00:00 2001 From: Matt Amos Date: Sun, 29 Mar 2009 01:31:04 +0000 Subject: [PATCH 341/381] Moved a bunch of time functions into UTC. Fixes bugs which we only see for 4 hours a year. --- app/controllers/amf_controller.rb | 2 +- app/controllers/api_controller.rb | 2 +- app/controllers/changeset_controller.rb | 4 ++-- app/controllers/message_controller.rb | 2 +- app/controllers/trace_controller.rb | 4 ++-- app/models/changeset.rb | 12 ++++++------ app/models/node.rb | 2 +- app/models/relation.rb | 4 ++-- app/models/user.rb | 2 +- app/models/way.rb | 4 ++-- test/functional/api_controller_test.rb | 4 ++-- test/functional/changeset_controller_test.rb | 2 +- 12 files changed, 22 insertions(+), 22 deletions(-) diff --git a/app/controllers/amf_controller.rb b/app/controllers/amf_controller.rb index b4cbaeaf0..e701f2170 100644 --- a/app/controllers/amf_controller.rb +++ b/app/controllers/amf_controller.rb @@ -157,7 +157,7 @@ class AmfController < ApplicationController cs.tags = cstags cs.user_id = user.id # smsm1 doesn't like the next two lines and thinks they need to be abstracted to the model more/better - cs.created_at = Time.now + cs.created_at = Time.now.getutc cs.closed_at = cs.created_at + Changeset::IDLE_TIMEOUT cs.save_with_tags! return [0,cs.id] diff --git a/app/controllers/api_controller.rb b/app/controllers/api_controller.rb index cae510284..f0ba32568 100644 --- a/app/controllers/api_controller.rb +++ b/app/controllers/api_controller.rb @@ -225,7 +225,7 @@ class ApiController < ApplicationController endtime = Time.parse(params[:end]) else hours = (params[:hours] || '1').to_i.hours - endtime = Time.now + endtime = Time.now.getutc starttime = endtime - hours end diff --git a/app/controllers/changeset_controller.rb b/app/controllers/changeset_controller.rb index d69e7f4c1..e88387a76 100644 --- a/app/controllers/changeset_controller.rb +++ b/app/controllers/changeset_controller.rb @@ -461,7 +461,7 @@ private # if parameter 'open' is nill then open and closed changsets are returned def conditions_open(open) return open.nil? ? nil : ['closed_at >= ? and num_changes <= ?', - DateTime.now, Changeset::MAX_ELEMENTS] + Time.now.getutc, Changeset::MAX_ELEMENTS] end ## @@ -469,7 +469,7 @@ private # ('closed at' time has passed or changes limit is hit) def conditions_closed(closed) return closed.nil? ? nil : ['closed_at < ? and num_changes > ?', - DateTime.now, Changeset::MAX_ELEMENTS] + Time.now.getutc, Changeset::MAX_ELEMENTS] end ## diff --git a/app/controllers/message_controller.rb b/app/controllers/message_controller.rb index c039fe636..a04aa82c0 100644 --- a/app/controllers/message_controller.rb +++ b/app/controllers/message_controller.rb @@ -15,7 +15,7 @@ class MessageController < ApplicationController @message = Message.new(params[:message]) @message.to_user_id = @to_user.id @message.from_user_id = @user.id - @message.sent_on = Time.now + @message.sent_on = Time.now.getutc if @message.save flash[:notice] = 'Message sent' diff --git a/app/controllers/trace_controller.rb b/app/controllers/trace_controller.rb index 6528dffde..c0a0b36b5 100644 --- a/app/controllers/trace_controller.rb +++ b/app/controllers/trace_controller.rb @@ -117,7 +117,7 @@ class TraceController < ApplicationController :description => params[:trace][:description], :public => params[:trace][:public], :inserted => false, :user => @user, - :timestamp => Time.now}) + :timestamp => Time.now.getutc}) @trace.valid? @trace.errors.add(:gpx_file, "can't be blank") end @@ -313,7 +313,7 @@ private :description => description, :public => public}) @trace.inserted = false @trace.user = @user - @trace.timestamp = Time.now + @trace.timestamp = Time.now.getutc if @trace.save FileUtils.mv(filename, @trace.trace_name) diff --git a/app/models/changeset.rb b/app/models/changeset.rb index d420f537a..d41a82989 100644 --- a/app/models/changeset.rb +++ b/app/models/changeset.rb @@ -42,12 +42,12 @@ class Changeset < ActiveRecord::Base # 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 > Time.now) and (num_changes <= MAX_ELEMENTS)) + return ((closed_at > Time.now.getutc) and (num_changes <= MAX_ELEMENTS)) end def set_closed_time_now if is_open? - self.closed_at = Time.now + self.closed_at = Time.now.getutc end end @@ -60,10 +60,10 @@ class Changeset < ActiveRecord::Base doc.find('//osm/changeset').each do |pt| if create - cs.created_at = Time.now + cs.created_at = Time.now.getutc # initial close time is 1h ahead, but will be increased on each # modification. - cs.closed_at = Time.now + IDLE_TIMEOUT + cs.closed_at = cs.created_at + IDLE_TIMEOUT # initially we have no changes in a changeset cs.num_changes = 0 end @@ -140,7 +140,7 @@ class Changeset < ActiveRecord::Base end def save_with_tags! - t = Time.now + t = Time.now.getutc # do the changeset update and the changeset tags update in the # same transaction to ensure consistency. @@ -151,7 +151,7 @@ class Changeset < ActiveRecord::Base if (closed_at - created_at) > (MAX_TIME_OPEN - IDLE_TIMEOUT) self.closed_at = created_at + MAX_TIME_OPEN else - self.closed_at = Time.now + IDLE_TIMEOUT + self.closed_at = Time.now.getutc + IDLE_TIMEOUT end self.save! diff --git a/app/models/node.rb b/app/models/node.rb index 05aae0896..679048b4e 100644 --- a/app/models/node.rb +++ b/app/models/node.rb @@ -267,7 +267,7 @@ class Node < ActiveRecord::Base private def save_with_history! - t = Time.now + t = Time.now.getutc Node.transaction do self.version += 1 self.timestamp = t diff --git a/app/models/relation.rb b/app/models/relation.rb index 4b5d9e32b..a8789bc78 100644 --- a/app/models/relation.rb +++ b/app/models/relation.rb @@ -51,7 +51,7 @@ class Relation < ActiveRecord::Base # 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.timestamp = Time.now.getutc relation.visible = true relation.version = 0 else @@ -334,7 +334,7 @@ class Relation < ActiveRecord::Base # changed then we have to monitor their before and after state. tags_changed = false - t = Time.now + t = Time.now.getutc self.version += 1 self.timestamp = t self.save! diff --git a/app/models/user.rb b/app/models/user.rb index ce244fe02..4113662aa 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -30,7 +30,7 @@ class User < ActiveRecord::Base file_column :image, :magick => { :geometry => "100x100>" } def after_initialize - self.creation_time = Time.now if self.creation_time.nil? + self.creation_time = Time.now.getutc if self.creation_time.nil? end def encrypt_password diff --git a/app/models/way.rb b/app/models/way.rb index 94a6fa754..6e4f30d81 100644 --- a/app/models/way.rb +++ b/app/models/way.rb @@ -51,7 +51,7 @@ class Way < ActiveRecord::Base # This next section isn't required for the create, update, or delete of ways if create - way.timestamp = Time.now + way.timestamp = Time.now.getutc way.visible = true else if pt['timestamp'] @@ -296,7 +296,7 @@ class Way < ActiveRecord::Base private def save_with_history! - t = Time.now + t = Time.now.getutc # update the bounding box, note that this has to be done both before # and after the save, so that nodes from both versions are included in the diff --git a/test/functional/api_controller_test.rb b/test/functional/api_controller_test.rb index a8e808716..32f19265a 100644 --- a/test/functional/api_controller_test.rb +++ b/test/functional/api_controller_test.rb @@ -160,7 +160,7 @@ class ApiControllerTest < ActionController::TestCase #print @response.body # As we have loaded the fixtures, we can assume that there are no # changes recently - now = Time.now + now = Time.now.getutc 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 @@ -181,7 +181,7 @@ class ApiControllerTest < ActionController::TestCase 1.upto(16) do |zoom| get :changes, :zoom => zoom assert_response :success - now = Time.now + now = Time.now.getutc 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 diff --git a/test/functional/changeset_controller_test.rb b/test/functional/changeset_controller_test.rb index 7faa56028..6465c894d 100644 --- a/test/functional/changeset_controller_test.rb +++ b/test/functional/changeset_controller_test.rb @@ -37,7 +37,7 @@ class ChangesetControllerTest < ActionController::TestCase 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})" + assert_equal 3600, duration.round, "initial idle timeout should be an hour (#{cs.created_at} -> #{cs.closed_at})" end end From 18633b7c9e66a7f25dafa68c7bc2d90885a61789 Mon Sep 17 00:00:00 2001 From: Matt Amos Date: Sun, 29 Mar 2009 02:14:40 +0000 Subject: [PATCH 342/381] Fixed a problem where rails was setting the ID column to null, which violates the not-null constraint. Also fixed possible typo in AMF controller. --- app/controllers/amf_controller.rb | 4 ++-- app/models/node.rb | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/app/controllers/amf_controller.rb b/app/controllers/amf_controller.rb index e701f2170..1ee5e9cee 100644 --- a/app/controllers/amf_controller.rb +++ b/app/controllers/amf_controller.rb @@ -490,8 +490,8 @@ class AmfController < ApplicationController new_relation.changeset_id = changeset_id new_relation.version = version - - if id <= 0 + # NOTE: id or relid here? id doesn't seem to be set above + if relid <= 0 # We're creating the node new_relation.create_with_history(user) elsif visible diff --git a/app/models/node.rb b/app/models/node.rb index 679048b4e..f6726f91a 100644 --- a/app/models/node.rb +++ b/app/models/node.rb @@ -171,7 +171,6 @@ class Node < ActiveRecord::Base def create_with_history(user) check_create_consistency(self, user) - self.id = nil self.version = 0 self.visible = true From bf0d8b3a5e79cc94eb0206c82f58128ee4598e75 Mon Sep 17 00:00:00 2001 From: Harry Wood Date: Thu, 2 Apr 2009 17:19:33 +0000 Subject: [PATCH 343/381] lose the unnecessary <% embed syntax %> --- app/views/site/edit.rhtml | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/app/views/site/edit.rhtml b/app/views/site/edit.rhtml index 9c3c78fda..72ef5aa0d 100644 --- a/app/views/site/edit.rhtml +++ b/app/views/site/edit.rhtml @@ -21,21 +21,24 @@ <%= render :partial => 'sidebar', :locals => { :onopen => "resizeMap();", :onclose => "resizeMap();" } %> <%= render :partial => 'search' %> -<% session[:token] = @user.tokens.create.token unless session[:token] %> +<% +session[:token] = @user.tokens.create.token unless session[:token] -<% if params['mlon'] and params['mlat'] %> - <% lon = h(params['mlon']) %> - <% lat = h(params['mlat']) %> - <% zoom = h(params['zoom'] || '14') %> -<% elsif @user and params['lon'].nil? and params['lat'].nil? and params['gpx'].nil? %> - <% lon = @user.home_lon %> - <% lat = @user.home_lat %> - <% zoom = '14' %> -<% else %> - <% lon = h(params['lon'] || 'null') %> - <% lat = h(params['lat'] || 'null') %> - <% zoom = h(params['zoom'] || '14') %> -<% end %> +if params['mlon'] and params['mlat'] + lon = h(params['mlon']) + lat = h(params['mlat']) + zoom = h(params['zoom'] || '14') + +elsif @user and params['lon'].nil? and params['lat'].nil? and params['gpx'].nil? + lon = @user.home_lon + lat = @user.home_lat + zoom = '14' +else + lon = h(params['lon'] || 'null') + lat = h(params['lat'] || 'null') + zoom = h(params['zoom'] || '14') +end +%>
      You need a Flash player to use Potlatch, the From c321c026f265c6c2b25a3d564232a12f6ca48c0a Mon Sep 17 00:00:00 2001 From: Harry Wood Date: Thu, 2 Apr 2009 18:08:46 +0000 Subject: [PATCH 344/381] tweak lat lon decision logic for the edit tab. made it more similar to logic in index, including using cookie --- app/views/site/edit.rhtml | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/app/views/site/edit.rhtml b/app/views/site/edit.rhtml index 72ef5aa0d..daa78f3b9 100644 --- a/app/views/site/edit.rhtml +++ b/app/views/site/edit.rhtml @@ -24,20 +24,33 @@ <% session[:token] = @user.tokens.create.token unless session[:token] -if params['mlon'] and params['mlat'] +# Decide on a lat lon to initialise potlatch with. Various ways of doing this +if params['lon'] and params['lat'] + lon = h(params['lon']) + lat = h(params['lat']) + zoom = h(params['zoom']) + +elsif params['mlon'] and params['mlat'] lon = h(params['mlon']) lat = h(params['mlat']) - zoom = h(params['zoom'] || '14') + zoom = h(params['zoom']) -elsif @user and params['lon'].nil? and params['lat'].nil? and params['gpx'].nil? +elsif params['gpx'] + #use gpx id to locate (dealt with below) + +elsif cookies.key?("_osm_location") + lon,lat,zoom,layers = cookies["_osm_location"].split("|") + +elsif @user and !@user.home_lon.nil? and !@user.home_lat.nil? lon = @user.home_lon lat = @user.home_lat - zoom = '14' + else - lon = h(params['lon'] || 'null') - lat = h(params['lat'] || 'null') - zoom = h(params['zoom'] || '14') + #catch all. Do nothing. lat=nil, lon=nil + #Currently this results in potlatch starting up at 0,0 (Atlantic ocean). end + +zoom='14' if zoom.nil? %>
      From 9b72debd9af91a5d312bdf41168c1e37bcbaa696 Mon Sep 17 00:00:00 2001 From: Tom Hughes Date: Wed, 8 Apr 2009 13:35:35 +0000 Subject: [PATCH 345/381] Fix formatting of changeset details. --- app/views/browse/_changeset_details.rhtml | 37 ++++++++++++----------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/app/views/browse/_changeset_details.rhtml b/app/views/browse/_changeset_details.rhtml index 2b711611b..5b724012a 100644 --- a/app/views/browse/_changeset_details.rhtml +++ b/app/views/browse/_changeset_details.rhtml @@ -33,24 +33,27 @@ <% 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? %> - + + Bounding box: + <% if changeset_details.max_lat.nil? or changeset_details.min_lat.nil? or changeset_details.max_lon.nil? or changeset_details.min_lon.nil? %> No bounding box has been stored for this changeset. - - <% else %> - - - - - - - - - - - -
      Max Latitude: <%= changeset_details.max_lat/GeoRecord::SCALE.to_f -%>
      Min Longitude: <%= changeset_details.min_lon/GeoRecord::SCALE.to_f -%>Max Longitude: <%= changeset_details.max_lon/GeoRecord::SCALE.to_f -%>
      Min Latitude: <%= changeset_details.min_lon/GeoRecord::SCALE.to_f -%>
      - <% end %> + <% else %> + + + + + + + + + + + + +
      <%= changeset_details.max_lat/GeoRecord::SCALE.to_f -%>
      <%= changeset_details.min_lon/GeoRecord::SCALE.to_f -%><%= changeset_details.max_lon/GeoRecord::SCALE.to_f -%>
      <%= changeset_details.min_lon/GeoRecord::SCALE.to_f -%>
      + + <% end %> + <% unless @nodes.empty? %> From fdd5d2d7791f9138c339c9f18bcaa8946bd18d0c Mon Sep 17 00:00:00 2001 From: Harry Wood Date: Wed, 8 Apr 2009 16:55:22 +0000 Subject: [PATCH 346/381] new support for box=yes param on the homepage, and then link to this from various changeset display --- app/views/browse/_changeset_details.rhtml | 17 +++++++++---- app/views/changeset/_changeset.rhtml | 11 ++++----- app/views/changeset/list.rhtml | 2 +- app/views/changeset/list_bbox.rhtml | 17 +++++++------ app/views/changeset/list_user.rhtml | 4 ++++ app/views/site/index.rhtml | 8 +++++-- public/javascripts/map.js | 29 +++++++++++++++++++++++ 7 files changed, 67 insertions(+), 21 deletions(-) diff --git a/app/views/browse/_changeset_details.rhtml b/app/views/browse/_changeset_details.rhtml index 5b724012a..07f076e21 100644 --- a/app/views/browse/_changeset_details.rhtml +++ b/app/views/browse/_changeset_details.rhtml @@ -37,18 +37,25 @@ Bounding box: <% if changeset_details.max_lat.nil? or changeset_details.min_lat.nil? or changeset_details.max_lon.nil? or changeset_details.min_lon.nil? %> No bounding box has been stored for this changeset. - <% else %> + <% + else + minlon = changeset_details.min_lon/GeoRecord::SCALE.to_f + minlat = changeset_details.min_lat/GeoRecord::SCALE.to_f + maxlon = changeset_details.max_lon/GeoRecord::SCALE.to_f + maxlat = changeset_details.max_lat/GeoRecord::SCALE.to_f + %> - + - - + + + - +
      <%= changeset_details.max_lat/GeoRecord::SCALE.to_f -%><%=maxlat -%>
      <%= changeset_details.min_lon/GeoRecord::SCALE.to_f -%><%= changeset_details.max_lon/GeoRecord::SCALE.to_f -%><%=minlon -%>(box)<%=maxlon -%>
      <%= changeset_details.min_lon/GeoRecord::SCALE.to_f -%><%= minlon -%>
      diff --git a/app/views/changeset/_changeset.rhtml b/app/views/changeset/_changeset.rhtml index 049be5d33..a149a72e9 100644 --- a/app/views/changeset/_changeset.rhtml +++ b/app/views/changeset/_changeset.rhtml @@ -33,13 +33,12 @@ <% if changeset.min_lat.nil? %> (no edits) <% else - lat1 = changeset.min_lat/GeoRecord::SCALE.to_f - lat2 = changeset.max_lat/GeoRecord::SCALE.to_f - lon1 = changeset.min_lon/GeoRecord::SCALE.to_f - lon2 = changeset.max_lon/GeoRecord::SCALE.to_f + minlon = changeset.min_lon/GeoRecord::SCALE.to_f + minlat = changeset.min_lat/GeoRecord::SCALE.to_f + maxlon = changeset.max_lon/GeoRecord::SCALE.to_f + maxlat = changeset.max_lat/GeoRecord::SCALE.to_f %> - (<%= format("%0.3f",lat1) -%>,<%= format("%0.3f",lon1) -%>) to - (<%= format("%0.3f",lat2) -%>,<%= format("%0.3f",lon2) -%>) + (<%= format("%0.3f",minlon) -%>,<%= format("%0.3f",minlat) -%>,<%= format("%0.3f",maxlon) -%>,<%= format("%0.3f",maxlat) -%>) <% end %> diff --git a/app/views/changeset/list.rhtml b/app/views/changeset/list.rhtml index aa0d92758..4ac5082e1 100644 --- a/app/views/changeset/list.rhtml +++ b/app/views/changeset/list.rhtml @@ -16,4 +16,4 @@

      For more changesets, select a user and view their edits, or see the editing 'history' of a specific area.

      - +
      diff --git a/app/views/changeset/list_bbox.rhtml b/app/views/changeset/list_bbox.rhtml index 63e54e11d..e1cf9aedb 100644 --- a/app/views/changeset/list_bbox.rhtml +++ b/app/views/changeset/list_bbox.rhtml @@ -1,16 +1,16 @@

      History

      <% if @bbox!=nil - lon1 = @bbox[0] - lat1 = @bbox[1] - lon2 = @bbox[2] - lat2 = @bbox[3] + minlon = @bbox[0] + minlat = @bbox[1] + maxlon = @bbox[2] + maxlat = @bbox[3] %>

      Changesets within the area: -(<%= format("%0.3f",lat1) -%>,<%= format("%0.3f",lon1) -%>) to -(<%= format("%0.3f",lat2) -%>,<%= format("%0.3f",lon2) -%>) + (<%= format("%0.3f",minlon) -%>,<%= format("%0.3f",minlat) -%>,<%= format("%0.3f",maxlon) -%>,<%= format("%0.3f",maxlat) -%>) +

      <% if @edits.nil? or @edits.empty? %> @@ -32,6 +32,9 @@ Changesets within the area: <%= render :partial => 'changeset_paging_nav' %> + +

      For all changes everywhere see <%= link_to("Recent Changes", :controller => "browse", :action => "changesets") %>

      + <% end @@ -47,4 +50,4 @@ else end %>
      -
      + diff --git a/app/views/changeset/list_user.rhtml b/app/views/changeset/list_user.rhtml index 2f3ca04ce..386423a56 100644 --- a/app/views/changeset/list_user.rhtml +++ b/app/views/changeset/list_user.rhtml @@ -16,3 +16,7 @@ <%= render :partial => 'changeset_paging_nav' %> <% end %> + +

      For changes by all users see <%= link_to("Recent Changes", :controller => "browse", :action => "changesets") %>

      +
      + diff --git a/app/views/site/index.rhtml b/app/views/site/index.rhtml index 35509f4e0..66f69960b 100644 --- a/app/views/site/index.rhtml +++ b/app/views/site/index.rhtml @@ -45,6 +45,7 @@ if params['minlon'] and params['minlat'] and params['maxlon'] and params['maxlat minlat = h(params['minlat']) maxlon = h(params['maxlon']) maxlat = h(params['maxlat']) + box = true if params['box']=="yes" end # Decide on a lat lon to initialise the map with. Various ways of doing this @@ -109,13 +110,16 @@ end var bbox = new OpenLayers.Bounds(<%= minlon %>, <%= minlat %>, <%= maxlon %>, <%= maxlat %>); setMapExtent(bbox); + <% if box %> + box = addBoxToMap(bbox); + <% end %> <% else %> var centre = new OpenLayers.LonLat(<%= lon %>, <%= lat %>); var zoom = <%= zoom %>; - <% if params['scale'] and params['scale'].length > 0 then %> + <% if params['scale'] and params['scale'].length > 0 then %> zoom = scaleToZoom(<%= params['scale'].to_f() %>); - <% end %> + <% end %> setMapCenter(centre, zoom); <% end %> diff --git a/public/javascripts/map.js b/public/javascripts/map.js index a25564f0d..a52cd0a70 100644 --- a/public/javascripts/map.js +++ b/public/javascripts/map.js @@ -1,6 +1,7 @@ var epsg4326 = new OpenLayers.Projection("EPSG:4326"); var map; var markers; +var vectors; var popup; var nonamekeys = { @@ -82,6 +83,17 @@ function createMap(divName, options) { projection: "EPSG:900913" }); map.addLayer(markers); + + vectors = new OpenLayers.Layer.Vector("Vectors", { + displayInLayerSwitcher: false, + numZoomLevels: numZoomLevels, + maxExtent: new OpenLayers.Bounds(-20037508,-20037508,20037508,20037508), + maxResolution: 156543, + units: "m", + projection: "EPSG:900913" + }); + map.addLayer(vectors); + return map; } @@ -106,6 +118,19 @@ function addMarkerToMap(position, icon, description) { return marker; } +function addBoxToMap(boxbounds) { + box = new OpenLayers.Feature.Vector( + boxbounds.toGeometry().transform(new OpenLayers.Projection("EPSG:4326"), map.getProjectionObject()) ); + + box.style = { + 'strokeWidth': 3, + 'strokeColor': '#0000ff', + 'fillOpacity': 0, + }; + vectors.addFeatures(box); + return box; +} + function openMapPopup(marker, description) { closeMapPopup(); @@ -129,6 +154,10 @@ function removeMarkerFromMap(marker){ markers.removeMarker(marker); } +function removeBoxFromMap(box){ + vectors.removeFeature(box); +} + function getMapCenter(center, zoom) { return map.getCenter().clone().transform(map.getProjectionObject(), epsg4326); } From 7daa4f5d2bf9ad43f4e415a4f0b3f208fa414ffe Mon Sep 17 00:00:00 2001 From: Tom Hughes Date: Wed, 8 Apr 2009 23:14:50 +0000 Subject: [PATCH 347/381] Tidy up new box code. --- public/javascripts/map.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/public/javascripts/map.js b/public/javascripts/map.js index a52cd0a70..71edee56c 100644 --- a/public/javascripts/map.js +++ b/public/javascripts/map.js @@ -118,16 +118,16 @@ function addMarkerToMap(position, icon, description) { return marker; } -function addBoxToMap(boxbounds) { - box = new OpenLayers.Feature.Vector( - boxbounds.toGeometry().transform(new OpenLayers.Projection("EPSG:4326"), map.getProjectionObject()) ); +function addBoxToMap(boxbounds) { + var geometry = boxbounds.toGeometry().transform(epsg4326, map.getProjectionObject()); + var box = new OpenLayers.Feature.Vector(geometry, {}, { + strokeWidth: 3, + strokeColor: '#0000ff', + fillOpacity: 0, + }); - box.style = { - 'strokeWidth': 3, - 'strokeColor': '#0000ff', - 'fillOpacity': 0, - }; vectors.addFeatures(box); + return box; } From b02c873c0e892b4bd0b0e511020caaf86337b1de Mon Sep 17 00:00:00 2001 From: Tom Hughes Date: Wed, 8 Apr 2009 23:19:41 +0000 Subject: [PATCH 348/381] Eliminate duplication of tag printing logic. --- app/views/browse/_changeset_details.rhtml | 16 +--------------- app/views/browse/_common_details.rhtml | 11 +---------- app/views/browse/_tag_details.rhtml | 10 ++++++++++ 3 files changed, 12 insertions(+), 25 deletions(-) create mode 100644 app/views/browse/_tag_details.rhtml diff --git a/app/views/browse/_changeset_details.rhtml b/app/views/browse/_changeset_details.rhtml index 07f076e21..baeb44508 100644 --- a/app/views/browse/_changeset_details.rhtml +++ b/app/views/browse/_changeset_details.rhtml @@ -17,21 +17,7 @@ <% end %> - <% unless changeset_details.tags_as_hash.empty? %> - - Tags: - - - <%= render :partial => "tag", :collection => changeset_details.tags_as_hash %> -
      - - - <% else %> - - Tags - There are no tags for this changeset - - <% end %> + <%= render :partial => "tag_details", :object => changeset_details %> Bounding box: diff --git a/app/views/browse/_common_details.rhtml b/app/views/browse/_common_details.rhtml index 71a9dd314..09cf4cf2d 100644 --- a/app/views/browse/_common_details.rhtml +++ b/app/views/browse/_common_details.rhtml @@ -20,13 +20,4 @@ <%= link_to common_details.changeset_id, :action => :changeset, :id => common_details.changeset_id %> -<% unless common_details.tags_as_hash.empty? %> - - Tags: - - - <%= render :partial => "tag", :collection => common_details.tags_as_hash %> -
      - - -<% end %> +<%= render :partial => "tag_details", :object => common_details %> diff --git a/app/views/browse/_tag_details.rhtml b/app/views/browse/_tag_details.rhtml new file mode 100644 index 000000000..dab36266b --- /dev/null +++ b/app/views/browse/_tag_details.rhtml @@ -0,0 +1,10 @@ +<% unless tag_details.tags_as_hash.empty? %> + + Tags: + + + <%= render :partial => "tag", :collection => tag_details.tags_as_hash %> +
      + + +<% end %> From 1262b376ef56da9f5b8ec6cfd708620069f0ff87 Mon Sep 17 00:00:00 2001 From: Tom Hughes Date: Wed, 8 Apr 2009 23:44:43 +0000 Subject: [PATCH 349/381] Add a mini map to the changeset view in the data browser. --- app/views/browse/_map.rhtml | 26 +++++++++++++++++++++----- app/views/browse/changeset.rhtml | 7 +++++-- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/app/views/browse/_map.rhtml b/app/views/browse/_map.rhtml index ad2d2d307..d972104ba 100644 --- a/app/views/browse/_map.rhtml +++ b/app/views/browse/_map.rhtml @@ -2,7 +2,7 @@ <%= javascript_include_tag '/openlayers/OpenStreetMap.js' %> <%= javascript_include_tag 'map.js' %> - <% if map.visible %> + <% if map.instance_of? Changeset or map.visible %>
      Loading... @@ -13,6 +13,25 @@