From: Shaun McDonald Date: Thu, 25 Sep 2008 15:06:05 +0000 (+0000) Subject: Doing a resync from mainline 8633:10895. There was one simple to resolve conflict... X-Git-Tag: live~8606^2~305 X-Git-Url: https://git.openstreetmap.org./rails.git/commitdiff_plain/d9e070e06956801aba2378c1b79b4d9f13ae12ee?hp=c2854a8056a19219d4ecd4e29099f2ef6224f2d0 Doing a resync from mainline 8633:10895. There was one simple to resolve conflict on app/models/node.rb. Also moving the migrations for API0.6 to new sequence numbers since there was some new migrations added to mainline, where the migration numbers would conflict if not moved. --- diff --git a/app/controllers/api_controller.rb b/app/controllers/api_controller.rb index 6b36b41ae..2f040a92b 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 @@ -254,8 +254,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; diff --git a/app/controllers/application.rb b/app/controllers/application.rb index ce13a6aa3..1c27cb4d5 100644 --- a/app/controllers/application.rb +++ b/app/controllers/application.rb @@ -71,7 +71,7 @@ class ApplicationController < ActionController::Base # phrase from that, we can also put the error message into the status # message. For now, rails won't let us) def report_error(message) - render :nothing => true, :status => :bad_request + render :text => message, :status => :bad_request # Todo: some sort of escaping of problem characters in the message response.headers['Error'] = message end @@ -82,6 +82,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 diff --git a/app/controllers/changeset_controller.rb b/app/controllers/changeset_controller.rb new file mode 100644 index 000000000..8668611eb --- /dev/null +++ b/app/controllers/changeset_controller.rb @@ -0,0 +1,169 @@ +# The ChangesetController is the RESTful interface to Changeset objects + +class ChangesetController < ApplicationController + require 'xml/libxml' + + 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 + 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 + + 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]) + render :text => changeset.to_xml.to_s, :content_type => "text/xml" + rescue ActiveRecord::RecordNotFound + 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? + 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} + + 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 + 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 + 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/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/controllers/node_controller.rb b/app/controllers/node_controller.rb index edc3675e5..4a7527734 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! @@ -50,17 +51,14 @@ class NodeController < ApplicationController new_node = Node.from_xml(request.raw_post) if new_node and new_node.id == node.id - node.user_id = @user.id - node.latitude = new_node.latitude - node.longitude = new_node.longitude - node.tags = new_node.tags - node.visible = true - node.save_with_history! - - render :nothing => true + node.update_from(new_node, @user) + render :text => node.version.to_s, :content_type => "text/plain" else render :nothing => true, :status => :bad_request end + rescue OSM::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 @@ -71,24 +69,13 @@ class NodeController < ApplicationController def delete begin node = Node.find(params[:id]) + node.delete_with_history(@user) - 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 - else - render :text => "", :status => :gone - end + render :nothing => true rescue ActiveRecord::RecordNotFound render :nothing => true, :status => :not_found + 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 2b1ba6c75..c49ecd4d7 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! @@ -50,24 +51,15 @@ 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 + render :text => relation.version.to_s, :content_type => "text/plain" else render :nothing => true, :status => :bad_request end rescue ActiveRecord::RecordNotFound render :nothing => true, :status => :not_found - rescue - render :nothing => true, :status => :internal_server_error + rescue OSM::APIError => ex + render ex.render_opts end end @@ -75,26 +67,11 @@ class RelationController < ApplicationController #XXX check if member somewhere! begin relation = Relation.find(params[:id]) - - if relation.visible - if RelationMember.find(:first, :joins => "INNER JOIN current_relations ON current_relations.id=current_relation_members.id", :conditions => [ "visible = 1 AND member_type='relation' and member_id=?", params[:id]]) - render :text => "", :status => :precondition_failed - else - relation.user_id = @user.id - relation.tags = [] - relation.members = [] - relation.visible = false - relation.save_with_history! - - render :nothing => true - end - else - render :text => "", :status => :gone - end + relation.delete_with_history(@user) + 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 3b6491cf0..cf1634fa5 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! @@ -39,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 @@ -50,20 +53,13 @@ class WayController < ApplicationController new_way = Way.from_xml(request.raw_post) if new_way and new_way.id == way.id - if !new_way.preconditions_ok? - render :text => "", :status => :precondition_failed - else - way.user_id = @user.id - way.tags = new_way.tags - way.nds = new_way.nds - way.visible = true - way.save_with_history! - - render :nothing => true - end + way.update_from(new_way, @user) + render :text => way.version.to_s, :content_type => "text/plain" else render :nothing => true, :status => :bad_request end + rescue OSM::APIError => ex + render ex.render_opts rescue ActiveRecord::RecordNotFound render :nothing => true, :status => :not_found end @@ -73,14 +69,12 @@ 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 - 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/app/models/changeset.rb b/app/models/changeset.rb new file mode 100644 index 000000000..c9eeb0018 --- /dev/null +++ b/app/models/changeset.rb @@ -0,0 +1,111 @@ +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 + + 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 + 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 + + return el1 + 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/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/app/models/node.rb b/app/models/node.rb index cec755f47..677023179 100644 --- a/app/models/node.rb +++ b/app/models/node.rb @@ -17,6 +17,8 @@ class Node < ActiveRecord::Base has_many :way_nodes has_many :ways, :through => :way_nodes + has_many :node_tags, :foreign_key => :id + has_many :old_way_nodes has_many :ways_via_history, :class_name=> "Way", :through => :old_way_nodes, :source => :way @@ -60,64 +62,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 Node.from_xml_node(pt, create) + end + rescue + return nil + end + end - return nil unless node.in_world? + 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 - unless create - if pt['id'] != '0' - node.id = pt['id'].to_i - end - end + return nil unless node.in_world? - node.visible = pt['visible'] and pt['visible'] == 'true' + unless create + if pt['id'] != '0' + node.id = pt['id'].to_i + end + end - if create - node.timestamp = Time.now - else - if pt['timestamp'] - node.timestamp = Time.parse(pt['timestamp']) - end - end + node.visible = pt['visible'] and pt['visible'] == 'true' - tags = [] + if create + node.timestamp = Time.now + else + if pt['timestamp'] + node.timestamp = Time.parse(pt['timestamp']) + end + end - pt.find('tag').each do |tag| - tags << [tag['k'],tag['v']] - end + tags = [] - node.tags = Tags.join(tags) - end - rescue - node = nil + 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 + + 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 - # 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 @@ -136,7 +183,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 @@ -145,15 +192,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 + return tags + end + + def tags + unless @tags + @tags = {} + self.node_tags.each do |tag| + @tags[tag.k] = tag.v + end end - hash + @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/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.rb b/app/models/old_node.rb index 76eab8427..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 @@ -29,8 +23,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 +40,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 +49,58 @@ 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 [] + 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/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 diff --git a/app/models/old_relation.rb b/app/models/old_relation.rb index bac03c4d2..f5885f39f 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 @@ -91,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 63265d6bf..3c88c4673 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 @@ -94,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 c8516b58a..e46da5ade 100644 --- a/app/models/relation.rb +++ b/app/models/relation.rb @@ -19,32 +19,38 @@ 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 + return Relation.from_xml_node(pt, create) + end + rescue + return nil + end + end - if create - relation.timestamp = Time.now - relation.visible = true - else - if pt['timestamp'] - relation.timestamp = Time.parse(pt['timestamp']) - end - end + def self.from_xml_node(pt, create=false) + relation = Relation.new - pt.find('tag').each do |tag| - relation.add_tag_keyval(tag['k'], tag['v']) - end + if !create and pt['id'] != '0' + relation.id = pt['id'].to_i + end - pt.find('member').each do |member| - relation.add_member(member['type'], member['ref'], member['role']) - end + relation.version = pt['version'] + + if create + relation.timestamp = Time.now + relation.visible = true + else + if pt['timestamp'] + relation.timestamp = Time.parse(pt['timestamp']) end - rescue - relation = nil + 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 @@ -61,6 +67,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? @@ -177,13 +184,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 @@ -193,9 +199,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 @@ -211,6 +215,36 @@ 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 + elsif new_relation.version != version + raise OSM::APIVersionMismatchError.new(new_relation.version, version) + 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? # These are hastables that store an id in the index of all # the nodes/way/relations that have already been added. diff --git a/app/models/way.rb b/app/models/way.rb index 958944200..3bc8bcebe 100644 --- a/app/models/way.rb +++ b/app/models/way.rb @@ -21,32 +21,38 @@ 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 + return Way.from_xml_node(pt, create) + end + rescue + return nil + end + end - if create - way.timestamp = Time.now - way.visible = true - else - if pt['timestamp'] - way.timestamp = Time.parse(pt['timestamp']) - end - end + def self.from_xml_node(pt, create=false) + way = Way.new - pt.find('tag').each do |tag| - way.add_tag_keyval(tag['k'], tag['v']) - end + if !create and pt['id'] != '0' + way.id = pt['id'].to_i + end + + way.version = pt['version'] - pt.find('nd').each do |nd| - way.add_nd_num(nd['ref']) - end + if create + way.timestamp = Time.now + way.visible = true + else + if pt['timestamp'] + way.timestamp = Time.parse(pt['timestamp']) end - rescue - way = nil + end + + pt.find('tag').each do |tag| + way.add_tag_keyval(tag['k'], tag['v']) + end + + pt.find('nd').each do |nd| + way.add_nd_num(nd['ref']) end return way @@ -74,6 +80,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? @@ -162,15 +169,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 @@ -178,13 +182,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 @@ -193,11 +193,25 @@ class Way < ActiveRecord::Base nd.save! sequence += 1 end + + old_way = OldWay.from_way(self) + old_way.timestamp = t + old_way.save_with_dependencies! end + end - old_way = OldWay.from_way(self) - old_way.timestamp = t - old_way.save_with_dependencies! + 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 + self.nds = new_way.nds + self.visible = true + save_with_history! + end end def preconditions_ok? @@ -211,12 +225,13 @@ 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, # 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 @@ -234,6 +249,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) # delete the nodes not used by other ways self.unshared_node_ids.each do |node_id| @@ -245,7 +262,7 @@ class Way < ActiveRecord::Base self.user_id = user.id - self.delete_with_relations_and_history(user) + self.delete_with_history(user) end # Find nodes that belong to this way only 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 << < '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+/ + 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+/ map.connect "api/#{API_VERSION}/node/:id/relations", :controller => 'relation', :action => 'relations_for_node', :id => /\d+/ @@ -54,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' diff --git a/db/migrate/016_add_timestamp_indexes.rb b/db/migrate/016_add_timestamp_indexes.rb new file mode 100644 index 000000000..c6b3bc7c2 --- /dev/null +++ b/db/migrate/016_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 diff --git a/db/migrate/017_populate_node_tags_and_remove.rb b/db/migrate/017_populate_node_tags_and_remove.rb new file mode 100644 index 000000000..29a91c70b --- /dev/null +++ b/db/migrate/017_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/017_populate_node_tags_and_remove_helper.c b/db/migrate/017_populate_node_tags_and_remove_helper.c new file mode 100644 index 000000000..5a0fbb6cd --- /dev/null +++ b/db/migrate/017_populate_node_tags_and_remove_helper.c @@ -0,0 +1,241 @@ +#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, tmp; + + while (*i) { + if (*i == '\\') { + i++; + switch (tmp = *i++) { + case 's': *o++ = ';'; break; + case 'e': *o++ = '='; break; + case '\\': *o++ = '\\'; break; + default: *o++ = tmp; break; + } + } else { + *o++ = *i++; + } + } +} + +static int read_node_tags(char **tags, char **k, char **v) { + if (!**tags) return 0; + char *i = strchr(*tags, ';'); + if (!i) i = *tags + strlen(*tags); + char *j = strchr(*tags, '='); + *k = *tags; + if (j && j < i) { + *v = j + 1; + } else { + *v = i; + } + *tags = *i ? i + 1 : i; + *i = '\0'; + if (j) *j = '\0'; + + unescape(*k); + unescape(*v); + + return 1; +} + +struct data { + MYSQL *mysql; + size_t version_size; + uint16_t *version; +}; + +static void proc_nodes(struct data *d, const char *tbl, FILE *out, FILE *out_tags, int hist) { + MYSQL_RES *res; + MYSQL_ROW row; + char query[256]; + + snprintf(query, sizeof(query), "SELECT id, latitude, longitude, " + "user_id, visible, tags, timestamp, tile FROM %s", tbl); + if (mysql_query(d->mysql, query)) + exit_mysql_err(d->mysql); + + res = mysql_use_result(d->mysql); + if (!res) exit_mysql_err(d->mysql); + + while ((row = mysql_fetch_row(res))) { + unsigned long id = strtoul(row[0], NULL, 10); + uint32_t version; + + if (id >= d->version_size) { + fprintf(stderr, "preallocated nodes size exceeded"); + abort(); + } + + if (hist) { + version = ++(d->version[id]); + + fprintf(out, "\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",\"%u\"\n", + row[0], row[1], row[2], row[3], row[4], row[6], row[7], version); + } else { + /*fprintf(out, "\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",\"%s\"\n", + row[0], row[1], row[2], row[3], row[4], row[6], row[7]);*/ + } + + char *tags_it = row[5], *k, *v; + while (read_node_tags(&tags_it, &k, &v)) { + if (hist) { + fprintf(out_tags, "\"%s\",\"%u\",", row[0], version); + } else { + fprintf(out_tags, "\"%s\",", row[0]); + } + + write_csv_col(out_tags, k, ','); + write_csv_col(out_tags, v, '\n'); + } + } + if (mysql_errno(d->mysql)) exit_mysql_err(d->mysql); + + mysql_free_result(res); +} + +static size_t select_size(MYSQL *mysql, const char *q) { + MYSQL_RES *res; + MYSQL_ROW row; + size_t ret; + + if (mysql_query(mysql, q)) + exit_mysql_err(mysql); + + res = mysql_store_result(mysql); + if (!res) exit_mysql_err(mysql); + + row = mysql_fetch_row(res); + if (!row) exit_mysql_err(mysql); + + if (row[0]) { + ret = strtoul(row[0], NULL, 10); + } else { + ret = 0; + } + + mysql_free_result(res); + + return ret; +} + +static MYSQL *connect_to_mysql(char **argv) { + MYSQL *mysql = mysql_init(NULL); + if (!mysql) exit_mysql_err(mysql); + + if (!mysql_real_connect(mysql, argv[1], argv[2], argv[3], argv[4], + argv[5][0] ? atoi(argv[5]) : 0, argv[6][0] ? argv[6] : NULL, 0)) + exit_mysql_err(mysql); + + if (mysql_set_character_set(mysql, "utf8")) + exit_mysql_err(mysql); + + return mysql; +} + +static void open_file(FILE **f, char *fn) { + *f = fopen(fn, "w+"); + if (!*f) { + perror("fopen"); + exit(EXIT_FAILURE); + } +} + +int main(int argc, char **argv) { + size_t prefix_len; + FILE *current_nodes, *current_node_tags, *nodes, *node_tags; + char *tempfn; + struct data data, *d = &data; + + if (argc != 8) { + printf("Usage: 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 = (uint16_t *) malloc(sizeof(uint16_t) * d->version_size); + if (!d->version) { + perror("malloc"); + abort(); + exit(EXIT_FAILURE); + } + memset(d->version, 0, sizeof(uint16_t) * d->version_size); + + prefix_len = strlen(argv[7]); + tempfn = (char *) malloc(prefix_len + 32); + strcpy(tempfn, argv[7]); + + strcpy(tempfn + prefix_len, "current_nodes"); + open_file(¤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/018_move_to_innodb.rb b/db/migrate/018_move_to_innodb.rb new file mode 100644 index 000000000..c551b0ef8 --- /dev/null +++ b/db/migrate/018_move_to_innodb.rb @@ -0,0 +1,30 @@ +class MoveToInnodb < ActiveRecord::Migration + @@conv_tables = ['nodes', 'ways', 'way_tags', 'way_nodes', + 'current_way_tags', 'relation_members', + 'relations', 'relation_tags', 'current_relation_tags'] + + @@ver_tbl = ['nodes', 'ways', 'relations'] + + def self.up + 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) FROM #{tbl} WHERE #{tbl}.id = current_#{tbl}.id)" + } + end + + def self.down + raise IrreversibleMigration.new + end +end diff --git a/db/migrate/019_key_constraints.rb b/db/migrate/019_key_constraints.rb new file mode 100644 index 000000000..40f98be02 --- /dev/null +++ b/db/migrate/019_key_constraints.rb @@ -0,0 +1,50 @@ +class KeyConstraints < ActiveRecord::Migration + def self.up + # Primary keys + add_primary_key :current_node_tags, [:id, :k] + add_primary_key :current_way_tags, [:id, :k] + add_primary_key :current_relation_tags, [:id, :k] + + add_primary_key :node_tags, [:id, :version, :k] + add_primary_key :way_tags, [:id, :version, :k] + add_primary_key :relation_tags, [:id, :version, :k] + + add_primary_key :nodes, [:id, :version] + + # Remove indexes superseded by primary keys + remove_index :current_way_tags, :name => :current_way_tags_id_idx + remove_index :current_relation_tags, :name => :current_relation_tags_id_idx + + remove_index :way_tags, :name => :way_tags_id_version_idx + remove_index :relation_tags, :name => :relation_tags_id_version_idx + + remove_index :nodes, :name => :nodes_uid_idx + + # Foreign keys (between ways, way_tags, way_nodes, etc.) + add_foreign_key :current_node_tags, [:id], :current_nodes + add_foreign_key :node_tags, [:id, :version], :nodes + + add_foreign_key :current_way_tags, [:id], :current_ways + add_foreign_key :current_way_nodes, [:id], :current_ways + add_foreign_key :way_tags, [:id, :version], :ways + add_foreign_key :way_nodes, [:id, :version], :ways + + add_foreign_key :current_relation_tags, [:id], :current_relations + add_foreign_key :current_relation_members, [:id], :current_relations + add_foreign_key :relation_tags, [:id, :version], :relations + add_foreign_key :relation_members, [:id, :version], :relations + + # Foreign keys (between different types of primitives) + add_foreign_key :current_way_nodes, [:node_id], :current_nodes, [:id] + + # FIXME: We don't have foreign keys for relation members since the id + # might point to a different table depending on the `type' column. + # We'd probably need different current_relation_member_nodes, + # current_relation_member_ways and current_relation_member_relations + # tables for this to work cleanly. + end + + def self.down + raise IrreversibleMigration.new + end +end diff --git a/db/migrate/020_add_changesets.rb b/db/migrate/020_add_changesets.rb new file mode 100644 index 000000000..40455ec68 --- /dev/null +++ b/db/migrate/020_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 diff --git a/lib/geo_record.rb b/lib/geo_record.rb index f1a923c42..2740eab0c 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 private 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) diff --git a/lib/osm.rb b/lib/osm.rb index 9c271607d..a64aa8c48 100644 --- a/lib/osm.rb +++ b/lib/osm.rb @@ -10,6 +10,9 @@ 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 +21,30 @@ 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. + class APIVersionMismatchError < APIError + def initialize(provided, latest) + @provided, @latest = provided, latest + end + + attr_reader :provided, :latest + + def render_opts + { :text => "Version mismatch: Provided " + provided.to_s + + ", server had: " + latest.to_s, :status => :bad_request } + end end # Helper methods for going to/from mercator and lat/lng. 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 diff --git a/test/fixtures/current_node_tags.yml b/test/fixtures/current_node_tags.yml new file mode 100644 index 000000000..ce68a5439 --- /dev/null +++ b/test/fixtures/current_node_tags.yml @@ -0,0 +1,15 @@ +t1: + id: 1 + k: testvisible + v: yes + +t2: + id: 2 + k: testused + v: yes + +t3: + id: 3 + k: test + v: yes + diff --git a/test/fixtures/current_nodes.yml b/test/fixtures/current_nodes.yml index dd3bd2487..19fad704e 100644 --- a/test/fixtures/current_nodes.yml +++ b/test/fixtures/current_nodes.yml @@ -1,45 +1,143 @@ # 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 - tags: test=yes + version: 1 + tile: <%= QuadTile.tile_for_point(1,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 - tags: test=yes + version: 1 + tile: <%= QuadTile.tile_for_point(2,2) %> timestamp: 2007-01-01 00:00:00 used_node_1: id: 3 - latitude: 3 - longitude: 3 + latitude: <%= 3*SCALE %> + longitude: <%= 3*SCALE %> user_id: 1 visible: 1 - tags: test=yes + version: 1 + tile: <%= QuadTile.tile_for_point(3,3) %> timestamp: 2007-01-01 00:00:00 used_node_2: id: 4 - latitude: 4 - longitude: 4 + latitude: <%= 4*SCALE %> + longitude: <%= 4*SCALE %> user_id: 1 visible: 1 - tags: test=yes + version: 1 + tile: <%= QuadTile.tile_for_point(4,4) %> timestamp: 2007-01-01 00:00:00 node_used_by_relationship: id: 5 - latitude: 5 - longitude: 5 + latitude: <%= 5*SCALE %> + 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 - tags: test=yes + version: 1 + tile: <%= QuadTile.tile_for_point(200,200) %> timestamp: 2007-01-01 00:00:00 + 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 b49c4eb4e..22fab1863 100644 --- a/test/fixtures/messages.yml +++ b/test/fixtures/messages.yml @@ -1,5 +1,16 @@ # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html one: - id: 1 + from_user_id: 1 + title: test message 1 + body: some body text + sent_on: "2008-05-01 12:34:56" + message_read: false + to_user_id: 2 + two: - id: 2 + from_user_id: 2 + title: test message 2 + body: some body test + sent_on: "2008-05-02 12:45:23" + message_read: true + to_user_id: 1 diff --git a/test/fixtures/node_tags.yml b/test/fixtures/node_tags.yml new file mode 100644 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..9699e395c 100644 --- a/test/fixtures/nodes.yml +++ b/test/fixtures/nodes.yml @@ -1,46 +1,143 @@ # 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 - tags: test=yes + version: 1 + tile: <%= QuadTile.tile_for_point(1,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 - tags: test=yes + version: 1 + tile: <%= QuadTile.tile_for_point(2,2) %> timestamp: 2007-01-01 00:00:00 used_node_1: id: 3 - latitude: 3 - longitude: 3 + latitude: <%= 3*SCALE %> + longitude: <%= 3*SCALE %> user_id: 1 visible: 1 - tags: test=yes + version: 1 + tile: <%= QuadTile.tile_for_point(3,3) %> timestamp: 2007-01-01 00:00:00 used_node_2: id: 4 - latitude: 4 - longitude: 4 + latitude: <%= 4*SCALE %> + longitude: <%= 4*SCALE %> user_id: 1 visible: 1 - tags: test=yes + version: 1 + tile: <%= QuadTile.tile_for_point(4,4) %> timestamp: 2007-01-01 00:00:00 node_used_by_relationship: id: 5 - latitude: 5 - longitude: 5 + latitude: <%= 5*SCALE %> + longitude: <%= 5*SCALE %> user_id: 1 visible: 1 - tags: test=yes + 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/fixtures/user_preferences.yml b/test/fixtures/user_preferences.yml index 5bf02933a..59ebd0542 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_id: 1 + k: "key" + v: "value" + +two: + user_id: 1 + k: "some_key" + v: "some_value" diff --git a/test/fixtures/users.yml b/test/fixtures/users.yml index bcce2f7db..28e1aca3d 100644 --- a/test/fixtures/users.yml +++ b/test/fixtures/users.yml @@ -1,13 +1,38 @@ # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html normal_user: - email: test@openstreetmap.org id: 1 + email: test@openstreetmap.org active: 1 pass_crypt: <%= Digest::MD5.hexdigest('test') %> creation_time: "2007-01-01 00:00:00" 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: + id: 2 + email: test@example.com + 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 + +inactive_user: + id: 3 + email: inactive@openstreetmap.org + active: 0 + pass_crypt: <%= Digest::MD5::hexdigest('test2') %> + display_name: Inactive User + data_public: 1 + description: description + home_lat: 123.4 + home_lon: 12.34 + home_zoom: 15 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/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 # ------------------------------------- diff --git a/test/test_helper.rb b/test/test_helper.rb index b1d7a8fcc..22cc0e15c 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 @@ -28,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 diff --git a/test/unit/current_node_tag_test.rb b/test/unit/current_node_tag_test.rb new file mode 100644 index 000000000..7fb1deff5 --- /dev/null +++ b/test/unit/current_node_tag_test.rb @@ -0,0 +1,20 @@ +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 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/message_test.rb b/test/unit/message_test.rb index 8804fe003..4de1a7b29 100644 --- a/test/unit/message_test.rb +++ b/test/unit/message_test.rb @@ -1,10 +1,34 @@ require File.dirname(__FILE__) + '/../test_helper' class MessageTest < Test::Unit::TestCase - fixtures :messages + fixtures :messages, :users - # Replace this with your real tests. - def test_truth - assert true + # This needs to be updated when new fixtures are added + # or removed. + def test_check_message_count + assert_equal 2, Message.count + end + + def test_check_empty_message_fails + message = Message.new + assert !message.valid? + assert message.errors.invalid?(:title) + assert message.errors.invalid?(:body) + assert message.errors.invalid?(:sent_on) + assert true, message.message_read + end + + def test_validating_msgs + message = messages(:one) + assert message.valid? + massage = messages(:two) + assert message.valid? + end + + def test_invalid_send_recipient + message = messages(:one) + message.sender = nil + message.recipient = nil + assert !message.valid? end end diff --git a/test/unit/node_test.rb b/test/unit/node_test.rb index 95321b5cf..bb2b7dfa4 100644 --- a/test/unit/node_test.rb +++ b/test/unit/node_test.rb @@ -1,16 +1,87 @@ require File.dirname(__FILE__) + '/../test_helper' class NodeTest < Test::Unit::TestCase - fixtures :current_nodes, :nodes, :users + 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 = current_nodes(nod) + dbnode = Node.find(node.id) + assert_equal dbnode.lat, node.latitude.to_f/SCALE + assert_equal dbnode.lon, node.longitude.to_f/SCALE + assert_equal dbnode.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 helper method will check to make sure that a node is outwith the world, + # and has the same lat, lon and timesamp than what was put into the db by the + # fixture + def invalid_node_test(nod) + node = current_nodes(nod) + dbnode = Node.find(node.id) + assert_equal dbnode.lat, node.latitude.to_f/SCALE + assert_equal dbnode.lon, node.longitude.to_f/SCALE + assert_equal dbnode.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 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 +90,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 +104,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 +113,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 +122,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 +133,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 +154,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 +165,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 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 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 5468f7a2d..587fc71fb 100644 --- a/test/unit/user_test.rb +++ b/test/unit/user_test.rb @@ -2,9 +2,138 @@ require File.dirname(__FILE__) + '/../test_helper' class UserTest < Test::Unit::TestCase fixtures :users - - # Replace this with your real tests. - def test_truth - assert true + + def test_invalid_with_empty_attributes + user = User.new + assert !user.valid? + assert user.errors.invalid?(:email) + assert user.errors.invalid?(:pass_crypt) + assert user.errors.invalid?(:display_name) + assert user.errors.invalid?(:email) + assert !user.errors.invalid?(:home_lat) + assert !user.errors.invalid?(:home_lon) + assert !user.errors.invalid?(:home_zoom) + end + + def test_unique_email + new_user = User.new(:email => users(:normal_user).email, + :active => 1, + :pass_crypt => Digest::MD5.hexdigest('test'), + :display_name => "new user", + :data_public => 1, + :description => "desc") + assert !new_user.save + assert_equal ActiveRecord::Errors.default_error_messages[:taken], new_user.errors.on(:email) + end + + def test_unique_display_name + new_user = User.new(:email => "tester@openstreetmap.org", + :active => 0, + :pass_crypt => Digest::MD5.hexdigest('test'), + :display_name => users(:normal_user).display_name, + :data_public => 1, + :description => "desc") + assert !new_user.save + assert_equal ActiveRecord::Errors.default_error_messages[:taken], new_user.errors.on(:display_name) + end + + def test_email_valid + ok = %w{ a@s.com test@shaunmcdonald.me.uk hello_local@ping-d.ng + test_local@openstreetmap.org test-local@example.com + 輕觸搖晃的遊戲@ah.com も対応します@s.name } + bad = %w{ hi ht@ n@ @.com help@.me.uk help"hi.me.uk も対@応します } + + ok.each do |name| + user = users(:normal_user) + user.email = name + assert user.valid?, user.errors.full_messages + end + + bad.each do |name| + user = users(:normal_user) + user.email = name + assert !user.valid?, "#{name} is valid when it shouldn't be" + end + end + + def test_display_name_length + user = users(:normal_user) + user.display_name = "123" + assert user.valid?, " should allow nil display name" + user.display_name = "12" + assert !user.valid?, "should not allow 2 char name" + user.display_name = "" + assert !user.valid? + user.display_name = nil + # Don't understand why it isn't allowing a nil value, + # when the validates statements specifically allow it + # It appears the database does not allow null values + assert !user.valid? + end + + def test_display_name_valid + # Due to sanitisation in the view some of these that you might not + # expact are allowed + # However, would they affect the xml planet dumps? + ok = [ "Name", "'me", "he\"", "#ping", "
", "*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?", "/;.,?", "も対応します/" ] + ok.each do |display_name| + user = users(:normal_user) + user.display_name = display_name + assert user.valid?, "#{display_name} is invalid, when it should be" + end + + bad.each do |display_name| + user = users(:normal_user) + user.display_name = display_name + assert !user.valid?, "#{display_name} is valid when it shouldn't be" + assert_equal "is invalid", user.errors.on(:display_name) + end + end + + def test_friend_with + assert_equal 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 + + 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 + assert_equal [sec], norm.nearby + assert_equal 1, norm.nearby.size + assert_equal 1, Friend.count + assert_equal true, norm.is_friends_with?(sec) + assert_equal false, sec.is_friends_with?(norm) + assert_equal false, users(:normal_user).is_friends_with?(users(:inactive_user)) + assert_equal false, users(:second_user).is_friends_with?(users(:normal_user)) + assert_equal false, users(:second_user).is_friends_with?(users(:inactive_user)) + assert_equal false, users(:inactive_user).is_friends_with?(users(:normal_user)) + assert_equal false, users(:inactive_user).is_friends_with?(users(:second_user)) + Friend.delete(friend) + assert_equal 0, Friend.count end end