]> git.openstreetmap.org Git - rails.git/commitdiff
Doing a resync from mainline 8633:10895. There was one simple to resolve conflict...
authorShaun McDonald <shaun@shaunmcdonald.me.uk>
Thu, 25 Sep 2008 15:06:05 +0000 (15:06 +0000)
committerShaun McDonald <shaun@shaunmcdonald.me.uk>
Thu, 25 Sep 2008 15:06:05 +0000 (15:06 +0000)
52 files changed:
app/controllers/api_controller.rb
app/controllers/application.rb
app/controllers/changeset_controller.rb [new file with mode: 0644]
app/controllers/changeset_tag_controller.rb [new file with mode: 0644]
app/controllers/node_controller.rb
app/controllers/relation_controller.rb
app/controllers/way_controller.rb
app/models/changeset.rb [new file with mode: 0644]
app/models/changeset_tag.rb [new file with mode: 0644]
app/models/message.rb
app/models/node.rb
app/models/node_tag.rb [new file with mode: 0644]
app/models/old_node.rb
app/models/old_node_tag.rb [new file with mode: 0644]
app/models/old_relation.rb
app/models/old_way.rb
app/models/relation.rb
app/models/way.rb
app/views/browse/start.rjs
config/database.yml
config/environment.rb
config/routes.rb
db/migrate/016_add_timestamp_indexes.rb [new file with mode: 0644]
db/migrate/017_populate_node_tags_and_remove.rb [new file with mode: 0644]
db/migrate/017_populate_node_tags_and_remove_helper.c [new file with mode: 0644]
db/migrate/018_move_to_innodb.rb [new file with mode: 0644]
db/migrate/019_key_constraints.rb [new file with mode: 0644]
db/migrate/020_add_changesets.rb [new file with mode: 0644]
lib/geo_record.rb
lib/migrate.rb
lib/osm.rb
lib/tasks/populate_node_tags.rake [deleted file]
test/fixtures/current_node_tags.yml [new file with mode: 0644]
test/fixtures/current_nodes.yml
test/fixtures/current_ways.yml
test/fixtures/messages.yml
test/fixtures/node_tags.yml [new file with mode: 0644]
test/fixtures/nodes.yml
test/fixtures/user_preferences.yml
test/fixtures/users.yml
test/fixtures/way_nodes.yml
test/functional/api_controller_test.rb
test/functional/node_controller_test.rb
test/functional/old_way_controller_test.rb
test/functional/way_controller_test.rb
test/test_helper.rb
test/unit/current_node_tag_test.rb [new file with mode: 0644]
test/unit/message_test.rb
test/unit/node_test.rb
test/unit/old_node_test.rb [new file with mode: 0644]
test/unit/user_preference_test.rb
test/unit/user_test.rb

index 6b36b41ae947e25fdfe035f5377e2cc652e0d3a5..2f040a92bade1c64892bc6806c3d400f18469eff 100644 (file)
@@ -120,7 +120,7 @@ class ApiController < ApplicationController
       return
     end
     if node_ids.length == 0
-      render :text => "<osm version='0.5'></osm>", :content_type => "text/xml"
+      render :text => "<osm version='#{API_VERSION}'></osm>", :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;
index ce13a6aa3a6ae625407d3ac7fe2eaa11f7ab6ed9..1c27cb4d5e3ce3c9d752d805dcfe63952c99e50d 100644 (file)
@@ -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 (file)
index 0000000..8668611
--- /dev/null
@@ -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 (file)
index 0000000..3e8db3f
--- /dev/null
@@ -0,0 +1,9 @@
+class ChangesetTagController < ApplicationController
+  layout 'site'
+
+  def search
+    @tags = ChangesetTag.find(:all, :limit => 11, :conditions => ["match(v) against (?)", params[:query][:query].to_s] )
+  end
+
+
+end
index edc3675e58382fce0b8b5801a2e7180ab280cec2..4a75277348e1d6dc4de8a0a89424a9cadd441298 100644 (file)
@@ -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
 
index 2b1ba6c753c70df6579d381facebc7bd451be754..c49ecd4d701733b035271a922e3466c40bba0881 100644 (file)
@@ -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
 
index 3b6491cf0b3ceda5ed90ee7e1da5e49579c6af21..cf1634fa55699cccdae1084536256c3f7c5fc4bd 100644 (file)
@@ -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 (file)
index 0000000..c9eeb00
--- /dev/null
@@ -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 (file)
index 0000000..6298fbe
--- /dev/null
@@ -0,0 +1,5 @@
+class ChangesetTag < ActiveRecord::Base
+
+  belongs_to :changeset, :foreign_key => 'id'
+
+end
index 97e411192b0df5cedbd89d7255f236ac7fb2cf35..ec712be252a706b9c1e68541dd3e4a816b90f7a1 100644 (file)
@@ -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
index cec755f4765bfc35e9679256934512be093f74da..6770231790735b0ffc4e3f1957e5d4598de8e3df 100644 (file)
@@ -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 <osm> 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 <osm> 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 (file)
index 0000000..9795ff4
--- /dev/null
@@ -0,0 +1,5 @@
+class NodeTag < ActiveRecord::Base
+  set_table_name 'current_node_tags'
+
+  belongs_to :node, :foreign_key => 'id'
+end
index 76eab8427b2c570cce79846887706eb6c10923b6..6b6b71b53eaae06d0f7c591c919aaa51c81f3dd1 100644 (file)
@@ -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 (file)
index 0000000..26a6c92
--- /dev/null
@@ -0,0 +1,7 @@
+class OldNodeTag < ActiveRecord::Base
+  belongs_to :user
+
+  set_table_name 'node_tags'
+
+
+end
index bac03c4d2eff6811a9dc4b5d2c32e3bb988e76b3..f5885f39ffb59adc799f4b5ea7832066f9654f5a 100644 (file)
@@ -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'
index 63265d6bf5c77814e90205cc4c0c5138a65a04c3..3c88c4673401fb20b3cb5dcedac4067ff56a1db5 100644 (file)
@@ -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'
index c8516b58a3441c9f3b0ec38262d7628c8888d00f..e46da5ade03f3356a18d09008f9d598f0eaaff5d 100644 (file)
@@ -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.
index 958944200df628054c67c6c0eb8e60105d99bb38..3bc8bcebec9f7b99b7c318d594a18d2307556260 100644 (file)
@@ -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
index f22796afeb81d98576361c5158784bd367518ade..e257005a360ba993fd1ddc249f80a7af01024558 100644 (file)
@@ -189,7 +189,7 @@ page << <<EOJ
     if (size > 0.25) {
       setStatus("Unable to load: Bounding box size of " + size + " is too large (must be smaller than 0.25)");
     } else {
-      loadGML("/api/0.5/map?bbox=" + projected.toBBOX());
+      loadGML("/api/#{API_VERSION}/map?bbox=" + projected.toBBOX());
     }
   }
 
@@ -393,7 +393,7 @@ page << <<EOJ
     this.link.href = "";
     this.link.innerHTML = "Wait...";
 
-    new Ajax.Request("/api/0.5/" + this.type + "/" + this.feature.osm_id + "/history", {
+    new Ajax.Request("/api/#{API_VERSION}/" + this.type + "/" + this.feature.osm_id + "/history", {
       onComplete: OpenLayers.Function.bind(displayHistory, this)
     });
 
index b884f3b938fea8c5ea541e0d4af5fbbc16529dd7..a80e11b70784068b19c360ea1c9218abb538b9f5 100644 (file)
 #   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
@@ -23,14 +26,14 @@ development:
 test:
   adapter: mysql
   database: osm_test
-  username: root
-  password:
+  username: osm_test
+  password: osm_test
   host: localhost
 
 production:
   adapter: mysql
-  database: openstreetmap
-  username: openstreetmap
-  password: openstreetmap
-  host: db.openstreetmap.org
+  database: osm
+  username: osm
+  password: osm
+  host: localhost
 
index e6af619eb82b03aaaf8400db39ea951dbba227b6..08c43378820509f9efa9044430a945174e91a763 100644 (file)
@@ -11,7 +11,7 @@ RAILS_GEM_VERSION = '2.0.2' unless defined? RAILS_GEM_VERSION
 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 application status - possible settings are:
 #
index f040e1c1d791820e6b53d2cd9879eccb8664be13..a45ad1e0b3707b07e919169aa4e24d8bf3834e17 100644 (file)
@@ -1,6 +1,11 @@
 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', :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 (file)
index 0000000..c6b3bc7
--- /dev/null
@@ -0,0 +1,11 @@
+class AddTimestampIndexes < ActiveRecord::Migration
+  def self.up
+    add_index :current_ways, :timestamp, :name => :current_ways_timestamp_idx
+    add_index :current_relations, :timestamp, :name => :current_relations_timestamp_idx
+  end
+
+  def self.down
+    remove_index :current_ways, :name => :current_ways_timestamp_idx
+    remove_index :current_relations, :name => :current_relations_timestamp_idx
+  end
+end
diff --git a/db/migrate/017_populate_node_tags_and_remove.rb b/db/migrate/017_populate_node_tags_and_remove.rb
new file mode 100644 (file)
index 0000000..29a91c7
--- /dev/null
@@ -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 (file)
index 0000000..5a0fbb6
--- /dev/null
@@ -0,0 +1,241 @@
+#include <mysql.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+static void exit_mysql_err(MYSQL *mysql) {
+  const char *err = mysql_error(mysql);
+  if (err) {
+    fprintf(stderr, "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(&current_nodes, tempfn);
+
+  strcpy(tempfn + prefix_len, "current_node_tags");
+  open_file(&current_node_tags, tempfn);
+
+  strcpy(tempfn + prefix_len, "nodes");
+  open_file(&nodes, tempfn);
+
+  strcpy(tempfn + prefix_len, "node_tags");
+  open_file(&node_tags, tempfn);
+
+  free(tempfn);
+
+  proc_nodes(d, "nodes", nodes, node_tags, 1);
+  proc_nodes(d, "current_nodes", current_nodes, current_node_tags, 0);
+
+  free(d->version);
+
+  mysql_close(d->mysql);
+
+  fclose(current_nodes);
+  fclose(current_node_tags);
+  fclose(nodes);
+  fclose(node_tags);
+
+  exit(EXIT_SUCCESS);
+}
diff --git a/db/migrate/018_move_to_innodb.rb b/db/migrate/018_move_to_innodb.rb
new file mode 100644 (file)
index 0000000..c551b0e
--- /dev/null
@@ -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 (file)
index 0000000..40f98be
--- /dev/null
@@ -0,0 +1,50 @@
+class KeyConstraints < ActiveRecord::Migration
+  def self.up
+    # Primary keys
+    add_primary_key :current_node_tags, [:id, :k]
+    add_primary_key :current_way_tags, [:id, :k]
+    add_primary_key :current_relation_tags, [:id, :k]
+
+    add_primary_key :node_tags, [:id, :version, :k]
+    add_primary_key :way_tags, [:id, :version, :k]
+    add_primary_key :relation_tags, [:id, :version, :k]
+
+    add_primary_key :nodes, [:id, :version]
+
+    # Remove indexes superseded by primary keys
+    remove_index :current_way_tags, :name => :current_way_tags_id_idx
+    remove_index :current_relation_tags, :name => :current_relation_tags_id_idx
+
+    remove_index :way_tags, :name => :way_tags_id_version_idx
+    remove_index :relation_tags, :name => :relation_tags_id_version_idx
+
+    remove_index :nodes, :name => :nodes_uid_idx
+
+    # Foreign keys (between ways, way_tags, way_nodes, etc.)
+    add_foreign_key :current_node_tags, [:id], :current_nodes
+    add_foreign_key :node_tags, [:id, :version], :nodes
+
+    add_foreign_key :current_way_tags, [:id], :current_ways
+    add_foreign_key :current_way_nodes, [:id], :current_ways
+    add_foreign_key :way_tags, [:id, :version], :ways
+    add_foreign_key :way_nodes, [:id, :version], :ways
+
+    add_foreign_key :current_relation_tags, [:id], :current_relations
+    add_foreign_key :current_relation_members, [:id], :current_relations
+    add_foreign_key :relation_tags, [:id, :version], :relations
+    add_foreign_key :relation_members, [:id, :version], :relations
+
+    # Foreign keys (between different types of primitives)
+    add_foreign_key :current_way_nodes, [:node_id], :current_nodes, [:id]
+
+    # FIXME: We don't have foreign keys for relation members since the id
+    # might point to a different table depending on the `type' column.
+    # We'd probably need different current_relation_member_nodes,
+    # current_relation_member_ways and current_relation_member_relations
+    # tables for this to work cleanly.
+  end
+
+  def self.down
+    raise IrreversibleMigration.new
+  end
+end
diff --git a/db/migrate/020_add_changesets.rb b/db/migrate/020_add_changesets.rb
new file mode 100644 (file)
index 0000000..40455ec
--- /dev/null
@@ -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
index f1a923c42c1e48b3b0de75d2e67b744bdecea23a..2740eab0c5472da4c76d95128c5f8253dd440cbb 100644 (file)
@@ -1,4 +1,9 @@
 module GeoRecord
+  # This scaling factor is used to convert between the float lat/lon that is 
+  # returned by the API, and the integer lat/lon equivalent that is stored in
+  # the database.
+  SCALE = 10000000
+  
   def self.included(base)
     base.extend(ClassMethods)
   end
@@ -20,21 +25,21 @@ module GeoRecord
   end
 
   def lat=(l)
-    self.latitude = (l * 10000000).round
+    self.latitude = (l * SCALE).round
   end
 
   def lon=(l)
-    self.longitude = (l * 10000000).round
+    self.longitude = (l * SCALE).round
   end
 
   # Return WGS84 latitude
   def lat
-    return self.latitude.to_f / 10000000
+    return self.latitude.to_f / SCALE
   end
 
   # Return WGS84 longitude
   def lon
-    return self.longitude.to_f / 10000000
+    return self.longitude.to_f / SCALE
   end
 
 private
index 1d32d175d77d0fb85055f8f60c19dff7cc86de2c..26e95a496264142d2414e63a199c300942142154 100644 (file)
@@ -1,6 +1,10 @@
 module ActiveRecord
   module ConnectionAdapters
     module SchemaStatements
+      def quote_column_names(column_name)
+        Array(column_name).map { |e| quote_column_name(e) }.join(", ")
+      end
+
       def add_primary_key(table_name, column_name, options = {})
         column_names = Array(column_name)
         quoted_column_names = column_names.map { |e| quote_column_name(e) }.join(", ")
@@ -11,6 +15,12 @@ module ActiveRecord
         execute "ALTER TABLE #{table_name} DROP PRIMARY KEY"
       end
 
+      def add_foreign_key(table_name, column_name, reftbl, refcol = nil)
+        execute "ALTER TABLE #{table_name} ADD " +
+         "FOREIGN KEY (#{quote_column_names(column_name)}) " +
+         "REFERENCES #{reftbl} (#{quote_column_names(refcol || column_name)})"
+      end
+
       alias_method :old_options_include_default?, :options_include_default?
 
       def options_include_default?(options)
index 9c271607dc0160d1d7e5b2b78138dc04ed2dedf0..a64aa8c4870afc4ee2331ef49769682f318467d3 100644 (file)
@@ -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 (file)
index 86747cf..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-namespace 'db' do
-  desc 'Populate the node_tags table'
-  task :node_tags  do
-    require File.dirname(__FILE__) + '/../../config/environment'
-
-    node_count = Node.count
-    limit = 1000 #the number of nodes to grab in one go
-    offset = 0   
-
-    while offset < node_count
-        Node.find(:all, :limit => limit, :offset => offset).each do |node|
-        seq_id = 1
-        node.tags.split(';').each do |tag|
-          nt = NodeTag.new
-          nt.id = node.id
-          nt.k = tag.split('=')[0] || ''
-          nt.v = tag.split('=')[1] || ''
-          nt.sequence_id = seq_id 
-          nt.save! || raise
-          seq_id += 1
-        end
-
-        version = 1 #version refers to one set of histories
-        node.old_nodes.find(:all, :order => 'timestamp asc').each do |old_node|
-        sequence_id = 1 #sequence_id refers to the sequence of node tags within a history
-        old_node.tags.split(';').each do |tag|
-          ont = OldNodeTag.new
-          ont.id = node.id #the id of the node tag
-          ont.k = tag.split('=')[0] || ''
-          ont.v = tag.split('=')[1] || ''
-          ont.version = version
-          ont.sequence_id = sequence_id
-          ont.save! || raise
-          sequence_id += 1
-          end     
-        version += 1
-        end
-      end
-    offset += limit
-    end
-  end
-end
diff --git a/test/fixtures/current_node_tags.yml b/test/fixtures/current_node_tags.yml
new file mode 100644 (file)
index 0000000..ce68a54
--- /dev/null
@@ -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
+
index dd3bd248772a314f52e0bf8c92a68b0b076fca0c..19fad704e1ac98c876ad8542d8aad90ed4665027 100644 (file)
 # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
+<% SCALE = 10000000 unless defined?(SCALE) %>
+
 visible_node:
   id: 1
-  latitude: 1
-  longitude: 1
+  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
+  
index b129d7f45eb7d1a026f361bd0d3b0c5fafb7d22d..9b5b5ab8a0619d2aa1153103501ba1e0ab1495a7 100644 (file)
@@ -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
 
index b49c4eb4e1e9d522e9424c822ce1ce80662e94cc..22fab186322bca351586e0297f3d89aa98b4b15a 100644 (file)
@@ -1,5 +1,16 @@
 # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
 one:
-  id: 1
+  from_user_id: 1
+  title: test message 1
+  body: some body text
+  sent_on: "2008-05-01 12:34:56"
+  message_read: false
+  to_user_id: 2
+  
 two:
-  id: 2
+  from_user_id: 2
+  title: test message 2
+  body: some body test
+  sent_on: "2008-05-02 12:45:23"
+  message_read: true
+  to_user_id: 1
diff --git a/test/fixtures/node_tags.yml b/test/fixtures/node_tags.yml
new file mode 100644 (file)
index 0000000..c32dc6c
--- /dev/null
@@ -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
index 37152c4d3ddee9fe1cdbe4537bf8ea637a7a2a32..9699e395c79e14f68966aa282b2b630a6b3357e7 100644 (file)
 # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
+<% SCALE = 10000000 unless defined?(SCALE) %>
+
 visible_node:
   id: 1
-  latitude: 1
-  longitude: 1
+  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
+  
index 5bf02933a3a79bfacb88dad9d4f4799f3cf20600..59ebd0542bd9cdcf672414c30a1ec8d91c43497d 100644 (file)
@@ -1,7 +1,11 @@
 # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
 
-# one:
-#   column: value
-#
-# two:
-#   column: value
+a:
+  user_id: 1
+  k: "key"
+  v: "value"
+
+two:
+  user_id: 1
+  k: "some_key"
+  v: "some_value"
index bcce2f7db5b9732ad1ff59109917c83060427711..28e1aca3d0fd26a757cb39d01fab696fe89ab582 100644 (file)
@@ -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
index caeac16b1d7b16a1bbc1f580432b6047785d718e..c12a248a0cea4ec75408745c43b8f355961db686 100644 (file)
@@ -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
index 05cbe2af0ac5d8ab4a33816dfb30485e3dde1c72..4c4787ff76e0e0e05e0da2971ad983f0098bf944 100644 (file)
@@ -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
index a380eeb208313f08672104595eef0d188ec72e06..3f316d01224e9d63aae5a3823f89b9f58ff078f0 100644 (file)
@@ -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
index 374ea7dc2de42bbba429d9c2946d24b0d601ccf5..b4e3c5127721210ac025f856f2a28bd8c196f057 100644 (file)
@@ -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
index 933dfb542edc9be778923735378a9052439b0de0..6fd3e234c94ca326ae338c80b82a243a1afb3fb9 100644 (file)
@@ -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
 
   # -------------------------------------
index b1d7a8fcc280dec9ac39ddbbad7c90de4adf0c2c..22cc0e15c36bf880e219170652cd76e4bdb7a14b 100644 (file)
@@ -1,6 +1,7 @@
 ENV["RAILS_ENV"] = "test"
 require File.expand_path(File.dirname(__FILE__) + "/../config/environment")
 require 'test_help'
+load 'composite_primary_keys/fixtures.rb'
 
 class Test::Unit::TestCase
   # Transactional fixtures accelerate your tests by wrapping each test method
@@ -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 (file)
index 0000000..7fb1def
--- /dev/null
@@ -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
index 8804fe003b6c54f794acfa8f56aaec792d44b08b..4de1a7b295557420a114e711a89b1b5a36791698 100644 (file)
@@ -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
index 95321b5cf0cb6d2e8803c484de0069163faa7c41..bb2b7dfa4fd6ab682b87be1002e669f9fd535326 100644 (file)
@@ -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 (file)
index 0000000..85c2037
--- /dev/null
@@ -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
index bd4e800150c89d8200c20855ee78919cfeff4b98..d591db69d5d0ce35fac9dfeafd829dcab5225b8d 100644 (file)
@@ -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
index 5468f7a2d90fc88f295f8beb1cfc595699bfce10..587fc71fbf78aaaff4b8881b0cdc4ba623299d59 100644 (file)
@@ -2,9 +2,138 @@ require File.dirname(__FILE__) + '/../test_helper'
 
 class UserTest < Test::Unit::TestCase
   fixtures :users
-
-  # Replace this with your real tests.
-  def test_truth
-    assert true
+  
+  def test_invalid_with_empty_attributes
+    user = User.new
+    assert !user.valid?
+    assert user.errors.invalid?(:email)
+    assert user.errors.invalid?(:pass_crypt)
+    assert user.errors.invalid?(:display_name)
+    assert user.errors.invalid?(:email)
+    assert !user.errors.invalid?(:home_lat)
+    assert !user.errors.invalid?(:home_lon)
+    assert !user.errors.invalid?(:home_zoom)
+  end
+  
+  def test_unique_email
+    new_user = User.new(:email => users(:normal_user).email,
+      :active => 1, 
+      :pass_crypt => Digest::MD5.hexdigest('test'),
+      :display_name => "new user",
+      :data_public => 1,
+      :description => "desc")
+    assert !new_user.save
+    assert_equal ActiveRecord::Errors.default_error_messages[:taken], new_user.errors.on(:email)
+  end
+  
+  def test_unique_display_name
+    new_user = User.new(:email => "tester@openstreetmap.org",
+      :active => 0,
+      :pass_crypt => Digest::MD5.hexdigest('test'),
+      :display_name => users(:normal_user).display_name, 
+      :data_public => 1,
+      :description => "desc")
+    assert !new_user.save
+    assert_equal ActiveRecord::Errors.default_error_messages[:taken], new_user.errors.on(:display_name)
+  end
+  
+  def test_email_valid
+    ok = %w{ a@s.com test@shaunmcdonald.me.uk hello_local@ping-d.ng 
+    test_local@openstreetmap.org test-local@example.com
+    輕觸搖晃的遊戲@ah.com も対応します@s.name }
+    bad = %w{ hi ht@ n@ @.com help@.me.uk help"hi.me.uk も対@応します }
+    
+    ok.each do |name|
+      user = users(:normal_user)
+      user.email = name
+      assert user.valid?, user.errors.full_messages
+    end
+    
+    bad.each do |name|
+      user = users(:normal_user)
+      user.email = name
+      assert !user.valid?, "#{name} is valid when it shouldn't be" 
+    end
+  end
+  
+  def test_display_name_length
+    user = users(:normal_user)
+    user.display_name = "123"
+    assert user.valid?, " should allow nil display name"
+    user.display_name = "12"
+    assert !user.valid?, "should not allow 2 char name"
+    user.display_name = ""
+    assert !user.valid?
+    user.display_name = nil
+    # Don't understand why it isn't allowing a nil value, 
+    # when the validates statements specifically allow it
+    # It appears the database does not allow null values
+    assert !user.valid?
+  end
+  
+  def test_display_name_valid
+    # Due to sanitisation in the view some of these that you might not 
+    # expact are allowed
+    # However, would they affect the xml planet dumps?
+    ok = [ "Name", "'me", "he\"", "#ping", "<hr>", "*ho", "\"help\"@", 
+           "vergrößern", "ルシステムにも対応します", "輕觸搖晃的遊戲" ]
+    # These need to be 3 chars in length, otherwise the length test above
+    # should be used.
+    bad = [ "<hr/>", "test@example.com", "s/f", "aa/", "aa;", "aa.",
+            "aa,", "aa?", "/;.,?", "も対応します/" ]
+    ok.each do |display_name|
+      user = users(:normal_user)
+      user.display_name = display_name
+      assert user.valid?, "#{display_name} is invalid, when it should be"
+    end
+    
+    bad.each do |display_name|
+      user = users(:normal_user)
+      user.display_name = display_name
+      assert !user.valid?, "#{display_name} is valid when it shouldn't be"
+      assert_equal "is invalid", user.errors.on(:display_name)
+    end
+  end
+  
+  def test_friend_with
+    assert_equal 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