]> git.openstreetmap.org Git - rails.git/commitdiff
Merge api06 branch to trunk.
authorTom Hughes <tom@compton.nu>
Mon, 20 Apr 2009 09:12:03 +0000 (09:12 +0000)
committerTom Hughes <tom@compton.nu>
Mon, 20 Apr 2009 09:12:03 +0000 (09:12 +0000)
196 files changed:
app/controllers/amf_controller.rb
app/controllers/api_controller.rb
app/controllers/application.rb
app/controllers/browse_controller.rb
app/controllers/changeset_controller.rb [new file with mode: 0644]
app/controllers/changeset_tag_controller.rb [new file with mode: 0644]
app/controllers/diary_entry_controller.rb
app/controllers/export_controller.rb
app/controllers/message_controller.rb
app/controllers/node_controller.rb
app/controllers/old_node_controller.rb
app/controllers/old_relation_controller.rb
app/controllers/old_way_controller.rb
app/controllers/relation_controller.rb
app/controllers/trace_controller.rb
app/controllers/user_controller.rb
app/controllers/user_preference_controller.rb
app/controllers/way_controller.rb
app/helpers/browse_helper.rb
app/models/acl.rb
app/models/changeset.rb [new file with mode: 0644]
app/models/changeset_tag.rb [new file with mode: 0644]
app/models/diary_entry.rb
app/models/message.rb
app/models/node.rb
app/models/old_node.rb
app/models/old_relation.rb
app/models/old_relation_member.rb
app/models/old_relation_tag.rb
app/models/old_way.rb
app/models/old_way_tag.rb
app/models/relation.rb
app/models/relation_member.rb
app/models/relation_tag.rb
app/models/trace.rb
app/models/tracetag.rb
app/models/user.rb
app/models/user_preference.rb
app/models/way.rb
app/models/way_tag.rb
app/views/browse/_changeset_details.rhtml [new file with mode: 0644]
app/views/browse/_common_details.rhtml
app/views/browse/_map.rhtml
app/views/browse/_paging_nav.rhtml [new file with mode: 0644]
app/views/browse/_tag_details.rhtml [new file with mode: 0644]
app/views/browse/changeset.rhtml [new file with mode: 0644]
app/views/browse/index.rhtml [deleted file]
app/views/browse/not_found.rhtml [new file with mode: 0644]
app/views/browse/start.rjs
app/views/changeset/_changeset.rhtml [new file with mode: 0644]
app/views/changeset/_changeset_paging_nav.rhtml [new file with mode: 0644]
app/views/changeset/list.rhtml [new file with mode: 0644]
app/views/changeset/list_bbox.rhtml [new file with mode: 0644]
app/views/changeset/list_user.rhtml [new file with mode: 0644]
app/views/diary_entry/edit.rhtml
app/views/diary_entry/list.rhtml
app/views/diary_entry/no_such_entry.rhtml [new file with mode: 0644]
app/views/layouts/site.rhtml
app/views/message/_message_summary.rhtml
app/views/message/_sent_message_summary.rhtml
app/views/message/new.rhtml
app/views/message/no_such_user.rhtml [new file with mode: 0644]
app/views/site/edit.rhtml
app/views/site/index.rhtml
app/views/trace/_trace_header.rhtml
app/views/trace/no_such_user.rhtml [new file with mode: 0644]
app/views/user/_friend_map.rhtml
app/views/user/account.rhtml
app/views/user/login.rhtml
app/views/user/new.rhtml
app/views/user/view.rhtml
config/application.yml
config/database.yml
config/environment.rb
config/environments/development.rb
config/initializers/composite_primary_keys.rb [deleted file]
config/initializers/libxml.rb
config/initializers/sql_session_store.rb
config/potlatch/colours.txt
config/routes.rb
db/README
db/functions/Makefile
db/functions/maptile.c
db/migrate/001_create_osm_db.rb
db/migrate/002_cleanup_osm_db.rb
db/migrate/003_sql_session_store_setup.rb
db/migrate/004_user_enhancements.rb
db/migrate/005_tile_tracepoints.rb
db/migrate/006_tile_nodes.rb
db/migrate/007_add_relations.rb
db/migrate/010_diary_comments.rb
db/migrate/013_add_email_valid.rb
db/migrate/015_add_user_visible.rb
db/migrate/018_create_acls.rb
db/migrate/019_add_timestamp_indexes.rb [new file with mode: 0644]
db/migrate/020_populate_node_tags_and_remove.rb [new file with mode: 0644]
db/migrate/020_populate_node_tags_and_remove_helper.c [new file with mode: 0644]
db/migrate/021_move_to_innodb.rb [new file with mode: 0644]
db/migrate/022_key_constraints.rb [new file with mode: 0644]
db/migrate/023_add_changesets.rb [new file with mode: 0644]
db/migrate/024_order_relation_members.rb [new file with mode: 0644]
db/migrate/025_add_end_time_to_changesets.rb [new file with mode: 0644]
doc/README_FOR_APP
lib/consistency_validations.rb [new file with mode: 0644]
lib/diff_reader.rb [new file with mode: 0644]
lib/geo_record.rb
lib/map_boundary.rb
lib/migrate.rb
lib/osm.rb
lib/tags.rb [deleted file]
lib/tasks/populate_node_tags.rake [deleted file]
lib/validators.rb [new file with mode: 0644]
public/404.html
public/500.html
public/javascripts/map.js
public/javascripts/site.js
public/potlatch/beep.mp3
public/potlatch/potlatch.swf
public/stylesheets/site.css
script/statistics
test/fixtures/changeset_tags.yml [new file with mode: 0644]
test/fixtures/changesets.yml [new file with mode: 0644]
test/fixtures/current_node_tags.yml [new file with mode: 0644]
test/fixtures/current_nodes.yml
test/fixtures/current_relation_members.yml
test/fixtures/current_relation_tags.yml
test/fixtures/current_relations.yml
test/fixtures/current_way_nodes.yml
test/fixtures/current_way_tags.yml
test/fixtures/current_ways.yml
test/fixtures/diary_comments.yml [new file with mode: 0644]
test/fixtures/diary_entries.yml [new file with mode: 0644]
test/fixtures/friends.yml [new file with mode: 0644]
test/fixtures/gps_points.yml [new file with mode: 0644]
test/fixtures/gpx_file_tags.yml [new file with mode: 0644]
test/fixtures/gpx_files.yml [new file with mode: 0644]
test/fixtures/messages.yml
test/fixtures/node_tags.yml [new file with mode: 0644]
test/fixtures/nodes.yml
test/fixtures/relation_members.yml
test/fixtures/relation_tags.yml
test/fixtures/relations.yml
test/fixtures/user_preferences.yml
test/fixtures/users.yml
test/fixtures/way_nodes.yml
test/fixtures/way_tags.yml
test/fixtures/ways.yml
test/functional/amf_controller_test.rb [new file with mode: 0644]
test/functional/api_controller_test.rb
test/functional/browse_controller_test.rb [new file with mode: 0644]
test/functional/changeset_controller_test.rb [new file with mode: 0644]
test/functional/changeset_tag_controller_test.rb [new file with mode: 0644]
test/functional/diary_entry_controller_test.rb [new file with mode: 0644]
test/functional/export_controller_test.rb [new file with mode: 0644]
test/functional/friend_controller_test.rb [new file with mode: 0644]
test/functional/geocoder_controller_test.rb
test/functional/message_controller_test.rb
test/functional/node_controller_test.rb
test/functional/old_node_controller_test.rb [new file with mode: 0644]
test/functional/old_relation_controller_test.rb
test/functional/old_way_controller_test.rb
test/functional/relation_controller_test.rb
test/functional/search_controller_test.rb [new file with mode: 0644]
test/functional/site_controller_test.rb [new file with mode: 0644]
test/functional/swf_controller_test.rb [new file with mode: 0644]
test/functional/trace_controller_test.rb [new file with mode: 0644]
test/functional/user_controller_test.rb [new file with mode: 0644]
test/functional/user_preference_controller_test.rb
test/functional/way_controller_test.rb
test/integration/user_creation_test.rb [new file with mode: 0644]
test/integration/user_diaries_test.rb [new file with mode: 0644]
test/test_helper.rb
test/unit/changeset_tag_test.rb [new file with mode: 0644]
test/unit/changeset_test.rb [new file with mode: 0644]
test/unit/diary_comment_test.rb [new file with mode: 0644]
test/unit/diary_entry_test.rb [new file with mode: 0644]
test/unit/friend_test.rb [new file with mode: 0644]
test/unit/message_test.rb
test/unit/node_tag_test.rb [new file with mode: 0644]
test/unit/node_test.rb
test/unit/old_node_tag_test.rb [new file with mode: 0644]
test/unit/old_node_test.rb [new file with mode: 0644]
test/unit/old_relation_tag_test.rb [new file with mode: 0644]
test/unit/old_way_tag_test.rb [new file with mode: 0644]
test/unit/relation_member_test.rb [new file with mode: 0644]
test/unit/relation_tag_test.rb [new file with mode: 0644]
test/unit/relation_test.rb [new file with mode: 0644]
test/unit/trace_test.rb [new file with mode: 0644]
test/unit/tracepoint_test.rb [new file with mode: 0644]
test/unit/tracetag_test.rb [new file with mode: 0644]
test/unit/user_preference_test.rb
test/unit/user_test.rb
test/unit/user_token_test.rb [new file with mode: 0644]
test/unit/way_node_test.rb [new file with mode: 0644]
test/unit/way_tag_test.rb [new file with mode: 0644]
test/unit/way_test.rb [new file with mode: 0644]

index 7f85280b79d29d5b320460d01f028f14727a3fd0..b0b3f13cf6c1947914caa7583e3fef58ff4bc392 100644 (file)
@@ -3,7 +3,7 @@
 # OSM database takes place using this controller. Messages are 
 # encoded in the Actionscript Message Format (AMF).
 #
-# Helper functions are in /lib/potlatch.
+# Helper functions are in /lib/potlatch.rb
 #
 # Author::     editions Systeme D / Richard Fairhurst 2004-2008
 # Licence::    public domain.
 # from the AMF message), each method generally takes arguments in the order 
 # they were sent by the Potlatch SWF. Do not assume typing has been preserved. 
 # Methods all return an array to the SWF.
+#
+# == API 0.6
+#
+# Note that this requires a patched version of composite_primary_keys 1.1.0
+# (see http://groups.google.com/group/compositekeys/t/a00e7562b677e193) 
+# if you are to run with POTLATCH_USE_SQL=false .
 # 
 # == Debugging
 # 
 #      return(-1,"message")            <-- just puts up a dialogue
 #      return(-2,"message")            <-- also asks the user to e-mail me
 # 
-# To write to the Rails log, use RAILS_DEFAULT_LOGGER.info("message").
+# To write to the Rails log, use logger.info("message").
+
+# Remaining issues:
+# * version conflict when POIs and ways are reverted
 
 class AmfController < ApplicationController
   require 'stringio'
 
   include Potlatch
 
+  # Help methods for checking boundary sanity and area size
+  include MapBoundary
+
   session :off
   before_filter :check_api_writable
 
@@ -36,235 +48,403 @@ class AmfController < ApplicationController
   # ** FIXME: refactor to reduce duplication of code across read/write
   
   def amf_read
-       req=StringIO.new(request.raw_post+0.chr)# Get POST data as request
-                                                                                       # (cf http://www.ruby-forum.com/topic/122163)
-       req.read(2)                                                             # Skip version indicator and client ID
-       results={}                                                              # Results of each body
+    req=StringIO.new(request.raw_post+0.chr)# Get POST data as request
+                              # (cf http://www.ruby-forum.com/topic/122163)
+    req.read(2)                                                                # Skip version indicator and client ID
+    results={}                                                         # Results of each body
 
-       # Parse request
+    # Parse request
 
        headers=AMF.getint(req)                                 # Read number of headers
 
-       headers.times do                                                # Read each header
-         name=AMF.getstring(req)                               #  |
-         req.getc                                                              #  | skip boolean
-         value=AMF.getvalue(req)                               #  |
-         header["name"]=value                                  #  |
-       end
-
-       bodies=AMF.getint(req)                                  # Read number of bodies
-       bodies.times do                                                 # Read each body
-         message=AMF.getstring(req)                    #  | get message name
-         index=AMF.getstring(req)                              #  | get index in response sequence
-         bytes=AMF.getlong(req)                                #  | get total size in bytes
-         args=AMF.getvalue(req)                                #  | get response (probably an array)
-
-         case message
-               when 'getpresets';                      results[index]=AMF.putdata(index,getpresets())
-               when 'whichways';                       results[index]=AMF.putdata(index,whichways(*args))
-               when 'whichways_deleted';       results[index]=AMF.putdata(index,whichways_deleted(*args))
-               when 'getway';                          results[index]=AMF.putdata(index,getway(args[0].to_i))
-               when 'getrelation';                     results[index]=AMF.putdata(index,getrelation(args[0].to_i))
-               when 'getway_old';                      results[index]=AMF.putdata(index,getway_old(args[0].to_i,args[1].to_i))
-               when 'getway_history';          results[index]=AMF.putdata(index,getway_history(args[0].to_i))
-               when 'getnode_history';         results[index]=AMF.putdata(index,getnode_history(args[0].to_i))
-               when 'findrelations';           results[index]=AMF.putdata(index,findrelations(*args))
-               when 'getpoi';                          results[index]=AMF.putdata(index,getpoi(*args))
-         end
-       end
+    headers.times do                                           # Read each header
+      name=AMF.getstring(req)                          #  |
+      req.getc                                                         #  | skip boolean
+      value=AMF.getvalue(req)                          #  |
+      header["name"]=value                                     #  |
+    end
+
+    bodies=AMF.getint(req)                                     # Read number of bodies
+    bodies.times do                                                    # Read each body
+      message=AMF.getstring(req)                       #  | get message name
+      index=AMF.getstring(req)                         #  | get index in response sequence
+      bytes=AMF.getlong(req)                           #  | get total size in bytes
+      args=AMF.getvalue(req)                           #  | get response (probably an array)
+      logger.info("Executing AMF #{message}:#{index}")
+
+      case message
+        when 'getpresets';                     results[index]=AMF.putdata(index,getpresets())
+        when 'whichways';                      results[index]=AMF.putdata(index,whichways(*args))
+        when 'whichways_deleted';      results[index]=AMF.putdata(index,whichways_deleted(*args))
+        when 'getway';                         results[index]=AMF.putdata(index,getway(args[0].to_i))
+        when 'getrelation';                    results[index]=AMF.putdata(index,getrelation(args[0].to_i))
+        when 'getway_old';                     results[index]=AMF.putdata(index,getway_old(args[0].to_i,args[1]))
+        when 'getway_history';         results[index]=AMF.putdata(index,getway_history(args[0].to_i))
+        when 'getnode_history';                results[index]=AMF.putdata(index,getnode_history(args[0].to_i))
+        when 'findgpx';                                results[index]=AMF.putdata(index,findgpx(*args))
+        when 'findrelations';          results[index]=AMF.putdata(index,findrelations(*args))
+        when 'getpoi';                         results[index]=AMF.putdata(index,getpoi(*args))
+      end
+    end
+    logger.info("encoding AMF results")
     sendresponse(results)
   end
 
   def amf_write
-       req=StringIO.new(request.raw_post+0.chr)
-       req.read(2)
-       results={}
-       renumberednodes={}                                              # Shared across repeated putways
-       renumberedways={}                                               # Shared across repeated putways
-
-       headers=AMF.getint(req)                                 # Read number of headers
-       headers.times do                                                # Read each header
-         name=AMF.getstring(req)                               #  |
-         req.getc                                                              #  | skip boolean
-         value=AMF.getvalue(req)                               #  |
-         header["name"]=value                                  #  |
-       end
-
-       bodies=AMF.getint(req)                                  # Read number of bodies
-       bodies.times do                                                 # Read each body
-         message=AMF.getstring(req)                    #  | get message name
-         index=AMF.getstring(req)                              #  | get index in response sequence
-         bytes=AMF.getlong(req)                                #  | get total size in bytes
-         args=AMF.getvalue(req)                                #  | get response (probably an array)
-
-         case message
-               when 'putway';                          r=putway(renumberednodes,*args)
+    req=StringIO.new(request.raw_post+0.chr)
+    req.read(2)
+    results={}
+    renumberednodes={}                                         # Shared across repeated putways
+    renumberedways={}                                          # Shared across repeated putways
+
+    headers=AMF.getint(req)                                    # Read number of headers
+    headers.times do                                           # Read each header
+      name=AMF.getstring(req)                          #  |
+      req.getc                                                         #  | skip boolean
+      value=AMF.getvalue(req)                          #  |
+      header["name"]=value                                     #  |
+    end
+
+    bodies=AMF.getint(req)                                     # Read number of bodies
+    bodies.times do                                                    # Read each body
+      message=AMF.getstring(req)                       #  | get message name
+      index=AMF.getstring(req)                         #  | get index in response sequence
+      bytes=AMF.getlong(req)                           #  | get total size in bytes
+      args=AMF.getvalue(req)                           #  | get response (probably an array)
+
+      logger.info("Executing AMF #{message}:#{index}")
+      case message
+        when 'putway';                         r=putway(renumberednodes,*args)
                                                                        renumberednodes=r[3]
-                                                                       if r[1] != r[2]
-                                                                         renumberedways[r[1]] = r[2]
-                                                                       end
+                                                                       if r[1] != r[2] then renumberedways[r[1]] = r[2] end
                                                                        results[index]=AMF.putdata(index,r)
-               when 'putrelation';                     results[index]=AMF.putdata(index,putrelation(renumberednodes, renumberedways, *args))
-               when 'deleteway';                       results[index]=AMF.putdata(index,deleteway(args[0],args[1].to_i))
-               when 'putpoi';                          results[index]=AMF.putdata(index,putpoi(*args))
-         end
-       end
+        when 'putrelation';                    results[index]=AMF.putdata(index,putrelation(renumberednodes, renumberedways, *args))
+        when 'deleteway';                      results[index]=AMF.putdata(index,deleteway(*args))
+        when 'putpoi';                         r=putpoi(*args)
+                                                                       if r[1] != r[2] then renumberednodes[r[1]] = r[2] end
+                                                               results[index]=AMF.putdata(index,r)
+        when 'startchangeset';         results[index]=AMF.putdata(index,startchangeset(*args))
+      end
+    end
+    logger.info("encoding AMF results")
     sendresponse(results)
   end
 
   private
 
+  # Start new changeset
+  
+  def startchangeset(usertoken, cstags, closeid, closecomment)
+    user = getuser(usertoken)
+    if !user then return -1,"You are not logged in, so Potlatch can't write any changes to the database." end
+
+    # close previous changeset and add comment
+    if closeid
+      cs = Changeset.find(closeid)
+      cs.set_closed_time_now
+      if cs.user_id!=user.id
+        return -2,"You cannot close that changeset because you're not the person who opened it."
+      elsif closecomment.empty?
+        cs.save!
+      else
+        cs.tags['comment']=closecomment
+        cs.save_with_tags!
+      end
+    end
+       
+    # open a new changeset
+    cs = Changeset.new
+    cs.tags = cstags
+    cs.user_id = user.id
+    # smsm1 doesn't like the next two lines and thinks they need to be abstracted to the model more/better
+    cs.created_at = Time.now.getutc
+    cs.closed_at = cs.created_at + Changeset::IDLE_TIMEOUT
+    cs.save_with_tags!
+    return [0,cs.id]
+  end
+
   # Return presets (default tags, localisation etc.):
   # uses POTLATCH_PRESETS global, set up in OSM::Potlatch.
 
   def getpresets() #:doc:
-       return POTLATCH_PRESETS
+    return POTLATCH_PRESETS
   end
 
+  ##
   # Find all the ways, POI nodes (i.e. not part of ways), and relations
   # in a given bounding box. Nodes are returned in full; ways and relations 
   # are IDs only. 
-
+  #
+  # return is of the form: 
+  # [error_code, 
+  #  [[way_id, way_version], ...],
+  #  [[node_id, lat, lon, [tags, ...], node_version], ...],
+  #  [[rel_id, rel_version], ...]]
+  # where the ways are any visible ways which refer to any visible
+  # nodes in the bbox, nodes are any visible nodes in the bbox but not
+  # used in any way, rel is any relation which refers to either a way
+  # or node that we're returning.
   def whichways(xmin, ymin, xmax, ymax) #:doc:
-       enlarge = [(xmax-xmin)/8,0.01].min
-       xmin -= enlarge; ymin -= enlarge
-       xmax += enlarge; ymax += enlarge
-
-       if POTLATCH_USE_SQL then
-         way_ids = sql_find_way_ids_in_area(xmin, ymin, xmax, ymax)
-         points = sql_find_pois_in_area(xmin, ymin, xmax, ymax)
-         relation_ids = sql_find_relations_in_area_and_ways(xmin, ymin, xmax, ymax, way_ids)
-       else
-         # find the way ids in an area
-         nodes_in_area = Node.find_by_area(ymin, xmin, ymax, xmax, :conditions => "current_nodes.visible = 1", :include => :ways)
-         way_ids = nodes_in_area.collect { |node| node.way_ids }.flatten.uniq
-
-         # find the node ids in an area that aren't part of ways
-         nodes_not_used_in_area = nodes_in_area.select { |node| node.ways.empty? }
-         points = nodes_not_used_in_area.collect { |n| [n.id, n.lon, n.lat, n.tags_as_hash] }
-
-         # find the relations used by those nodes and ways
-         relations = Relation.find_for_nodes(nodes_in_area.collect { |n| n.id }, :conditions => "visible = 1") +
-                  Relation.find_for_ways(way_ids, :conditions => "visible = 1")
-         relation_ids = relations.collect { |relation| relation.id }.uniq
-       end
-
-       [way_ids, points, relation_ids]
+    enlarge = [(xmax-xmin)/8,0.01].min
+    xmin -= enlarge; ymin -= enlarge
+    xmax += enlarge; ymax += enlarge
+    
+    # check boundary is sane and area within defined
+    # see /config/application.yml
+    check_boundaries(xmin, ymin, xmax, ymax)
+
+    if POTLATCH_USE_SQL then
+      ways = sql_find_ways_in_area(xmin, ymin, xmax, ymax)
+      points = sql_find_pois_in_area(xmin, ymin, xmax, ymax)
+      relations = sql_find_relations_in_area_and_ways(xmin, ymin, xmax, ymax, ways.collect {|x| x[0]})
+    else
+      # find the way ids in an area
+      nodes_in_area = Node.find_by_area(ymin, xmin, ymax, xmax, :conditions => ["current_nodes.visible = ?", true], :include => :ways)
+      ways = nodes_in_area.inject([]) { |sum, node| 
+        visible_ways = node.ways.select { |w| w.visible? }
+        sum + visible_ways.collect { |w| [w.id,w.version] }
+      }.uniq
+      ways.delete([])
+
+      # find the node ids in an area that aren't part of ways
+      nodes_not_used_in_area = nodes_in_area.select { |node| node.ways.empty? }
+      points = nodes_not_used_in_area.collect { |n| [n.id, n.lon, n.lat, n.tags, n.version] }.uniq
+
+      # find the relations used by those nodes and ways
+      relations = Relation.find_for_nodes(nodes_in_area.collect { |n| n.id }, :conditions => {:visible => true}) +
+                  Relation.find_for_ways(ways.collect { |w| w[0] }, :conditions => {:visible => true})
+      relations = relations.collect { |relation| [relation.id,relation.version] }.uniq
+    end
+
+    [0, ways, points, relations]
+
+  rescue Exception => err
+    [-2,"Sorry - I can't get the map for that area."]
   end
 
   # Find deleted ways in current bounding box (similar to whichways, but ways
   # with a deleted node only - not POIs or relations).
 
   def whichways_deleted(xmin, ymin, xmax, ymax) #:doc:
-       xmin -= 0.01; ymin -= 0.01
-       xmax += 0.01; ymax += 0.01
-
-       nodes_in_area = Node.find_by_area(ymin, xmin, ymax, xmax, :conditions => "current_nodes.visible = 0 AND current_ways.visible = 0", :include => :ways_via_history)
-       way_ids = nodes_in_area.collect { |node| node.ways_via_history_ids }.flatten.uniq
-
-       [way_ids]
+    enlarge = [(xmax-xmin)/8,0.01].min
+    xmin -= enlarge; ymin -= enlarge
+    xmax += enlarge; ymax += enlarge
+
+    # check boundary is sane and area within defined
+    # see /config/application.yml
+    begin
+      check_boundaries(xmin, ymin, xmax, ymax)
+    rescue Exception => err
+      return [-2,"Sorry - I can't get the map for that area."]
+    end
+
+    nodes_in_area = Node.find_by_area(ymin, xmin, ymax, xmax, :conditions => ["current_ways.visible = ?", false], :include => :ways_via_history)
+    way_ids = nodes_in_area.collect { |node| node.ways_via_history_ids }.flatten.uniq
+
+    [0,way_ids]
   end
 
   # Get a way including nodes and tags.
-  # Returns 0 (success), a Potlatch-style array of points, and a hash of tags.
+  # Returns the way id, a Potlatch-style array of points, a hash of tags, and the version number.
 
   def getway(wayid) #:doc:
-       if POTLATCH_USE_SQL then
-         points = sql_get_nodes_in_way(wayid)
-         tags = sql_get_tags_in_way(wayid)
-       else
-         # Ideally we would do ":include => :nodes" here but if we do that
-         # then rails only seems to return the first copy of a node when a
-         # way includes a node more than once
-         way = Way.find(wayid)
-         points = way.nodes.collect do |node|
-               nodetags=node.tags_as_hash
-               nodetags.delete('created_by')
-               [node.lon, node.lat, node.id, nodetags]
-         end
-         tags = way.tags
-       end
-
-       [wayid, points, tags]
+    if POTLATCH_USE_SQL then
+        points = sql_get_nodes_in_way(wayid)
+        tags = sql_get_tags_in_way(wayid)
+        version = sql_get_way_version(wayid)
+      else
+        # Ideally we would do ":include => :nodes" here but if we do that
+        # then rails only seems to return the first copy of a node when a
+        # way includes a node more than once
+        begin
+          way = Way.find(wayid)
+        rescue ActiveRecord::RecordNotFound
+          return [wayid,[],{}]
+        end
+
+        # check case where way has been deleted or doesn't exist
+        return [wayid,[],{}] if way.nil? or !way.visible
+
+        points = way.nodes.collect do |node|
+        nodetags=node.tags
+        nodetags.delete('created_by')
+        [node.lon, node.lat, node.id, nodetags, node.version]
+      end
+      tags = way.tags
+      version = way.version
+    end
+
+    [wayid, points, tags, version]
   end
-
+  
   # Get an old version of a way, and all constituent nodes.
   #
-  # For undelete (version=0), always uses the most recent version of each node, 
-  # even if it's moved.  For revert (version=1+), uses the node in existence 
+  # For undelete (version<0), always uses the most recent version of each node, 
+  # even if it's moved.  For revert (version >= 0), uses the node in existence 
   # at the time, generating a new id if it's still visible and has been moved/
   # retagged.
-
-  def getway_old(id, version) #:doc:
-       if version < 0
-         old_way = OldWay.find(:first, :conditions => ['visible = 1 AND id = ?', id], :order => 'version DESC')
-         points = old_way.get_nodes_undelete
-       else
-         old_way = OldWay.find(:first, :conditions => ['id = ? AND version = ?', id, version])
-         points = old_way.get_nodes_revert
-       end
-
-       old_way.tags['history'] = "Retrieved from v#{old_way.version}"
-
-       [0, id, points, old_way.tags, old_way.version]
+  #
+  # Returns:
+  # 0. success code, 
+  # 1. id, 
+  # 2. array of points, 
+  # 3. hash of tags, 
+  # 4. version, 
+  # 5. is this the current, visible version? (boolean)
+  
+  def getway_old(id, timestamp) #:doc:
+    if timestamp == ''
+      # undelete
+      old_way = OldWay.find(:first, :conditions => ['visible = ? AND id = ?', true, id], :order => 'version DESC')
+      points = old_way.get_nodes_undelete unless old_way.nil?
+    else
+      begin
+        # revert
+        timestamp = DateTime.strptime(timestamp.to_s, "%d %b %Y, %H:%M:%S")
+        old_way = OldWay.find(:first, :conditions => ['id = ? AND timestamp <= ?', id, timestamp], :order => 'timestamp DESC')
+        unless old_way.nil?
+          points = old_way.get_nodes_revert(timestamp)
+          if !old_way.visible
+            return [-1, "Sorry, the way was deleted at that time - please revert to a previous version."]
+          end
+        end
+      rescue ArgumentError
+        # thrown by date parsing method. leave old_way as nil for
+        # the superb error handler below.
+      end
+    end
+
+    if old_way.nil?
+      # *** FIXME: shouldn't this be returning an error?
+      return [-1, id, [], {}, -1,0]
+    else
+      curway=Way.find(id)
+      old_way.tags['history'] = "Retrieved from v#{old_way.version}"
+      return [0, id, points, old_way.tags, curway.version, (curway.version==old_way.version and curway.visible)]
+    end
   end
   
-  # Find history of a way. Returns 'way', id, and 
-  # an array of previous versions.
+  # Find history of a way.
+  # Returns 'way', id, and an array of previous versions:
+  # - formerly [old_way.version, old_way.timestamp.strftime("%d %b %Y, %H:%M"), old_way.visible ? 1 : 0, user, uid]
+  # - now [timestamp,user,uid]
+  #
+  # Heuristic: Find all nodes that have ever been part of the way; 
+  # get a list of their revision dates; add revision dates of the way;
+  # sort and collapse list (to within 2 seconds); trim all dates before the 
+  # start date of the way.
 
   def getway_history(wayid) #:doc:
-       history = Way.find(wayid).old_ways.reverse.collect do |old_way|
-         user = old_way.user.data_public? ? old_way.user.display_name : 'anonymous'
-         uid  = old_way.user.data_public? ? old_way.user.id : 0
-         [old_way.version, old_way.timestamp.strftime("%d %b %Y, %H:%M"), old_way.visible ? 1 : 0, user, uid]
-       end
 
-       ['way',wayid,history]
+    begin
+      # Find list of revision dates for way and all constituent nodes
+      revdates=[]
+      revusers={}
+      Way.find(wayid).old_ways.collect do |a|
+        revdates.push(a.timestamp)
+        unless revusers.has_key?(a.timestamp.to_i) then revusers[a.timestamp.to_i]=change_user(a) end
+        a.nds.each do |n|
+          Node.find(n).old_nodes.collect do |o|
+            revdates.push(o.timestamp)
+            unless revusers.has_key?(o.timestamp.to_i) then revusers[o.timestamp.to_i]=change_user(o) end
+          end
+        end
+      end
+      waycreated=revdates[0]
+      revdates.uniq!
+      revdates.sort!
+         revdates.reverse!
+
+      # Remove any dates (from nodes) before first revision date of way
+      revdates.delete_if { |d| d<waycreated }
+      # Remove any elements where 2 seconds doesn't elapse before next one
+      revdates.delete_if { |d| revdates.include?(d+1) or revdates.include?(d+2) }
+      # Collect all in one nested array
+      revdates.collect! {|d| [d.strftime("%d %b %Y, %H:%M:%S")] + revusers[d.to_i] }
+
+      return ['way',wayid,revdates]
+    rescue ActiveRecord::RecordNotFound
+      return ['way', wayid, []]
+    end
   end
-
-  # Find history of a node. Returns 'node', id, and 
-  # an array of previous versions.
+  
+  # Find history of a node. Returns 'node', id, and an array of previous versions as above.
 
   def getnode_history(nodeid) #:doc:
-       history = Node.find(nodeid).old_nodes.reverse.collect do |old_node|
-         user = old_node.user.data_public? ? old_node.user.display_name : 'anonymous'
-         uid  = old_node.user.data_public? ? old_node.user.id : 0
-         [old_node.timestamp.to_i, old_node.timestamp.strftime("%d %b %Y, %H:%M"), old_node.visible ? 1 : 0, user, uid]
-       end
+    begin 
+      history = Node.find(nodeid).old_nodes.reverse.collect do |old_node|
+        [old_node.timestamp.strftime("%d %b %Y, %H:%M:%S")] + change_user(old_node)
+      end
+      return ['node', nodeid, history]
+    rescue ActiveRecord::RecordNotFound
+      return ['node', nodeid, []]
+    end
+  end
 
-       ['node',nodeid,history]
+  def change_user(obj)
+    user_object = obj.changeset.user
+    user = user_object.data_public? ? user_object.display_name : 'anonymous'
+    uid  = user_object.data_public? ? user_object.id : 0
+    [user,uid]
+  end
+
+  # Find GPS traces with specified name/id.
+  # Returns array listing GPXs, each one comprising id, name and description.
+  
+  def findgpx(searchterm, usertoken)
+    user = getuser(usertoken)
+    if !uid then return -1,"You must be logged in to search for GPX traces." end
+
+    gpxs = []
+    if searchterm.to_i>0 then
+      gpx = Trace.find(searchterm.to_i, :conditions => ["visible=? AND (public=? OR user_id=?)",true,true,user.id] )
+      if gpx then
+        gpxs.push([gpx.id, gpx.name, gpx.description])
+      end
+    else
+      Trace.find(:all, :limit => 21, :conditions => ["visible=? AND (public=? OR user_id=?) AND MATCH(name) AGAINST (?)",true,true,user.id,searchterm] ).each do |gpx|
+      gpxs.push([gpx.id, gpx.name, gpx.description])
+         end
+       end
+    gpxs
   end
 
   # Get a relation with all tags and members.
   # Returns:
   # 0. relation id,
   # 1. hash of tags,
-  # 2. list of members.
+  # 2. list of members,
+  # 3. version.
   
   def getrelation(relid) #:doc:
-       rel = Relation.find(relid)
-
-       [relid, rel.tags, rel.members]
+    begin
+      rel = Relation.find(relid)
+    rescue ActiveRecord::RecordNotFound
+      return [relid, {}, []]
+    end
+
+    return [relid, {}, [], nil] if rel.nil? or !rel.visible
+    [relid, rel.tags, rel.members, rel.version]
   end
 
   # Find relations with specified name/id.
   # Returns array of relations, each in same form as getrelation.
   
   def findrelations(searchterm)
-       rels = []
-       if searchterm.to_i>0 then
-         rel = Relation.find(searchterm.to_i)
-         if rel and rel.visible then
-           rels.push([rel.id, rel.tags, rel.members])
-         end
-       else
-         RelationTag.find(:all, :limit => 11, :conditions => ["match(v) against (?)", searchterm] ).each do |t|
-               if t.relation.visible then
+    rels = []
+    if searchterm.to_i>0 then
+      rel = Relation.find(searchterm.to_i)
+      if rel and rel.visible then
+        rels.push([rel.id, rel.tags, rel.members])
+      end
+    else
+      RelationTag.find(:all, :limit => 11, :conditions => ["match(v) against (?)", searchterm] ).each do |t|
+      if t.relation.visible then
              rels.push([t.relation.id, t.relation.tags, t.relation.members])
            end
          end
        end
-       rels
+    rels
   end
 
   # Save a relation.
@@ -273,226 +453,335 @@ class AmfController < ApplicationController
   # 1. original relation id (unchanged),
   # 2. new relation id.
 
-  def putrelation(renumberednodes, renumberedways, usertoken, relid, tags, members, visible) #:doc:
-       uid = getuserid(usertoken)
-       if !uid then return -1,"You are not logged in, so the relation could not be saved." end
-
-       relid = relid.to_i
-       visible = visible.to_i
-
-       # create a new relation, or find the existing one
-       if relid <= 0
-         rel = Relation.new
-       else
-         rel = Relation.find(relid)
-       end
-
-       # check the members are all positive, and correctly type
-       typedmembers = []
-       members.each do |m|
-         mid = m[1].to_i
-         if mid < 0
-               mid = renumberednodes[mid] if m[0] == 'node'
-               mid = renumberedways[mid] if m[0] == 'way'
-         end
-      if mid
-           typedmembers << [m[0], mid, m[2]]
-         end
-       end
-
-       # assign new contents
-       rel.members = typedmembers
-       rel.tags = tags
-       rel.visible = visible
-       rel.user_id = uid
-
-       # check it then save it
-       # BUG: the following is commented out because it always fails on my
-       #  install. I think it's a Rails bug.
-
-       #if !rel.preconditions_ok?
-       #  return -2, "Relation preconditions failed"
-       #else
-         rel.save_with_history!
-       #end
-
-       [0, relid, rel.id]
+  def putrelation(renumberednodes, renumberedways, usertoken, changeset_id, version, relid, tags, members, visible) #:doc:
+    user = getuser(usertoken)
+    if !user then return -1,"You are not logged in, so the relation could not be saved." end
+
+    relid = relid.to_i
+    visible = (visible.to_i != 0)
+
+    new_relation = nil
+    relation = nil
+    Relation.transaction do
+      # create a new relation, or find the existing one
+      if relid > 0
+        relation = Relation.find(relid)
+      end
+      # We always need a new node, based on the data that has been sent to us
+      new_relation = Relation.new
+
+      # check the members are all positive, and correctly type
+      typedmembers = []
+      members.each do |m|
+        mid = m[1].to_i
+        if mid < 0
+          mid = renumberednodes[mid] if m[0] == 'node'
+          mid = renumberedways[mid] if m[0] == 'way'
+        end
+        if mid
+          typedmembers << [m[0], mid, m[2]]
+        end
+      end
+
+      # assign new contents
+      new_relation.members = typedmembers
+      new_relation.tags = tags
+      new_relation.visible = visible
+      new_relation.changeset_id = changeset_id
+      new_relation.version = version
+
+      # NOTE: id or relid here? id doesn't seem to be set above
+      if relid <= 0
+        # We're creating the node
+        new_relation.create_with_history(user)
+      elsif visible
+        # We're updating the node
+        relation.update_from(new_relation, user)
+      else
+        # We're deleting the node
+        relation.delete_with_history!(new_relation, user)
+      end
+    end # transaction
+      
+    if id <= 0
+      return [0, relid, new_relation.id, new_relation.version]
+    else
+      return [0, relid, relation.id, relation.version]
+    end
+  rescue OSM::APIChangesetAlreadyClosedError => ex
+    return [-1, "The changeset #{ex.changeset.id} was closed at #{ex.changeset.closed_at}."]
+  rescue OSM::APIVersionMismatchError => ex
+    # Really need to check to see whether this is a server load issue, and the 
+    # last version was in the same changeset, or belongs to the same user, then
+    # we can return something different
+    return [-3, "Sorry, someone else has changed this relation since you started editing. Please click the 'Edit' tab to reload the area."]
+  rescue OSM::APIAlreadyDeletedError => ex
+    return [-1, "The relation has already been deleted."]
+  rescue OSM::APIError => ex
+    # Some error that we don't specifically catch
+    return [-2, "Something really bad happened :-( ."]
   end
 
   # Save a way to the database, including all nodes. Any nodes in the previous
   # version and no longer used are deleted.
   # 
+  # Parameters:
+  # 0. hash of renumbered nodes (added by amf_controller)
+  # 1. current user token (for authentication)
+  # 2. current changeset
+  # 3. new way version
+  # 4. way ID
+  # 5. list of nodes in way
+  # 6. hash of way tags
+  # 7. array of nodes to change (each one is [lon,lat,id,version,tags])
+  # 
   # Returns:
   # 0. '0' (code for success),
   # 1. original way id (unchanged),
   # 2. new way id,
-  # 3. hash of renumbered nodes (old id=>new id)
+  # 3. hash of renumbered nodes (old id=>new id),
+  # 4. way version,
+  # 5. hash of node versions (node=>version)
 
-  def putway(renumberednodes, usertoken, originalway, points, attributes) #:doc:
+  def putway(renumberednodes, usertoken, changeset_id, wayversion, originalway, pointlist, attributes, nodes) #:doc:
 
-       # -- Initialise and carry out checks
+    # -- Initialise
        
-       uid = getuserid(usertoken)
-       if !uid then return -1,"You are not logged in, so the way could not be saved." end
-
-       originalway = originalway.to_i
-
-       points.each do |a|
-         if a[2] == 0 or a[2].nil? then return -2,"Server error - node with id 0 found in way #{originalway}." end
-         if a[1] == 90 then return -2,"Server error - node with lat -90 found in way #{originalway}." end
-       end
-
-       if points.length < 2 then return -2,"Server error - way is only #{points.length} points long." end
-
-       # -- Get unique nodes
-
-       if originalway < 0
-         way = Way.new
-         uniques = []
-       else
-         way = Way.find(originalway)
-         uniques = way.unshared_node_ids
-       end
-
-       # -- Compare nodes and save changes to any that have changed
-
-       nodes = []
-
-       points.each do |n|
-         lon = n[0].to_f
-         lat = n[1].to_f
-         id = n[2].to_i
-         savenode = false
-
-         if renumberednodes[id]
-           id = renumberednodes[id]
-         elsif id < 0
-               # Create new node
-               node = Node.new
-               savenode = true
-         else
-               node = Node.find(id)
-               nodetags=node.tags_as_hash
-               nodetags.delete('created_by')
-               if !fpcomp(lat, node.lat) or !fpcomp(lon, node.lon) or
-                  n[4] != nodetags or !node.visible?
-                 savenode = true
-               end
-         end
-
-         if savenode
-               node.user_id = uid
-           node.lat = lat
+    user = getuser(usertoken)
+    if !user then return -1,"You are not logged in, so the way could not be saved." end
+    if pointlist.length < 2 then return -2,"Server error - way is only #{points.length} points long." end
+
+    originalway = originalway.to_i
+       pointlist.collect! {|a| a.to_i }
+
+    way=nil    # this is returned, so scope it outside the transaction
+    nodeversions = {}
+    Way.transaction do
+
+      # -- Get unique nodes
+
+      if originalway <= 0
+        uniques = []
+      else
+        way = Way.find(originalway)
+        uniques = way.unshared_node_ids
+      end
+
+      #-- Update each changed node
+
+      nodes.each do |a|
+        lon = a[0].to_f
+        lat = a[1].to_f
+        id = a[2].to_i
+        version = a[3].to_i
+        if id == 0  then return -2,"Server error - node with id 0 found in way #{originalway}." end
+        if lat== 90 then return -2,"Server error - node with latitude -90 found in way #{originalway}." end
+        if renumberednodes[id] then id = renumberednodes[id] end
+
+        node = Node.new
+        node.changeset_id = changeset_id
+        node.lat = lat
         node.lon = lon
-           node.tags = Tags.join(n[4])
-           node.visible = true
-           node.save_with_history!
-
-               if id != node.id
-                 renumberednodes[id] = node.id
-                 id = node.id
-           end
-         end
-
-         uniques = uniques - [id]
-         nodes.push(id)
-       end
-
-       # -- Save revised way
-
-       way.tags = attributes
-       way.nds = nodes
-       way.user_id = uid
-       way.visible = true
-       way.save_with_history!
-
-       # -- Delete any unique nodes
-       
-       uniques.each do |n|
-         deleteitemrelations(n, 'node')
-
-         node = Node.find(n)
-         node.user_id = uid
-         node.visible = false
-         node.save_with_history!
-       end
-
-       [0, originalway, way.id, renumberednodes]
+        node.tags = a[4]
+        node.tags.delete('created_by')
+        node.version = version
+        if id <= 0
+          # We're creating the node
+          node.create_with_history(user)
+          renumberednodes[id] = node.id
+          nodeversions[node.id] = node.version
+        else
+          # We're updating an existing node
+          previous=Node.find(id)
+          previous.update_from(node, user)
+          nodeversions[previous.id] = previous.version
+        end
+      end
+
+      # -- Save revised way
+
+         pointlist.collect! {|a|
+               renumberednodes[a] ? renumberednodes[a]:a
+         } # renumber nodes
+      new_way = Way.new
+      new_way.tags = attributes
+      new_way.nds = pointlist
+      new_way.changeset_id = changeset_id
+      new_way.version = wayversion
+      if originalway <= 0
+        new_way.create_with_history(user)
+        way=new_way    # so we can get way.id and way.version
+      elsif way.tags!=attributes or way.nds!=pointlist or !way.visible?
+        way.update_from(new_way, user)
+      end
+
+      # -- Delete any unique nodes no longer used
+
+      uniques=uniques-pointlist
+      uniques.each do |n|
+        node = Node.find(n)
+        deleteitemrelations(user, changeset_id, id, 'node', node.version)
+        new_node = Node.new
+        new_node.changeset_id = changeset_id
+        new_node.version = node.version
+        node.delete_with_history!(new_node, user)
+      end
+
+    end # transaction
+
+    [0, originalway, way.id, renumberednodes, way.version, nodeversions]
+  rescue OSM::APIChangesetAlreadyClosedError => ex
+    return [-2, "Sorry, your changeset #{ex.changeset.id} has been closed (at #{ex.changeset.closed_at})."]
+  rescue OSM::APIVersionMismatchError => ex
+    # Really need to check to see whether this is a server load issue, and the 
+    # last version was in the same changeset, or belongs to the same user, then
+    # we can return something different
+    return [-3, "Sorry, someone else has changed this way since you started editing. Please click the 'Edit' tab to reload the area."]
+  rescue OSM::APITooManyWayNodesError => ex
+    return [-1, "You have tried to upload a really long way with #{ex.provided} points: only #{ex.max} are allowed."]
+  rescue OSM::APIAlreadyDeletedError => ex
+    return [-1, "The point has already been deleted."]
+  rescue OSM::APIError => ex
+    # Some error that we don't specifically catch
+    return [-2, "Something really bad happened :-(."]
   end
 
   # Save POI to the database.
   # Refuses save if the node has since become part of a way.
-  # Returns:
+  # Returns array with:
   # 0. 0 (success),
   # 1. original node id (unchanged),
-  # 2. new node id.
-
-  def putpoi(usertoken, id, lon, lat, tags, visible) #:doc:
-       uid = getuserid(usertoken)
-       if !uid then return -1,"You are not logged in, so the point could not be saved." end
-
-       id = id.to_i
-       visible = (visible.to_i == 1)
-
-       if id > 0 then
-         node = Node.find(id)
-
-         if !visible then
-           unless node.ways.empty? then return -1,"The point has since become part of a way, so you cannot save it as a POI." end
-           deleteitemrelations(id, 'node')
-         end
-       else
-         node = Node.new
-       end
-
-       node.user_id = uid
-       node.lat = lat
-       node.lon = lon
-       node.tags = Tags.join(tags)
-       node.visible = visible
-       node.save_with_history!
-
-       [0, id, node.id]
+  # 2. new node id,
+  # 3. version.
+
+  def putpoi(usertoken, changeset_id, version, id, lon, lat, tags, visible) #:doc:
+    user = getuser(usertoken)
+    if !user then return -1,"You are not logged in, so the point could not be saved." end
+
+    id = id.to_i
+    visible = (visible.to_i == 1)
+    node = nil
+    new_node = nil
+    Node.transaction do
+      if id > 0 then
+        node = Node.find(id)
+
+        if !visible then
+          unless node.ways.empty? then return -1,"The point has since become part of a way, so you cannot save it as a POI." end
+        end
+      end
+      # We always need a new node, based on the data that has been sent to us
+      new_node = Node.new
+
+      new_node.changeset_id = changeset_id
+      new_node.version = version
+      new_node.lat = lat
+      new_node.lon = lon
+      new_node.tags = tags
+      if id <= 0 
+        # We're creating the node
+        new_node.create_with_history(user)
+      elsif visible
+        # We're updating the node
+        node.update_from(new_node, user)
+      else
+        # We're deleting the node
+        node.delete_with_history!(new_node, user)
+      end
+     end # transaction
+
+    if id <= 0
+      return [0, id, new_node.id, new_node.version]
+    else
+      return [0, id, node.id, node.version]
+    end 
+  rescue OSM::APIChangesetAlreadyClosedError => ex
+    return [-1, "The changeset #{ex.changeset.id} was closed at #{ex.changeset.closed_at}"]
+  rescue OSM::APIVersionMismatchError => ex
+    # Really need to check to see whether this is a server load issue, and the 
+    # last version was in the same changeset, or belongs to the same user, then
+    # we can return something different
+    return [-3, "Sorry, someone else has changed this point since you started editing. Please click the 'Edit' tab to reload the area."]
+  rescue OSM::APIAlreadyDeletedError => ex
+    return [-1, "The point has already been deleted"]
+  rescue OSM::APIError => ex
+    # Some error that we don't specifically catch
+    return [-2, "Something really bad happened :-()"]
   end
 
   # Read POI from database
   # (only called on revert: POIs are usually read by whichways).
   #
-  # Returns array of id, long, lat, hash of tags.
+  # Returns array of id, long, lat, hash of tags, version.
 
   def getpoi(id,timestamp) #:doc:
-       if timestamp>0 then
-         n = OldNode.find(id, :conditions=>['UNIX_TIMESTAMP(timestamp)=?',timestamp])
-       else
-         n = Node.find(id)
-       end
-
-       if n
-         return [n.id, n.lon, n.lat, n.tags_as_hash]
-       else
-         return [nil, nil, nil, '']
-       end
+    if timestamp == '' then
+      n = Node.find(id)
+    else
+      n = OldNode.find(id, :conditions=>['timestamp=?',DateTime.strptime(timestamp, "%d %b %Y, %H:%M:%S")])
+    end
+
+    if n
+      return [n.id, n.lon, n.lat, n.tags, n.version]
+    else
+      return [nil, nil, nil, {}, nil]
+    end
   end
 
   # Delete way and all constituent nodes. Also removes from any relations.
+  # Params:
+  # * The user token
+  # * the changeset id
+  # * the id of the way to change
+  # * the version of the way that was downloaded
+  # * a hash of the id and versions of all the nodes that are in the way, if any 
+  # of the nodes have been changed by someone else then, there is a problem!
   # Returns 0 (success), unchanged way id.
 
-  def deleteway(usertoken, way_id) #:doc:
-       uid = getuserid(usertoken)
-       if !uid then return -1,"You are not logged in, so the way could not be deleted." end
-
-       # FIXME: would be good not to make two history entries when removing
-       #                two nodes from the same relation
-       user = User.find(uid)
-       way = Way.find(way_id)
-       way.unshared_node_ids.each do |n|
-         deleteitemrelations(n, 'node')
-       end
-       deleteitemrelations(way_id, 'way')
-
-       way.delete_with_relations_and_nodes_and_history(user)  
-
-       [0, way_id]
+  def deleteway(usertoken, changeset_id, way_id, way_version, node_id_version) #:doc:
+    user = getuser(usertoken)
+    unless user then return -1,"You are not logged in, so the way could not be deleted." end
+      
+    way_id = way_id.to_i
+    # Need a transaction so that if one item fails to delete, the whole delete fails.
+    Way.transaction do
+
+      # delete the way
+      old_way = Way.find(way_id)
+      delete_way = Way.new
+      delete_way.version = way_version
+      delete_way.changeset_id = changeset_id
+      old_way.delete_with_history!(delete_way, user)
+
+      old_way.unshared_node_ids.each do |node_id|
+        # delete the node
+        node = Node.find(node_id)
+        delete_node = Node.new
+        delete_node.changeset_id = changeset_id
+        if node_id_version[node_id.to_s]
+          delete_node.version = node_id_version[node_id.to_s]
+        else
+          # in case the node wasn't passed (i.e. if it was previously removed
+          # from the way in Potlatch)
+          deleteitemrelations(user, changeset_id, node_id, 'node', node.version)
+             delete_node.version = node.version
+           end
+        node.delete_with_history!(delete_node, user)
+      end
+    end # transaction
+    [0, way_id]
+  rescue OSM::APIChangesetAlreadyClosedError => ex
+    return [-1, "The changeset #{ex.changeset.id} was closed at #{ex.changeset.closed_at}"]
+  rescue OSM::APIVersionMismatchError => ex
+    # Really need to check to see whether this is a server load issue, and the 
+    # last version was in the same changeset, or belongs to the same user, then
+    # we can return something different
+    return [-3, "Sorry, someone else has changed this way since you started editing. Please click the 'Edit' tab to reload the area."]
+  rescue OSM::APIAlreadyDeletedError => ex
+    return [-1, "The way has already been deleted."]
+  rescue OSM::APIError => ex
+    # Some error that we don't specifically catch
+    return [-2, "Something really bad happened :-( ."]
   end
 
 
@@ -500,141 +789,147 @@ class AmfController < ApplicationController
   # Support functions
 
   # Remove a node or way from all relations
+  # This is only used by putway and deleteway when deleting nodes removed 
+  # from a way (because Potlatch itself doesn't keep track of these - 
+  # possible FIXME).
 
-  def deleteitemrelations(objid, type) #:doc:
-       relations = RelationMember.find(:all, 
+  def deleteitemrelations(user, changeset_id, objid, type, version) #:doc:
+    relations = RelationMember.find(:all, 
                                                                        :conditions => ['member_type = ? and member_id = ?', type, objid], 
                                                                        :include => :relation).collect { |rm| rm.relation }.uniq
 
-       relations.each do |rel|
-         rel.members.delete_if { |x| x[0] == type and x[1] == objid }
-         rel.save_with_history!
-       end
-  end
-
-  # Break out node tags into a hash
-  # (should become obsolete as of API 0.6)
-
-  def tagstring_to_hash(a) #:doc:
-       tags={}
-       Tags.split(a) do |k, v|
-         tags[k]=v
-       end
-       tags
+    relations.each do |rel|
+      rel.members.delete_if { |x| x[0] == type and x[1] == objid }
+      new_rel = Relation.new
+      new_rel.tags = rel.tags
+      new_rel.visible = rel.visible
+      new_rel.version = rel.version
+      new_rel.members = rel.members
+      new_rel.changeset_id = changeset_id
+      rel.update_from(new_rel, user)
+    end
   end
 
   # Authenticate token
   # (can also be of form user:pass)
-
-  def getuserid(token) #:doc:
-       if (token =~ /^(.+)\:(.+)$/) then
-         user = User.authenticate(:username => $1, :password => $2)
-       else
-         user = User.authenticate(:token => token)
-       end
-
-       return user ? user.id : nil;
-  end
-
-  # Compare two floating-point numbers to within 0.0000001
-
-  def fpcomp(a,b) #:doc:
-       return ((a/0.0000001).round==(b/0.0000001).round)
+  # When we are writing to the api, we need the actual user model, 
+  # not just the id, hence this abstraction
+
+  def getuser(token) #:doc:
+    if (token =~ /^(.+)\:(.+)$/) then
+      user = User.authenticate(:username => $1, :password => $2)
+    else
+      user = User.authenticate(:token => token)
+    end
+    return user
   end
 
   # Send AMF response
   
   def sendresponse(results)
-       a,b=results.length.divmod(256)
-       render :content_type => "application/x-amf", :text => proc { |response, output| 
-         output.write 0.chr+0.chr+0.chr+0.chr+a.chr+b.chr
-         results.each do |k,v|
-               output.write(v)
-         end
-       }
+    a,b=results.length.divmod(256)
+    render :content_type => "application/x-amf", :text => proc { |response, output| 
+      # ** move amf writing loop into here - 
+      # basically we read the messages in first (into an array of some sort),
+      # then iterate through that array within here, and do all the AMF writing
+      output.write 0.chr+0.chr+0.chr+0.chr+a.chr+b.chr
+      results.each do |k,v|
+        output.write(v)
+      end
+    }
   end
 
 
   # ====================================================================
   # Alternative SQL queries for getway/whichways
 
-  def sql_find_way_ids_in_area(xmin,ymin,xmax,ymax)
-       sql=<<-EOF
-  SELECT DISTINCT current_way_nodes.id AS wayid
-               FROM current_way_nodes
-  INNER JOIN current_nodes ON current_nodes.id=current_way_nodes.node_id
-  INNER JOIN current_ways  ON current_ways.id =current_way_nodes.id
-          WHERE current_nodes.visible=1 
-                AND current_ways.visible=1 
-                AND #{OSM.sql_for_area(ymin, xmin, ymax, xmax, "current_nodes.")}
-       EOF
-       return ActiveRecord::Base.connection.select_all(sql).collect { |a| a['wayid'].to_i }
+  def sql_find_ways_in_area(xmin,ymin,xmax,ymax)
+    sql=<<-EOF
+    SELECT DISTINCT current_ways.id AS wayid,current_ways.version AS version
+      FROM current_way_nodes
+    INNER JOIN current_nodes ON current_nodes.id=current_way_nodes.node_id
+    INNER JOIN current_ways  ON current_ways.id =current_way_nodes.id
+       WHERE current_nodes.visible=TRUE 
+       AND current_ways.visible=TRUE 
+       AND #{OSM.sql_for_area(ymin, xmin, ymax, xmax, "current_nodes.")}
+    EOF
+    return ActiveRecord::Base.connection.select_all(sql).collect { |a| [a['wayid'].to_i,a['version'].to_i] }
   end
        
   def sql_find_pois_in_area(xmin,ymin,xmax,ymax)
-       sql=<<-EOF
-                 SELECT current_nodes.id,current_nodes.latitude*0.0000001 AS lat,current_nodes.longitude*0.0000001 AS lon,current_nodes.tags 
+    pois=[]
+    sql=<<-EOF
+                 SELECT current_nodes.id,current_nodes.latitude*0.0000001 AS lat,current_nodes.longitude*0.0000001 AS lon,current_nodes.version 
                        FROM current_nodes 
- LEFT OUTER JOIN current_way_nodes cwn ON cwn.node_id=current_nodes.id 
-                  WHERE current_nodes.visible=1
      LEFT OUTER JOIN current_way_nodes cwn ON cwn.node_id=current_nodes.id 
+                  WHERE current_nodes.visible=TRUE
                         AND cwn.id IS NULL
                         AND #{OSM.sql_for_area(ymin, xmin, ymax, xmax, "current_nodes.")}
-       EOF
-       return ActiveRecord::Base.connection.select_all(sql).collect { |n| [n['id'].to_i,n['lon'].to_f,n['lat'].to_f,tagstring_to_hash(n['tags'])] }
+    EOF
+    ActiveRecord::Base.connection.select_all(sql).each do |row|
+      poitags={}
+      ActiveRecord::Base.connection.select_all("SELECT k,v FROM current_node_tags WHERE id=#{row['id']}").each do |n|
+        poitags[n['k']]=n['v']
+      end
+      pois << [row['id'].to_i, row['lon'].to_f, row['lat'].to_f, poitags, row['version'].to_i]
+    end
+    pois
   end
        
   def sql_find_relations_in_area_and_ways(xmin,ymin,xmax,ymax,way_ids)
-       # ** It would be more Potlatchy to get relations for nodes within ways
-       #    during 'getway', not here
-       sql=<<-EOF
-         SELECT DISTINCT cr.id AS relid 
-               FROM current_relations cr
-  INNER JOIN current_relation_members crm ON crm.id=cr.id 
-  INNER JOIN current_nodes cn ON crm.member_id=cn.id AND crm.member_type='node' 
-          WHERE #{OSM.sql_for_area(ymin, xmin, ymax, xmax, "cn.")}
-       EOF
-       unless way_ids.empty?
-         sql+=<<-EOF
-          UNION
-         SELECT DISTINCT cr.id AS relid
-               FROM current_relations cr
-  INNER JOIN current_relation_members crm ON crm.id=cr.id
-          WHERE crm.member_type='way' 
-                AND crm.member_id IN (#{way_ids.join(',')})
-         EOF
-       end
-       return ActiveRecord::Base.connection.select_all(sql).collect { |a| a['relid'].to_i }.uniq
+    # ** It would be more Potlatchy to get relations for nodes within ways
+    #    during 'getway', not here
+    sql=<<-EOF
+      SELECT DISTINCT cr.id AS relid,cr.version AS version 
+      FROM current_relations cr
+      INNER JOIN current_relation_members crm ON crm.id=cr.id 
+      INNER JOIN current_nodes cn ON crm.member_id=cn.id AND crm.member_type='node' 
+       WHERE #{OSM.sql_for_area(ymin, xmin, ymax, xmax, "cn.")}
+      EOF
+    unless way_ids.empty?
+      sql+=<<-EOF
+       UNION
+        SELECT DISTINCT cr.id AS relid,cr.version AS version
+        FROM current_relations cr
+        INNER JOIN current_relation_members crm ON crm.id=cr.id
+         WHERE crm.member_type='way' 
+         AND crm.member_id IN (#{way_ids.join(',')})
+        EOF
+    end
+    return ActiveRecord::Base.connection.select_all(sql).collect { |a| [a['relid'].to_i,a['version'].to_i] }
   end
        
   def sql_get_nodes_in_way(wayid)
-       points=[]
-       sql=<<-EOF
-               SELECT latitude*0.0000001 AS lat,longitude*0.0000001 AS lon,current_nodes.id,tags 
-                 FROM current_way_nodes,current_nodes 
-                WHERE current_way_nodes.id=#{wayid.to_i} 
+    points=[]
+    sql=<<-EOF
+      SELECT latitude*0.0000001 AS lat,longitude*0.0000001 AS lon,current_nodes.id 
+      FROM current_way_nodes,current_nodes 
+       WHERE current_way_nodes.id=#{wayid.to_i} 
                   AND current_way_nodes.node_id=current_nodes.id 
-                  AND current_nodes.visible=1
-         ORDER BY sequence_id
+                  AND current_nodes.visible=TRUE
+      ORDER BY sequence_id
          EOF
-       ActiveRecord::Base.connection.select_all(sql).each do |row|
-         nodetags=tagstring_to_hash(row['tags'])
-         nodetags.delete('created_by')
-         points << [row['lon'].to_f,row['lat'].to_f,row['id'].to_i,nodetags]
-       end
-       points
+    ActiveRecord::Base.connection.select_all(sql).each do |row|
+      nodetags={}
+      ActiveRecord::Base.connection.select_all("SELECT k,v FROM current_node_tags WHERE id=#{row['id']}").each do |n|
+        nodetags[n['k']]=n['v']
+      end
+      nodetags.delete('created_by')
+      points << [row['lon'].to_f,row['lat'].to_f,row['id'].to_i,nodetags]
+    end
+    points
   end
        
   def sql_get_tags_in_way(wayid)
-       tags={}
-       ActiveRecord::Base.connection.select_all("SELECT k,v FROM current_way_tags WHERE id=#{wayid.to_i}").each do |row|
-         tags[row['k']]=row['v']
-       end
-       tags
+    tags={}
+    ActiveRecord::Base.connection.select_all("SELECT k,v FROM current_way_tags WHERE id=#{wayid.to_i}").each do |row|
+      tags[row['k']]=row['v']
+    end
+    tags
   end
 
+  def sql_get_way_version(wayid)
+    ActiveRecord::Base.connection.select_one("SELECT version FROM current_ways WHERE id=#{wayid.to_i}")
+  end
 end
 
-# Local Variables:
-# indent-tabs-mode: t
-# tab-width: 4
-# End:
index 029e4c8a6544353845213575e57243c49e67f5b4..ebf729afc322812002231adeeefe6e486a756367 100644 (file)
@@ -11,12 +11,13 @@ class ApiController < ApplicationController
   @@count = COUNT
 
   # The maximum area you're allowed to request, in square degrees
-  MAX_REQUEST_AREA = 0.25
+  MAX_REQUEST_AREA = APP_CONFIG['max_request_area']
 
   # Number of GPS trace/trackpoints returned per-page
-  TRACEPOINTS_PER_PAGE = 5000
+  TRACEPOINTS_PER_PAGE = APP_CONFIG['tracepoints_per_page']
 
-  
+  # Get an XML response containing a list of tracepoints that have been uploaded
+  # within the specified bounding box, and in the specified page.
   def trackpoints
     @@count+=1
     #retrieve the page number
@@ -84,6 +85,15 @@ class ApiController < ApplicationController
     render :text => doc.to_s, :content_type => "text/xml"
   end
 
+  # This is probably the most common call of all. It is used for getting the 
+  # OSM data for a specified bounding box, usually for editing. First the
+  # bounding box (bbox) is checked to make sure that it is sane. All nodes 
+  # are searched, then all the ways that reference those nodes are found.
+  # All Nodes that are referenced by those ways are fetched and added to the list
+  # of nodes.
+  # Then all the relations that reference the already found nodes and ways are
+  # fetched. All the nodes and ways that are referenced by those ways are then 
+  # fetched. Finally all the xml is returned.
   def map
     GC.start
     @@count+=1
@@ -109,18 +119,19 @@ class ApiController < ApplicationController
       return
     end
 
-    @nodes = Node.find_by_area(min_lat, min_lon, max_lat, max_lon, :conditions => "visible = 1", :limit => APP_CONFIG['max_number_of_nodes']+1)
+    # FIXME um why is this area using a different order for the lat/lon from above???
+    @nodes = Node.find_by_area(min_lat, min_lon, max_lat, max_lon, :conditions => {:visible => true}, :limit => APP_CONFIG['max_number_of_nodes']+1)
     # get all the nodes, by tag not yet working, waiting for change from NickB
     # need to be @nodes (instance var) so tests in /spec can be performed
     #@nodes = Node.search(bbox, params[:tag])
 
     node_ids = @nodes.collect(&:id)
     if node_ids.length > APP_CONFIG['max_number_of_nodes']
-      report_error("You requested too many nodes (limit is 50,000). Either request a smaller area, or use planet.osm")
+      report_error("You requested too many nodes (limit is #{APP_CONFIG['max_number_of_nodes']}). Either request a smaller area, or use planet.osm")
       return
     end
     if node_ids.length == 0
-      render :text => "<osm version='0.5'></osm>", :content_type => "text/xml"
+      render :text => "<osm version='#{API_VERSION}' generator='#{GENERATOR}'></osm>", :content_type => "text/xml"
       return
     end
 
@@ -176,15 +187,15 @@ class ApiController < ApplicationController
       end
     end 
 
-    relations = Relation.find_for_nodes(visible_nodes.keys, :conditions => "visible = 1") +
-                Relation.find_for_ways(way_ids, :conditions => "visible = 1")
+    relations = Relation.find_for_nodes(visible_nodes.keys, :conditions => {:visible => true}) +
+                Relation.find_for_ways(way_ids, :conditions => {:visible => true})
 
     # we do not normally return the "other" partners referenced by an relation, 
     # e.g. if we return a way A that is referenced by relation X, and there's 
     # another way B also referenced, that is not returned. But we do make 
     # an exception for cases where an relation references another *relation*; 
     # in that case we return that as well (but we don't go recursive here)
-    relations += Relation.find_for_relations(relations.collect { |r| r.id }, :conditions => "visible = 1")
+    relations += Relation.find_for_relations(relations.collect { |r| r.id }, :conditions => {:visible => true})
 
     # this "uniq" may be slightly inefficient; it may be better to first collect and output
     # all node-related relations, then find the *not yet covered* way-related ones etc.
@@ -204,6 +215,8 @@ class ApiController < ApplicationController
     end
   end
 
+  # Get a list of the tiles that have changed within a specified time
+  # period
   def changes
     zoom = (params[:zoom] || '12').to_i
 
@@ -212,12 +225,12 @@ class ApiController < ApplicationController
       endtime = Time.parse(params[:end])
     else
       hours = (params[:hours] || '1').to_i.hours
-      endtime = Time.now
+      endtime = Time.now.getutc
       starttime = endtime - hours
     end
 
     if zoom >= 1 and zoom <= 16 and
-       endtime >= starttime and endtime - starttime <= 24.hours
+       endtime > starttime and endtime - starttime <= 24.hours
       mask = (1 << zoom) - 1
 
       tiles = Node.count(:conditions => ["timestamp BETWEEN ? AND ?", starttime, endtime],
@@ -245,21 +258,32 @@ class ApiController < ApplicationController
 
       render :text => doc.to_s, :content_type => "text/xml"
     else
-      render :nothing => true, :status => :bad_request
+      render :text => "Requested zoom is invalid, or the supplied start is after the end time, or the start duration is more than 24 hours", :status => :bad_request
     end
   end
 
+  # External apps that use the api are able to query the api to find out some 
+  # parameters of the API. It currently returns: 
+  # * minimum and maximum API versions that can be used.
+  # * maximum area that can be requested in a bbox request in square degrees
+  # * number of tracepoints that are returned in each tracepoints page
   def capabilities
     doc = OSM::API.new.get_xml_doc
 
     api = XML::Node.new 'api'
     version = XML::Node.new 'version'
-    version['minimum'] = '0.5';
-    version['maximum'] = '0.5';
+    version['minimum'] = "#{API_VERSION}";
+    version['maximum'] = "#{API_VERSION}";
     api << version
     area = XML::Node.new 'area'
     area['maximum'] = MAX_REQUEST_AREA.to_s;
     api << area
+    tracepoints = XML::Node.new 'tracepoints'
+    tracepoints['per_page'] = APP_CONFIG['tracepoints_per_page'].to_s
+    api << tracepoints
+    waynodes = XML::Node.new 'waynodes'
+    waynodes['maximum'] = APP_CONFIG['max_number_of_way_nodes'].to_s
+    api << waynodes
     
     doc.root << api
 
index 6150226569365df57a19a7e261e328ed2acb7879..bfd2e9c546e852dcabad7f11ee2af4694a3df373 100644 (file)
@@ -8,7 +8,7 @@ class ApplicationController < ActionController::Base
 
   def authorize_web
     if session[:user]
-      @user = User.find(session[:user], :conditions => "visible = 1")
+      @user = User.find(session[:user], :conditions => {:visible => true})
     elsif session[:token]
       @user = User.authenticate(:token => session[:token])
       session[:user] = @user.id
@@ -22,7 +22,11 @@ class ApplicationController < ActionController::Base
     redirect_to :controller => 'user', :action => 'login', :referer => request.request_uri unless @user
   end
 
-  def authorize(realm='Web Password', errormessage="Couldn't authenticate you") 
+  ##
+  # sets up the @user object for use by other methods. this is mostly called
+  # from the authorize method, but can be called elsewhere if authorisation
+  # is optional.
+  def setup_user_auth
     username, passwd = get_auth_data # parse from headers
     # authenticate per-scheme
     if username.nil?
@@ -32,6 +36,11 @@ class ApplicationController < ActionController::Base
     else
       @user = User.authenticate(:username => username, :password => passwd) # basic auth
     end
+  end
+
+  def authorize(realm='Web Password', errormessage="Couldn't authenticate you") 
+    # make the @user object from any auth sources we have
+    setup_user_auth
 
     # handle authenticate pass/fail
     unless @user
@@ -73,13 +82,21 @@ class ApplicationController < ActionController::Base
     end
   end
 
+  def require_public_data
+    unless @user.data_public?
+      response.headers['Error'] = "You must make your edits public to upload new data"
+      render :nothing => true, :status => :forbidden
+      return false
+    end
+  end
+
   # Report and error to the user
   # (If anyone ever fixes Rails so it can set a http status "reason phrase",
   #  rather than only a status code and having the web engine make up a 
   #  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
@@ -90,6 +107,8 @@ private
   def get_auth_data 
     if request.env.has_key? 'X-HTTP_AUTHORIZATION'          # where mod_rewrite might have put it 
       authdata = request.env['X-HTTP_AUTHORIZATION'].to_s.split 
+    elsif request.env.has_key? 'REDIRECT_X_HTTP_AUTHORIZATION'          # mod_fcgi 
+      authdata = request.env['REDIRECT_X_HTTP_AUTHORIZATION'].to_s.split 
     elsif request.env.has_key? 'HTTP_AUTHORIZATION'         # regular location
       authdata = request.env['HTTP_AUTHORIZATION'].to_s.split
     end 
index df48a209044b32891958adb1df72a65bf60bcf52..6ace0817b01353a7f1f4a270354b9075c05e5e27 100644 (file)
@@ -7,103 +7,109 @@ class BrowseController < ApplicationController
   def start 
   end
   
-  def index
-    @nodes = Node.find(:all, :order => "timestamp DESC", :limit=> 20)  
-  end
+  
   
   def relation 
-    begin
-      @relation = Relation.find(params[:id])
-     
-      @name = @relation.tags['name'].to_s 
-      if @name.length == 0:
-       @name = "#" + @relation.id.to_s
-      end
-       
-      @title = 'Relation | ' + (@name)
-      @next = Relation.find(:first, :order => "id ASC", :conditions => [ "visible = true AND id > :id", { :id => @relation.id }] ) 
-      @prev = Relation.find(:first, :order => "id DESC", :conditions => [ "visible = true AND id < :id", { :id => @relation.id }] ) 
-    rescue ActiveRecord::RecordNotFound
-      render :nothing => true, :status => :not_found
+    @relation = Relation.find(params[:id])
+   
+    @name = @relation.tags['name'].to_s 
+    if @name.length == 0:
+      @name = "#" + @relation.id.to_s
     end
+       
+    @title = 'Relation | ' + (@name)
+    @next = Relation.find(:first, :order => "id ASC", :conditions => [ "visible = true AND id > :id", { :id => @relation.id }] ) 
+    @prev = Relation.find(:first, :order => "id DESC", :conditions => [ "visible = true AND id < :id", { :id => @relation.id }] ) 
+  rescue ActiveRecord::RecordNotFound
+    @type = "relation"
+    render :action => "not_found", :status => :not_found
   end
   
   def relation_history
-    begin
-      @relation = Relation.find(params[:id])
+    @relation = Relation.find(params[:id])
      
-      @name = @relation.tags['name'].to_s 
-      if @name.length == 0:
-       @name = "#" + @relation.id.to_s
-      end
-       
-      @title = 'Relation History | ' + (@name)
-    rescue ActiveRecord::RecordNotFound
-      render :nothing => true, :status => :not_found
+    @name = @relation.tags['name'].to_s 
+    if @name.length == 0:
+      @name = "#" + @relation.id.to_s
     end
+       
+    @title = 'Relation History | ' + (@name)
+  rescue ActiveRecord::RecordNotFound
+    @type = "relation"
+    render :action => "not_found", :status => :not_found
   end
   
   def way 
-    begin
-      @way = Way.find(params[:id])
+    @way = Way.find(params[:id])
      
-      @name = @way.tags['name'].to_s 
-      if @name.length == 0:
-       @name = "#" + @way.id.to_s
-      end
-       
-      @title = 'Way | ' + (@name)
-      @next = Way.find(:first, :order => "id ASC", :conditions => [ "visible = true AND id > :id", { :id => @way.id }] ) 
-      @prev = Way.find(:first, :order => "id DESC", :conditions => [ "visible = true AND id < :id", { :id => @way.id }] ) 
-    rescue ActiveRecord::RecordNotFound
-      render :nothing => true, :status => :not_found
+    @name = @way.tags['name'].to_s 
+    if @name.length == 0:
+      @name = "#" + @way.id.to_s
     end
+       
+    @title = 'Way | ' + (@name)
+    @next = Way.find(:first, :order => "id ASC", :conditions => [ "visible = true AND id > :id", { :id => @way.id }] ) 
+    @prev = Way.find(:first, :order => "id DESC", :conditions => [ "visible = true AND id < :id", { :id => @way.id }] ) 
+  rescue ActiveRecord::RecordNotFound
+    @type = "way"
+    render :action => "not_found", :status => :not_found
   end
   
   def way_history 
-    begin
-      @way = Way.find(params[:id])
+    @way = Way.find(params[:id])
      
-      @name = @way.tags['name'].to_s 
-      if @name.length == 0:
-       @name = "#" + @way.id.to_s
-      end
-       
-      @title = 'Way History | ' + (@name)
-    rescue ActiveRecord::RecordNotFound
-      render :nothing => true, :status => :not_found
+    @name = @way.tags['name'].to_s 
+    if @name.length == 0:
+      @name = "#" + @way.id.to_s
     end
+       
+    @title = 'Way History | ' + (@name)
+  rescue ActiveRecord::RecordNotFound
+    @type = "way"
+    render :action => "not_found", :status => :not_found
   end
 
   def node 
-    begin
-      @node = Node.find(params[:id])
+    @node = Node.find(params[:id])
      
-      @name = @node.tags_as_hash['name'].to_s 
-      if @name.length == 0:
-       @name = "#" + @node.id.to_s
-      end
-       
-      @title = 'Node | ' + (@name)
-      @next = Node.find(:first, :order => "id ASC", :conditions => [ "visible = true AND id > :id", { :id => @node.id }] ) 
-      @prev = Node.find(:first, :order => "id DESC", :conditions => [ "visible = true AND id < :id", { :id => @node.id }] ) 
-    rescue ActiveRecord::RecordNotFound
-      render :nothing => true, :status => :not_found
+    @name = @node.tags_as_hash['name'].to_s 
+    if @name.length == 0:
+      @name = "#" + @node.id.to_s
     end
+       
+    @title = 'Node | ' + (@name)
+    @next = Node.find(:first, :order => "id ASC", :conditions => [ "visible = true AND id > :id", { :id => @node.id }] ) 
+    @prev = Node.find(:first, :order => "id DESC", :conditions => [ "visible = true AND id < :id", { :id => @node.id }] ) 
+  rescue ActiveRecord::RecordNotFound
+    @type = "node"
+    render :action => "not_found", :status => :not_found
   end
   
   def node_history 
-    begin
-      @node = Node.find(params[:id])
+    @node = Node.find(params[:id])
      
-      @name = @node.tags_as_hash['name'].to_s 
-      if @name.length == 0:
-       @name = "#" + @node.id.to_s
-      end
-       
-      @title = 'Node History | ' + (@name)
-    rescue ActiveRecord::RecordNotFound
-      render :nothing => true, :status => :not_found
+    @name = @node.tags_as_hash['name'].to_s 
+    if @name.length == 0:
+      @name = "#" + @node.id.to_s
     end
+       
+    @title = 'Node History | ' + (@name)
+  rescue ActiveRecord::RecordNotFound
+    @type = "way"
+    render :action => "not_found", :status => :not_found
+  end
+  
+  def changeset
+    @changeset = Changeset.find(params[:id])
+    @node_pages, @nodes = paginate(:old_nodes, :conditions => {:changeset_id => @changeset.id}, :per_page => 20, :parameter => 'node_page')
+    @way_pages, @ways = paginate(:old_ways, :conditions => {:changeset_id => @changeset.id}, :per_page => 20, :parameter => 'way_page')
+    @relation_pages, @relations = paginate(:old_relations, :conditions => {:changeset_id => @changeset.id}, :per_page => 20, :parameter => 'relation_page')
+      
+    @title = "Changeset | #{@changeset.id}"
+    @next = Changeset.find(:first, :order => "id ASC", :conditions => [ "id > :id", { :id => @changeset.id }] ) 
+    @prev = Changeset.find(:first, :order => "id DESC", :conditions => [ "id < :id", { :id => @changeset.id }] ) 
+  rescue ActiveRecord::RecordNotFound
+    @type = "changeset"
+    render :action => "not_found", :status => :not_found
   end
 end
diff --git a/app/controllers/changeset_controller.rb b/app/controllers/changeset_controller.rb
new file mode 100644 (file)
index 0000000..4913a60
--- /dev/null
@@ -0,0 +1,485 @@
+# The ChangesetController is the RESTful interface to Changeset objects
+
+class ChangesetController < ApplicationController
+  layout 'site'
+  require 'xml/libxml'
+
+  session :off, :except => [:list, :list_user, :list_bbox]
+  before_filter :authorize_web, :only => [:list, :list_user, :list_bbox]
+  before_filter :authorize, :only => [:create, :update, :delete, :upload, :include, :close]
+  before_filter :require_public_data, :only => [:create, :update, :delete, :upload, :include, :close]
+  before_filter :check_api_writable, :only => [:create, :update, :delete, :upload, :include]
+  before_filter :check_api_readable, :except => [:create, :update, :delete, :upload, :download, :query]
+  after_filter :compress_output
+
+  # Help methods for checking boundary sanity and area size
+  include MapBoundary
+
+  # Helper methods for checking consistency
+  include ConsistencyValidations
+
+  # Create a changeset from XML.
+  def create
+    if request.put?
+      cs = Changeset.from_xml(request.raw_post, true)
+
+      if cs
+        cs.user_id = @user.id
+        cs.save_with_tags!
+        render :text => cs.id.to_s, :content_type => "text/plain"
+      else
+        render :nothing => true, :status => :bad_request
+      end
+    else
+      render :nothing => true, :status => :method_not_allowed
+    end
+  end
+
+  ##
+  # Return XML giving the basic info about the changeset. Does not 
+  # return anything about the nodes, ways and relations in the changeset.
+  def read
+    begin
+      changeset = Changeset.find(params[:id])
+      render :text => changeset.to_xml.to_s, :content_type => "text/xml"
+    rescue ActiveRecord::RecordNotFound
+      render :nothing => true, :status => :not_found
+    end
+  end
+  
+  ##
+  # marks a changeset as closed. this may be called multiple times
+  # on the same changeset, so is idempotent.
+  def close 
+    unless request.put?
+      render :nothing => true, :status => :method_not_allowed
+      return
+    end
+    
+    changeset = Changeset.find(params[:id])    
+    check_changeset_consistency(changeset, @user)
+
+    # to close the changeset, we'll just set its closed_at time to
+    # now. this might not be enough if there are concurrency issues, 
+    # but we'll have to wait and see.
+    changeset.set_closed_time_now
+
+    changeset.save!
+    render :nothing => true
+  rescue ActiveRecord::RecordNotFound
+    render :nothing => true, :status => :not_found
+  rescue OSM::APIError => ex
+    render ex.render_opts
+  end
+
+  ##
+  # insert a (set of) points into a changeset bounding box. this can only
+  # increase the size of the bounding box. this is a hint that clients can
+  # set either before uploading a large number of changes, or changes that
+  # the client (but not the server) knows will affect areas further away.
+  def expand_bbox
+    # only allow POST requests, because although this method is
+    # idempotent, there is no "document" to PUT really...
+    if request.post?
+      cs = Changeset.find(params[:id])
+      check_changeset_consistency(cs, @user)
+
+      # keep an array of lons and lats
+      lon = Array.new
+      lat = Array.new
+
+      # the request is in pseudo-osm format... this is kind-of an
+      # abuse, maybe should change to some other format?
+      doc = XML::Parser.string(request.raw_post).parse
+      doc.find("//osm/node").each do |n|
+        lon << n['lon'].to_f * GeoRecord::SCALE
+        lat << n['lat'].to_f * GeoRecord::SCALE
+      end
+
+      # add the existing bounding box to the lon-lat array
+      lon << cs.min_lon unless cs.min_lon.nil?
+      lat << cs.min_lat unless cs.min_lat.nil?
+      lon << cs.max_lon unless cs.max_lon.nil?
+      lat << cs.max_lat unless cs.max_lat.nil?
+
+      # collapse the arrays to minimum and maximum
+      cs.min_lon, cs.min_lat, cs.max_lon, cs.max_lat = 
+        lon.min, lat.min, lon.max, lat.max
+
+      # save the larger bounding box and return the changeset, which
+      # will include the bigger bounding box.
+      cs.save!
+      render :text => cs.to_xml.to_s, :content_type => "text/xml"
+
+    else
+      render :nothing => true, :status => :method_not_allowed
+    end
+
+  rescue LibXML::XML::Error, ArgumentError => ex
+    raise OSM::APIBadXMLError.new("osm", xml, ex.message)
+  rescue ActiveRecord::RecordNotFound
+    render :nothing => true, :status => :not_found
+  rescue OSM::APIError => ex
+    render ex.render_opts
+  end
+
+  ##
+  # Upload a diff in a single transaction.
+  #
+  # This means that each change within the diff must succeed, i.e: that
+  # each version number mentioned is still current. Otherwise the entire
+  # transaction *must* be rolled back.
+  #
+  # Furthermore, each element in the diff can only reference the current
+  # changeset.
+  #
+  # Returns: a diffResult document, as described in 
+  # http://wiki.openstreetmap.org/index.php/OSM_Protocol_Version_0.6
+  def upload
+    # only allow POST requests, as the upload method is most definitely
+    # not idempotent, as several uploads with placeholder IDs will have
+    # different side-effects.
+    # see http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.1.2
+    unless request.post?
+      render :nothing => true, :status => :method_not_allowed
+      return
+    end
+
+    changeset = Changeset.find(params[:id])
+    check_changeset_consistency(changeset, @user)
+    
+    diff_reader = DiffReader.new(request.raw_post, changeset)
+    Changeset.transaction do
+      result = diff_reader.commit
+      render :text => result.to_s, :content_type => "text/xml"
+    end
+    
+  rescue ActiveRecord::RecordNotFound
+    render :nothing => true, :status => :not_found
+  rescue OSM::APIError => ex
+    render ex.render_opts
+  end
+
+  ##
+  # download the changeset as an osmChange document.
+  #
+  # to make it easier to revert diffs it would be better if the osmChange
+  # format were reversible, i.e: contained both old and new versions of 
+  # modified elements. but it doesn't at the moment...
+  #
+  # this method cannot order the database changes fully (i.e: timestamp and
+  # version number may be too coarse) so the resulting diff may not apply
+  # to a different database. however since changesets are not atomic this 
+  # behaviour cannot be guaranteed anyway and is the result of a design
+  # choice.
+  def download
+    changeset = Changeset.find(params[:id])
+    
+    # get all the elements in the changeset and stick them in a big array.
+    elements = [changeset.old_nodes, 
+                changeset.old_ways, 
+                changeset.old_relations].flatten
+    
+    # sort the elements by timestamp and version number, as this is the 
+    # almost sensible ordering available. this would be much nicer if 
+    # global (SVN-style) versioning were used - then that would be 
+    # unambiguous.
+    elements.sort! do |a, b| 
+      if (a.timestamp == b.timestamp)
+        a.version <=> b.version
+      else
+        a.timestamp <=> b.timestamp 
+      end
+    end
+    
+    # create an osmChange document for the output
+    result = OSM::API.new.get_xml_doc
+    result.root.name = "osmChange"
+
+    # generate an output element for each operation. note: we avoid looking
+    # at the history because it is simpler - but it would be more correct to 
+    # check these assertions.
+    elements.each do |elt|
+      result.root <<
+        if (elt.version == 1) 
+          # first version, so it must be newly-created.
+          created = XML::Node.new "create"
+          created << elt.to_xml_node
+        else
+          # get the previous version from the element history
+          prev_elt = elt.class.find(:first, :conditions => 
+                                    ['id = ? and version = ?',
+                                     elt.id, elt.version])
+          unless elt.visible
+            # if the element isn't visible then it must have been deleted, so
+            # output the *previous* XML
+            deleted = XML::Node.new "delete"
+            deleted << prev_elt.to_xml_node
+          else
+            # must be a modify, for which we don't need the previous version
+            # yet...
+            modified = XML::Node.new "modify"
+            modified << elt.to_xml_node
+          end
+        end
+    end
+
+    render :text => result.to_s, :content_type => "text/xml"
+            
+  rescue ActiveRecord::RecordNotFound
+    render :nothing => true, :status => :not_found
+  rescue OSM::APIError => ex
+    render ex.render_opts
+  end
+
+  ##
+  # query changesets by bounding box, time, user or open/closed status.
+  def query
+    # create the conditions that the user asked for. some or all of
+    # these may be nil.
+    conditions = conditions_bbox(params['bbox'])
+    conditions = cond_merge conditions, conditions_user(params['user'])
+    conditions = cond_merge conditions, conditions_time(params['time'])
+    conditions = cond_merge conditions, conditions_open(params['open'])
+    conditions = cond_merge conditions, conditions_closed(params['closed'])
+
+    # create the results document
+    results = OSM::API.new.get_xml_doc
+
+    # add all matching changesets to the XML results document
+    Changeset.find(:all, 
+                   :conditions => conditions, 
+                   :limit => 100,
+                   :order => 'created_at desc').each do |cs|
+      results.root << cs.to_xml_node
+    end
+
+    render :text => results.to_s, :content_type => "text/xml"
+
+  rescue ActiveRecord::RecordNotFound
+    render :nothing => true, :status => :not_found
+  rescue OSM::APIError => ex
+    render ex.render_opts
+  end
+  
+  ##
+  # updates a changeset's tags. none of the changeset's attributes are
+  # user-modifiable, so they will be ignored.
+  #
+  # changesets are not (yet?) versioned, so we don't have to deal with
+  # history tables here. changesets are locked to a single user, however.
+  #
+  # after succesful update, returns the XML of the changeset.
+  def update
+    # request *must* be a PUT.
+    unless request.put?
+      render :nothing => true, :status => :method_not_allowed
+      return
+    end
+    
+    changeset = Changeset.find(params[:id])
+    new_changeset = Changeset.from_xml(request.raw_post)
+
+    unless new_changeset.nil?
+      check_changeset_consistency(changeset, @user)
+      changeset.update_from(new_changeset, @user)
+      render :text => changeset.to_xml, :mime_type => "text/xml"
+    else
+      
+      render :nothing => true, :status => :bad_request
+    end
+      
+  rescue ActiveRecord::RecordNotFound
+    render :nothing => true, :status => :not_found
+  rescue OSM::APIError => ex
+    render ex.render_opts
+  end
+
+  
+  
+  ##
+  # list edits (open changesets) in reverse chronological order
+  def list
+    conditions = conditions_nonempty
+    
+    
+   # @changesets = Changeset.find(:all, :order => "closed_at DESC", :conditions => ['closed_at < ?', DateTime.now], :limit=> 20)
+   
+   
+   #@edit_pages, @edits = paginate(:changesets,
+   #                                :include => [:user, :changeset_tags],
+   #                                :conditions => conditions,
+   #                                :order => "changesets.created_at DESC",
+   #                                :per_page => 20)
+   #
+    
+   @edits =  Changeset.find(:all,
+                                   :order => "changesets.created_at DESC",
+                                   :conditions => conditions,
+                                   :limit => 20)
+    
+  end
+  
+  ##
+  # list edits (changesets) belonging to a user
+  def list_user
+    user = User.find_by_display_name(params[:display_name], :conditions => {:visible => true})
+    
+    if user
+      @display_name = user.display_name
+      if not user.data_public? and @user != user
+        @edits = nil
+        render
+      else
+        conditions = cond_merge conditions, ['user_id = ?', user.id]
+        conditions = cond_merge conditions, conditions_nonempty
+        @edit_pages, @edits = paginate(:changesets,
+                                        :include => [:user, :changeset_tags],
+                                        :conditions => conditions,
+                                        :order => "changesets.created_at DESC",
+                                        :per_page => 20)
+      end
+    else
+      @not_found_user = params[:display_name]
+      render :template => 'user/no_such_user', :status => :not_found
+    end
+  end
+  
+  ##
+  # list changesets in a bbox
+  def list_bbox
+    # support 'bbox' param or alternatively 'minlon', 'minlat' etc       
+    if params['bbox']
+       bbox = params['bbox']
+    elsif params['minlon'] and params['minlat'] and params['maxlon'] and params['maxlat']
+       bbox = h(params['minlon']) + ',' + h(params['minlat']) + ',' + h(params['maxlon']) + ',' + h(params['maxlat'])
+    else
+      #TODO: fix bugs in location determination for history tab (and other tabs) then uncomment this redirect
+      #redirect_to :action => 'list'
+    end
+       
+    conditions = conditions_bbox(bbox);
+    conditions = cond_merge conditions, conditions_nonempty
+    
+    @edit_pages, @edits = paginate(:changesets,
+                                   :include => [:user, :changeset_tags],
+                                   :conditions => conditions,
+                                   :order => "changesets.created_at DESC",
+                                   :per_page => 20)
+                                   
+    @bbox = sanitise_boundaries(bbox.split(/,/)) unless bbox==nil
+  end
+  
+private
+  #------------------------------------------------------------
+  # utility functions below.
+  #------------------------------------------------------------  
+
+  ##
+  # merge two conditions
+  def cond_merge(a, b)
+    if a and b
+      a_str = a.shift
+      b_str = b.shift
+      return [ a_str + " AND " + b_str ] + a + b
+    elsif a 
+      return a
+    else b
+      return b
+    end
+  end
+
+  ##
+  # if a bounding box was specified then parse it and do some sanity 
+  # checks. this is mostly the same as the map call, but without the 
+  # area restriction.
+  def conditions_bbox(bbox)
+    unless bbox.nil?
+      raise OSM::APIBadUserInput.new("Bounding box should be min_lon,min_lat,max_lon,max_lat") unless bbox.count(',') == 3
+      bbox = sanitise_boundaries(bbox.split(/,/))
+      raise OSM::APIBadUserInput.new("Minimum longitude should be less than maximum.") unless bbox[0] <= bbox[2]
+      raise OSM::APIBadUserInput.new("Minimum latitude should be less than maximum.") unless bbox[1] <= bbox[3]
+      return ['min_lon < ? and max_lon > ? and min_lat < ? and max_lat > ?',
+              bbox[2] * GeoRecord::SCALE, bbox[0] * GeoRecord::SCALE, bbox[3]* GeoRecord::SCALE, bbox[1] * GeoRecord::SCALE]
+    else
+      return nil
+    end
+  end
+
+  ##
+  # restrict changesets to those by a particular user
+  def conditions_user(user)
+    unless user.nil?
+      # user input checking, we don't have any UIDs < 1
+      raise OSM::APIBadUserInput.new("invalid user ID") if user.to_i < 1
+
+      u = User.find(user.to_i)
+      # should be able to get changesets of public users only, or 
+      # our own changesets regardless of public-ness.
+      unless u.data_public?
+        # get optional user auth stuff so that users can see their own
+        # changesets if they're non-public
+        setup_user_auth
+        
+        raise OSM::APINotFoundError if @user.nil? or @user.id != u.id
+      end
+      return ['user_id = ?', u.id]
+    else
+      return nil
+    end
+  end
+
+  ##
+  # restrict changes to those closed during a particular time period
+  def conditions_time(time) 
+    unless time.nil?
+      # if there is a range, i.e: comma separated, then the first is 
+      # low, second is high - same as with bounding boxes.
+      if time.count(',') == 1
+        # check that we actually have 2 elements in the array
+        times = time.split(/,/)
+        raise OSM::APIBadUserInput.new("bad time range") if times.size != 2 
+
+        from, to = times.collect { |t| DateTime.parse(t) }
+        return ['closed_at >= ? and created_at <= ?', from, to]
+      else
+        # if there is no comma, assume its a lower limit on time
+        return ['closed_at >= ?', DateTime.parse(time)]
+      end
+    else
+      return nil
+    end
+    # stupid DateTime seems to throw both of these for bad parsing, so
+    # we have to catch both and ensure the correct code path is taken.
+  rescue ArgumentError => ex
+    raise OSM::APIBadUserInput.new(ex.message.to_s)
+  rescue RuntimeError => ex
+    raise OSM::APIBadUserInput.new(ex.message.to_s)
+  end
+
+  ##
+  # return changesets which are open (haven't been closed yet)
+  # we do this by seeing if the 'closed at' time is in the future. Also if we've
+  # hit the maximum number of changes then it counts as no longer open.
+  # if parameter 'open' is nill then open and closed changsets are returned
+  def conditions_open(open)
+    return open.nil? ? nil : ['closed_at >= ? and num_changes <= ?', 
+                              Time.now.getutc, Changeset::MAX_ELEMENTS]
+  end
+  
+  ##
+  # query changesets which are closed
+  # ('closed at' time has passed or changes limit is hit)
+  def conditions_closed(closed)
+    return closed.nil? ? nil : ['closed_at < ? and num_changes > ?', 
+                                Time.now.getutc, Changeset::MAX_ELEMENTS]
+  end
+
+  ##
+  # eliminate empty changesets (where the bbox has not been set)
+  # this should be applied to all changeset list displays
+  def conditions_nonempty()
+    return ['min_lat IS NOT NULL']
+  end
+  
+end
diff --git a/app/controllers/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 52e2ab22bcae7bb7742be8a98bed12c797220165..3ee36af21a9fb53ac5777344ada62cc4a23bce9e 100644 (file)
@@ -39,6 +39,8 @@ class DiaryEntryController < ApplicationController
         redirect_to :controller => 'diary_entry', :action => 'view', :id => params[:id]
       end
     end
+  rescue ActiveRecord::RecordNotFound
+    render :action => "no_such_entry", :status => :not_found
   end
 
   def comment
@@ -55,7 +57,7 @@ class DiaryEntryController < ApplicationController
 
   def list
     if params[:display_name]
-      @this_user = User.find_by_display_name(params[:display_name], :conditions => "visible = 1")
+      @this_user = User.find_by_display_name(params[:display_name], :conditions => {:visible => true})
 
       if @this_user
         @title = @this_user.display_name + "'s diary"
@@ -71,7 +73,7 @@ class DiaryEntryController < ApplicationController
     else
       @title = "Users' diaries"
       @entry_pages, @entries = paginate(:diary_entries, :include => :user,
-                                        :conditions => "users.visible = 1",
+                                        :conditions => ["users.visible = ?", true],
                                         :order => 'created_at DESC',
                                         :per_page => 20)
     end
@@ -79,13 +81,13 @@ class DiaryEntryController < ApplicationController
 
   def rss
     if params[:display_name]
-      user = User.find_by_display_name(params[:display_name], :conditions => "visible = 1")
+      user = User.find_by_display_name(params[:display_name], :conditions => {:visible => true})
 
       if user
         @entries = DiaryEntry.find(:all, :conditions => ['user_id = ?', user.id], :order => 'created_at DESC', :limit => 20)
         @title = "OpenStreetMap diary entries for #{user.display_name}"
         @description = "Recent OpenStreetmap diary entries from #{user.display_name}"
-        @link = "http://www.openstreetmap.org/user/#{user.display_name}/diary"
+        @link = "http://#{SERVER_URL}/user/#{user.display_name}/diary"
 
         render :content_type => Mime::RSS
       else
@@ -93,21 +95,22 @@ class DiaryEntryController < ApplicationController
       end
     else
       @entries = DiaryEntry.find(:all, :include => :user,
-                                 :conditions => "users.visible = 1",
+                                 :conditions => ["users.visible = ?", true],
                                  :order => 'created_at DESC', :limit => 20)
       @title = "OpenStreetMap diary entries"
       @description = "Recent diary entries from users of OpenStreetMap"
-      @link = "http://www.openstreetmap.org/diary"
+      @link = "http://#{SERVER_URL}/diary"
 
       render :content_type => Mime::RSS
     end
   end
 
   def view
-    user = User.find_by_display_name(params[:display_name], :conditions => "visible = 1")
+    user = User.find_by_display_name(params[:display_name], :conditions => {:visible => true})
 
     if user
       @entry = DiaryEntry.find(:first, :conditions => ['user_id = ? AND id = ?', user.id, params[:id]])
+      @title = "Users' diaries | #{params[:display_name]}"
     else
       @not_found_user = params[:display_name]
 
index a773d4b72d2c177cadad071b0de2a77777f3f8f0..ab25fcbc67a2ed407955953287d3550c99c46812 100644 (file)
@@ -2,18 +2,24 @@ class ExportController < ApplicationController
   def start
   end
 
+  #When the user clicks 'Export' we redirect to a URL which generates the export download
   def finish
     bbox = BoundingBox.new(params[:minlon], params[:minlat], params[:maxlon], params[:maxlat])
     format = params[:format]
 
     if format == "osm"
+      #redirect to API map get
       redirect_to "http://api.openstreetmap.org/api/#{API_VERSION}/map?bbox=#{bbox}"
+
     elsif format == "mapnik"
+      #redirect to a special 'export' cgi script
       format = params[:mapnik_format]
       scale = params[:mapnik_scale]
 
       redirect_to "http://tile.openstreetmap.org/cgi-bin/export?bbox=#{bbox}&scale=#{scale}&format=#{format}"
+
     elsif format == "osmarender"
+      #redirect to the t@h 'MapOf' service
       format = params[:osmarender_format]
       zoom = params[:osmarender_zoom].to_i
       width = bbox.slippy_width(zoom).to_i
index d6e5d7fdadc4b86c326bc06217530fc76a169f48..2f71107f4b39e35720b78193d549a6f716d74e83 100644 (file)
@@ -6,13 +6,18 @@ class MessageController < ApplicationController
   before_filter :check_database_readable
   before_filter :check_database_writable, :only => [:new, :reply, :mark]
 
+  # Allow the user to write a new message to another user. This action also 
+  # deals with the sending of that message to the other user when the user
+  # clicks send.
+  # The user_id param is the id of the user that the message is being sent to.
   def new
     @title = 'send message'
+    @to_user = User.find(params[:user_id])
     if params[:message]
       @message = Message.new(params[:message])
-      @message.to_user_id = params[:user_id]
+      @message.to_user_id = @to_user.id
       @message.from_user_id = @user.id
-      @message.sent_on = Time.now
+      @message.sent_on = Time.now.getutc
    
       if @message.save
         flash[:notice] = 'Message sent'
@@ -22,27 +27,32 @@ class MessageController < ApplicationController
     else
       @title = params[:title]
     end
+  rescue ActiveRecord::RecordNotFound
+    render :action => 'no_such_user', :status => :not_found
   end
 
+  # Allow the user to reply to another message.
   def reply
     message = Message.find(params[:message_id], :conditions => ["to_user_id = ? or from_user_id = ?", @user.id, @user.id ])
     @body = "On #{message.sent_on} #{message.sender.display_name} wrote:\n\n#{message.body.gsub(/^/, '> ')}" 
     @title = "Re: #{message.title.sub(/^Re:\s*/, '')}"
-    @user_id = message.from_user_id
+    @to_user = User.find(message.from_user_id)
     render :action => 'new'
   rescue ActiveRecord::RecordNotFound
-    render :nothing => true, :status => :not_found
+    render :action => 'no_such_user', :status => :not_found
   end
 
+  # Show a message
   def read
     @title = 'read message'
     @message = Message.find(params[:message_id], :conditions => ["to_user_id = ? or from_user_id = ?", @user.id, @user.id ])
-    @message.message_read = 1 if @message.to_user_id == @user.id
+    @message.message_read = true if @message.to_user_id == @user.id
     @message.save
   rescue ActiveRecord::RecordNotFound
-    render :nothing => true, :status => :not_found
+    render :action => 'no_such_user', :status => :not_found
   end
 
+  # Display the list of messages that have been sent to the user.
   def inbox
     @title = 'inbox'
     if @user and params[:display_name] == @user.display_name
@@ -51,6 +61,7 @@ class MessageController < ApplicationController
     end
   end
 
+  # Display the list of messages that the user has sent to other users.
   def outbox
     @title = 'outbox'
     if @user and params[:display_name] == @user.display_name
@@ -59,15 +70,16 @@ class MessageController < ApplicationController
     end
   end
 
+  # Set the message as being read or unread.
   def mark
     if params[:message_id]
       id = params[:message_id]
       message = Message.find_by_id(id)
       if params[:mark] == 'unread'
-        message_read = 0 
+        message_read = false 
         mark_type = 'unread'
       else
-        message_read = 1
+        message_read = true
         mark_type = 'read'
       end
       message.message_read = message_read
@@ -76,5 +88,7 @@ class MessageController < ApplicationController
         redirect_to :controller => 'message', :action => 'inbox', :display_name => @user.display_name
       end
     end
+  rescue ActiveRecord::RecordNotFound
+    render :action => 'no_such_user', :status => :not_found
   end
 end
index 6ce18f5662b51fb3fe1bf2b7caeb8a49484bc6b5..6e96d31ca4cac18a5a9130369eade7b402056083 100644 (file)
@@ -5,26 +5,28 @@ class NodeController < ApplicationController
 
   session :off
   before_filter :authorize, :only => [:create, :update, :delete]
+  before_filter :require_public_data, :only => [:create, :update, :delete]
   before_filter :check_api_writable, :only => [:create, :update, :delete]
   before_filter :check_api_readable, :except => [:create, :update, :delete]
   after_filter :compress_output
 
   # Create a node from XML.
   def create
-    if request.put?
-      node = Node.from_xml(request.raw_post, true)
-
-      if node
-        node.user_id = @user.id
-        node.visible = true
-        node.save_with_history!
+    begin
+      if request.put?
+        node = Node.from_xml(request.raw_post, true)
 
-        render :text => node.id.to_s, :content_type => "text/plain"
+        if node
+          node.create_with_history @user
+          render :text => node.id.to_s, :content_type => "text/plain"
+        else
+          render :nothing => true, :status => :bad_request
+        end
       else
-        render :nothing => true, :status => :bad_request
+        render :nothing => true, :status => :method_not_allowed
       end
-    else
-      render :nothing => true, :status => :method_not_allowed
+    rescue OSM::APIError => ex
+      render ex.render_opts
     end
   end
 
@@ -32,7 +34,7 @@ class NodeController < ApplicationController
   def read
     begin
       node = Node.find(params[:id])
-      if node.visible
+      if node.visible?
         response.headers['Last-Modified'] = node.timestamp.rfc822
         render :text => node.to_xml.to_s, :content_type => "text/xml"
        else
@@ -42,7 +44,7 @@ class NodeController < ApplicationController
       render :nothing => true, :status => :not_found
     end
   end
-
+  
   # Update a node from given XML
   def update
     begin
@@ -50,49 +52,40 @@ class NodeController < ApplicationController
       new_node = Node.from_xml(request.raw_post)
 
       if new_node and new_node.id == node.id
-        node.user_id = @user.id
-        node.latitude = new_node.latitude 
-        node.longitude = new_node.longitude
-        node.tags = new_node.tags
-        node.visible = true
-        node.save_with_history!
-
-        render :nothing => true
+        node.update_from(new_node, @user)
+        render :text => node.version.to_s, :content_type => "text/plain"
       else
         render :nothing => true, :status => :bad_request
       end
+    rescue OSM::APIError => ex
+      render ex.render_opts
     rescue ActiveRecord::RecordNotFound
       render :nothing => true, :status => :not_found
     end
   end
 
-  # Delete a node. Doesn't actually delete it, but retains its history in a wiki-like way.
-  # FIXME remove all the fricking SQL
+  # Delete a node. Doesn't actually delete it, but retains its history 
+  # in a wiki-like way. We therefore treat it like an update, so the delete
+  # method returns the new version number.
   def delete
     begin
       node = Node.find(params[:id])
-
-      if node.visible
-        if WayNode.find(:first, :joins => "INNER JOIN current_ways ON current_ways.id = current_way_nodes.id", :conditions => [ "current_ways.visible = 1 AND current_way_nodes.node_id = ?", node.id ])
-          render :text => "", :status => :precondition_failed
-        elsif RelationMember.find(:first, :joins => "INNER JOIN current_relations ON current_relations.id=current_relation_members.id", :conditions => [ "visible = 1 AND member_type='node' and member_id=?", params[:id]])
-          render :text => "", :status => :precondition_failed
-        else
-          node.user_id = @user.id
-          node.visible = 0
-          node.save_with_history!
-
-          render :nothing => true
-        end
+      new_node = Node.from_xml(request.raw_post)
+      
+      if new_node and new_node.id == node.id
+        node.delete_with_history!(new_node, @user)
+        render :text => node.version.to_s, :content_type => "text/plain"
       else
-        render :text => "", :status => :gone
+        render :nothing => true, :status => :bad_request
       end
     rescue ActiveRecord::RecordNotFound
       render :nothing => true, :status => :not_found
+    rescue OSM::APIError => ex
+      render ex.render_opts
     end
   end
 
-  # WTF does this do?
+  # Dump the details on many nodes whose ids are given in the "nodes" parameter.
   def nodes
     ids = params['nodes'].split(',').collect { |n| n.to_i }
 
index 40f4093e3d99171105c3a0a532e551cbb7a1222c..0976a0c9aae87c679bd254b028b45830f9a47077 100644 (file)
@@ -22,4 +22,21 @@ class OldNodeController < ApplicationController
       render :nothing => true, :status => :internal_server_error
     end
   end
+  
+  def version
+    begin
+      old_node = OldNode.find(:first, :conditions => {:id => params[:id], :version => params[:version]} )
+      
+      response.headers['Last-Modified'] = old_node.timestamp.rfc822
+
+      doc = OSM::API.new.get_xml_doc
+      doc.root << old_node.to_xml_node
+
+      render :text => doc.to_s, :content_type => "text/xml"
+    rescue ActiveRecord::RecordNotFound
+      render :nothing => true, :status => :not_found
+    rescue
+      render :nothing => true, :status => :internal_server_error
+    end
+  end
 end
index 0b5aa89be885f18c03cf83c46097778efc839610..f8ebbd070be3609901aeed5c7cc0cf60dc441763 100644 (file)
@@ -2,6 +2,7 @@ class OldRelationController < ApplicationController
   require 'xml/libxml'
 
   session :off
+  before_filter :check_api_readable
   after_filter :compress_output
 
   def history
@@ -20,4 +21,21 @@ class OldRelationController < ApplicationController
       render :nothing => true, :status => :internal_server_error
     end
   end
+  
+  def version
+    begin
+      old_relation = OldRelation.find(:first, :conditions => {:id => params[:id], :version => params[:version]} )
+      
+      response.headers['Last-Modified'] = old_relation.timestamp.rfc822
+
+      doc = OSM::API.new.get_xml_doc
+      doc.root << old_relation.to_xml_node
+
+      render :text => doc.to_s, :content_type => "text/xml"
+    rescue ActiveRecord::RecordNotFound
+      render :nothing => true, :status => :not_found
+    rescue
+      render :nothing => true, :status => :internetal_service_error
+    end
+  end
 end
index 3d913c0f7660ce49b86a251bf3a19ccc4918d229..a42496687311d255c3d9c391634f9b6fe55cf728 100644 (file)
@@ -13,7 +13,7 @@ class OldWayController < ApplicationController
 
       way.old_ways.each do |old_way|
         doc.root << old_way.to_xml_node
-     end
+      end
 
       render :text => doc.to_s, :content_type => "text/xml"
     rescue ActiveRecord::RecordNotFound
@@ -22,4 +22,21 @@ class OldWayController < ApplicationController
       render :nothing => true, :status => :internal_server_error
     end
   end
+  
+  def version
+    begin
+      old_way = OldWay.find(:first, :conditions => {:id => params[:id], :version => params[:version]} )
+      
+      response.headers['Last-Modified'] = old_way.timestamp.rfc822
+      
+      doc = OSM::API.new.get_xml_doc
+      doc.root << old_way.to_xml_node
+      
+      render :text => doc.to_s, :content_type => "text/xml"
+    rescue ActiveRecord::RecordNotFound
+      render :nothing => true, :status => :not_found
+    rescue
+      render :nothing => true, :status => :internal_server_error
+    end
+  end
 end
index 45724916229594526ea3c8b84385554a1d11a5b9..a249e42a02f9b1b4142b2993e0abef9e3c0fe765 100644 (file)
@@ -3,28 +3,29 @@ class RelationController < ApplicationController
 
   session :off
   before_filter :authorize, :only => [:create, :update, :delete]
+  before_filter :require_public_data, :only => [:create, :update, :delete]
   before_filter :check_api_writable, :only => [:create, :update, :delete]
   before_filter :check_api_readable, :except => [:create, :update, :delete]
   after_filter :compress_output
 
   def create
-    if request.put?
-      relation = Relation.from_xml(request.raw_post, true)
-
-      if relation
-        if !relation.preconditions_ok?
-          render :text => "", :status => :precondition_failed
-        else
-          relation.user_id = @user.id
-          relation.save_with_history!
-
-         render :text => relation.id.to_s, :content_type => "text/plain"
-        end
+    begin
+      if request.put?
+        relation = Relation.from_xml(request.raw_post, true)
+
+        # We assume that an exception has been thrown if there was an error 
+        # generating the relation
+        #if relation
+          relation.create_with_history @user
+          render :text => relation.id.to_s, :content_type => "text/plain"
+        #else
+         # render :text => "Couldn't get turn the input into a relation.", :status => :bad_request
+        #end
       else
-        render :nothing => true, :status => :bad_request
+        render :nothing => true, :status => :method_not_allowed
       end
-    else
-      render :nothing => true, :status => :method_not_allowed
+    rescue OSM::APIError => ex
+      render ex.render_opts
     end
   end
 
@@ -45,56 +46,38 @@ class RelationController < ApplicationController
   end
 
   def update
+    logger.debug request.raw_post
     begin
       relation = Relation.find(params[:id])
       new_relation = Relation.from_xml(request.raw_post)
 
       if new_relation and new_relation.id == relation.id
-        if !new_relation.preconditions_ok?
-          render :text => "", :status => :precondition_failed
-        else
-          relation.user_id = @user.id
-          relation.tags = new_relation.tags
-          relation.members = new_relation.members
-          relation.visible = true
-          relation.save_with_history!
-
-          render :nothing => true
-        end
+        relation.update_from new_relation, @user
+        render :text => relation.version.to_s, :content_type => "text/plain"
       else
         render :nothing => true, :status => :bad_request
       end
     rescue ActiveRecord::RecordNotFound
       render :nothing => true, :status => :not_found
-    rescue
-      render :nothing => true, :status => :internal_server_error
+    rescue OSM::APIError => ex
+      render ex.render_opts
     end
   end
 
   def delete
-#XXX check if member somewhere!
     begin
       relation = Relation.find(params[:id])
-
-      if relation.visible
-        if RelationMember.find(:first, :joins => "INNER JOIN current_relations ON current_relations.id=current_relation_members.id", :conditions => [ "visible = 1 AND member_type='relation' and member_id=?", params[:id]])
-          render :text => "", :status => :precondition_failed
-        else
-          relation.user_id = @user.id
-          relation.tags = []
-          relation.members = []
-          relation.visible = false
-          relation.save_with_history!
-
-          render :nothing => true
-        end
+      new_relation = Relation.from_xml(request.raw_post)
+      if new_relation and new_relation.id == relation.id
+        relation.delete_with_history!(new_relation, @user)
+        render :text => relation.version.to_s, :content_type => "text/plain"
       else
-        render :text => "", :status => :gone
+        render :nothing => true, :status => :bad_request
       end
+    rescue OSM::APIError => ex
+      render ex.render_opts
     rescue ActiveRecord::RecordNotFound
       render :nothing => true, :status => :not_found
-    rescue
-      render :nothing => true, :status => :internal_server_error
     end
   end
 
@@ -115,12 +98,12 @@ class RelationController < ApplicationController
         # first collect nodes, ways, and relations referenced by this relation.
         
         ways = Way.find_by_sql("select w.* from current_ways w,current_relation_members rm where "+
-            "rm.member_type='way' and rm.member_id=w.id and rm.id=#{relation.id}");
+            "rm.member_type='Way' and rm.member_id=w.id and rm.id=#{relation.id}");
         nodes = Node.find_by_sql("select n.* from current_nodes n,current_relation_members rm where "+
-            "rm.member_type='node' and rm.member_id=n.id and rm.id=#{relation.id}");
+            "rm.member_type='Node' and rm.member_id=n.id and rm.id=#{relation.id}");
         # note query is built to exclude self just in case.
         relations = Relation.find_by_sql("select r.* from current_relations r,current_relation_members rm where "+
-            "rm.member_type='relation' and rm.member_id=r.id and rm.id=#{relation.id} and r.id<>rm.id");
+            "rm.member_type='Relation' and rm.member_id=r.id and rm.id=#{relation.id} and r.id<>rm.id");
 
         # now additionally collect nodes referenced by ways. Note how we recursively 
         # evaluate ways but NOT relations.
@@ -160,8 +143,7 @@ class RelationController < ApplicationController
         render :text => doc.to_s, :content_type => "text/xml"
 
       else
-
-        render :text => "", :status => :gone
+        render :nothing => true, :status => :gone
       end
 
     rescue ActiveRecord::RecordNotFound
@@ -184,27 +166,29 @@ class RelationController < ApplicationController
 
       render :text => doc.to_s, :content_type => "text/xml"
     else
-      render :nothing => true, :status => :bad_request
+      render :text => "You need to supply a comma separated list of ids.", :status => :bad_request
     end
+  rescue ActiveRecord::RecordNotFound
+    render :text => "Could not find one of the relations", :status => :not_found
   end
 
   def relations_for_way
-    relations_for_object("way")
+    relations_for_object("Way")
   end
   def relations_for_node
-    relations_for_object("node")
+    relations_for_object("Node")
   end
   def relations_for_relation
-    relations_for_object("relation")
+    relations_for_object("Relation")
   end
 
   def relations_for_object(objtype)
-    relationids = RelationMember.find(:all, :conditions => ['member_type=? and member_id=?', objtype, params[:id]]).collect { |ws| ws.id }.uniq
+    relationids = RelationMember.find(:all, :conditions => ['member_type=? and member_id=?', objtype, params[:id]]).collect { |ws| ws.id[0] }.uniq
 
     doc = OSM::API.new.get_xml_doc
 
     Relation.find(relationids).each do |relation|
-      doc.root << relation.to_xml_node
+      doc.root << relation.to_xml_node if relation.visible
     end
 
     render :text => doc.to_s, :content_type => "text/xml"
index 3942cb7fe3d8964eb80eb7733eacee0f9c490de3..dc7456c45099ec923731ccd43944be57001bd459 100644 (file)
@@ -1,8 +1,8 @@
 class TraceController < ApplicationController
   layout 'site'
 
-  before_filter :authorize_web  
-  before_filter :require_user, :only => [:mine, :edit, :delete, :make_public]
+  before_filter :authorize_web
+  before_filter :require_user, :only => [:mine, :create, :edit, :delete, :make_public]
   before_filter :authorize, :only => [:api_details, :api_data, :api_create]
   before_filter :check_database_readable, :except => [:api_details, :api_data, :api_create]
   before_filter :check_database_writable, :only => [:create, :edit, :delete, :make_public]
@@ -15,7 +15,12 @@ class TraceController < ApplicationController
     # from display name, pick up user id if one user's traces only
     display_name = params[:display_name]
     if target_user.nil? and !display_name.blank?
-      target_user = User.find(:first, :conditions => [ "visible = 1 and display_name = ?", display_name])
+      target_user = User.find(:first, :conditions => [ "visible = ? and display_name = ?", true, display_name])
+      if target_user.nil?
+        @not_found_user = display_name
+        render :action => 'no_such_user', :status => :not_found
+        return
+      end
     end
 
     # set title
@@ -36,15 +41,15 @@ class TraceController < ApplicationController
     # 4 - user's traces, not logged in as that user = all user's public traces
     if target_user.nil? # all traces
       if @user
-        conditions = ["(gpx_files.public = 1 OR gpx_files.user_id = ?)", @user.id] #1
+        conditions = ["(gpx_files.public = ? OR gpx_files.user_id = ?)", true, @user.id] #1
       else
-        conditions  = ["gpx_files.public = 1"] #2
+        conditions  = ["gpx_files.public = ?", true] #2
       end
     else
       if @user and @user == target_user
         conditions = ["gpx_files.user_id = ?", @user.id] #3 (check vs user id, so no join + can't pick up non-public traces by changing name)
       else
-        conditions = ["gpx_files.public = 1 AND gpx_files.user_id = ?", target_user.id] #4
+        conditions = ["gpx_files.public = ? AND gpx_files.user_id = ?", true, target_user.id] #4
       end
     end
     
@@ -55,7 +60,8 @@ class TraceController < ApplicationController
       conditions[0] += " AND gpx_files.id IN (#{files.join(',')})"
     end
     
-    conditions[0] += " AND gpx_files.visible = 1"
+    conditions[0] += " AND gpx_files.visible = ?"
+    conditions << true
 
     @trace_pages, @traces = paginate(:traces,
                                      :include => [:user, :tags],
@@ -100,26 +106,28 @@ class TraceController < ApplicationController
   end
 
   def create
-    logger.info(params[:trace][:gpx_file].class.name)
-    if params[:trace][:gpx_file].respond_to?(:read)
-      do_create(params[:trace][:gpx_file], params[:trace][:tagstring],
-                params[:trace][:description], params[:trace][:public])
+    if params[:trace]
+      logger.info(params[:trace][:gpx_file].class.name)
+      if params[:trace][:gpx_file].respond_to?(:read)
+        do_create(params[:trace][:gpx_file], params[:trace][:tagstring],
+                  params[:trace][:description], params[:trace][:public])
 
-      if @trace.id
-        logger.info("id is #{@trace.id}")
-        flash[:notice] = "Your GPX file has been uploaded and is awaiting insertion in to the database. This will usually happen within half an hour, and an email will be sent to you on completion."
+        if @trace.id
+          logger.info("id is #{@trace.id}")
+          flash[:notice] = "Your GPX file has been uploaded and is awaiting insertion in to the database. This will usually happen within half an hour, and an email will be sent to you on completion."
 
-        redirect_to :action => 'mine'
+          redirect_to :action => 'mine'
+        end
+      else
+        @trace = Trace.new({:name => "Dummy",
+                            :tagstring => params[:trace][:tagstring],
+                            :description => params[:trace][:description],
+                            :public => params[:trace][:public],
+                            :inserted => false, :user => @user,
+                            :timestamp => Time.now.getutc})
+        @trace.valid?
+        @trace.errors.add(:gpx_file, "can't be blank")
       end
-    else
-      @trace = Trace.new({:name => "Dummy",
-                          :tagstring => params[:trace][:tagstring],
-                          :description => params[:trace][:description],
-                          :public => params[:trace][:public],
-                          :inserted => false, :user => @user,
-                          :timestamp => Time.now})
-      @trace.valid?
-      @trace.errors.add(:gpx_file, "can't be blank")
     end
   end
 
@@ -196,7 +204,7 @@ class TraceController < ApplicationController
   end
 
   def georss
-    conditions = ["gpx_files.public = 1"]
+    conditions = ["gpx_files.public = ?", true]
 
     if params[:display_name]
       conditions[0] += " AND users.display_name = ?"
@@ -278,12 +286,20 @@ class TraceController < ApplicationController
 
   def api_create
     if request.post?
-      do_create(params[:file], params[:tags], params[:description], params[:public])
-
-      if @trace.id
-        render :text => @trace.id.to_s, :content_type => "text/plain"
-      elsif @trace.valid?
-        render :nothing => true, :status => :internal_server_error
+      tags = params[:tags] || ""
+      description = params[:description] || ""
+      pub = params[:public] || false
+      
+      if params[:file].respond_to?(:read)
+        do_create(params[:file], tags, description, pub)
+
+        if @trace.id
+          render :text => @trace.id.to_s, :content_type => "text/plain"
+        elsif @trace.valid?
+          render :nothing => true, :status => :internal_server_error
+        else
+          render :nothing => true, :status => :bad_request
+        end
       else
         render :nothing => true, :status => :bad_request
       end
@@ -313,7 +329,7 @@ private
       :public => public,
       :inserted => true,
       :user => @user,
-      :timestamp => Time.now
+      :timestamp => Time.now.getutc
     })
 
     # Save the trace object
@@ -328,6 +344,17 @@ private
       # Remove the file as we have failed to update the database
       FileUtils.rm_f(filename)
     end
+    
+    # Finally save whether the user marked the trace as being public
+    if @trace.public?
+      if @user.trace_public_default.nil?
+        @user.preferences.create(:k => "gps.trace.public", :v => "default")
+      end
+    else
+      pref = @user.trace_public_default
+      pref.destroy unless pref.nil?
+    end
+    
   end
 
 end
index 1cd34900a19835b6553e29b676a9c5c61a9ef38f..9544dd8a888c29aa5caf9990360398451b302dda 100644 (file)
@@ -83,7 +83,7 @@ class UserController < ApplicationController
   def lost_password
     @title = 'lost password'
     if params[:user] and params[:user][:email]
-      user = User.find_by_email(params[:user][:email], :conditions => "visible = 1")
+      user = User.find_by_email(params[:user][:email], :conditions => {:visible => true})
 
       if user
         token = user.tokens.create
@@ -120,9 +120,21 @@ class UserController < ApplicationController
 
   def new
     @title = 'create account'
+    # The user is logged in already, so don't show them the signup page, instead
+    # send them to the home page
+    redirect_to :controller => 'site', :action => 'index' if session[:user]
   end
 
   def login
+    if session[:user]
+      # The user is logged in already, if the referer param exists, redirect them to that
+      if params[:referer]
+        redirect_to params[:referer]
+      else
+        redirect_to :controller => 'site', :action => 'index'
+      end
+      return
+    end
     @title = 'login'
     if params[:user]
       email_or_display_name = params[:user][:email]
@@ -223,7 +235,7 @@ class UserController < ApplicationController
   end
 
   def view
-    @this_user = User.find_by_display_name(params[:display_name], :conditions => "visible = 1")
+    @this_user = User.find_by_display_name(params[:display_name], :conditions => {:visible => true})
 
     if @this_user
       @title = @this_user.display_name
@@ -236,7 +248,7 @@ class UserController < ApplicationController
   def make_friend
     if params[:display_name]     
       name = params[:display_name]
-      new_friend = User.find_by_display_name(name, :conditions => "visible = 1")
+      new_friend = User.find_by_display_name(name, :conditions => {:visible => true})
       friend = Friend.new
       friend.user_id = @user.id
       friend.friend_user_id = new_friend.id
@@ -258,7 +270,7 @@ class UserController < ApplicationController
   def remove_friend
     if params[:display_name]     
       name = params[:display_name]
-      friend = User.find_by_display_name(name, :conditions => "visible = 1")
+      friend = User.find_by_display_name(name, :conditions => {:visible => true})
       if @user.is_friends_with?(friend)
         Friend.delete_all "user_id = #{@user.id} AND friend_user_id = #{friend.id}"
         flash[:notice] = "#{friend.display_name} was removed from your friends."
index 3a48ee65e85667222c1e2994f93efa18bd16363c..59573047acc3591e22621f1fb041b99abe3f95d9 100644 (file)
@@ -5,11 +5,9 @@ class UserPreferenceController < ApplicationController
   def read_one
     pref = UserPreference.find(@user.id, params[:preference_key])
 
-    if pref
-      render :text => pref.v.to_s
-    else
-      render :text => 'OH NOES! PREF NOT FOUND!', :status => 404
-    end
+    render :text => pref.v.to_s
+  rescue ActiveRecord::RecordNotFound => ex
+    render :text => 'OH NOES! PREF NOT FOUND!', :status => :not_found
   end
 
   def update_one
@@ -32,6 +30,8 @@ class UserPreferenceController < ApplicationController
     UserPreference.delete(@user.id, params[:preference_key])
 
     render :nothing => true
+  rescue ActiveRecord::RecordNotFound => ex
+    render :text => "param: #{params[:preference_key]} not found", :status => :not_found
   end
 
   # print out all the preferences as a big xml block
@@ -54,46 +54,44 @@ class UserPreferenceController < ApplicationController
   def update
     begin
       p = XML::Parser.string(request.raw_post)
-      doc = p.parse
-
-      prefs = []
-
-      keyhash = {}
-
-      doc.find('//preferences/preference').each do |pt|
-        pref = UserPreference.new
+    rescue LibXML::XML::Error, ArgumentError => ex
+      raise OSM::APIBadXMLError.new("preferences", xml, ex.message)
+    end
+    doc = p.parse
 
-        unless keyhash[pt['k']].nil? # already have that key
-          render :text => 'OH NOES! CAN HAS UNIQUE KEYS?', :status => :not_acceptable
-          return
-        end
+    prefs = []
 
-        keyhash[pt['k']] = 1
+    keyhash = {}
 
-        pref.k = pt['k']
-        pref.v = pt['v']
-        pref.user_id = @user.id
-        prefs << pref
-      end
+    doc.find('//preferences/preference').each do |pt|
+      pref = UserPreference.new
 
-      if prefs.size > 150
-        render :text => 'Too many preferences', :status => :request_entity_too_large
-        return
+      unless keyhash[pt['k']].nil? # already have that key
+        render :text => 'OH NOES! CAN HAS UNIQUE KEYS?', :status => :not_acceptable
       end
 
-      # kill the existing ones
-      UserPreference.delete_all(['user_id = ?', @user.id])
+      keyhash[pt['k']] = 1
 
-      # save the new ones
-      prefs.each do |pref|
-        pref.save!
-      end
+      pref.k = pt['k']
+      pref.v = pt['v']
+      pref.user_id = @user.id
+      prefs << pref
+    end
 
-    rescue Exception => ex
-      render :text => 'OH NOES! FAIL!: ' + ex.to_s, :status => :internal_server_error
-      return
+    if prefs.size > 150
+      render :text => 'Too many preferences', :status => :request_entity_too_large
     end
 
+    # kill the existing ones
+    UserPreference.delete_all(['user_id = ?', @user.id])
+
+    # save the new ones
+    prefs.each do |pref|
+      pref.save!
+    end
     render :nothing => true
+
+  rescue Exception => ex
+    render :text => 'OH NOES! FAIL!: ' + ex.to_s, :status => :internal_server_error
   end
 end
index 9b93ec2b3689b2cc375cf07f9cdd1cc1fd80d44a..ab83d4ec3cbb1a2ea334e0e8b9936450a60972f3 100644 (file)
@@ -3,28 +3,28 @@ class WayController < ApplicationController
 
   session :off
   before_filter :authorize, :only => [:create, :update, :delete]
+  before_filter :require_public_data, :only => [:create, :update, :delete]
   before_filter :check_api_writable, :only => [:create, :update, :delete]
   before_filter :check_api_readable, :except => [:create, :update, :delete]
   after_filter :compress_output
 
   def create
-    if request.put?
-      way = Way.from_xml(request.raw_post, true)
-
-      if way
-        if !way.preconditions_ok?
-          render :text => "", :status => :precondition_failed
-        else
-          way.user_id = @user.id
-          way.save_with_history!
+    begin
+      if request.put?
+        way = Way.from_xml(request.raw_post, true)
 
+        if way
+          way.create_with_history @user
           render :text => way.id.to_s, :content_type => "text/plain"
+        else
+          render :nothing => true, :status => :bad_request
         end
       else
-        render :nothing => true, :status => :bad_request
+        render :nothing => true, :status => :method_not_allowed
       end
-    else
-      render :nothing => true, :status => :method_not_allowed
+    rescue OSM::APIError => ex
+      logger.warn request.raw_post
+      render ex.render_opts
     end
   end
 
@@ -39,6 +39,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 +52,14 @@ class WayController < ApplicationController
       new_way = Way.from_xml(request.raw_post)
 
       if new_way and new_way.id == way.id
-        if !new_way.preconditions_ok?
-          render :text => "", :status => :precondition_failed
-        else
-          way.user_id = @user.id
-          way.tags = new_way.tags
-          way.nds = new_way.nds
-          way.visible = true
-          way.save_with_history!
-
-          render :nothing => true
-        end
+        way.update_from(new_way, @user)
+        render :text => way.version.to_s, :content_type => "text/plain"
       else
         render :nothing => true, :status => :bad_request
       end
+    rescue OSM::APIError => ex
+      logger.warn request.raw_post
+      render ex.render_opts
     rescue ActiveRecord::RecordNotFound
       render :nothing => true, :status => :not_found
     end
@@ -73,14 +69,16 @@ class WayController < ApplicationController
   def delete
     begin
       way = Way.find(params[:id])
-      way.delete_with_relations_and_history(@user)
-
-      # if we get here, all is fine, otherwise something will catch below.  
-      render :nothing => true
-    rescue OSM::APIAlreadyDeletedError
-      render :text => "", :status => :gone
-    rescue OSM::APIPreconditionFailedError
-      render :text => "", :status => :precondition_failed
+      new_way = Way.from_xml(request.raw_post)
+
+      if new_way and new_way.id == way.id
+        way.delete_with_history!(new_way, @user)
+        render :text => way.version.to_s, :content_type => "text/plain"
+      else
+        render :nothing => true, :status => :bad_request
+      end
+    rescue OSM::APIError => ex
+      render ex.render_opts
     rescue ActiveRecord::RecordNotFound
       render :nothing => true, :status => :not_found
     end
@@ -92,7 +90,7 @@ class WayController < ApplicationController
 
       if way.visible
         nd_ids = way.nds + [-1]
-        nodes = Node.find(:all, :conditions => "visible = 1 AND id IN (#{nd_ids.join(',')})")
+        nodes = Node.find(:all, :conditions => ["visible = ? AND id IN (#{nd_ids.join(',')})", true])
 
         # Render
         doc = OSM::API.new.get_xml_doc
@@ -130,13 +128,19 @@ class WayController < ApplicationController
     end
   end
 
+  ##
+  # returns all the ways which are currently using the node given in the 
+  # :id parameter. note that this used to return deleted ways as well, but
+  # this seemed not to be the expected behaviour, so it was removed.
   def ways_for_node
-    wayids = WayNode.find(:all, :conditions => ['node_id = ?', params[:id]]).collect { |ws| ws.id[0] }.uniq
+    wayids = WayNode.find(:all, 
+                          :conditions => ['node_id = ?', params[:id]]
+                          ).collect { |ws| ws.id[0] }.uniq
 
     doc = OSM::API.new.get_xml_doc
 
     Way.find(wayids).each do |way|
-      doc.root << way.to_xml_node
+      doc.root << way.to_xml_node if way.visible
     end
 
     render :text => doc.to_s, :content_type => "text/xml"
index c86ad5b71c0701bd1b3f65a7ce7381b4afcc74ad..34302a8af2d7f8af01c963d1b7ba1b2059216742 100644 (file)
@@ -1,2 +1,5 @@
 module BrowseHelper
+  def link_to_page(page, page_param)
+    return link_to(page, page_param => page)
+  end
 end
index 5fb99b9e5bfd7bbfeb0f93a3c64da86ee19846f9..3ff19d35f6d8374bba709485641b3080ae62d595 100644 (file)
@@ -1,13 +1,23 @@
 class Acl < ActiveRecord::Base
   def self.find_by_address(address, options)
-    self.with_scope(:find => {:conditions => ["inet_aton(?) & netmask = address", address]}) do
+    self.with_scope(:find => {:conditions => ["#{inet_aton} & netmask = address", address]}) do
       return self.find(:first, options)
     end
   end
 
   def self.find_all_by_address(address, options)
-    self.with_scope(:find => {:conditions => ["inet_aton(?) & netmask = address", address]}) do
+    self.with_scope(:find => {:conditions => ["#{inet_aton} & netmask = address", address]}) do
       return self.find(:all, options)
     end
   end
+
+private
+
+  def self.inet_aton
+    if self.connection.adapter_name == "MySQL"
+      "inet_aton(?)"
+    else
+      "?"
+    end
+  end
 end
diff --git a/app/models/changeset.rb b/app/models/changeset.rb
new file mode 100644 (file)
index 0000000..fa2d556
--- /dev/null
@@ -0,0 +1,241 @@
+class Changeset < ActiveRecord::Base
+  require 'xml/libxml'
+
+  belongs_to :user
+
+  has_many :changeset_tags, :foreign_key => 'id'
+  
+  has_many :nodes
+  has_many :ways
+  has_many :relations
+  has_many :old_nodes
+  has_many :old_ways
+  has_many :old_relations
+  
+  validates_presence_of :id, :on => :update
+  validates_presence_of :user_id, :created_at, :closed_at, :num_changes
+  validates_uniqueness_of :id
+  validates_numericality_of :id, :on => :update, :integer_only => true
+  validates_numericality_of :min_lat, :max_lat, :min_lon, :max_lat, :allow_nil => true, :integer_only => true
+  validates_numericality_of :user_id,  :integer_only => true
+  validates_numericality_of :num_changes, :integer_only => true, :greater_than_or_equal_to => 0
+  validates_associated :user
+
+  # over-expansion factor to use when updating the bounding box
+  EXPAND = 0.1
+
+  # maximum number of elements allowed in a changeset
+  MAX_ELEMENTS = 50000
+
+  # maximum time a changeset is allowed to be open for.
+  MAX_TIME_OPEN = 1.day
+
+  # idle timeout increment, one hour seems reasonable.
+  IDLE_TIMEOUT = 1.hour
+
+  # Use a method like this, so that we can easily change how we
+  # determine whether a changeset is open, without breaking code in at 
+  # least 6 controllers
+  def is_open?
+    # a changeset is open (that is, it will accept further changes) when
+    # it has not yet run out of time and its capacity is small enough.
+    # note that this may not be a hard limit - due to timing changes and
+    # concurrency it is possible that some changesets may be slightly 
+    # longer than strictly allowed or have slightly more changes in them.
+    return ((closed_at > Time.now.getutc) and (num_changes <= MAX_ELEMENTS))
+  end
+
+  def set_closed_time_now
+    if is_open?
+      self.closed_at = Time.now.getutc
+    end
+  end
+  
+  def self.from_xml(xml, create=false)
+    begin
+      p = XML::Parser.string(xml)
+      doc = p.parse
+
+      cs = Changeset.new
+
+      doc.find('//osm/changeset').each do |pt|
+        if create
+          cs.created_at = Time.now.getutc
+          # initial close time is 1h ahead, but will be increased on each
+          # modification.
+          cs.closed_at = cs.created_at + IDLE_TIMEOUT
+          # initially we have no changes in a changeset
+          cs.num_changes = 0
+        end
+
+        pt.find('tag').each do |tag|
+          cs.add_tag_keyval(tag['k'], tag['v'])
+        end
+      end
+    rescue Exception => ex
+      cs = nil
+    end
+
+    return cs
+  end
+
+  ##
+  # returns the bounding box of the changeset. it is possible that some
+  # or all of the values will be nil, indicating that they are undefined.
+  def bbox
+    @bbox ||= [ min_lon, min_lat, max_lon, max_lat ]
+  end
+  
+  def has_valid_bbox?
+    not bbox.include? nil
+  end
+
+  ##
+  # expand the bounding box to include the given bounding box. also, 
+  # expand a little bit more in the direction of the expansion, so that
+  # further expansions may be unnecessary. this is an optimisation 
+  # suggested on the wiki page by kleptog.
+  def update_bbox!(array)
+    # ensure that bbox is cached and has no nils in it. if there are any
+    # nils, just use the bounding box update to write over them.
+    @bbox = bbox.zip(array).collect { |a, b| a.nil? ? b : a }
+
+    # FIXME - this looks nasty and violates DRY... is there any prettier 
+    # way to do this? 
+    @bbox[0] = array[0] + EXPAND * (@bbox[0] - @bbox[2]) if array[0] < @bbox[0]
+    @bbox[1] = array[1] + EXPAND * (@bbox[1] - @bbox[3]) if array[1] < @bbox[1]
+    @bbox[2] = array[2] + EXPAND * (@bbox[2] - @bbox[0]) if array[2] > @bbox[2]
+    @bbox[3] = array[3] + EXPAND * (@bbox[3] - @bbox[1]) if array[3] > @bbox[3]
+
+    # update active record. rails 2.1's dirty handling should take care of
+    # whether this object needs saving or not.
+    self.min_lon, self.min_lat, self.max_lon, self.max_lat = @bbox
+  end
+
+  ##
+  # the number of elements is also passed in so that we can ensure that
+  # a single changeset doesn't contain too many elements. this, of course,
+  # destroys the optimisation described in the bbox method above.
+  def add_changes!(elements)
+    self.num_changes += elements
+  end
+
+  def tags_as_hash
+    return tags
+  end
+
+  def tags
+    unless @tags
+      @tags = {}
+      self.changeset_tags.each do |tag|
+        @tags[tag.k] = tag.v
+      end
+    end
+    @tags
+  end
+
+  def tags=(t)
+    @tags = t
+  end
+
+  def add_tag_keyval(k, v)
+    @tags = Hash.new unless @tags
+    @tags[k] = v
+  end
+
+  def save_with_tags!
+    t = Time.now.getutc
+
+    # do the changeset update and the changeset tags update in the
+    # same transaction to ensure consistency.
+    Changeset.transaction do
+      # set the auto-close time to be one hour in the future unless
+      # that would make it more than 24h long, in which case clip to
+      # 24h, as this has been decided is a reasonable time limit.
+      if (closed_at - created_at) > (MAX_TIME_OPEN - IDLE_TIMEOUT)
+        self.closed_at = created_at + MAX_TIME_OPEN
+      else
+        self.closed_at = Time.now.getutc + IDLE_TIMEOUT
+      end
+      self.save!
+
+      tags = self.tags
+      ChangesetTag.delete_all(['id = ?', self.id])
+
+      tags.each do |k,v|
+        tag = ChangesetTag.new
+        tag.k = k
+        tag.v = v
+        tag.id = self.id
+        tag.save!
+      end
+    end
+  end
+  
+  def to_xml
+    doc = OSM::API.new.get_xml_doc
+    doc.root << to_xml_node()
+    return doc
+  end
+  
+  def to_xml_node(user_display_name_cache = nil)
+    el1 = XML::Node.new 'changeset'
+    el1['id'] = self.id.to_s
+
+    user_display_name_cache = {} if user_display_name_cache.nil?
+
+    if user_display_name_cache and user_display_name_cache.key?(self.user_id)
+      # use the cache if available
+    elsif self.user.data_public?
+      user_display_name_cache[self.user_id] = self.user.display_name
+    else
+      user_display_name_cache[self.user_id] = nil
+    end
+
+    el1['user'] = user_display_name_cache[self.user_id] unless user_display_name_cache[self.user_id].nil?
+    el1['uid'] = self.user_id.to_s if self.user.data_public?
+
+    self.tags.each do |k,v|
+      el2 = XML::Node.new('tag')
+      el2['k'] = k.to_s
+      el2['v'] = v.to_s
+      el1 << el2
+    end
+    
+    el1['created_at'] = self.created_at.xmlschema
+    el1['closed_at'] = self.closed_at.xmlschema unless is_open?
+    el1['open'] = is_open?.to_s
+
+    el1['min_lon'] = (bbox[0].to_f / GeoRecord::SCALE).to_s unless bbox[0].nil?
+    el1['min_lat'] = (bbox[1].to_f / GeoRecord::SCALE).to_s unless bbox[1].nil?
+    el1['max_lon'] = (bbox[2].to_f / GeoRecord::SCALE).to_s unless bbox[2].nil?
+    el1['max_lat'] = (bbox[3].to_f / GeoRecord::SCALE).to_s unless bbox[3].nil?
+    
+    # NOTE: changesets don't include the XML of the changes within them,
+    # they are just structures for tagging. to get the osmChange of a
+    # changeset, see the download method of the controller.
+
+    return el1
+  end
+
+  ##
+  # update this instance from another instance given and the user who is
+  # doing the updating. note that this method is not for updating the
+  # bounding box, only the tags of the changeset.
+  def update_from(other, user)
+    # ensure that only the user who opened the changeset may modify it.
+    unless user.id == self.user_id 
+      raise OSM::APIUserChangesetMismatchError.new
+    end
+    
+    # can't change a closed changeset
+    unless is_open?
+      raise OSM::APIChangesetAlreadyClosedError.new(self)
+    end
+
+    # copy the other's tags
+    self.tags = other.tags
+
+    save_with_tags!
+  end
+end
diff --git a/app/models/changeset_tag.rb b/app/models/changeset_tag.rb
new file mode 100644 (file)
index 0000000..6a414a0
--- /dev/null
@@ -0,0 +1,8 @@
+class ChangesetTag < ActiveRecord::Base
+  belongs_to :changeset, :foreign_key => 'id'
+
+  validates_presence_of :id
+  validates_length_of :k, :v, :maximum => 255, :allow_blank => true
+  validates_uniqueness_of :id, :scope => :k
+  validates_numericality_of :id, :only_integer => true
+end
index dd1f9882a7a4726ffff14147292b1891a0584893..46d96fec7d86fbf85b85bed3b6646d5784033c9d 100644 (file)
@@ -1,11 +1,15 @@
 class DiaryEntry < ActiveRecord::Base
   belongs_to :user
   has_many :diary_comments, :include => :user,
-                            :conditions => "users.visible = 1",
+                            :conditions => ["users.visible = ?", true],
                             :order => "diary_comments.id"
 
   validates_presence_of :title, :body
-  validates_numericality_of :latitude, :allow_nil => true
-  validates_numericality_of :longitude, :allow_nil => true
+  validates_length_of :title, :within => 1..255
+  validates_length_of :language, :within => 2..3, :allow_nil => true
+  validates_numericality_of :latitude, :allow_nil => true,
+                            :greater_than_or_equal_to => -90, :less_than_or_equal_to => 90
+  validates_numericality_of :longitude, :allow_nil => true,
+                            :greater_than_or_equal_to => -180, :less_than_or_equal_to => 180
   validates_associated :user
 end
index 97e411192b0df5cedbd89d7255f236ac7fb2cf35..464c5502837b56c5ffdf409ff32288f6b5779cac 100644 (file)
@@ -1,8 +1,12 @@
+require 'validators'
+
 class Message < ActiveRecord::Base
   belongs_to :sender, :class_name => "User", :foreign_key => :from_user_id
   belongs_to :recipient, :class_name => "User", :foreign_key => :to_user_id
 
-  validates_presence_of :title, :body, :sent_on
+  validates_presence_of :title, :body, :sent_on, :sender, :recipient
+  validates_length_of :title, :within => 1..255
   validates_inclusion_of :message_read, :in => [ true, false ]
   validates_associated :sender, :recipient
+  validates_as_utf8 :title
 end
index af88a117d83879ef0eb21d438a11d211cb670dc7..1392fc65099f4123ccb06447f6f3d8f624cbad33 100644 (file)
@@ -2,27 +2,34 @@ class Node < ActiveRecord::Base
   require 'xml/libxml'
 
   include GeoRecord
+  include ConsistencyValidations
 
   set_table_name 'current_nodes'
-  
-  validates_presence_of :user_id, :timestamp
-  validates_inclusion_of :visible, :in => [ true, false ]
-  validates_numericality_of :latitude, :longitude
-  validate :validate_position
 
-  belongs_to :user
+  belongs_to :changeset
 
   has_many :old_nodes, :foreign_key => :id
 
   has_many :way_nodes
   has_many :ways, :through => :way_nodes
 
+  has_many :node_tags, :foreign_key => :id
+  
   has_many :old_way_nodes
   has_many :ways_via_history, :class_name=> "Way", :through => :old_way_nodes, :source => :way
 
   has_many :containing_relation_members, :class_name => "RelationMember", :as => :member
   has_many :containing_relations, :class_name => "Relation", :through => :containing_relation_members, :source => :relation, :extend => ObjectFinder
 
+  validates_presence_of :id, :on => :update
+  validates_presence_of :timestamp,:version,  :changeset_id
+  validates_uniqueness_of :id
+  validates_inclusion_of :visible, :in => [ true, false ]
+  validates_numericality_of :latitude, :longitude, :changeset_id, :version, :integer_only => true
+  validates_numericality_of :id, :on => :update, :integer_only => true
+  validate :validate_position
+  validates_associated :changeset
+
   # Sanity check the latitude and longitude and add an error if it's broken
   def validate_position
     errors.add_to_base("Node is not in the world") unless in_world?
@@ -50,7 +57,7 @@ class Node < ActiveRecord::Base
     #conditions = keys.join(' AND ')
  
     find_by_area(min_lat, min_lon, max_lat, max_lon,
-                    :conditions => 'visible = 1',
+                    :conditions => {:visible => true},
                     :limit => APP_CONFIG['max_number_of_nodes']+1)  
   end
 
@@ -59,83 +66,150 @@ class Node < ActiveRecord::Base
     begin
       p = XML::Parser.string(xml)
       doc = p.parse
-  
-      node = Node.new
 
       doc.find('//osm/node').each do |pt|
-        node.lat = pt['lat'].to_f
-        node.lon = pt['lon'].to_f
+        return Node.from_xml_node(pt, create)
+      end
+    rescue LibXML::XML::Error, ArgumentError => ex
+      raise OSM::APIBadXMLError.new("node", xml, ex.message)
+    end
+  end
+
+  def self.from_xml_node(pt, create=false)
+    node = Node.new
+    
+    raise OSM::APIBadXMLError.new("node", pt, "lat missing") if pt['lat'].nil?
+    raise OSM::APIBadXMLError.new("node", pt, "lon missing") if pt['lon'].nil?
+    node.lat = pt['lat'].to_f
+    node.lon = pt['lon'].to_f
+    raise OSM::APIBadXMLError.new("node", pt, "changeset id missing") if pt['changeset'].nil?
+    node.changeset_id = pt['changeset'].to_i
 
-        return nil unless node.in_world?
+    raise OSM::APIBadUserInput.new("The node is outside this world") unless node.in_world?
 
-        unless create
-          if pt['id'] != '0'
-            node.id = pt['id'].to_i
-          end
-        end
+    # version must be present unless creating
+    raise OSM::APIBadXMLError.new("node", pt, "Version is required when updating") unless create or not pt['version'].nil?
+    node.version = create ? 0 : pt['version'].to_i
 
-        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
+    # visible if it says it is, or as the default if the attribute
+    # is missing.
+    # Don't need to set the visibility, when it is set explicitly in the create/update/delete
+    #node.visible = pt['visible'].nil? or pt['visible'] == 'true'
 
-        tags = []
+    # We don't care about the time, as it is explicitly set on create/update/delete
 
-        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!
+  ##
+  # the bounding box around a node, which is used for determining the changeset's
+  # bounding box
+  def bbox
+    [ longitude, latitude, longitude, latitude ]
+  end
+
+  # Should probably be renamed delete_from to come in line with update
+  def delete_with_history!(new_node, user)
+    unless self.visible
+      raise OSM::APIAlreadyDeletedError.new
+    end
+
+    # need to start the transaction here, so that the database can 
+    # provide repeatable reads for the used-by checks. this means it
+    # shouldn't be possible to get race conditions.
     Node.transaction do
-      self.timestamp = Time.now
-      self.save!
-      old_node = OldNode.from_node(self)
-      old_node.save!
+      check_consistency(self, new_node, user)
+      if WayNode.find(:first, :joins => "INNER JOIN current_ways ON current_ways.id = current_way_nodes.id", :conditions => [ "current_ways.visible = ? AND current_way_nodes.node_id = ?", true, self.id ])
+        raise OSM::APIPreconditionFailedError.new
+      elsif RelationMember.find(:first, :joins => "INNER JOIN current_relations ON current_relations.id=current_relation_members.id", :conditions => [ "visible = ? AND member_type='Node' and member_id=? ", true, self.id])
+        raise OSM::APIPreconditionFailedError.new
+      else
+        self.changeset_id = new_node.changeset_id
+        self.visible = false
+        
+        # update the changeset with the deleted position
+        changeset.update_bbox!(bbox)
+        
+        save_with_history!
+      end
     end
   end
 
-  # Turn this Node in to a complete OSM XML object with <osm> wrapper
+  def update_from(new_node, user)
+    check_consistency(self, new_node, user)
+
+    # update changeset first
+    self.changeset_id = new_node.changeset_id
+    self.changeset = new_node.changeset
+
+    # update changeset bbox with *old* position first
+    changeset.update_bbox!(bbox);
+
+    # FIXME logic needs to be double checked
+    self.latitude = new_node.latitude 
+    self.longitude = new_node.longitude
+    self.tags = new_node.tags
+    self.visible = true
+
+    # update changeset bbox with *new* position
+    changeset.update_bbox!(bbox);
+
+    save_with_history!
+  end
+  
+  def create_with_history(user)
+    check_create_consistency(self, user)
+    self.version = 0
+    self.visible = true
+
+    # update the changeset to include the new location
+    changeset.update_bbox!(bbox)
+
+    save_with_history!
+  end
+
   def to_xml
     doc = OSM::API.new.get_xml_doc
     doc.root << to_xml_node()
     return doc
   end
 
-  # Turn this Node in to an XML Node without the <osm> wrapper.
   def to_xml_node(user_display_name_cache = nil)
     el1 = XML::Node.new 'node'
     el1['id'] = self.id.to_s
     el1['lat'] = self.lat.to_s
     el1['lon'] = self.lon.to_s
-
+    el1['version'] = self.version.to_s
+    el1['changeset'] = self.changeset_id.to_s
+    
     user_display_name_cache = {} if user_display_name_cache.nil?
 
-    if user_display_name_cache and user_display_name_cache.key?(self.user_id)
+    if user_display_name_cache and user_display_name_cache.key?(self.changeset.user_id)
       # use the cache if available
-    elsif self.user.data_public?
-      user_display_name_cache[self.user_id] = self.user.display_name
+    elsif self.changeset.user.data_public?
+      user_display_name_cache[self.changeset.user_id] = self.changeset.user.display_name
     else
-      user_display_name_cache[self.user_id] = nil
+      user_display_name_cache[self.changeset.user_id] = nil
     end
 
-    el1['user'] = user_display_name_cache[self.user_id] unless user_display_name_cache[self.user_id].nil?
+    if not user_display_name_cache[self.changeset.user_id].nil?
+      el1['user'] = user_display_name_cache[self.changeset.user_id]
+      el1['uid'] = self.changeset.user_id.to_s
+    end
 
-    Tags.split(self.tags) do |k,v|
+    self.tags.each do |k,v|
       el2 = XML::Node.new('tag')
       el2['k'] = k.to_s
       el2['v'] = v.to_s
@@ -147,12 +221,79 @@ class Node < ActiveRecord::Base
     return el1
   end
 
-  # Return the node's tags as a Hash of keys and their values
   def tags_as_hash
-    hash = {}
-    Tags.split(self.tags) do |k,v|
-      hash[k] = v
+    return tags
+  end
+
+  def tags
+    unless @tags
+      @tags = {}
+      self.node_tags.each do |tag|
+        @tags[tag.k] = tag.v
+      end
+    end
+    @tags
+  end
+
+  def tags=(t)
+    @tags = t 
+  end 
+
+  def add_tag_key_val(k,v)
+    @tags = Hash.new unless @tags
+
+    # duplicate tags are now forbidden, so we can't allow values
+    # in the hash to be overwritten.
+    raise OSM::APIDuplicateTagsError.new("node", self.id, k) if @tags.include? k
+
+    @tags[k] = v
+  end
+
+  ##
+  # are the preconditions OK? this is mainly here to keep the duck
+  # typing interface the same between nodes, ways and relations.
+  def preconditions_ok?
+    in_world?
+  end
+
+  ##
+  # dummy method to make the interfaces of node, way and relation
+  # more consistent.
+  def fix_placeholders!(id_map)
+    # nodes don't refer to anything, so there is nothing to do here
+  end
+  
+  private
+
+  def save_with_history!
+    t = Time.now.getutc
+    Node.transaction do
+      self.version += 1
+      self.timestamp = t
+      self.save!
+
+      # Create a NodeTag
+      tags = self.tags
+      NodeTag.delete_all(['id = ?', self.id])
+      tags.each do |k,v|
+        tag = NodeTag.new
+        tag.k = k 
+        tag.v = v 
+        tag.id = self.id
+        tag.save!
+      end 
+
+      # Create an OldNode
+      old_node = OldNode.from_node(self)
+      old_node.timestamp = t
+      old_node.save_with_dependencies!
+
+      # tell the changeset we updated one element only
+      changeset.add_changes! 1
+
+      # save the changeset in case of bounding box updates
+      changeset.save!
     end
-    hash
   end
+  
 end
index 76eab8427b2c570cce79846887706eb6c10923b6..be115c53eaaaa026d38559ad63c5316326872968 100644 (file)
@@ -1,25 +1,21 @@
 class OldNode < ActiveRecord::Base
   include GeoRecord
+  include ConsistencyValidations
 
   set_table_name 'nodes'
   
-  validates_presence_of :user_id, :timestamp
+  validates_presence_of :changeset_id, :timestamp
   validates_inclusion_of :visible, :in => [ true, false ]
   validates_numericality_of :latitude, :longitude
   validate :validate_position
+  validates_associated :changeset
 
-  belongs_to :user
+  belongs_to :changeset
  
   def validate_position
     errors.add_to_base("Node is not in the world") unless in_world?
   end
 
-  def in_world?
-    return false if self.lat < -90 or self.lat > 90
-    return false if self.lon < -180 or self.lon > 180
-    return true
-  end
-
   def self.from_node(node)
     old_node = OldNode.new
     old_node.latitude = node.latitude
@@ -27,19 +23,30 @@ class OldNode < ActiveRecord::Base
     old_node.visible = node.visible
     old_node.tags = node.tags
     old_node.timestamp = node.timestamp
-    old_node.user_id = node.user_id
+    old_node.changeset_id = node.changeset_id
     old_node.id = node.id
+    old_node.version = node.version
     return old_node
   end
+  
+  def to_xml
+    doc = OSM::API.new.get_xml_doc
+    doc.root << to_xml_node()
+    return doc
+  end
 
   def to_xml_node
     el1 = XML::Node.new 'node'
     el1['id'] = self.id.to_s
     el1['lat'] = self.lat.to_s
     el1['lon'] = self.lon.to_s
-    el1['user'] = self.user.display_name if self.user.data_public?
+    el1['changeset'] = self.changeset.id.to_s
+    if self.changeset.user.data_public?
+      el1['user'] = self.changeset.user.display_name
+      el1['uid'] = self.changeset.user.id.to_s
+    end
 
-    Tags.split(self.tags) do |k,v|
+    self.tags.each do |k,v|
       el2 = XML::Node.new('tag')
       el2['k'] = k.to_s
       el2['v'] = v.to_s
@@ -48,24 +55,54 @@ class OldNode < ActiveRecord::Base
 
     el1['visible'] = self.visible.to_s
     el1['timestamp'] = self.timestamp.xmlschema
+    el1['version'] = self.version.to_s
     return el1
   end
-  
-  def tags_as_hash
-    hash = {}
-    Tags.split(self.tags) do |k,v|
-      hash[k] = v
+
+  def save_with_dependencies!
+    save!
+    #not sure whats going on here
+    clear_aggregation_cache
+    clear_association_cache
+    #ok from here
+    @attributes.update(OldNode.find(:first, :conditions => ['id = ? AND timestamp = ? AND version = ?', self.id, self.timestamp, self.version]).instance_variable_get('@attributes'))
+   
+    self.tags.each do |k,v|
+      tag = OldNodeTag.new
+      tag.k = k
+      tag.v = v
+      tag.id = self.id
+      tag.version = self.version
+      tag.save!
     end
-    hash
   end
 
-  # Pretend we're not in any ways
-  def ways
-    return []
+  def tags
+    unless @tags
+        @tags = Hash.new
+        OldNodeTag.find(:all, :conditions => ["id = ? AND version = ?", self.id, self.version]).each do |tag|
+            @tags[tag.k] = tag.v
+        end
+    end
+    @tags = Hash.new unless @tags
+    @tags
   end
 
-  # Pretend we're not in any relations
-  def containing_relation_members
-    return []
+  def tags=(t)
+    @tags = t 
   end
+
+  def tags_as_hash 
+    return self.tags
+  end 
+  # Pretend we're not in any ways 
+  def ways 
+    return [] 
+  end 
+  # Pretend we're not in any relations 
+  def containing_relation_members 
+    return [] 
+  end 
 end
index bac03c4d2eff6811a9dc4b5d2c32e3bb988e76b3..b2fdf926e1c353720a40b362e03eafcdd8bd2a28 100644 (file)
@@ -1,14 +1,19 @@
 class OldRelation < ActiveRecord::Base
+  include ConsistencyValidations
+  
   set_table_name 'relations'
 
-  belongs_to :user
+  belongs_to :changeset
+  
+  validates_associated :changeset
 
   def self.from_relation(relation)
     old_relation = OldRelation.new
     old_relation.visible = relation.visible
-    old_relation.user_id = relation.user_id
+    old_relation.changeset_id = relation.changeset_id
     old_relation.timestamp = relation.timestamp
     old_relation.id = relation.id
+    old_relation.version = relation.version
     old_relation.members = relation.members
     old_relation.tags = relation.tags
     return old_relation
@@ -33,14 +38,12 @@ class OldRelation < ActiveRecord::Base
       tag.save!
     end
 
-    i = 1
-    self.members.each do |m|
+    self.members.each_with_index do |m,i|
       member = OldRelationMember.new
-      member.id = self.id
-      member.member_type = m[0]
+      member.id = [self.id, self.version, i]
+      member.member_type = m[0].classify
       member.member_id = m[1]
       member.member_role = m[2]
-      member.version = self.version
       member.save!
     end
   end
@@ -48,7 +51,7 @@ class OldRelation < ActiveRecord::Base
   def members
     unless @members
         @members = Array.new
-        OldRelationMember.find(:all, :conditions => ["id = ? AND version = ?", self.id, self.version]).each do |m|
+        OldRelationMember.find(:all, :conditions => ["id = ? AND version = ?", self.id, self.version], :order => "sequence_id").each do |m|
             @members += [[m.type,m.id,m.role]]
         end
     end
@@ -85,16 +88,27 @@ class OldRelation < ActiveRecord::Base
     OldRelationTag.find(:all, :conditions => ['id = ? AND version = ?', self.id, self.version])    
   end
 
+  def to_xml
+    doc = OSM::API.new.get_xml_doc
+    doc.root << to_xml_node()
+    return doc
+  end
+
   def to_xml_node
     el1 = XML::Node.new 'relation'
     el1['id'] = self.id.to_s
     el1['visible'] = self.visible.to_s
     el1['timestamp'] = self.timestamp.xmlschema
-    el1['user'] = self.user.display_name if self.user.data_public?
+    if self.changeset.user.data_public?
+      el1['user'] = self.changeset.user.display_name
+      el1['uid'] = self.changeset.user.id.to_s
+    end
+    el1['version'] = self.version.to_s
+    el1['changeset'] = self.changeset_id.to_s
     
     self.old_members.each do |member|
       e = XML::Node.new 'member'
-      e['type'] = member.member_type.to_s
+      e['type'] = member.member_type.to_s.downcase
       e['ref'] = member.member_id.to_s # "id" is considered uncool here as it should be unique in XML
       e['role'] = member.member_role.to_s
       el1 << e
index d8b68585428da54515eb579d9a31ca4f04f04576..f0294d33952bd015f43c48d860ff30721b7a8b4e 100644 (file)
@@ -1,3 +1,6 @@
 class OldRelationMember < ActiveRecord::Base
   set_table_name 'relation_members'
+
+  set_primary_keys :id, :version, :sequence_id
+  belongs_to :relation, :foreign_key=> :id
 end
index 7ce6f694e633bb2a2110711229b1be8b6886e275..0fcb113269ea475fef4438d747ccee8eb9c69e26 100644 (file)
@@ -1,3 +1,10 @@
 class OldRelationTag < ActiveRecord::Base
   set_table_name 'relation_tags'
+  
+  belongs_to :old_relation, :foreign_key => [:id, :version]
+  
+  validates_presence_of :id, :version
+  validates_length_of :k, :v, :maximum => 255, :allow_blank => true
+  validates_uniqueness_of :id, :scope => [:k, :version]
+  validates_numericality_of :id, :version, :only_integer => true
 end
index 63265d6bf5c77814e90205cc4c0c5138a65a04c3..425478a5b93a81586d6849081fc296f4583bf680 100644 (file)
@@ -1,14 +1,19 @@
 class OldWay < ActiveRecord::Base
+  include ConsistencyValidations
+  
   set_table_name 'ways'
 
-  belongs_to :user
+  belongs_to :changeset
 
+  validates_associated :changeset
+  
   def self.from_way(way)
     old_way = OldWay.new
     old_way.visible = way.visible
-    old_way.user_id = way.user_id
+    old_way.changeset_id = way.changeset_id
     old_way.timestamp = way.timestamp
     old_way.id = way.id
+    old_way.version = way.version
     old_way.nds = way.nds
     old_way.tags = way.tags
     return old_way
@@ -93,7 +98,12 @@ class OldWay < ActiveRecord::Base
     el1['id'] = self.id.to_s
     el1['visible'] = self.visible.to_s
     el1['timestamp'] = self.timestamp.xmlschema
-    el1['user'] = self.user.display_name if self.user.data_public?
+    if self.changeset.user.data_public?
+      el1['user'] = self.changeset.user.display_name
+      el1['uid'] = self.changeset.user.id.to_s
+    end
+    el1['version'] = self.version.to_s
+    el1['changeset'] = self.changeset.id.to_s
     
     self.old_nodes.each do |nd| # FIXME need to make sure they come back in the right order
       e = XML::Node.new 'nd'
@@ -114,27 +124,29 @@ class OldWay < ActiveRecord::Base
   # For get_nodes_undelete, uses same nodes, even if they've moved since
   # For get_nodes_revert,   allocates new ids 
   # Currently returns Potlatch-style array
-  
+  # where [5] indicates whether latest version is usable as is (boolean)
+  # (i.e. is it visible? are we actually reverting to an earlier version?)
+
   def get_nodes_undelete
        points = []
        self.nds.each do |n|
          node=Node.find(n)
-         points << [node.lon, node.lat, n, node.visible ? 1 : 0, node.tags_as_hash]
+         points << [node.lon, node.lat, n, node.version, node.tags_as_hash, node.visible]
     end
        points
   end
   
-  def get_nodes_revert
+  def get_nodes_revert(timestamp)
     points=[]
     self.nds.each do |n|
-      oldnode=OldNode.find(:first, :conditions=>['id=? AND timestamp<=?',n,self.timestamp], :order=>"timestamp DESC")
+      oldnode=OldNode.find(:first, :conditions=>['id=? AND timestamp<=?',n,timestamp], :order=>"timestamp DESC")
       curnode=Node.find(n)
-      id=n; v=curnode.visible ? 1 : 0
+      id=n; reuse=curnode.visible
       if oldnode.lat!=curnode.lat or oldnode.lon!=curnode.lon or oldnode.tags!=curnode.tags then
         # node has changed: if it's in other ways, give it a new id
-        if curnode.ways-[self.id] then id=-1; v=nil end
+        if curnode.ways-[self.id] then id=-1; reuse=false end
       end
-      points << [oldnode.lon, oldnode.lat, id, v, oldnode.tags_as_hash]
+      points << [oldnode.lon, oldnode.lat, id, curnode.version, oldnode.tags_as_hash, reuse]
     end
     points
   end
index b02fd45b93ad8f23ba3aea7271bcef1ddfce5051..801532dbaa7632d2074e33fbb48ac4a5251285a3 100644 (file)
@@ -1,6 +1,10 @@
 class OldWayTag < ActiveRecord::Base
-  belongs_to :user
-
   set_table_name 'way_tags'
 
+  belongs_to :old_way, :foreign_key => [:id, :version]
+
+  validates_presence_of :id
+  validates_length_of :k, :v, :maximum => 255, :allow_blank => true
+  validates_uniqueness_of :id, :scope => [:k, :version]
+  validates_numericality_of :id, :version, :only_integer => true
 end
index d9dba303fd8a6bbca4450dfa0260b8e4d4e8d4d9..36d6943d97106e9ecfd42242fc8575c1932839e2 100644 (file)
@@ -1,51 +1,83 @@
 class Relation < ActiveRecord::Base
   require 'xml/libxml'
   
+  include ConsistencyValidations
+  
   set_table_name 'current_relations'
 
-  belongs_to :user
+  belongs_to :changeset
 
   has_many :old_relations, :foreign_key => 'id', :order => 'version'
 
-  has_many :relation_members, :foreign_key => 'id'
+  has_many :relation_members, :foreign_key => 'id', :order => 'sequence_id'
   has_many :relation_tags, :foreign_key => 'id'
 
   has_many :containing_relation_members, :class_name => "RelationMember", :as => :member
   has_many :containing_relations, :class_name => "Relation", :through => :containing_relation_members, :source => :relation, :extend => ObjectFinder
 
+  validates_presence_of :id, :on => :update
+  validates_presence_of :timestamp,:version,  :changeset_id 
+  validates_uniqueness_of :id
+  validates_inclusion_of :visible, :in => [ true, false ]
+  validates_numericality_of :id, :on => :update, :integer_only => true
+  validates_numericality_of :changeset_id, :version, :integer_only => true
+  validates_associated :changeset
+  
+  TYPES = ["node", "way", "relation"]
+
   def self.from_xml(xml, create=false)
     begin
       p = XML::Parser.string(xml)
       doc = p.parse
 
-      relation = Relation.new
-
       doc.find('//osm/relation').each do |pt|
-        if !create and pt['id'] != '0'
-          relation.id = pt['id'].to_i
-        end
+        return Relation.from_xml_node(pt, create)
+      end
+    rescue LibXML::XML::Error, ArgumentError => ex
+      raise OSM::APIBadXMLError.new("relation", xml, ex.message)
+    end
+  end
 
-        if create
-          relation.timestamp = Time.now
-          relation.visible = true
-        else
-          if pt['timestamp']
-            relation.timestamp = Time.parse(pt['timestamp'])
-          end
-        end
+  def self.from_xml_node(pt, create=false)
+    relation = Relation.new
 
-        pt.find('tag').each do |tag|
-          relation.add_tag_keyval(tag['k'], tag['v'])
-        end
+    if !create and pt['id'] != '0'
+      relation.id = pt['id'].to_i
+    end
 
-        pt.find('member').each do |member|
-          relation.add_member(member['type'], member['ref'], member['role'])
-        end
+    raise OSM::APIBadXMLError.new("relation", pt, "You are missing the required changeset in the relation") if pt['changeset'].nil?
+    relation.changeset_id = pt['changeset']
+
+    # The follow block does not need to be executed because they are dealt with 
+    # in create_with_history, update_from and delete_with_history
+    if create
+      relation.timestamp = Time.now.getutc
+      relation.visible = true
+      relation.version = 0
+    else
+      if pt['timestamp']
+        relation.timestamp = Time.parse(pt['timestamp'])
       end
-    rescue
-      relation = nil
+      relation.version = pt['version']
+    end
+
+    pt.find('tag').each do |tag|
+      relation.add_tag_keyval(tag['k'], tag['v'])
     end
 
+    pt.find('member').each do |member|
+      #member_type = 
+      logger.debug "each member"
+      raise OSM::APIBadXMLError.new("relation", pt, "The #{member['type']} is not allowed only, #{TYPES.inspect} allowed") unless TYPES.include? member['type']
+      logger.debug "after raise"
+      #member_ref = member['ref']
+      #member_role
+      member['role'] ||= "" # Allow  the upload to not include this, in which case we default to an empty string.
+      logger.debug member['role']
+      relation.add_member(member['type'].classify, member['ref'], member['role'])
+    end
+    raise OSM::APIBadUserInput.new("Some bad xml in relation") if relation.nil?
+
     return relation
   end
 
@@ -60,18 +92,23 @@ class Relation < ActiveRecord::Base
     el1['id'] = self.id.to_s
     el1['visible'] = self.visible.to_s
     el1['timestamp'] = self.timestamp.xmlschema
+    el1['version'] = self.version.to_s
+    el1['changeset'] = self.changeset_id.to_s
 
     user_display_name_cache = {} if user_display_name_cache.nil?
     
-    if user_display_name_cache and user_display_name_cache.key?(self.user_id)
+    if user_display_name_cache and user_display_name_cache.key?(self.changeset.user_id)
       # use the cache if available
-    elsif self.user.data_public?
-      user_display_name_cache[self.user_id] = self.user.display_name
+    elsif self.changeset.user.data_public?
+      user_display_name_cache[self.changeset.user_id] = self.changeset.user.display_name
     else
-      user_display_name_cache[self.user_id] = nil
+      user_display_name_cache[self.changeset.user_id] = nil
     end
 
-    el1['user'] = user_display_name_cache[self.user_id] unless user_display_name_cache[self.user_id].nil?
+    if not user_display_name_cache[self.changeset.user_id].nil?
+      el1['user'] = user_display_name_cache[self.changeset.user_id]
+      el1['uid'] = self.changeset.user_id.to_s
+    end
 
     self.relation_members.each do |member|
       p=0
@@ -88,7 +125,7 @@ class Relation < ActiveRecord::Base
       #end
       if p
         e = XML::Node.new 'member'
-        e['type'] = member.member_type
+        e['type'] = member.member_type.downcase
         e['ref'] = member.member_id.to_s 
         e['role'] = member.member_role
         el1 << e
@@ -108,7 +145,7 @@ class Relation < ActiveRecord::Base
     if ids.empty?
       return []
     else
-      self.with_scope(:find => { :joins => "INNER JOIN current_relation_members ON current_relation_members.id = current_relations.id", :conditions => "current_relation_members.member_type = 'node' AND current_relation_members.member_id IN (#{ids.join(',')})" }) do
+      self.with_scope(:find => { :joins => "INNER JOIN current_relation_members ON current_relation_members.id = current_relations.id", :conditions => "current_relation_members.member_type = 'Node' AND current_relation_members.member_id IN (#{ids.join(',')})" }) do
         return self.find(:all, options)
       end
     end
@@ -118,7 +155,7 @@ class Relation < ActiveRecord::Base
     if ids.empty?
       return []
     else
-      self.with_scope(:find => { :joins => "INNER JOIN current_relation_members ON current_relation_members.id = current_relations.id", :conditions => "current_relation_members.member_type = 'way' AND current_relation_members.member_id IN (#{ids.join(',')})" }) do
+      self.with_scope(:find => { :joins => "INNER JOIN current_relation_members ON current_relation_members.id = current_relations.id", :conditions => "current_relation_members.member_type = 'Way' AND current_relation_members.member_id IN (#{ids.join(',')})" }) do
         return self.find(:all, options)
       end
     end
@@ -128,7 +165,7 @@ class Relation < ActiveRecord::Base
     if ids.empty?
       return []
     else
-      self.with_scope(:find => { :joins => "INNER JOIN current_relation_members ON current_relation_members.id = current_relations.id", :conditions => "current_relation_members.member_type = 'relation' AND current_relation_members.member_id IN (#{ids.join(',')})" }) do
+      self.with_scope(:find => { :joins => "INNER JOIN current_relation_members ON current_relation_members.id = current_relations.id", :conditions => "current_relation_members.member_type = 'Relation' AND current_relation_members.member_id IN (#{ids.join(',')})" }) do
         return self.find(:all, options)
       end
     end
@@ -170,19 +207,164 @@ class Relation < ActiveRecord::Base
 
   def add_tag_keyval(k, v)
     @tags = Hash.new unless @tags
+
+    # duplicate tags are now forbidden, so we can't allow values
+    # in the hash to be overwritten.
+    raise OSM::APIDuplicateTagsError.new("relation", self.id, k) if @tags.include? k
+
     @tags[k] = v
   end
 
+  ##
+  # updates the changeset bounding box to contain the bounding box of 
+  # the element with given +type+ and +id+. this only works with nodes
+  # and ways at the moment, as they're the only elements to respond to
+  # the :bbox call.
+  def update_changeset_element(type, id)
+    element = Kernel.const_get(type.capitalize).find(id)
+    changeset.update_bbox! element.bbox
+  end    
+
+  def delete_with_history!(new_relation, user)
+    unless self.visible
+      raise OSM::APIAlreadyDeletedError.new
+    end
+
+    # need to start the transaction here, so that the database can 
+    # provide repeatable reads for the used-by checks. this means it
+    # shouldn't be possible to get race conditions.
+    Relation.transaction do
+      check_consistency(self, new_relation, user)
+      # This will check to see if this relation is used by another relation
+      if RelationMember.find(:first, :joins => "INNER JOIN current_relations ON current_relations.id=current_relation_members.id", :conditions => [ "visible = ? AND member_type='Relation' and member_id=? ", true, self.id ])
+        raise OSM::APIPreconditionFailedError.new("The relation #{new_relation.id} is a used in another relation")
+      end
+      self.changeset_id = new_relation.changeset_id
+      self.tags = {}
+      self.members = []
+      self.visible = false
+      save_with_history!
+    end
+  end
+
+  def update_from(new_relation, user)
+    check_consistency(self, new_relation, user)
+    if !new_relation.preconditions_ok?
+      raise OSM::APIPreconditionFailedError.new
+    end
+    self.changeset_id = new_relation.changeset_id
+    self.changeset = new_relation.changeset
+    self.tags = new_relation.tags
+    self.members = new_relation.members
+    self.visible = true
+    save_with_history!
+  end
+  
+  def create_with_history(user)
+    check_create_consistency(self, user)
+    if !self.preconditions_ok?
+      raise OSM::APIPreconditionFailedError.new
+    end
+    self.version = 0
+    self.visible = true
+    save_with_history!
+  end
+
+  def preconditions_ok?
+    # These are hastables that store an id in the index of all 
+    # the nodes/way/relations that have already been added.
+    # If the member is valid and visible then we add it to the 
+    # relevant hash table, with the value true as a cache.
+    # Thus if you have nodes with the ids of 50 and 1 already in the
+    # relation, then the hash table nodes would contain:
+    # => {50=>true, 1=>true}
+    elements = { :node => Hash.new, :way => Hash.new, :relation => Hash.new }
+    self.members.each do |m|
+      # find the hash for the element type or die
+      logger.debug m[0]
+      hash = elements[m[0].downcase.to_sym] or return false
+      # unless its in the cache already
+      unless hash.key? m[1]
+        # use reflection to look up the appropriate class
+        model = Kernel.const_get(m[0].capitalize)
+        # get the element with that ID
+        element = model.find(m[1])
+
+        # and check that it is OK to use.
+        unless element and element.visible? and element.preconditions_ok?
+          return false
+        end
+        hash[m[1]] = true
+      end
+    end
+
+    return true
+  rescue
+    return false
+  end
+
+  # Temporary method to match interface to nodes
+  def tags_as_hash
+    return self.tags
+  end
+
+  ##
+  # if any members are referenced by placeholder IDs (i.e: negative) then
+  # this calling this method will fix them using the map from placeholders 
+  # to IDs +id_map+. 
+  def fix_placeholders!(id_map)
+    self.members.map! do |type, id, role|
+      old_id = id.to_i
+      if old_id < 0
+        new_id = id_map[type.downcase.to_sym][old_id]
+        raise "invalid placeholder" if new_id.nil?
+        [type, new_id, role]
+      else
+        [type, id, role]
+      end
+    end
+  end
+
+  private
+  
   def save_with_history!
     Relation.transaction do
-      t = Time.now
+      # have to be a little bit clever here - to detect if any tags
+      # changed then we have to monitor their before and after state.
+      tags_changed = false
+
+      t = Time.now.getutc
+      self.version += 1
       self.timestamp = t
       self.save!
 
       tags = self.tags
+      self.relation_tags.each do |old_tag|
+        key = old_tag.k
+        # if we can match the tags we currently have to the list
+        # of old tags, then we never set the tags_changed flag. but
+        # if any are different then set the flag and do the DB 
+        # update.
+        if tags.has_key? key 
+          # rails 2.1 dirty handling should take care of making this
+          # somewhat efficient... hopefully...
+          old_tag.v = tags[key]
+          tags_changed |= old_tag.changed?
+          old_tag.save!
+
+          # remove from the map, so that we can expect an empty map
+          # at the end if there are no new tags
+          tags.delete key
 
-      RelationTag.delete_all(['id = ?', self.id])
-
+        else
+          # this means a tag was deleted
+          tags_changed = true
+          RelationTag.delete_all ['id = ? and k = ?', self.id, old_tag.k]
+        end
+      end
+      # if there are left-over tags then they are new and will have to
+      # be added.
+      tags_changed |= (not tags.empty?)
       tags.each do |k,v|
         tag = RelationTag.new
         tag.k = k
@@ -190,81 +372,87 @@ class Relation < ActiveRecord::Base
         tag.id = self.id
         tag.save!
       end
+      
+      # reload, so that all of the members are accessible in their
+      # new state.
+      self.reload
+
+      # same pattern as before, but this time we're collecting the
+      # changed members in an array, as the bounding box updates for
+      # elements are per-element, not blanked on/off like for tags.
+      changed_members = Array.new
+      members = Hash.new
+      self.members.each do |m|
+        # should be: h[[m.id, m.type]] = m.role, but someone prefers arrays
+        members[[m[1], m[0]]] = m[2]
+      end
+      relation_members.each do |old_member|
+        key = [old_member.member_id.to_s, old_member.member_type]
+        if members.has_key? key
+          members.delete key
+        else
+          changed_members << key
+        end
+      end
+      # any remaining members must be new additions
+      changed_members += members.keys
 
+      # update the members. first delete all the old members, as the new
+      # members may be in a different order and i don't feel like implementing
+      # a longest common subsequence algorithm to optimise this.
       members = self.members
-
-      RelationMember.delete_all(['id = ?', self.id])
-
-      members.each do |n|
+      RelationMember.delete_all(:id => self.id)
+      members.each_with_index do |m,i|
         mem = RelationMember.new
-        mem.id = self.id
-        mem.member_type = n[0];
-        mem.member_id = n[1];
-        mem.member_role = n[2];
+        mem.id = [self.id, i]
+        mem.member_type = m[0]
+        mem.member_id = m[1]
+        mem.member_role = m[2]
         mem.save!
       end
 
       old_relation = OldRelation.from_relation(self)
       old_relation.timestamp = t
       old_relation.save_with_dependencies!
-    end
-  end
 
-  def preconditions_ok?
-    # These are hastables that store an id in the index of all 
-    # the nodes/way/relations that have already been added.
-    # Once we know the id of the node/way/relation exists
-    # we check to see if it is already existing in the hashtable
-    # if it does, then we return false. Otherwise
-    # we add it to the relevant hash table, with the value true..
-    # Thus if you have nodes with the ids of 50 and 1 already in the
-    # relation, then the hash table nodes would contain:
-    # => {50=>true, 1=>true}
-    nodes = Hash.new
-    ways = Hash.new
-    relations = Hash.new
-    self.members.each do |m|
-      if (m[0] == "node")
-        n = Node.find(:first, :conditions => ["id = ?", m[1]])
-        unless n and n.visible 
-          return false
-        end
-        if nodes[m[1]]
-          return false
-        else
-          nodes[m[1]] = true
-        end
-      elsif (m[0] == "way")
-        w = Way.find(:first, :conditions => ["id = ?", m[1]])
-        unless w and w.visible and w.preconditions_ok?
-          return false
-        end
-        if ways[m[1]]
-          return false
-        else
-          ways[m[1]] = true
-        end
-      elsif (m[0] == "relation")
-        e = Relation.find(:first, :conditions => ["id = ?", m[1]])
-        unless e and e.visible and e.preconditions_ok?
-          return false
-        end
-        if relations[m[1]]
-          return false
-        else
-          relations[m[1]] = true
+      # update the bbox of the changeset and save it too.
+      # discussion on the mailing list gave the following definition for
+      # the bounding box update procedure of a relation:
+      #
+      # adding or removing nodes or ways from a relation causes them to be
+      # added to the changeset bounding box. adding a relation member or
+      # changing tag values causes all node and way members to be added to the
+      # bounding box. this is similar to how the map call does things and is
+      # reasonable on the assumption that adding or removing members doesn't
+      # materially change the rest of the relation.
+      any_relations = 
+        changed_members.collect { |id,type| type == "relation" }.
+        inject(false) { |b,s| b or s }
+
+      if tags_changed or any_relations
+        # add all non-relation bounding boxes to the changeset
+        # FIXME: check for tag changes along with element deletions and
+        # make sure that the deleted element's bounding box is hit.
+        self.members.each do |type, id, role|
+          if type != "Relation"
+            update_changeset_element(type, id)
+          end
         end
       else
-        return false
+        # add only changed members to the changeset
+        changed_members.each do |id, type|
+          if type != "Relation"
+            update_changeset_element(type, id)
+          end
+        end
       end
+
+      # tell the changeset we updated one element only
+      changeset.add_changes! 1
+
+      # save the (maybe updated) changeset bounding box
+      changeset.save!
     end
-    return true
-  rescue
-    return false
   end
 
-  # Temporary method to match interface to nodes
-  def tags_as_hash
-    return self.tags
-  end
 end
index 9ff4f46f3b8b0b6ae4fb1b02ba49ccab25a71b66..b385dd6d1d0362b0f4dc45f3fa81ef582c73a366 100644 (file)
@@ -1,19 +1,20 @@
 class RelationMember < ActiveRecord::Base
   set_table_name 'current_relation_members'
   
+  set_primary_keys :id, :sequence_id
   belongs_to :member, :polymorphic => true, :foreign_type => :member_class
   belongs_to :relation, :foreign_key => :id
 
   def after_find
-    self[:member_class] = self.member_type.capitalize
+    self[:member_class] = self.member_type.classify
   end
 
   def after_initialize
-    self[:member_class] = self.member_type.capitalize
+    self[:member_class] = self.member_type.classify unless self.member_type.nil?
   end
 
   def before_save
-    self.member_type = self[:member_class].downcase
+    self.member_type = self[:member_class].classify
   end
 
   def member_type=(type)
index 939165ebd30f9cc136a12d77f4b013165bf0be27..812b2ec3592f7f866a28b27b79259839b8658ad6 100644 (file)
@@ -3,4 +3,8 @@ class RelationTag < ActiveRecord::Base
 
   belongs_to :relation, :foreign_key => 'id'
 
+  validates_presence_of :id
+  validates_length_of :k, :v, :maximum => 255, :allow_blank => true
+  validates_uniqueness_of :id, :scope => :k
+  validates_numericality_of :id, :only_integer => true
 end
index 10e867badc71381fc42ba6c93b4a7071f53ea827..03dbeb0b35d53206753972cc3da8f3422f15d3cc 100644 (file)
@@ -3,6 +3,8 @@ class Trace < ActiveRecord::Base
 
   validates_presence_of :user_id, :name, :timestamp
   validates_presence_of :description, :on => :create
+  validates_length_of :name, :maximum => 255
+  validates_length_of :description, :maximum => 255
 #  validates_numericality_of :latitude, :longitude
   validates_inclusion_of :public, :inserted, :in => [ true, false]
   
index f1d5967d53dd5c38b75d591b792cf6a330cd69ad..f9833e141446125128a07ab5e03a6c21ba2f9bde 100644 (file)
@@ -2,6 +2,7 @@ class Tracetag < ActiveRecord::Base
   set_table_name 'gpx_file_tags'
 
   validates_format_of :tag, :with => /^[^\/;.,?]*$/
+  validates_length_of :tag, :within => 1..255
 
   belongs_to :trace, :foreign_key => 'gpx_id'
 end
index fae037110951ffe64d847fba40b99a0c37d40d9e..4113662aa411e3e5c3c60b1f809f2a0d2dc3a93d 100644 (file)
@@ -4,19 +4,21 @@ class User < ActiveRecord::Base
   has_many :traces
   has_many :diary_entries, :order => 'created_at DESC'
   has_many :messages, :foreign_key => :to_user_id, :order => 'sent_on DESC'
-  has_many :new_messages, :class_name => "Message", :foreign_key => :to_user_id, :conditions => "message_read = 0", :order => 'sent_on DESC'
+  has_many :new_messages, :class_name => "Message", :foreign_key => :to_user_id, :conditions => {:message_read => false}, :order => 'sent_on DESC'
   has_many :sent_messages, :class_name => "Message", :foreign_key => :from_user_id, :order => 'sent_on DESC'
-  has_many :friends, :include => :befriendee, :conditions => "users.visible = 1"
+  has_many :friends, :include => :befriendee, :conditions => ["users.visible = ?", true]
   has_many :tokens, :class_name => "UserToken"
   has_many :preferences, :class_name => "UserPreference"
+  has_many :changesets
 
   validates_presence_of :email, :display_name
   validates_confirmation_of :email, :message => 'Email addresses must match'
   validates_confirmation_of :pass_crypt, :message => 'Password must match the confirmation password'
   validates_uniqueness_of :display_name, :allow_nil => true
   validates_uniqueness_of :email
-  validates_length_of :pass_crypt, :minimum => 8
-  validates_length_of :display_name, :minimum => 3, :allow_nil => true
+  validates_length_of :pass_crypt, :within => 8..255
+  validates_length_of :display_name, :within => 3..255, :allow_nil => true
+  validates_length_of :email, :within => 6..255
   validates_format_of :email, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i
   validates_format_of :display_name, :with => /^[^\/;.,?]*$/
   validates_numericality_of :home_lat, :allow_nil => true
@@ -28,7 +30,7 @@ class User < ActiveRecord::Base
   file_column :image, :magick => { :geometry => "100x100>" }
 
   def after_initialize
-    self.creation_time = Time.now if self.creation_time.nil?
+    self.creation_time = Time.now.getutc if self.creation_time.nil?
   end
 
   def encrypt_password
@@ -80,7 +82,7 @@ class User < ActiveRecord::Base
     if self.home_lon and self.home_lat 
       gc = OSM::GreatCircle.new(self.home_lat, self.home_lon)
       bounds = gc.bounds(radius)
-      nearby = User.find(:all, :conditions => "visible = 1 and home_lat between #{bounds[:minlat]} and #{bounds[:maxlat]} and home_lon between #{bounds[:minlon]} and #{bounds[:maxlon]} and data_public = 1 and id != #{self.id}")
+      nearby = User.find(:all, :conditions => ["visible = ? and home_lat between #{bounds[:minlat]} and #{bounds[:maxlat]} and home_lon between #{bounds[:minlon]} and #{bounds[:maxlon]} and data_public = ? and id != #{self.id}", true, true])
       nearby.delete_if { |u| gc.distance(u.home_lat, u.home_lon) > radius }
       nearby.sort! { |u1,u2| gc.distance(u1.home_lat, u1.home_lon) <=> gc.distance(u2.home_lat, u2.home_lon) }
     else
@@ -104,6 +106,10 @@ class User < ActiveRecord::Base
     return false
   end
 
+  def trace_public_default
+    return self.preferences.find(:first, :conditions => {:k => "gps.trace.public", :v => "default"})
+  end
+
   def delete
     self.active = false
     self.display_name = "user_#{self.id}"
index 3985a527ec5f62e83b53b948b72164a48789a08d..28ef40f1d5c8347597c9f9a790b4d8d002cb63ce 100644 (file)
@@ -1,6 +1,9 @@
 class UserPreference < ActiveRecord::Base
   set_primary_keys :user_id, :k
   belongs_to :user
+  
+  validates_length_of :k, :within => 1..255
+  validates_length_of :v, :within => 1..255
 
   # Turn this Node in to an XML Node without the <osm> wrapper.
   def to_xml_node
index 6c3ea9e462dcda9b5d18b2a1d23e23a48d38ca0e..e134d697d63a2b14a5cd61a3c29624c8b00b7714 100644 (file)
@@ -1,9 +1,11 @@
 class Way < ActiveRecord::Base
   require 'xml/libxml'
+  
+  include ConsistencyValidations
 
   set_table_name 'current_ways'
-
-  belongs_to :user
+  
+  belongs_to :changeset
 
   has_many :old_ways, :foreign_key => 'id', :order => 'version'
 
@@ -15,37 +17,56 @@ class Way < ActiveRecord::Base
   has_many :containing_relation_members, :class_name => "RelationMember", :as => :member
   has_many :containing_relations, :class_name => "Relation", :through => :containing_relation_members, :source => :relation, :extend => ObjectFinder
 
+  validates_presence_of :id, :on => :update
+  validates_presence_of :changeset_id,:version,  :timestamp
+  validates_uniqueness_of :id
+  validates_inclusion_of :visible, :in => [ true, false ]
+  validates_numericality_of :changeset_id, :version, :integer_only => true
+  validates_numericality_of :id, :on => :update, :integer_only => true
+  validates_associated :changeset
+
   def self.from_xml(xml, create=false)
     begin
       p = XML::Parser.string(xml)
       doc = p.parse
 
-      way = Way.new
-
       doc.find('//osm/way').each do |pt|
-        if !create and pt['id'] != '0'
-          way.id = pt['id'].to_i
-        end
-
-        if create
-          way.timestamp = Time.now
-          way.visible = true
-        else
-          if pt['timestamp']
-            way.timestamp = Time.parse(pt['timestamp'])
-          end
-        end
+        return Way.from_xml_node(pt, create)
+      end
+    rescue LibXML::XML::Error, ArgumentError => ex
+      raise OSM::APIBadXMLError.new("way", xml, ex.message)
+    end
+  end
 
-        pt.find('tag').each do |tag|
-          way.add_tag_keyval(tag['k'], tag['v'])
-        end
+  def self.from_xml_node(pt, create=false)
+    way = Way.new
 
-        pt.find('nd').each do |nd|
-          way.add_nd_num(nd['ref'])
-        end
+    if !create and pt['id'] != '0'
+      way.id = pt['id'].to_i
+    end
+    
+    way.version = pt['version']
+    raise OSM::APIBadXMLError.new("node", pt, "Changeset is required") if pt['changeset'].nil?
+    way.changeset_id = pt['changeset']
+
+    # This next section isn't required for the create, update, or delete of ways
+    if create
+      way.timestamp = Time.now.getutc
+      way.visible = true
+    else
+      if pt['timestamp']
+        way.timestamp = Time.parse(pt['timestamp'])
       end
-    rescue
-      way = nil
+      # if visible isn't present then it defaults to true
+      way.visible = (pt['visible'] or true)
+    end
+
+    pt.find('tag').each do |tag|
+      way.add_tag_keyval(tag['k'], tag['v'])
+    end
+
+    pt.find('nd').each do |nd|
+      way.add_nd_num(nd['ref'])
     end
 
     return way
@@ -73,18 +94,23 @@ class Way < ActiveRecord::Base
     el1['id'] = self.id.to_s
     el1['visible'] = self.visible.to_s
     el1['timestamp'] = self.timestamp.xmlschema
+    el1['version'] = self.version.to_s
+    el1['changeset'] = self.changeset_id.to_s
 
     user_display_name_cache = {} if user_display_name_cache.nil?
 
-    if user_display_name_cache and user_display_name_cache.key?(self.user_id)
+    if user_display_name_cache and user_display_name_cache.key?(self.changeset.user_id)
       # use the cache if available
-    elsif self.user.data_public?
-      user_display_name_cache[self.user_id] = self.user.display_name
+    elsif self.changeset.user.data_public?
+      user_display_name_cache[self.changeset.user_id] = self.changeset.user.display_name
     else
-      user_display_name_cache[self.user_id] = nil
+      user_display_name_cache[self.changeset.user_id] = nil
     end
 
-    el1['user'] = user_display_name_cache[self.user_id] unless user_display_name_cache[self.user_id].nil?
+    if not user_display_name_cache[self.changeset.user_id].nil?
+      el1['user'] = user_display_name_cache[self.changeset.user_id]
+      el1['uid'] = self.changeset.user_id.to_s
+    end
 
     # make sure nodes are output in sequence_id order
     ordered_nodes = []
@@ -96,7 +122,7 @@ class Way < ActiveRecord::Base
         end
       else
         # otherwise, manually go to the db to check things
-        if nd.node.visible? and nd.node.visible?
+        if nd.node and nd.node.visible?
           ordered_nodes[nd.sequence_id] = nd.node_id.to_s
         end
       end
@@ -154,99 +180,86 @@ class Way < ActiveRecord::Base
 
   def add_tag_keyval(k, v)
     @tags = Hash.new unless @tags
-    @tags[k] = v
-  end
 
-  def save_with_history!
-    t = Time.now
+    # duplicate tags are now forbidden, so we can't allow values
+    # in the hash to be overwritten.
+    raise OSM::APIDuplicateTagsError.new("way", self.id, k) if @tags.include? k
 
-    Way.transaction do
-      self.timestamp = t
-      self.save!
-    end
-
-    WayTag.transaction do
-      tags = self.tags
+    @tags[k] = v
+  end
 
-      WayTag.delete_all(['id = ?', self.id])
+  ##
+  # the integer coords (i.e: unscaled) bounding box of the way, assuming
+  # straight line segments.
+  def bbox
+    lons = nodes.collect { |n| n.longitude }
+    lats = nodes.collect { |n| n.latitude }
+    [ lons.min, lats.min, lons.max, lats.max ]
+  end
 
-      tags.each do |k,v|
-        tag = WayTag.new
-        tag.k = k
-        tag.v = v
-        tag.id = self.id
-        tag.save!
-      end
+  def update_from(new_way, user)
+    check_consistency(self, new_way, user)
+    if !new_way.preconditions_ok?
+      raise OSM::APIPreconditionFailedError.new
     end
 
-    WayNode.transaction do
-      nds = self.nds
-
-      WayNode.delete_all(['id = ?', self.id])
+    self.changeset_id = new_way.changeset_id
+    self.changeset = new_way.changeset
+    self.tags = new_way.tags
+    self.nds = new_way.nds
+    self.visible = true
+    save_with_history!
+  end
 
-      sequence = 1
-      nds.each do |n|
-        nd = WayNode.new
-        nd.id = [self.id, sequence]
-        nd.node_id = n
-        nd.save!
-        sequence += 1
-      end
+  def create_with_history(user)
+    check_create_consistency(self, user)
+    if !self.preconditions_ok?
+      raise OSM::APIPreconditionFailedError.new
     end
-
-    old_way = OldWay.from_way(self)
-    old_way.timestamp = t
-    old_way.save_with_dependencies!
+    self.version = 0
+    self.visible = true
+    save_with_history!
   end
 
   def preconditions_ok?
     return false if self.nds.empty?
+    if self.nds.length > APP_CONFIG['max_number_of_way_nodes']
+      raise OSM::APITooManyWayNodesError.new(self.nds.count, APP_CONFIG['max_number_of_way_nodes'])
+    end
     self.nds.each do |n|
       node = Node.find(:first, :conditions => ["id = ?", n])
       unless node and node.visible
-        return false
+        raise OSM::APIPreconditionFailedError.new("The node with id #{n} either does not exist, or is not visible")
       end
     end
     return true
   end
 
-  # Delete the way and it's relations, but don't really delete it - set its visibility to false and update the history etc to maintain wiki-like functionality.
-  def delete_with_relations_and_history(user)
-    if self.visible
-         # FIXME
-         # this should actually delete the relations,
-         # not just throw a PreconditionFailed if it's a member of a relation!!
+  def delete_with_history!(new_way, user)
+    unless self.visible
+      raise OSM::APIAlreadyDeletedError
+    end
+    
+    # need to start the transaction here, so that the database can 
+    # provide repeatable reads for the used-by checks. this means it
+    # shouldn't be possible to get race conditions.
+    Way.transaction do
+      check_consistency(self, new_way, user)
       if RelationMember.find(:first, :joins => "INNER JOIN current_relations ON current_relations.id=current_relation_members.id",
-                             :conditions => [ "visible = 1 AND member_type='way' and member_id=?", self.id])
-        raise OSM::APIPreconditionFailedError
-      # end FIXME
+                             :conditions => [ "visible = ? AND member_type='Way' and member_id=? ", true, self.id])
+        raise OSM::APIPreconditionFailedError.new("You need to make sure that this way is not a member of a relation.")
       else
-        self.user_id = user.id
+        self.changeset_id = new_way.changeset_id
+        self.changeset = new_way.changeset
+
         self.tags = []
         self.nds = []
         self.visible = false
-        self.save_with_history!
+        save_with_history!
       end
-    else
-      raise OSM::APIAlreadyDeletedError
     end
   end
 
-  # delete a way and it's nodes that aren't part of other ways, with history
-  def delete_with_relations_and_nodes_and_history(user)
-    # delete the nodes not used by other ways
-    self.unshared_node_ids.each do |node_id|
-      n = Node.find(node_id)
-      n.user_id = user.id
-      n.visible = false
-      n.save_with_history!
-    end
-    
-    self.user_id = user.id
-
-    self.delete_with_relations_and_history(user)
-  end
-
   # Find nodes that belong to this way only
   def unshared_node_ids
     node_ids = self.nodes.collect { |node| node.id }
@@ -263,4 +276,78 @@ class Way < ActiveRecord::Base
   def tags_as_hash
     return self.tags
   end
+
+  ##
+  # if any referenced nodes are placeholder IDs (i.e: are negative) then
+  # this calling this method will fix them using the map from placeholders 
+  # to IDs +id_map+. 
+  def fix_placeholders!(id_map)
+    self.nds.map! do |node_id|
+      if node_id < 0
+        new_id = id_map[:node][node_id]
+        raise "invalid placeholder for #{node_id.inspect}: #{new_id.inspect}" if new_id.nil?
+        new_id
+      else
+        node_id
+      end
+    end
+  end
+
+  private
+  
+  def save_with_history!
+    t = Time.now.getutc
+
+    # update the bounding box, note that this has to be done both before 
+    # and after the save, so that nodes from both versions are included in the 
+    # bbox. we use a copy of the changeset so that it isn't reloaded
+    # later in the save.
+    cs = self.changeset
+    cs.update_bbox!(bbox) unless nodes.empty?
+
+    Way.transaction do
+      self.version += 1
+      self.timestamp = t
+      self.save!
+
+      tags = self.tags
+      WayTag.delete_all(['id = ?', self.id])
+      tags.each do |k,v|
+        tag = WayTag.new
+        tag.k = k
+        tag.v = v
+        tag.id = self.id
+        tag.save!
+      end
+
+      nds = self.nds
+      WayNode.delete_all(['id = ?', self.id])
+      sequence = 1
+      nds.each do |n|
+        nd = WayNode.new
+        nd.id = [self.id, sequence]
+        nd.node_id = n
+        nd.save!
+        sequence += 1
+      end
+
+      old_way = OldWay.from_way(self)
+      old_way.timestamp = t
+      old_way.save_with_dependencies!
+
+      # reload the way so that the nodes array points to the correct
+      # new set of nodes.
+      self.reload
+
+      # update and commit the bounding box, now that way nodes 
+      # have been updated and we're in a transaction.
+      cs.update_bbox!(bbox) unless nodes.empty?
+
+      # tell the changeset we updated one element only
+      cs.add_changes! 1
+
+      cs.save!
+    end
+  end
+
 end
index 4548674d4b950607e2396ecc079da2e3951bba4c..fa9b4336177061287d00d4b153a9da6cef28c6a9 100644 (file)
@@ -6,4 +6,9 @@ class WayTag < ActiveRecord::Base
   # FIXME add a real multipart key to waytags so that we can do eager loadin
 
   belongs_to :way, :foreign_key => 'id'
+  
+  validates_presence_of :id
+  validates_length_of :k, :v, :maximum => 255, :allow_blank => true
+  validates_uniqueness_of :id, :scope => :k
+  validates_numericality_of :id, :only_integer => true
 end
diff --git a/app/views/browse/_changeset_details.rhtml b/app/views/browse/_changeset_details.rhtml
new file mode 100644 (file)
index 0000000..12d4dfb
--- /dev/null
@@ -0,0 +1,95 @@
+<table>
+
+  <tr>
+    <th>Created at:</th>
+    <td><%= h(changeset_details.created_at) %></td>
+  </tr>
+  
+  <tr>
+    <th>Closed at:</th>
+    <td><%= h(changeset_details.closed_at) %></td>
+  </tr>
+  
+  <% if changeset_details.user.data_public? %>
+    <tr>
+      <th>Belongs to:</th>
+      <td><%= link_to h(changeset_details.user.display_name), :controller => "user", :action => "view", :display_name => changeset_details.user.display_name %></td>
+    </tr>
+  <% end %>
+  
+  <%= render :partial => "tag_details", :object => changeset_details %>
+
+  <tr>
+    <th>Bounding box:</th>
+    <% unless changeset_details.has_valid_bbox? %>
+      <td>No bounding box has been stored for this changeset.</td>
+    <% else
+          minlon = changeset_details.min_lon/GeoRecord::SCALE.to_f
+          minlat = changeset_details.min_lat/GeoRecord::SCALE.to_f
+          maxlon = changeset_details.max_lon/GeoRecord::SCALE.to_f
+          maxlat = changeset_details.max_lat/GeoRecord::SCALE.to_f
+    %>
+      <td>
+        <table>
+          <tr>
+            <td colspan="3" style="text-align:center"><%=maxlat -%></td>
+          </tr>
+          <tr>
+            <td><%=minlon -%></td>
+            <td>(<a href='/?minlon=<%= minlon %>&minlat=<%= minlat %>&maxlon=<%= maxlon %>&maxlat=<%= maxlat %>&box=yes' title='show area box'>box</a>)</td>
+            <td><%=maxlon -%></td>
+          </tr>
+          <tr>
+            <td colspan="3" style="text-align:center"><%= minlon -%></td>
+          </tr>
+        </table>
+      </td>
+    <% end %>
+  </tr>
+
+  <% unless @nodes.empty? %>
+    <tr valign="top">
+      <th>Has the following <%= @node_pages.item_count %> nodes:</th>
+      <td>
+        <table padding="0">
+          <% @nodes.each do |node| %>
+            <tr><td><%= link_to "Node #{node.id.to_s}, version #{node.version.to_s}", :action => "node", :id => node.id.to_s %></td></tr>
+          <% end %>
+        </table>
+      </td>
+    </tr>
+    <%= render :partial => 'paging_nav', :locals => { :pages => @node_pages, :page_param => "node_page"} %>
+  <% end %>
+  
+  <% unless @ways.empty? %>
+    <tr valign="top">
+      <th>Has the following <%= @way_pages.item_count %> ways:</th>
+      <td>
+        <table padding="0">
+          <% @ways.each do |way| %>
+            <tr><td><%= link_to "Way #{way.id.to_s}, version #{way.version.to_s}", :action => "way", :id => way.id.to_s %></td></tr>
+          <% end %>
+          <%=
+          #render :partial => "containing_relation", :collection => changeset_details.containing_relation_members 
+          %>
+        </table>
+      </td>
+    </tr>
+    <%= render :partial => 'paging_nav', :locals => { :pages => @way_pages, :page_param => "way_page" } %>
+  <% end %>
+  
+  <% unless @relations.empty? %>
+    <tr valign="top">
+      <th>Has the following <%= @relation_pages.item_count %> relations:</th>
+      <td>
+        <table padding="0">
+          <% @relations.each do |relation| %>
+            <tr><td><%= link_to "Relation #{relation.id.to_s}, version #{relation.version.to_s}", :action => "relation", :id => relation.id.to_s %></td></tr>
+          <% end %>
+        </table>
+      </td>
+    </tr>
+    <%= render :partial => 'paging_nav', :locals => { :pages => @relation_pages, :page_param => "relation_page" } %>
+  <% end %>
+
+</table>
index ee5f22ceebee4990058fb8be17b10dbaf43a3c4e..09cf4cf2da24cf66cc5b9d48aa879f92942a02b5 100644 (file)
@@ -3,20 +3,21 @@
   <td><%= h(common_details.timestamp) %></td>
 </tr>
 
-<% if common_details.user.data_public %>
+<% if common_details.changeset.user.data_public? %>
   <tr>
     <th>Edited by:</th>
-    <td><%= link_to h(common_details.user.display_name), :controller => "user", :action => "view", :display_name => common_details.user.display_name %></td>
+    <td><%= link_to h(common_details.changeset.user.display_name), :controller => "user", :action => "view", :display_name => common_details.changeset.user.display_name %></td>
   </tr>
 <% end %>
 
-<% unless common_details.tags_as_hash.empty? %>
-  <tr valign="top">
-    <th>Tags:</th>
-    <td>
-      <table padding="0">
-        <%= render :partial => "tag", :collection => common_details.tags_as_hash %>
-      </table>
-    </td>
-  </tr>      
-<% end %>
+<tr>
+  <th>Version:</th>
+  <td><%= h(common_details.version) %></td>
+</tr>
+
+<tr>
+  <th>In changeset:</th>
+  <td><%= link_to common_details.changeset_id, :action => :changeset, :id => common_details.changeset_id %></td>
+</tr>
+
+<%= render :partial => "tag_details", :object => common_details %>
index ad2d2d3071b309d3236d8b7257f2a5503070ab6c..d972104ba9d9c40cf08f5928575ca2e72766d09b 100644 (file)
@@ -2,7 +2,7 @@
 <%= javascript_include_tag '/openlayers/OpenStreetMap.js' %>
 <%= javascript_include_tag 'map.js' %>
 <td align="right">
-  <% if map.visible %>
+  <% if map.instance_of? Changeset or map.visible %>
     <div id="small_map" style="width:250px; height: 300px; border: solid 1px black">
     </div>
     <span id="loading">Loading...</span>
 </td>
 <script type="text/javascript">
   function init() {
+    var map = createMap("small_map", {
+      controls: [ new OpenLayers.Control.Navigation() ]
+    });
+
+    <% if map.instance_of? Changeset %>
+    var minlon = <%= map.min_lon / GeoRecord::SCALE.to_f %>;
+    var minlat = <%= map.min_lat / GeoRecord::SCALE.to_f %>;
+    var maxlon = <%= map.max_lon / GeoRecord::SCALE.to_f %>;
+    var maxlat = <%= map.max_lat / GeoRecord::SCALE.to_f %>;
+    var bbox = new OpenLayers.Bounds(minlon, minlat, maxlon, maxlat);
+
+    setMapExtent(bbox);
+    addBoxToMap(bbox);
+
+    $("loading").innerHTML = "";
+
+    $("larger_map").href = '/?minlon='+minlon+'&minlat='+minlat+'&maxlon='+maxlon+'&maxlat='+maxlat+'&box=yes';
+    $("larger_map").innerHTML = "View Larger Map";
+    <% else %>
     var obj_type = "<%= map.class.name.downcase %>";
     var obj_id = <%= map.id %>;
     var url = "/api/<%= "#{API_VERSION}" %>/<%= map.class.name.downcase %>/<%= map.id %>";
       url += "/full";
     }
 
-    var map = createMap("small_map", {
-      controls: [ new OpenLayers.Control.Navigation() ]
-    });
-
     var osm_layer = new OpenLayers.Layer.GML("OSM", url, {
       format: OpenLayers.Format.OSM,
       projection: new OpenLayers.Projection("EPSG:4326")
@@ -58,6 +73,7 @@
 
     osm_layer.loadGML();
     osm_layer.loaded = true;
+    <% end %>
   }
 
   window.onload = init;
diff --git a/app/views/browse/_paging_nav.rhtml b/app/views/browse/_paging_nav.rhtml
new file mode 100644 (file)
index 0000000..fcfbb05
--- /dev/null
@@ -0,0 +1,15 @@
+<tr><td colspan='2'>
+<% current_page = pages.current_page %>
+
+Showing page 
+<%= current_page.number %> (<%= current_page.first_item %><% 
+if (current_page.first_item < current_page.last_item) # if more than 1 trace on page 
+  %>-<%= current_page.last_item %><% 
+end %>
+of <%= pages.item_count %>)
+
+<% if pages.page_count > 1 %>
+| <%= pagination_links_each(pages, {}) { |n| link_to_page(n, page_param) } %>
+<% end %>
+</td>
+</tr>
diff --git a/app/views/browse/_tag_details.rhtml b/app/views/browse/_tag_details.rhtml
new file mode 100644 (file)
index 0000000..dab3626
--- /dev/null
@@ -0,0 +1,10 @@
+<% unless tag_details.tags_as_hash.empty? %>
+  <tr valign="top">
+    <th>Tags:</th>
+    <td>
+      <table padding="0">
+        <%= render :partial => "tag", :collection => tag_details.tags_as_hash %>
+      </table>
+    </td>
+  </tr>      
+<% end %>
diff --git a/app/views/browse/changeset.rhtml b/app/views/browse/changeset.rhtml
new file mode 100644 (file)
index 0000000..95fc1b1
--- /dev/null
@@ -0,0 +1,23 @@
+<table width="100%">
+  <tr>
+    <td>
+      <h2>Changeset: <%= h(@changeset.id) %></h2>
+    </td>
+    <td>
+      <%= render :partial => "navigation" %>
+    </td>
+  </tr>
+  <tr valign="top">
+    <td>
+    <%= render :partial => "changeset_details", :object => @changeset %>
+    <hr />
+    Download 
+    <%= link_to "Changeset XML", :controller => "changeset", :action => "read" %>
+    or
+    <%= link_to "osmChange XML", :controller => "changeset", :action => "download" %>
+    </td>
+    <% if @changeset.has_valid_bbox? %>
+    <%= render :partial => "map", :object => @changeset %>
+    <% end %>
+  </tr>
+</table>
diff --git a/app/views/browse/index.rhtml b/app/views/browse/index.rhtml
deleted file mode 100644 (file)
index 2cd5cc9..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-<h2><%= @nodes.length %> Recently Changed Nodes</h2> 
-<ul>
-<% @nodes.each do |node| 
-   name = node.tags_as_hash['name'].to_s 
-   if name.length == 0:
-     name = "(No name)"
-   end
-   name = name + " - " + node.id.to_s 
-%>
-   <li><%= link_to h(name), :action => "node", :id => node.id %></li>
-<% end %>
-</ul>
diff --git a/app/views/browse/not_found.rhtml b/app/views/browse/not_found.rhtml
new file mode 100644 (file)
index 0000000..1322a0a
--- /dev/null
@@ -0,0 +1 @@
+<p>Sorry, the <%= @type -%> with the id <%= params[:id] -%>, could not be found.</p>
index c17325ad19c6a94b1b87e245d39ee3fabc8e0f01..f38b1dc80937cb0c9f6355236fd87a2b50b01e49 100644 (file)
@@ -189,7 +189,7 @@ page << <<EOJ
     if (size > 0.25) {
       setStatus("Unable to load: Bounding box size of " + size + " is too large (must be smaller than 0.25)");
     } else {
-      loadGML("/api/0.5/map?bbox=" + projected.toBBOX());
+      loadGML("/api/#{API_VERSION}/map?bbox=" + projected.toBBOX());
     }
   }
 
@@ -393,7 +393,7 @@ page << <<EOJ
     this.link.href = "";
     this.link.innerHTML = "Wait...";
 
-    new Ajax.Request("/api/0.5/" + this.type + "/" + this.feature.osm_id + "/history", {
+    new Ajax.Request("/api/#{API_VERSION}/" + this.type + "/" + this.feature.osm_id + "/history", {
       onComplete: OpenLayers.Function.bind(displayHistory, this)
     });
 
diff --git a/app/views/changeset/_changeset.rhtml b/app/views/changeset/_changeset.rhtml
new file mode 100644 (file)
index 0000000..a149a72
--- /dev/null
@@ -0,0 +1,49 @@
+<tr>
+  <% cl = cycle('table0', 'table1') %>
+
+  <td class="<%= cl %>">
+    #<%= changeset.id %>
+  </td>
+
+  <td class="<%= cl %>">
+    <% if changeset.closed_at > DateTime.now %> (still editing)
+    <% else %><%= changeset.closed_at.strftime("%d %b %Y %H:%M") %><% end %>
+  </td>
+
+    
+  <%if showusername %>  
+    <td class="<%= cl %>">
+    <% if changeset.user.data_public? %>
+      <%= link_to h(changeset.user.display_name), :controller => "user", :action => "view", :display_name => changeset.user.display_name %>
+    <% else %>
+      <i>Anonymous</i>
+    <% end %>
+    </td>
+  <% end %>
+    
+  <td class="<%= cl %>">
+    <% if changeset.tags['comment'] %>
+      <%= h(changeset.tags['comment']) %>
+    <% else %>
+      (none)
+    <% end %>
+  </td>
+
+  <td class="<%= cl %>">
+    <% if changeset.min_lat.nil? %>
+      (no edits)
+    <% else 
+      minlon = changeset.min_lon/GeoRecord::SCALE.to_f
+      minlat = changeset.min_lat/GeoRecord::SCALE.to_f
+      maxlon = changeset.max_lon/GeoRecord::SCALE.to_f
+      maxlat = changeset.max_lat/GeoRecord::SCALE.to_f
+    %>
+      (<a href='/?minlon=<%= minlon %>&minlat=<%= minlat %>&maxlon=<%= maxlon %>&maxlat=<%= maxlat %>&box=yes' title='show area box'><%= format("%0.3f",minlon) -%>,<%= format("%0.3f",minlat) -%>,<%= format("%0.3f",maxlon) -%>,<%= format("%0.3f",maxlat) -%></a>) 
+  <% end %>
+  </td>
+
+  <td class="<%= cl %>">
+    <%= link_to 'more', {:controller => 'browse', :action => 'changeset', :id => changeset.id}, {:title => 'View changeset details'} %> 
+  </td>
+
+</tr>
diff --git a/app/views/changeset/_changeset_paging_nav.rhtml b/app/views/changeset/_changeset_paging_nav.rhtml
new file mode 100644 (file)
index 0000000..7be1530
--- /dev/null
@@ -0,0 +1,18 @@
+<% current_page = @edit_pages.current_page %>
+
+Showing page 
+<%= current_page.number %> (<%= current_page.first_item %><% 
+if (current_page.first_item < current_page.last_item) # if more than 1 changeset on page 
+  %>-<%= current_page.last_item %><% 
+end %>
+of <%= @edit_pages.item_count %>)
+
+<%
+if @edit_pages.page_count > 1 
+       bboxparam = h(params['bbox'])
+       bboxparam = nil if bboxparam==""
+%>
+ | <%= pagination_links_each(@edit_pages, {}) { |n| link_to(n,  :display_name => @display_name, :bbox => bboxparam , :page => n) }  %>
+<%
+end
+%>
diff --git a/app/views/changeset/list.rhtml b/app/views/changeset/list.rhtml
new file mode 100644 (file)
index 0000000..4ac5082
--- /dev/null
@@ -0,0 +1,19 @@
+<h1>Recent Changes</h1>
+<p>Recently closed changesets:</p>
+
+<table id="keyvalue" cellpadding="3">
+  <tr>
+    <th>ID</th>
+    <th>Saved at</th>
+    <th>User</th>
+    <th>Comment</th>
+    <th>Area</th>
+    <th></th>
+  </tr>
+  <%= render :partial => 'changeset', :locals => {:showusername => true},  :collection => @edits unless @edits.nil?  %>
+</table>
+
+<p>
+For more changesets, select a user and view their edits, or see the editing 'history' of a specific area.
+</p>
+<br>
diff --git a/app/views/changeset/list_bbox.rhtml b/app/views/changeset/list_bbox.rhtml
new file mode 100644 (file)
index 0000000..e1cf9ae
--- /dev/null
@@ -0,0 +1,53 @@
+<h1>History</h1>
+<%
+if @bbox!=nil
+       minlon = @bbox[0] 
+       minlat = @bbox[1] 
+       maxlon = @bbox[2]
+       maxlat = @bbox[3] 
+
+       %>
+<p>
+Changesets within the area: 
+      (<a href='/?minlon=<%= minlon %>&minlat=<%= minlat %>&maxlon=<%= maxlon %>&maxlat=<%= maxlat %>&box=yes' title='show area box'><%= format("%0.3f",minlon) -%>,<%= format("%0.3f",minlat) -%>,<%= format("%0.3f",maxlon) -%>,<%= format("%0.3f",maxlat) -%></a>) 
+
+</p>
+
+<%     if @edits.nil? or @edits.empty? %>
+<p><b>No changesets</b></p>
+<%     else %>
+
+<%= render :partial => 'changeset_paging_nav' %>
+
+<table id="keyvalue" cellpadding="3">
+  <tr>
+    <th>ID</th>
+    <th>Saved at</th>
+    <th>User</th>
+    <th>Comment</th>
+    <th>Area</th>
+    <th></th>
+  </tr>
+  <%= render :partial => 'changeset', :locals => {:showusername => true}, :collection => @edits unless @edits.nil? %>
+</table>
+
+<%= render :partial => 'changeset_paging_nav' %>
+
+<p>For all changes everywhere see <%=  link_to("Recent Changes", :controller => "browse", :action => "changesets") %> </p>
+
+<%
+       end
+
+else 
+       #bbox is nil. happens if the user surfs to this page directly.
+%>
+
+<p>No area specified</p>
+<p>First use the <a href="/" title="view the map">view tab</a> to pan and zoom to an area of interest, then click the history tab.</p>
+<p>Alternatively view all <%=  link_to("recent changes", :controller => "browse", :action => "changesets") %> </p>
+
+<%
+end
+%>
+<br>
+
diff --git a/app/views/changeset/list_user.rhtml b/app/views/changeset/list_user.rhtml
new file mode 100644 (file)
index 0000000..386423a
--- /dev/null
@@ -0,0 +1,22 @@
+<h1>Edits by <%= link_to(h(@display_name), {:controller=>'user', :action=>'view', :display_name=>@display_name}) %></h1>
+
+<% if not @edits or @edits.empty? %>
+<p><b>No visible edits by <%= h(@display_name) %>.</b></p>
+<% else %>
+<%= render :partial => 'changeset_paging_nav' %>
+<table id="keyvalue" cellpadding="3">
+  <tr>
+    <th>ID</th>
+    <th>Saved at</th>
+    <th>Comment</th>
+    <th>Area</th>
+    <th></th>
+  </tr>
+  <%= render :partial => 'changeset', :locals => {:showusername => false},  :collection => @edits %>
+</table>
+<%= render :partial => 'changeset_paging_nav' %>
+<% end %>
+
+<p>For changes by all users see <%=  link_to("Recent Changes", :controller => "browse", :action => "changesets") %> </p>
+<br>
+
index 87f5e9fac6bddce7c6509049c687e5b7082d0d49..a69f4fd9c9240bc7a666099ac806678ade8032e9 100644 (file)
@@ -5,24 +5,28 @@
 <% form_for :diary_entry do |f| %>
   <table>
     <tr valign="top">
-      <th>Subject</th>
+      <td class="fieldName">Subject:</td>
       <td><%= f.text_field :title, :size => 60 %></td>
     </tr>
     <tr valign="top">
-      <th>Body</th>
+      <td class="fieldName">Body:</td>
       <td><%= f.text_area :body, :cols => 80 %></td>
     </tr>
     <tr valign="top">
-      <th>Location</th>
+      <td class="fieldName">Location:</td>
       <td>
         <div id="map" style="border: 1px solid black; position: relative; width : 90%; height : 400px; display: none;"></div>
         <span class="location">Latitude: <%= f.text_field :latitude, :size => 20, :id => "latitude" %> Longitude: <%= f.text_field :longitude, :size => 20, :id => "longitude" %></span>
         <a href="javascript:openMap()" id="usemap">use map</a>
+        <br/><br/>
       </td>
     </tr>
     <tr>
-      <th></th>
-      <td><%= submit_tag 'Save' %></td>
+      <td></td>
+      <td>
+         <%= submit_tag 'Save' %>
+         <%# TODO: button should say 'publish' or 'save changes' depending on new/edit state %>
+      </td>
     </tr>
   </table>
 <% end %>
index 3d1f0bbcb16acf3c2270e92be3e6d5a04d939f1a..38157b183b976883282d86ba2bdcca717ccf0857 100644 (file)
@@ -4,29 +4,33 @@
  <%= image_tag url_for_file_column(@this_user, "image") %>
 <% end %>
 
-<br />
 
 <% if @this_user %>
   <% if @user == @this_user %>
-    <%= link_to 'New diary entry', :controller => 'diary_entry', :action => 'new', :display_name => @user.display_name %>
+    <%= link_to image_tag("new.png", :border=>0) + 'New diary entry', {:controller => 'diary_entry', :action => 'new', :display_name => @user.display_name}, {:title => 'Compose a new entry in your user diary'} %>
   <% end %>
 <% else %>
   <% if @user %>
-    <%= link_to 'New diary entry', :controller => 'diary_entry', :action => 'new', :display_name => @user.display_name %>
+    <%= link_to image_tag("new.png", :border=>0) + 'New diary entry', {:controller => 'diary_entry', :action => 'new', :display_name => @user.display_name}, {:title => 'Compose a new entry in your user diary'} %>
   <% end %>
 <% end %>
 
-<h3>Recent diary entries:</h3>
 
-<%= render :partial => 'diary_entry', :collection => @entries %>
+<% if @entries.empty? %>
+  <p>No diary entries</p>
+<% else %>
+  <p>Recent diary entries:</p>
 
-<%= link_to "Older Entries", { :page => @entry_pages.current.next } if @entry_pages.current.next %>
-<% if @entry_pages.current.next and @entry_pages.current.previous %>
-|
-<% end %>
-<%= link_to "Newer Entries", { :page => @entry_pages.current.previous } if @entry_pages.current.previous %>
+  <hr />
 
-<br />
+  <%= render :partial => 'diary_entry', :collection => @entries %>
+       
+  <%= link_to "Older Entries", { :page => @entry_pages.current.next } if @entry_pages.current.next %>
+  <% if @entry_pages.current.next and @entry_pages.current.previous %>|<% end %>
+  <%= link_to "Newer Entries", { :page => @entry_pages.current.previous } if @entry_pages.current.previous %>
+
+  <br />
+<% end %>
 
 <%= rss_link_to :action => 'rss' %>
 
diff --git a/app/views/diary_entry/no_such_entry.rhtml b/app/views/diary_entry/no_such_entry.rhtml
new file mode 100644 (file)
index 0000000..1ebcf26
--- /dev/null
@@ -0,0 +1,2 @@
+<h2>No entry with the id: <%= h(params[:id]) %></h2>
+<p>Sorry, there is no diary entry or comment with the id <%=h params[:id] -%>, or no id was given. Please check your spelling, or maybe the link you clicked is wrong.</p>
index ff17fcf1d3c071cbe798e2fbda5c0a6f173af6cc..534003031a4d9903fc522333b32d6cf2d505632b 100644 (file)
@@ -7,6 +7,7 @@
     <%= stylesheet_link_tag 'site' %>
     <%= stylesheet_link_tag 'print', :media => "print" %>
     <%= tag("link", { :rel => "search", :type => "application/opensearchdescription+xml", :title => "OpenStreetMap Search", :href => "/opensearch/osm.xml" }) %>
+    <%= tag("meta", { :name => "description", :content => "OpenStreetMap is the free wiki world map." }) %>
     <%= yield :head %>
     <title>OpenStreetMap<%= ' | '+ h(@title) if @title %></title>
   </head>
         <%
         viewclass = ''
         editclass = ''
+        historyclass = ''
         exportclass = ''
         traceclass = ''
         viewclass = 'active' if params['controller'] == 'site' and params['action'] == 'index' 
         editclass = 'active' if params['controller'] == 'site' and params['action'] == 'edit' 
+        historyclass = 'active' if params['controller'] == 'changeset' and params['action'] == 'list_bbox' 
         exportclass = 'active' if params['controller'] == 'site' and params['action'] == 'export'
         traceclass = 'active' if params['controller'] == 'trace'
         diaryclass = 'active' if params['controller'] == 'diary_entry'
         %>
         <li><%= link_to 'View', {:controller => 'site', :action => 'index'}, {:id => 'viewanchor', :title => 'view maps', :class => viewclass} %></li>
         <li><%= link_to 'Edit', {:controller => 'site', :action => 'edit'}, {:id => 'editanchor', :title => 'edit maps', :class => editclass} %></li>
+        <li><%= link_to 'History', {:controller => 'history' }, {:id => 'historyanchor', :title => 'changeset history', :class => historyclass} %></li>
         <% if params['controller'] == 'site' and (params['action'] == 'index' or params['action'] == 'export') %>
         <li><%= link_to_remote 'Export', {:url => {:controller => 'export', :action => 'start'}}, {:id => 'exportanchor', :title => 'export map data', :class => exportclass, :href => url_for(:controller => 'site', :action => 'export')} %></li>
         <% else %>
         </div>
 
         <div id="cclogo" class="button" style="width: 88px">
-          <a href="http://creativecommons.org/licenses/by-sa/2.0/"><img src="/images/cc_button.png" border="0" alt="" /></a>
+          <%= link_to image_tag("cc_button.png", :alt => "CC by-sa 2.0", :border => "0"), "http://creativecommons.org/licenses/by-sa/2.0/" %>
         </div>
       </center>
     </div>
index 6d45d33dd33f0eefca339ad37039ea59d35c9b2f..263e30e64089d2b93e25bd29d89b8bdac848ae70 100644 (file)
@@ -1,9 +1,10 @@
-<% this_colour = cycle('lightgrey', 'white') # can only call once for some dumb reason %>
+<% this_colour = cycle('lightgrey', 'white') # can only call once for some dumb reason 
+%>
 
 <tr class="inbox-row<%= "-unread" if not message_summary.message_read? %>">
-  <td class="inbox-sender" bgcolor='<%= this_colour %>'><%= link_to h(message_summary.sender.display_name), :controller => 'user', :action => message_summary.sender.display_name %></td>
-  <td class="inbox-subject" bgcolor='<%= this_colour %>'><%= link_to h(message_summary.title), :controller => 'message', :action => 'read', :message_id => message_summary.id  %></td>
-  <td class="inbox-sent" bgcolor='<%= this_colour %>'><%= message_summary.sent_on %></td>
+  <td class="inbox-sender" bgcolor="<%= this_colour %>"><%= link_to h(message_summary.sender.display_name), :controller => 'user', :action => message_summary.sender.display_name %></td>
+  <td class="inbox-subject" bgcolor="<%= this_colour %>"><%= link_to h(message_summary.title), :controller => 'message', :action => 'read', :message_id => message_summary.id  %></td>
+  <td class="inbox-sent" bgcolor="<%= this_colour %>"><%= message_summary.sent_on %></td>
   <% if message_summary.message_read? %>
     <td><%= button_to 'Mark as unread', :controller => 'message', :action => 'mark', :message_id => message_summary.id, :mark => 'unread' %></td>
   <% else %>
index f0d87aa27c98f16f348a88dd2b90ccf8f0abb451..91fafe901347fbb0aed847c2f328523a645e4ee4 100644 (file)
@@ -1,7 +1,8 @@
-<% this_colour = cycle('lightgrey', 'white') # can only call once for some dumb reason %>
+<% this_colour = cycle('lightgrey', 'white') # can only call once for some dumb reason
+%>
 
 <tr class="inbox-row">
-  <td class="inbox-sender" bgcolor='<%= this_colour %>'><%= link_to h(sent_message_summary.recipient.display_name), :controller => 'user', :action => sent_message_summary.recipient.display_name %></td>
-  <td class="inbox-subject" bgcolor='<%= this_colour %>'><%= link_to h(sent_message_summary.title), :controller => 'message', :action => 'read', :message_id => sent_message_summary.id  %></td>
-  <td class="inbox-sent" bgcolor='<%= this_colour %>'><%= sent_message_summary.sent_on %></td>
+  <td class="inbox-sender" bgcolor="<%= this_colour %>"><%= link_to h(sent_message_summary.recipient.display_name), :controller => 'user', :action => sent_message_summary.recipient.display_name %></td>
+  <td class="inbox-subject" bgcolor="<%= this_colour %>"><%= link_to h(sent_message_summary.title), :controller => 'message', :action => 'read', :message_id => sent_message_summary.id  %></td>
+  <td class="inbox-sent" bgcolor="<%= this_colour %>"><%= sent_message_summary.sent_on %></td>
 </tr>
index d7bb18f8e59461492982ab5ec2ccea114a2b5499..17f3588bb47b4b3a6668a0c06111066eea6674b5 100644 (file)
@@ -1,7 +1,4 @@
-<% user_id = params[:user_id] || @user_id %>
-<% display_name = User.find_by_id(user_id).display_name %>
-
-<h2>Send a new message to <%= h(display_name) %></h2>
+<h2>Send a new message to <%= h(@to_user.display_name) %></h2>
 
 <% if params[:display_name] %>
 <p>Writing a new message to <%= h(params[:display_name]) %></p>  
@@ -10,7 +7,7 @@
 
 <%= error_messages_for 'message' %>
 
-<% form_for :message, :url => { :action => "new", :user_id => user_id } do |f| %>
+<% form_for :message, :url => { :action => "new", :user_id => @to_user.id } do |f| %>
   <table>
     <tr valign="top">
       <th>Subject</th>
diff --git a/app/views/message/no_such_user.rhtml b/app/views/message/no_such_user.rhtml
new file mode 100644 (file)
index 0000000..c18733a
--- /dev/null
@@ -0,0 +1,2 @@
+<h1>No such user or message</h1>
+<p>Sorry there is no user or message with that name or id</p>
index 7d80a1705be3c51473f16589e7afddfcc114c5b1..e341305f591cad809733c2ca1e14e21f4ceb257c 100644 (file)
 <%= render :partial => 'sidebar', :locals => { :onopen => "resizeMap();", :onclose => "resizeMap();" } %>
 <%= render :partial => 'search' %>
 
-<% session[:token] = @user.tokens.create.token unless session[:token] %>
-
-<% if params['mlon'] and params['mlat'] %>
-       <% lon =  h(params['mlon']) %>
-       <% lat =  h(params['mlat'])  %>
-       <% zoom =  h(params['zoom'] || '14') %>
-<% elsif @user and params['lon'].nil? and params['lat'].nil? and params['gpx'].nil? %> 
-       <% lon =  @user.home_lon %>
-       <% lat =  @user.home_lat %>
-       <% zoom = '14' %>
-<% else %>
-       <% lon =  h(params['lon'] || 'null') %>
-       <% lat =  h(params['lat'] || 'null') %>
-       <% zoom =  h(params['zoom'] || '14') %>
-<% end %>
+<%
+session[:token] = @user.tokens.create.token unless session[:token]
+
+# Decide on a lat lon to initialise potlatch with. Various ways of doing this
+if params['lon'] and params['lat']
+       lon =  h(params['lon']) 
+       lat =  h(params['lat']) 
+       zoom =  h(params['zoom'])
+
+elsif params['mlon'] and params['mlat'] 
+       lon =  h(params['mlon']) 
+       lat =  h(params['mlat']) 
+       zoom =  h(params['zoom'])
+       
+elsif params['gpx']
+       #use gpx id to locate (dealt with below)
+       
+elsif cookies.key?("_osm_location")
+       lon,lat,zoom,layers = cookies["_osm_location"].split("|")
+       
+elsif @user and !@user.home_lon.nil? and !@user.home_lat.nil? 
+       lon =  @user.home_lon
+       lat =  @user.home_lat
+
+else
+       #catch all.  Do nothing.  lat=nil, lon=nil
+       #Currently this results in potlatch starting up at 0,0 (Atlantic ocean).
+end
+
+zoom='14' if zoom.nil?
+%>
 
 <div id="map">
        You need a Flash player to use Potlatch, the
index 0c6488129740669ba92bf97700e8a1f622b979af..54f28172739f2521a33a6dfd4bddc098eb3b4230 100644 (file)
   </table>
 </div>
 
-<% if params['mlon'] and params['mlat'] %>
-<% marker = true %>
-<% mlon = h(params['mlon']) %> 
-<% mlat = h(params['mlat']) %>
-<% end %>
-
-<% if params['minlon'] and params['minlat'] and params['maxlon'] and params['maxlat'] %>
-<% bbox = true %>
-<% minlon = h(params['minlon']) %>
-<% minlat = h(params['minlat']) %>
-<% maxlon = h(params['maxlon']) %>
-<% maxlat = h(params['maxlat']) %>
-<% end %>
-
-<% if params['lon'] and params['lat'] %>
-<% lon =  h(params['lon']) %>
-<% lat =  h(params['lat']) %>
-<% zoom =  h(params['zoom'] || '5') %>
-<% layers = h(params['layers']) %>
-<% elsif params['mlon'] and params['mlat'] %>
-<% lon = h(params['mlon']) %> 
-<% lat = h(params['mlat']) %>
-<% zoom =  h(params['zoom'] || '12') %>
-<% layers = h(params['layers']) %>
-<% elsif cookies.key?("_osm_location") %>
-<% lon,lat,zoom,layers = cookies["_osm_location"].split("|") %>
-<% elsif @user and !@user.home_lon.nil? and !@user.home_lat.nil? %> 
-<% lon =  @user.home_lon %>
-<% lat =  @user.home_lat %>
-<% zoom = '10' %>
-<% else %>
-<% session[:location] = OSM::IPLocation(request.env['REMOTE_ADDR']) unless session[:location] %>
-<% if session[:location] %>
-<% bbox = true %>
-<% minlon = session[:location][:minlon] %>
-<% minlat = session[:location][:minlat] %>
-<% maxlon = session[:location][:maxlon] %>
-<% maxlat = session[:location][:maxlat] %>
-<% else %>
-<% lon =  '-0.1' %>
-<% lat =  '51.5' %>
-<% zoom =  h(params['zoom'] || '5') %>
-<% end %>
-<% layers = h(params['layers']) %>
-<% end %>
+<%
+if params['mlon'] and params['mlat'] 
+       marker = true
+       mlon = h(params['mlon'])
+       mlat = h(params['mlat'])
+end
+
+if params['minlon'] and params['minlat'] and params['maxlon'] and params['maxlat']
+       bbox = true
+       minlon = h(params['minlon'])
+       minlat = h(params['minlat'])
+       maxlon = h(params['maxlon'])
+       maxlat = h(params['maxlat'])
+       box = true if params['box']=="yes" 
+end
+
+# Decide on a lat lon to initialise the map with. Various ways of doing this
+if params['lon'] and params['lat'] 
+       lon =  h(params['lon'])
+       lat =  h(params['lat'])
+       zoom =  h(params['zoom'] || '5')
+       layers = h(params['layers'])
+       
+elsif params['mlon'] and params['mlat']
+       lon = h(params['mlon']) 
+       lat = h(params['mlat'])
+       zoom =  h(params['zoom'] || '12')
+       layers = h(params['layers'])
+       
+elsif cookies.key?("_osm_location")
+       lon,lat,zoom,layers = cookies["_osm_location"].split("|")
+       
+elsif @user and !@user.home_lon.nil? and !@user.home_lat.nil? 
+       lon =  @user.home_lon
+       lat =  @user.home_lat
+       zoom = '10'
+else
+       session[:location] = OSM::IPLocation(request.env['REMOTE_ADDR']) unless session[:location]
+
+       if session[:location]
+               bbox = true
+               minlon = session[:location][:minlon]
+               minlat = session[:location][:minlat]
+               maxlon = session[:location][:maxlon]
+               maxlat = session[:location][:maxlat]
+       else
+               lon =  '-0.1' 
+               lat =  '51.5' 
+               zoom =  h(params['zoom'] || '5') 
+       end
+       layers = h(params['layers']) 
+end
+%>
 
 <%= javascript_include_tag '/openlayers/OpenLayers.js' %>
 <%= javascript_include_tag '/openlayers/OpenStreetMap.js' %>
     var bbox = new OpenLayers.Bounds(<%= minlon %>, <%= minlat %>, <%= maxlon %>, <%= maxlat %>);
 
     setMapExtent(bbox);
+    <%    if box %>
+    // IE requires Vector layers be initialised on page load, and not under deferred script conditions
+    Event.observe(window, 'load', function() {addBoxToMap(bbox)});
+    <%    end %>
     <% else %>
     var centre = new OpenLayers.LonLat(<%= lon %>, <%= lat %>);
     var zoom = <%= zoom %>;
 
-    <% if params['scale'] and params['scale'].length > 0 then %>
+    <%    if params['scale'] and params['scale'].length > 0 then %>
     zoom = scaleToZoom(<%= params['scale'].to_f() %>);
-    <% end %>
+    <%    end %>
 
     setMapCenter(centre, zoom);
     <% end %>
     var lonlat = getMapCenter();
     var zoom = map.getZoom();
     var layers = getMapLayers();
+    var extents = getMapExtent();
 
-    updatelinks(lonlat.lon, lonlat.lat, zoom, layers);
+    updatelinks(lonlat.lon, lonlat.lat, zoom, layers, extents);
 
     document.cookie = "_osm_location=" + lonlat.lon + "|" + lonlat.lat + "|" + zoom + "|" + layers;
   }
index f739d90532c7a46a5a995a4a7e2d931887caf416..cab6f7124504320d1eb19b8920bac3971b980e5b 100644 (file)
@@ -17,9 +17,9 @@
   <% end %>
 </p>
 
-<% if @user and @user.traces.count(:conditions => "inserted=0") > 4 %>
+<% if @user and @user.traces.count(:conditions => ["inserted=?", false]) > 4 %>
   <p>
-    You have <%= @user.traces.count(:conditions => "inserted=0") %> traces 
+    You have <%= @user.traces.count(:conditions => ["inserted=?", false]) %> traces 
     waiting for upload. Please consider waiting for these to finish before 
     uploading any more, so as not to block the queue for other users.
   </p>
diff --git a/app/views/trace/no_such_user.rhtml b/app/views/trace/no_such_user.rhtml
new file mode 100644 (file)
index 0000000..7820b48
--- /dev/null
@@ -0,0 +1,2 @@
+<h2><%= h(@not_found_user) %></h2>
+<p>Sorry, there is no user with the name <%= @not_found_user -%>. Please check your spelling, or maybe the link you clicked is wrong.</p>
index 9bdb39aae50796a7d802dbdc22a4e5bd7ca315a3..33d6e1f8566c1665c52b58a50bc2e8d98c438f3b 100644 (file)
@@ -56,7 +56,9 @@
       var nearmarker = addMarkerToMap(new OpenLayers.LonLat(nearest[i].home_lon, nearest[i].home_lat), near_icon.clone(), description);
     }
 
-    map.events.register("click", map, setHome);
+    if (document.getElementById('updatehome')) {
+      map.events.register("click", map, setHome);
+    }
   }        
 
   function setHome( e ) { 
index 501af7494102b7b13a3de2d7cea3347c5084ab1c..178a0e8dc3682997c0cc1878290e7366ddd97aac 100644 (file)
@@ -1,34 +1,51 @@
-<h2>User details</h2>
+<h2>My settings</h2>
 <%= error_messages_for 'user' %>
 <% form_for :user, @user do |f| %>
-<table style="width : 100%">
-  <tr><td>Email</td><td><%= f.text_field :email %></td></tr>
-  <tr><td>Mapper since</td><td><%= @user.creation_time %> (<%= time_ago_in_words(@user.creation_time) %> ago)</td></tr>
-  <tr><td>Display Name</td><td><%= f.text_field :display_name %></td></tr>
-  <tr><td>Password</td><td><%= f.password_field :pass_crypt, {:value => '', :size => 50, :maxlength => 255} %></td></tr>
-  <tr><td>Confirm Password</td><td><%= f.password_field :pass_crypt_confirmation, {:value => '', :size => 50, :maxlength => 255} %></td></tr>
+<table id="accountForm">
+  <tr><td class="fieldName">Display Name : </td><td><%= f.text_field :display_name %></td></tr>
+  <tr><td class="fieldName">Email : </td><td><%= f.text_field :email, {:size => 50, :maxlength => 255} %> <span class="minorNote">(never displayed publicly)</span></td></tr>
+  <tr><td class="fieldName" style="padding-bottom:0px;">Password : </td><td style="padding-bottom:0px;"><%= f.password_field :pass_crypt, {:value => '', :size => 30, :maxlength => 255} %></td></tr>
+  <tr><td class="fieldName">Confirm Password : </td><td><%= f.password_field :pass_crypt_confirmation, {:value => '', :size => 30, :maxlength => 255} %></td></tr>
 
-  <tr><td valign="top">Description</td><td><%= f.text_area :description, :class => "editDescription" %></td></tr>
+  <tr>
+  <td class="fieldName" valign="top">Public editing :</td>
+  <td>
+<% if @user.data_public? %>
+  Enabled. Not anonymous <span class="minorNote">(<a href="http://wiki.openstreetmap.org/index.php/Disabling_anonymous_edits" target="_new">what's this?</a>)</span>
+<% else %>
+  Disabled and anonymous. <span class="minorNote">(<a href="#public">why's this bad?</a>)</span>
+<% end %>
+  </td>
+  </tr>
+
+  <tr><td class="fieldName" valign="top">Profile Description : </td><td><%= f.text_area :description, :rows => '5', :cols => '60' %><br /><br /></td></tr>
 
-  <tr id="homerow" <% unless @user.home_lat and @user.home_lon %> class="nohome" <%end%> ><td>Your home</td><td><em class="message">You have not entered your home location.</em><span class="location">Latitude: <%= f.text_field :home_lat, :size => 20, :id => "home_lat" %> Longitude <%= f.text_field :home_lon, :size => 20, :id => "home_lon" %></span></td></tr>
+  <tr id="homerow" <% unless @user.home_lat and @user.home_lon %> class="nohome" <%end%> ><td class="fieldName">Home Location : </td><td><em class="message">You have not entered your home location.</em><span class="location">Latitude: <%= f.text_field :home_lat, :size => 20, :id => "home_lat" %> Longitude <%= f.text_field :home_lon, :size => 20, :id => "home_lon" %></span></td></tr>
 
   <tr><td></td><td>
   <p>Update home location when I click on the map? <input type="checkbox" value="1" <% unless @user.home_lat and @user.home_lon %> checked="checked" <% end %> id="updatehome" /> </p>
-  <div id="map" style="border: 1px solid black; position: relative; width : 90%; height : 400px;"></div>
+  <div id="map" style="border:1px solid black; position:relative; width:500px; height:400px;"></div>
   </td></tr>
+  
+  <tr><td></td><td align=right><br/></br><%= submit_tag 'Save Changes' %></td></tr>
 </table>
-<%= submit_tag 'Save Changes' %>
+<br/>
+
 <% end %>
 
 <%= render :partial => 'friend_map' %>
-
+<% unless @user.data_public? %>
+<a name="public"></a>
 <h2>Public editing</h2>
-<% if @user.data_public? %>
-  All your edits are public.
-<% else %>
-Currently your edits are anonymous and people can't send you messages or see your location. To show what you edited and allow people to contact you through the website, click the button below.
-<b>You will need to do this if you want to use the online editor and it is encouraged</b> (<a href="http://wiki.openstreetmap.org/index.php/Disabling_anonymous_edits">find out why</a>).
-This action cannot be reversed and all new users are now public by default.
+  Currently your edits are anonymous and people can't send you messages or see your location. To show what you edited and allow people to contact you through the website, click the button below.
+  <b>You will need to do this if you want to use the online editor and it is encouraged to do so</b> (<a href="http://wiki.openstreetmap.org/index.php/Disabling_anonymous_edits">find out why</a>).<br />
+  Your email address will not be revealed by becoming public.<br />
+  This action cannot be reversed and all new users are now public by default.<br />
   <br /><br />
   <%= button_to "Make all my edits public", :action => :go_public %>
 <% end %>
+<br/>
+<br/>
+<%= link_to 'return to profile', :controller => 'user', :action => @user.display_name %>
+<br/>
+<br/>
index ff988f070be1ff76b97a4e1811e8306ac61fc714..0422d213258f76539b5a15c24228b3e50a576a58 100644 (file)
@@ -1,13 +1,13 @@
-<h1>Login:</h1><br />
-Please login or <%= link_to 'create an account', :controller => 'user', :action => 'new' %>.<br />
+<h1>Login</h1>
+
+<p>Please login or <%= link_to 'create an account', :controller => 'user', :action => 'new' %>.</p>
 
 <% form_tag :action => 'login' do %>
 <%= hidden_field_tag('referer', h(params[:referer])) %>
 <table>
-  <tr><td>Email Address or username:</td><td><%= text_field('user', 'email',{:size => 50, :maxlength => 255}) %></td></tr>
-  <tr><td>Password:</td><td><%= password_field('user', 'password',{:size => 50, :maxlength => 255}) %></td></tr>
+  <tr><td class="fieldName">Email Address or Username:</td><td><%= text_field('user', 'email',{:size => 50, :maxlength => 255, :tabindex => 1}) %></td></tr>
+  <tr><td class="fieldName">Password:</td><td><%= password_field('user', 'password',{:size => 28, :maxlength => 255, :tabindex => 2}) %> <span class="minorNote">(<%= link_to 'Lost your password?', :controller => 'user', :action => 'lost_password' %>)</span></td></tr>
+  <tr><td colspan=2>&nbsp;<!--vertical spacer--></td></tr>
+  <tr><td></td><td align="right"><%= submit_tag 'Login', :tabindex => 3 %></td></tr>
 </table>
-
-<br />
-<%= submit_tag 'Login' %>
-<% end %> (<%= link_to 'Lost your password?', :controller => 'user', :action => 'lost_password' %>)
+<% end %>
index d0b5a9667174d51e93104bf5e3d83627d51fe6a6..a19582fa4588755d78c8a9db101987a2103f0f52 100644 (file)
@@ -1,4 +1,4 @@
-<h1>Create a user account</h1>
+<h1>Create a User Account</h1>
 
 <% if Acl.find_by_address(request.remote_ip, :conditions => {:k => "no_account_creation"}) %>
 
 <%= error_messages_for 'user' %>
 
 <% form_tag :action => 'save' do %>
-<table>
-  <tr><td>Email Address</td><td><%= text_field('user', 'email',{:size => 50, :maxlength => 255}) %></td></tr>
-  <tr><td>Confirm Email Address</td><td><%= text_field('user', 'email_confirmation',{:size => 50, :maxlength => 255}) %></td></tr>
-  <tr><td>Display Name</td><td><%= text_field('user', 'display_name',{:size => 50, :maxlength => 255}) %></td></tr>
-  <tr><td>Password</td><td><%= password_field('user', 'pass_crypt',{:size => 50, :maxlength => 255}) %></td></tr>
-  <tr><td>Confirm Password</td><td><%= password_field('user', 'pass_crypt_confirmation',{:size => 50, :maxlength => 255}) %></td></tr>
+<table id="loginForm">
+  <tr><td class="fieldName">Email Address : </td><td><%= text_field('user', 'email',{:size => 50, :maxlength => 255, :tabindex => 1}) %></td></tr>
+  <tr><td class="fieldName">Confirm Email Address : </td><td><%= text_field('user', 'email_confirmation',{:size => 50, :maxlength => 255, :tabindex => 2}) %></td></tr>
+  <tr><td></td><td><span class="minorNote">Not displayed publicly (see <a href="http://wiki.openstreetmap.org/index.php/Privacy_Policy" title="wiki privacy policy including section on email addresses">privacy policy)</span></td></tr>
+  <tr><td colspan=2>&nbsp;<!--vertical spacer--></td></tr>
+  <tr><td class="fieldName">Display Name : </td><td><%= text_field('user', 'display_name',{:size => 30, :maxlength => 255, :tabindex => 3}) %></td></tr>
+  <tr><td colspan=2>&nbsp;<!--vertical spacer--></td></tr>
+  <tr><td class="fieldName">Password : </td><td><%= password_field('user', 'pass_crypt',{:size => 30, :maxlength => 255, :tabindex => 4}) %></td></tr>
+  <tr><td class="fieldName">Confirm Password : </td><td><%= password_field('user', 'pass_crypt_confirmation',{:size => 30, :maxlength => 255, :tabindex => 5}) %></td></tr>
+  
+  <tr><td colspan=2>&nbsp;<!--vertical spacer--></td></tr>
+  <tr><td></td><td align=right><input type="submit" value="Signup" tabindex="6"></td></tr>
 </table>
-<br>
-<br>
-<input type="submit" value="Signup">
-
 <% end %>
 
 <% end %>
index 438de836d463be146c4364213471b9674fb1244c..c76ca23ae620055bdbe1db9aca8569bfaf371468 100644 (file)
@@ -2,13 +2,17 @@
 <h2><%= h(@this_user.display_name) %></h2>
 <div id="userinformation">
 <% if @user and @this_user.id == @user.id %>
+<!-- Displaying user's own profile page -->
 <%= link_to 'my diary', :controller => 'diary_entry', :action => 'list', :display_name => @user.display_name %>
 | <%= link_to 'new diary entry', :controller => 'diary_entry', :action => 'new', :display_name => @user.display_name %>
+| <%= link_to 'my edits', :controller => 'changeset', :action => 'list_user', :display_name => @user.display_name %>
 | <%= link_to 'my traces', :controller => 'trace', :action=>'mine' %>
 | <%= link_to 'my settings', :controller => 'user', :action => 'account', :display_name => @user.display_name %>
 <% else %>
+<!-- Displaying another user's profile page -->
 <%= link_to 'send message', :controller => 'message', :action => 'new', :user_id => @this_user.id %>
 | <%= link_to 'diary', :controller => 'diary_entry', :action => 'list', :display_name => @this_user.display_name %>
+| <%= link_to 'edits', :controller => 'changeset', :action => 'list_user', :display_name => @this_user.display_name %>
 | <%= link_to 'traces', :controller => 'trace', :action => 'view', :display_name => @this_user.display_name %>
 | <% if @user and @user.is_friends_with?(@this_user) %>
   <%= link_to 'remove as friend', :controller => 'user', :action => 'remove_friend', :display_name => @this_user.display_name %>
 <% end %>
 </div>
 
+<% if @this_user != nil %>
+<P>
+<b>Mapper since : </b><%= @this_user.creation_time %> (<%= time_ago_in_words(@this_user.creation_time) %> ago)
+</P>
+<% end %>
+  
 <h3>User image</h3>
 <% if @this_user.image %>
   <%= image_tag url_for_file_column(@this_user, "image") %>
     <% end %>
   <% end %>
 <% end %>
+
+<br/>
+<br/>
+<% if @user and @this_user.id == @user.id %>
+<%= link_to 'change your settings', :controller => 'user', :action => 'account', :display_name => @user.display_name %>
+<% end %>
index 85ebe9f2152884b044c2f73c136ffaee0cd642fa..7400a7b9a916088639aa0a092198f9c3000d82f4 100644 (file)
@@ -3,8 +3,10 @@ standard_settings: &standard_settings
   max_request_area: 0.25
   # Number of GPS trace/trackpoints returned per-page
   tracepoints_per_page: 5000
-  # Maximum number of nodes
+  # Maximum number of nodes that will be returned by the api in a map request
   max_number_of_nodes: 50000
+  # Maximum number of nodes that can be in a way (checked on save)
+  max_number_of_way_nodes: 2000
  
 development:
   <<: *standard_settings
index b884f3b938fea8c5ea541e0d4af5fbbc16529dd7..cc3f9a1a5fb26e4fd5b2186728c3e4d5c6b45127 100644 (file)
@@ -16,6 +16,7 @@ development:
   username: openstreetmap
   password: openstreetmap
   host: localhost
+  encoding: utf8
 
 # Warning: The database defined as 'test' will be erased and
 # re-generated from your development database when you run 'rake'.
@@ -23,14 +24,15 @@ development:
 test:
   adapter: mysql
   database: osm_test
-  username: root
-  password:
+  username: osm_test
+  password: osm_test
   host: localhost
+  encoding: utf8
 
 production:
   adapter: mysql
-  database: openstreetmap
-  username: openstreetmap
-  password: openstreetmap
-  host: db.openstreetmap.org
-
+  database: osm
+  username: osm
+  password: osm
+  host: localhost
+  encoding: utf8
index 930fc81e9bdbf30143b82bf6f7bc2c66c33e2269..7f83fd4ad7f87670a6b4e17a770bcce4345796e0 100644 (file)
@@ -5,13 +5,16 @@
 ENV['RAILS_ENV'] ||= 'production'
 
 # Specifies gem version of Rails to use when vendor/rails is not present
-RAILS_GEM_VERSION = '2.0.2' unless defined? RAILS_GEM_VERSION
+RAILS_GEM_VERSION = '2.1.2' unless defined? RAILS_GEM_VERSION
 
 # Set the server URL
 SERVER_URL = ENV['OSM_SERVER_URL'] || 'www.openstreetmap.org'
 
+# Set the generator
+GENERATOR = ENV['OSM_SERVER_GENERATOR'] || 'OpenStreetMap server'
+
 # Application constants needed for routes.rb - must go before Initializer call
-API_VERSION = ENV['OSM_API_VERSION'] || '0.5'
+API_VERSION = ENV['OSM_API_VERSION'] || '0.6'
 
 # Set application status - possible settings are:
 #
@@ -38,6 +41,16 @@ Rails::Initializer.run do |config|
     config.frameworks -= [ :active_record ]
   end
 
+  # Specify gems that this application depends on. 
+  # They can then be installed with "rake gems:install" on new installations.
+  # config.gem "bj"
+  # config.gem "hpricot", :version => '0.6', :source => "http://code.whytheluckystiff.net"
+  # config.gem "aws-s3", :lib => "aws/s3"
+  config.gem 'composite_primary_keys', :version => '1.1.0'
+  config.gem 'libxml-ruby', :version => '>= 1.1.1', :lib => 'libxml'
+  config.gem 'rmagick', :lib => 'RMagick'
+  config.gem 'mysql'
+
   # Only load the plugins named here, in the order given. By default, all plugins 
   # in vendor/plugins are loaded in alphabetical order.
   # :all can be used as a placeholder for all plugins not explicitly named
@@ -64,6 +77,12 @@ Rails::Initializer.run do |config|
   # (create the session table with 'rake db:sessions:create')
   config.action_controller.session_store = :sql_session_store
 
+  # We will use the old style of migrations, rather than the newer
+  # timestamped migrations that were introduced with Rails 2.1, as
+  # it will be confusing to have the numbered and timestamped migrations
+  # together in the same folder.
+  config.active_record.timestamped_migrations = false
+
   # Use SQL instead of Active Record's schema dumper when creating the test database.
   # This is necessary if your schema can't be completely dumped by the schema dumper,
   # like if you have constraints or database-specific column types
@@ -73,5 +92,5 @@ Rails::Initializer.run do |config|
   # config.active_record.observers = :cacher, :garbage_collector
 
   # Make Active Record use UTC-base instead of local time
-  config.active_record.default_timezone = :utc
+  config.active_record.default_timezone = :utc
 end
index 09a451f9a336aa17352c3421db1cc593c593155d..d67452f0c8680c25376243a069ff3527582d7a31 100644 (file)
@@ -12,7 +12,6 @@ config.whiny_nils = true
 config.action_controller.consider_all_requests_local = true
 config.action_view.debug_rjs                         = true
 config.action_controller.perform_caching             = false
-config.action_view.cache_template_extensions         = false
 
 # Don't care if the mailer can't send
-config.action_mailer.raise_delivery_errors = false
\ No newline at end of file
+config.action_mailer.raise_delivery_errors = false
diff --git a/config/initializers/composite_primary_keys.rb b/config/initializers/composite_primary_keys.rb
deleted file mode 100644 (file)
index 430bcfa..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-require 'rubygems'
-gem 'composite_primary_keys', '= 0.9.93'
-require 'composite_primary_keys'
index d9d34714be84a86a3f0e51bef919116bbfbada46..07f79660f8a8b13107a3bc84dd6f41430b960200 100644 (file)
@@ -1,7 +1,3 @@
-require 'rubygems'
-gem 'libxml-ruby', '>= 1.1.1'
-require 'libxml'
-
 # This is required otherwise libxml writes out memory errors to
 # the standard output and exits uncleanly
 LibXML::XML::Error.set_handler do |message|
index fd6c11d4447b2f66bfae1fcf8e861369201e9b2c..986037afe3d107d047be03a5c192c57e24b345d0 100644 (file)
@@ -1,2 +1,7 @@
-# Use the MySQL interface for SqlSessionStore
-SqlSessionStore.session_class = MysqlSession
+# Work out which session store adapter to use
+environment = Rails.configuration.environment
+adapter = Rails.configuration.database_configuration[environment]["adapter"]
+session_class = adapter + "_session"
+
+# Configure SqlSessionStore
+SqlSessionStore.session_class = session_class.camelize.constantize
index c54247cb162ce62606493ffdd924046ec3cd4ea3..ec1c2de03e6e7238b9d38c944da9b2ab1593d8ca 100644 (file)
@@ -12,6 +12,7 @@ secondary             0xFDBF6F        1       -
 tertiary               0xFEFECB        1       -
 unclassified   0xE8E8E8        1       -
 residential            0xE8E8E8        1       -
+road                   0xAAAAAA        1       -
 footway                        0xFF6644        -       -
 cycleway               0xFF6644        -       -
 bridleway              0xFF6644        -       -
index 6cbcb4bc328e7e8774f3e8a7a560ea72d8673577..fcf22bced98d12cd21a18101bc8b5073174917d6 100644 (file)
@@ -2,11 +2,21 @@ ActionController::Routing::Routes.draw do |map|
 
   # API
   map.connect "api/capabilities", :controller => 'api', :action => 'capabilities'
-
+  
+  map.connect "api/#{API_VERSION}/changeset/create", :controller => 'changeset', :action => 'create'
+  map.connect "api/#{API_VERSION}/changeset/:id/upload", :controller => 'changeset', :action => 'upload', :id => /\d+/
+  map.connect "api/#{API_VERSION}/changeset/:id/download", :controller => 'changeset', :action => 'download', :id => /\d+/
+  map.connect "api/#{API_VERSION}/changeset/:id/expand_bbox", :controller => 'changeset', :action => 'expand_bbox', :id => /\d+/
+  map.connect "api/#{API_VERSION}/changeset/:id", :controller => 'changeset', :action => 'read', :id => /\d+/, :conditions => { :method => :get }
+  map.connect "api/#{API_VERSION}/changeset/:id", :controller => 'changeset', :action => 'update', :id => /\d+/, :conditions => { :method => :put }
+  map.connect "api/#{API_VERSION}/changeset/:id/close", :controller => 'changeset', :action => 'close', :id =>/\d+/
+  map.connect "api/#{API_VERSION}/changesets", :controller => 'changeset', :action => 'query'
+  
   map.connect "api/#{API_VERSION}/node/create", :controller => 'node', :action => 'create'
   map.connect "api/#{API_VERSION}/node/:id/ways", :controller => 'way', :action => 'ways_for_node', :id => /\d+/
   map.connect "api/#{API_VERSION}/node/:id/relations", :controller => 'relation', :action => 'relations_for_node', :id => /\d+/
   map.connect "api/#{API_VERSION}/node/:id/history", :controller => 'old_node', :action => 'history', :id => /\d+/
+  map.connect "api/#{API_VERSION}/node/:id/:version", :controller => 'old_node', :action => 'version', :id => /\d+/, :version => /\d+/
   map.connect "api/#{API_VERSION}/node/:id", :controller => 'node', :action => 'read', :id => /\d+/, :conditions => { :method => :get }
   map.connect "api/#{API_VERSION}/node/:id", :controller => 'node', :action => 'update', :id => /\d+/, :conditions => { :method => :put }
   map.connect "api/#{API_VERSION}/node/:id", :controller => 'node', :action => 'delete', :id => /\d+/, :conditions => { :method => :delete }
@@ -16,12 +26,12 @@ ActionController::Routing::Routes.draw do |map|
   map.connect "api/#{API_VERSION}/way/:id/history", :controller => 'old_way', :action => 'history', :id => /\d+/
   map.connect "api/#{API_VERSION}/way/:id/full", :controller => 'way', :action => 'full', :id => /\d+/
   map.connect "api/#{API_VERSION}/way/:id/relations", :controller => 'relation', :action => 'relations_for_way', :id => /\d+/
+  map.connect "api/#{API_VERSION}/way/:id/:version", :controller => 'old_way', :action => 'version', :id => /\d+/, :version => /\d+/
   map.connect "api/#{API_VERSION}/way/:id", :controller => 'way', :action => 'read', :id => /\d+/, :conditions => { :method => :get }
   map.connect "api/#{API_VERSION}/way/:id", :controller => 'way', :action => 'update', :id => /\d+/, :conditions => { :method => :put }
   map.connect "api/#{API_VERSION}/way/:id", :controller => 'way', :action => 'delete', :id => /\d+/, :conditions => { :method => :delete }
   map.connect "api/#{API_VERSION}/ways", :controller => 'way', :action => 'ways', :id => nil
 
-  map.connect "api/#{API_VERSION}/capabilities", :controller => 'api', :action => 'capabilities'
   map.connect "api/#{API_VERSION}/relation/create", :controller => 'relation', :action => 'create'
   map.connect "api/#{API_VERSION}/relation/:id/relations", :controller => 'relation', :action => 'relations_for_relation', :id => /\d+/
   map.connect "api/#{API_VERSION}/relation/:id/history", :controller => 'old_relation', :action => 'history', :id => /\d+/
@@ -54,14 +64,14 @@ ActionController::Routing::Routes.draw do |map|
   map.connect "api/#{API_VERSION}/gpx/:id/details", :controller => 'trace', :action => 'api_details'
   map.connect "api/#{API_VERSION}/gpx/:id/data", :controller => 'trace', :action => 'api_data'
   
-  # Potlatch API
+  # AMF (ActionScript) API
   
   map.connect "api/#{API_VERSION}/amf/read", :controller =>'amf', :action =>'amf_read'
   map.connect "api/#{API_VERSION}/amf/write", :controller =>'amf', :action =>'amf_write'
   map.connect "api/#{API_VERSION}/swf/trackpoints", :controller =>'swf', :action =>'trackpoints'
   
   # Data browsing
-  map.connect '/browse', :controller => 'browse', :action => 'index'
+  map.connect '/browse', :controller => 'changeset', :action => 'list'
   map.connect '/browse/start', :controller => 'browse', :action => 'start'
   map.connect '/browse/way/:id', :controller => 'browse', :action => 'way', :id => /\d+/
   map.connect '/browse/way/:id/history', :controller => 'browse', :action => 'way_history', :id => /\d+/
@@ -69,11 +79,13 @@ ActionController::Routing::Routes.draw do |map|
   map.connect '/browse/node/:id/history', :controller => 'browse', :action => 'node_history', :id => /\d+/
   map.connect '/browse/relation/:id', :controller => 'browse', :action => 'relation', :id => /\d+/
   map.connect '/browse/relation/:id/history', :controller => 'browse', :action => 'relation_history', :id => /\d+/
+  map.connect '/browse/changeset/:id', :controller => 'browse', :action => 'changeset', :id => /\d+/
+  map.connect '/browse/changesets', :controller => 'changeset', :action => 'list'
   
   # web site
-
   map.connect '/', :controller => 'site', :action => 'index'
   map.connect '/edit', :controller => 'site', :action => 'edit'
+  map.connect '/history', :controller => 'changeset', :action => 'list_bbox'
   map.connect '/export', :controller => 'site', :action => 'export'
   map.connect '/login', :controller => 'user', :action => 'login'
   map.connect '/logout', :controller => 'user', :action => 'logout'
@@ -90,6 +102,7 @@ ActionController::Routing::Routes.draw do |map|
 
   map.connect '/index.html', :controller => 'site', :action => 'index'
   map.connect '/edit.html', :controller => 'site', :action => 'edit'
+  map.connect '/history.html', :controller => 'changeset', :action => 'list_bbox'
   map.connect '/export.html', :controller => 'site', :action => 'export'
   map.connect '/search.html', :controller => 'way_tag', :action => 'search'
   map.connect '/login.html', :controller => 'user', :action => 'login'
@@ -126,6 +139,7 @@ ActionController::Routing::Routes.draw do |map|
 
   # user pages
   map.connect '/user/:display_name', :controller => 'user', :action => 'view'
+  map.connect '/user/:display_name/edits', :controller => 'changeset', :action => 'list_user'
   map.connect '/user/:display_name/make_friend', :controller => 'user', :action => 'make_friend'
   map.connect '/user/:display_name/remove_friend', :controller => 'user', :action => 'remove_friend'
   map.connect '/user/:display_name/diary', :controller => 'diary_entry', :action => 'list'
@@ -140,7 +154,8 @@ ActionController::Routing::Routes.draw do |map|
   map.connect '/diary/rss', :controller => 'diary_entry', :action => 'rss'
   map.connect '/diary/:language', :controller => 'diary_entry', :action => 'list'
   map.connect '/diary/:language/rss', :controller => 'diary_entry', :action => 'rss'
-
+  
+  
   # test pages
   map.connect '/test/populate/:table/:from/:count', :controller => 'test', :action => 'populate'
   map.connect '/test/populate/:table/:count', :controller => 'test', :action => 'populate', :from => 1
index 447c63651f538952278eb69e10afb7dcbce6ffc3..39ce18023b2d1daa8638598e6e656dd7ec37931f 100644 (file)
--- a/db/README
+++ b/db/README
@@ -25,12 +25,12 @@ $ mysql -u <uid> -p
 > flush privileges;
 > exit
 
-Creating functions
-====================
+Creating functions For MySQL
+==============================
 
 Run this command in the db/functions directory:
 
-$ make
+$ make libmyosm.so
 
 Make sure the db/functions directory is on the MySQL server's library
 path and restart the MySQL server. 
@@ -49,6 +49,22 @@ $ mysql -u <uid> -p openstreetmap
 > create function maptile_for_point returns integer soname 'libmyosm.so';
 > exit
 
+Creating functions for PgSQL
+==============================
+
+Run this command in the db/functions directory:
+
+$ make libpgosm.so
+
+Now create the function as follows:
+
+$ psql openstreetmap
+(This may need authentication or a -u <dbowneruid>)
+
+> CREATE FUNCTION maptile_for_point(int8, int8, int4) RETURNS int4 
+  AS '/path/to/rails-port/db/functions/libpgosm.so', 'maptile_for_point'
+  LANGUAGE C STRICT;
+
 Creating database skeleton tables
 ===================================
 
index 7652862faf55b5bfad71eddf7e56184e5dc62efc..9158b4959f906a926ce58784067ea754214fed94 100644 (file)
@@ -7,11 +7,22 @@ else
     LDFLAGS=-shared
 endif
 
-libmyosm.so: quadtile.o maptile.o
-       cc ${LDFLAGS} -o libmyosm.so quadtile.o maptile.o
+all: libmyosm.so libpgosm.so
+
+clean:
+       $(RM) *.so *.o
+
+libmyosm.so: quadtile.o maptile-mysql.o
+       cc ${LDFLAGS} -o libmyosm.so quadtile.o maptile-mysql.o
+
+libpgosm.so: maptile-pgsql.o
+       cc ${LDFLAGS} -o libpgosm.so maptile-pgsql.o
 
 quadtile.o: quadtile.c ${QTDIR}/quad_tile.h
        cc `mysql_config --include` -I${QTDIR} -fPIC -O3 -c -o quadtile.o quadtile.c
 
-maptile.o: maptile.c
-       cc `mysql_config --include` -fPIC -O3 -c -o maptile.o maptile.c
+maptile-mysql.o: maptile.c
+       cc `mysql_config --include` -fPIC -O3 -DUSE_MYSQL -c -o maptile-mysql.o maptile.c
+
+maptile-pgsql.o: maptile.c
+       cc -I `pg_config --includedir-server` -O3 -fPIC -DUSE_PGSQL -c -o maptile-pgsql.o maptile.c
index f96f9c23e5e052078c90c2348ae70852b41bd2d2..c2baac5d467c109c2d392b18313b048a4319d7b4 100644 (file)
@@ -1,3 +1,31 @@
+#ifndef USE_MYSQL
+#ifndef USE_PGSQL
+#error One of USE_MYSQL or USE_PGSQL must be defined
+#endif
+#endif
+
+#include <math.h>
+
+/* The real maptile-for-point functionality is here */
+
+static long long internal_maptile_for_point(double lat, double lon, long long zoom)
+{
+   double       scale = pow(2, zoom);
+   double       r_per_d = M_PI / 180;
+   unsigned int x;
+   unsigned int y;
+
+   x = floor((lon + 180.0) * scale / 360.0);
+   y = floor((1 - log(tan(lat * r_per_d) + 1.0 / cos(lat * r_per_d)) / M_PI) * scale / 2.0);
+
+   return (x << zoom) | y;
+}
+
+#ifdef USE_MYSQL
+#ifdef USE_PGSQL
+#error ONLY one of USE_MYSQL and USE_PGSQL should be defined
+#endif
+
 #include <my_global.h>
 #include <my_sys.h>
 #include <m_string.h>
@@ -27,13 +55,39 @@ long long maptile_for_point(UDF_INIT *initid, UDF_ARGS *args, char *is_null, cha
    double       lat = *(long long *)args->args[0] / 10000000.0;
    double       lon = *(long long *)args->args[1] / 10000000.0;
    long long    zoom = *(long long *)args->args[2];
-   double       scale = pow(2, zoom);
-   double       r_per_d = M_PI / 180;
-   unsigned int x;
-   unsigned int y;
+   
+   return internal_maptile_for_point(lat, lon, zoom);
+}
+#endif
 
-   x = floor((lon + 180.0) * scale / 360.0);
-   y = floor((1 - log(tan(lat * r_per_d) + 1.0 / cos(lat * r_per_d)) / M_PI) * scale / 2.0);
+#ifdef USE_PGSQL
+#include <postgres.h>
+#include <fmgr.h>
 
-   return (x << zoom) | y;
+Datum
+maptile_for_point(PG_FUNCTION_ARGS)
+{
+  double lat = PG_GETARG_INT64(0) / 10000000.0;
+  double lon = PG_GETARG_INT64(1) / 10000000.0;
+  int zoom = PG_GETARG_INT32(2);
+  
+  PG_RETURN_INT32(internal_maptile_for_point(lat, lon, zoom));
 }
+
+PG_FUNCTION_INFO_V1(maptile_for_point);
+
+/*
+ * To bind this into PGSQL, try something like:
+ *
+ * CREATE FUNCTION maptile_for_point(int8, int8, int4) RETURNS int4 
+ *  AS '/path/to/rails-port/db/functions/libpgosm', 'maptile_for_point'
+ *  LANGUAGE C STRICT;
+ *
+ * (without all the *s)
+ */
+
+#ifdef PG_MODULE_MAGIC
+PG_MODULE_MAGIC;
+#endif
+
+#endif
index 2c80dd8adeac4c053a6ce5297d8849a1483d1671..3e3377921df5fa4504c7da242dcce94d648c2521 100644 (file)
@@ -16,7 +16,7 @@ class CreateOsmDb < ActiveRecord::Migration
     add_index "current_nodes", ["latitude", "longitude"], :name => "current_nodes_lat_lon_idx"
     add_index "current_nodes", ["timestamp"], :name => "current_nodes_timestamp_idx"
 
-    change_column "current_nodes", "id", :bigint, :limit => 64, :null => false, :options => "AUTO_INCREMENT"
+    change_column :current_nodes, :id, :bigint_auto_64
 
     create_table "current_segments", innodb_table do |t|
       t.column "id",        :bigint,   :limit => 64,                 :null => false
@@ -32,7 +32,7 @@ class CreateOsmDb < ActiveRecord::Migration
     add_index "current_segments", ["node_a"], :name => "current_segments_a_idx"
     add_index "current_segments", ["node_b"], :name => "current_segments_b_idx"
 
-    change_column "current_segments", "id", :bigint, :limit => 64, :null => false, :options => "AUTO_INCREMENT"
+    change_column :current_segments, :id, :bigint_auto_64
 
     create_table "current_way_segments", innodb_table do |t|
       t.column "id",          :bigint, :limit => 64
@@ -50,21 +50,17 @@ class CreateOsmDb < ActiveRecord::Migration
     end
 
     add_index "current_way_tags", ["id"], :name => "current_way_tags_id_idx"
-    execute "CREATE FULLTEXT INDEX `current_way_tags_v_idx` ON `current_way_tags` (`v`)"
+    add_fulltext_index "current_way_tags", "v"
 
     create_table "current_ways", myisam_table do |t|
-      t.column "id",        :bigint,   :limit => 64, :null => false
+      t.column "id",        :bigint_pk_64, :null => false
       t.column "user_id",   :bigint,   :limit => 20
       t.column "timestamp", :datetime
       t.column "visible",   :boolean
     end
 
-    add_primary_key "current_ways", ["id"]
-
-    change_column "current_ways", "id", :bigint, :limit => 64, :null => false, :options => "AUTO_INCREMENT"
-
     create_table "diary_entries", myisam_table do |t|
-      t.column "id",         :bigint,   :limit => 20, :null => false
+      t.column "id",         :bigint_pk, :null => false
       t.column "user_id",    :bigint,   :limit => 20, :null => false
       t.column "title",      :string
       t.column "body",       :text
@@ -72,21 +68,14 @@ class CreateOsmDb < ActiveRecord::Migration
       t.column "updated_at", :datetime
     end
 
-    add_primary_key "diary_entries", ["id"]
-
-    change_column "diary_entries", "id", :bigint, :limit => 20, :null => false, :options => "AUTO_INCREMENT"
-
     create_table "friends", myisam_table do |t|
-      t.column "id",             :bigint,  :limit => 20, :null => false
+      t.column "id",             :bigint_pk, :null => false
       t.column "user_id",        :bigint,  :limit => 20, :null => false
       t.column "friend_user_id", :bigint,  :limit => 20, :null => false
     end
 
-    add_primary_key "friends", ["id"]
     add_index "friends", ["friend_user_id"], :name => "user_id_idx"
 
-    change_column "friends", "id", :bigint, :limit => 20, :null => false, :options => "AUTO_INCREMENT"
-
     create_table "gps_points", myisam_table do |t|
       t.column "altitude",  :float
       t.column "user_id",   :integer,  :limit => 20
@@ -104,16 +93,13 @@ class CreateOsmDb < ActiveRecord::Migration
     create_table "gpx_file_tags", myisam_table do |t|
       t.column "gpx_id", :bigint,  :limit => 64, :default => 0, :null => false
       t.column "tag",    :string
-      t.column "id",     :integer, :limit => 20, :null => false
+      t.column "id",     :bigint_pk, :null => false
     end
 
-    add_primary_key "gpx_file_tags", ["id"]
     add_index "gpx_file_tags", ["gpx_id"], :name => "gpx_file_tags_gpxid_idx"
 
-    change_column "gpx_file_tags", "id", :integer, :limit => 20, :null => false, :options => "AUTO_INCREMENT"
-
     create_table "gpx_files", myisam_table do |t|
-      t.column "id",          :bigint,   :limit => 64,                   :null => false
+      t.column "id",          :bigint_pk_64,                   :null => false
       t.column "user_id",     :bigint,   :limit => 20
       t.column "visible",     :boolean,                :default => true, :null => false
       t.column "name",        :string,                 :default => "",   :null => false
@@ -126,12 +112,9 @@ class CreateOsmDb < ActiveRecord::Migration
       t.column "inserted",    :boolean
     end
 
-    add_primary_key "gpx_files", ["id"]
     add_index "gpx_files", ["timestamp"], :name => "gpx_files_timestamp_idx"
     add_index "gpx_files", ["visible", "public"], :name => "gpx_files_visible_public_idx"
 
-    change_column "gpx_files", "id", :bigint, :limit => 64, :null => false, :options => "AUTO_INCREMENT"
-
     create_table "gpx_pending_files", myisam_table do |t|
       t.column "originalname", :string
       t.column "tmpname",      :string
@@ -139,7 +122,7 @@ class CreateOsmDb < ActiveRecord::Migration
     end
 
     create_table "messages", myisam_table do |t|
-      t.column "id",                :bigint,   :limit => 20,                    :null => false
+      t.column "id",                :bigint_pk,                                 :null => false
       t.column "user_id",           :bigint,   :limit => 20,                    :null => false
       t.column "from_user_id",      :bigint,   :limit => 20,                    :null => false
       t.column "from_display_name", :string,                 :default => ""
@@ -150,21 +133,14 @@ class CreateOsmDb < ActiveRecord::Migration
       t.column "to_user_id",        :bigint,   :limit => 20,                    :null => false
     end
 
-    add_primary_key "messages", ["id"]
     add_index "messages", ["from_display_name"], :name => "from_name_idx"
 
-    change_column "messages", "id", :bigint, :limit => 20, :null => false, :options => "AUTO_INCREMENT"
-
     create_table "meta_areas", myisam_table do |t|
-      t.column "id",        :bigint,  :limit => 64, :null => false
+      t.column "id",        :bigint_pk_64, :null => false
       t.column "user_id",   :bigint,  :limit => 20
       t.column "timestamp", :datetime
     end
 
-    add_primary_key "meta_areas", ["id"]
-
-    change_column "meta_areas", "id", :bigint, :limit => 64, :null => false, :options => "AUTO_INCREMENT"
-
     create_table "nodes", myisam_table do |t|
       t.column "id",        :bigint,  :limit => 64
       t.column "latitude",  :double
@@ -194,7 +170,7 @@ class CreateOsmDb < ActiveRecord::Migration
 
     create_table "users", innodb_table do |t|
       t.column "email",         :string
-      t.column "id",            :bigint,   :limit => 20,                    :null => false
+      t.column "id",            :bigint_pk,                    :null => false
       t.column "token",         :string
       t.column "active",        :integer,                :default => 0,     :null => false
       t.column "pass_crypt",    :string
@@ -211,12 +187,9 @@ class CreateOsmDb < ActiveRecord::Migration
       t.column "home_zoom",     :integer,  :limit => 2,  :default => 3
     end
 
-    add_primary_key "users", ["id"]
     add_index "users", ["email"], :name => "users_email_idx"
     add_index "users", ["display_name"], :name => "users_display_name_idx"
 
-    change_column "users", "id", :bigint, :limit => 20, :null => false, :options => "AUTO_INCREMENT"
-
     create_table "way_segments", myisam_table do |t|
       t.column "id",          :bigint,  :limit => 64, :default => 0, :null => false
       t.column "segment_id",  :integer
@@ -226,7 +199,7 @@ class CreateOsmDb < ActiveRecord::Migration
 
     add_primary_key "way_segments", ["id", "version", "sequence_id"]
 
-    change_column "way_segments", "sequence_id", :bigint, :limit => 11, :null => false, :options => "AUTO_INCREMENT"
+    change_column "way_segments", "sequence_id", :bigint_auto_11
 
     create_table "way_tags", myisam_table do |t|
       t.column "id",      :bigint,  :limit => 64, :default => 0, :null => false
@@ -248,7 +221,7 @@ class CreateOsmDb < ActiveRecord::Migration
     add_primary_key "ways", ["id", "version"]
     add_index "ways", ["id"], :name => "ways_id_version_idx"
 
-    change_column "ways", "version", :bigint, :limit => 20, :null => false, :options => "AUTO_INCREMENT"
+    change_column "ways", "version", :bigint_auto_20
   end
 
   def self.down
index b99055e5244f1483031513982ac7aa4ca1527ea6..57a3dbca195141ff1cdac866a068ca97ba97486c 100644 (file)
@@ -29,7 +29,7 @@ class CleanupOsmDb < ActiveRecord::Migration
     change_column "current_ways", "user_id", :bigint, :limit => 20, :null => false
     change_column "current_ways", "timestamp", :datetime, :null => false
     change_column "current_ways", "visible", :boolean, :null => false
-    execute "ALTER TABLE current_ways ENGINE = InnoDB"
+    change_engine "current_ways", "InnoDB"
 
     change_column "diary_entries", "title", :string, :null => false
     change_column "diary_entries", "body", :text, :null => false
@@ -39,7 +39,9 @@ class CleanupOsmDb < ActiveRecord::Migration
     add_index "friends", ["user_id"], :name => "friends_user_id_idx"
 
     remove_index "gps_points", :name => "points_uid_idx"
+    remove_index "gps_points", :name => "points_idx"
     remove_column "gps_points", "user_id"
+    add_index "gps_points", ["latitude", "longitude"], :name => "points_idx"
     change_column "gps_points", "trackid", :integer, :null => false
     change_column "gps_points", "latitude", :integer, :null => false
     change_column "gps_points", "longitude", :integer, :null => false
@@ -87,8 +89,8 @@ class CleanupOsmDb < ActiveRecord::Migration
     change_column "users", "creation_time", :datetime, :null => false
     change_column "users", "display_name", :string, :default => "", :null => false
     change_column "users", "data_public", :boolean, :default => false, :null => false
-    change_column "users", "home_lat", :double
-    change_column "users", "home_lon", :double
+    change_column "users", "home_lat", :double, :default => nil
+    change_column "users", "home_lon", :double, :default => nil
     remove_index "users", :name => "users_email_idx"
     add_index "users", ["email"], :name => "users_email_idx", :unique => true
     remove_index "users", :name => "users_display_name_idx"
@@ -150,15 +152,11 @@ class CleanupOsmDb < ActiveRecord::Migration
     change_column "nodes", "id", :bigint, :limit => 64
 
     create_table "meta_areas", myisam_table do |t|
-      t.column "id",        :bigint,  :limit => 64, :null => false
+      t.column "id",        :bigint_pk_64, :null => false
       t.column "user_id",   :bigint,  :limit => 20
       t.column "timestamp", :datetime
     end
 
-    add_primary_key "meta_areas", ["id"]
-
-    change_column "meta_areas", "id", :bigint, :limit => 64, :null => false, :options => "AUTO_INCREMENT"
-
     remove_index "messages", :name => "messages_to_user_id_idx"
     change_column "messages", "message_read", :boolean, :default => false
     change_column "messages", "sent_on", :datetime
@@ -195,7 +193,7 @@ class CleanupOsmDb < ActiveRecord::Migration
     change_column "diary_entries", "body", :text
     change_column "diary_entries", "title", :string, :default => nil
 
-    execute "ALTER TABLE current_ways ENGINE = MyISAM"
+    change_engine "current_ways", "MyISAM"
     change_column "current_ways", "visible", :boolean
     change_column "current_ways", "timestamp", :datetime
     change_column "current_ways", "user_id", :bigint, :limit => 20
@@ -223,6 +221,6 @@ class CleanupOsmDb < ActiveRecord::Migration
     change_column "current_nodes", "user_id", :bigint, :limit => 20
     change_column "current_nodes", "longitude", :double
     change_column "current_nodes", "latitude", :double
-    change_column "current_nodes", "id", :bigint, :limit => 64, :null => false, :options => "AUTO_INCREMENT"
+    change_column "current_nodes", "id", :bigint_auto_64
   end
 end
index 7b1c75479ac0144798004ddae61f010478aa96a9..4de0dd4b19120e6613528918ea7d96c248e57751 100644 (file)
@@ -1,6 +1,6 @@
 class SqlSessionStoreSetup < ActiveRecord::Migration
   def self.up
-    create_table "sessions", :options => "ENGINE=InnoDB" do |t|
+    create_table "sessions", :options => innodb_option do |t|
       t.column "session_id", :string
       t.column "data",       :text
       t.column "created_at", :timestamp
index 92f01bf5d124b3ce61fe3f84788367e158996c4a..a6e81d222fcd190cef0aa969352ae923a4354b5e 100644 (file)
@@ -13,18 +13,15 @@ class UserEnhancements < ActiveRecord::Migration
     add_primary_key "user_preferences", ["user_id", "k"]
 
     create_table "user_tokens", innodb_table do |t|
-      t.column "id",      :bigint,   :limit => 20, :null => false
+      t.column "id",      :bigint_pk, :null => false
       t.column "user_id", :bigint,   :limit => 20, :null => false
       t.column "token",   :string,   :null => false
       t.column "expiry",  :datetime, :null => false
     end
 
-    add_primary_key "user_tokens", ["id"]
     add_index "user_tokens", ["token"], :name => "user_tokens_token_idx", :unique => true
     add_index "user_tokens", ["user_id"], :name => "user_tokens_user_id_idx"
 
-    change_column "user_tokens", "id", :bigint, :limit => 20, :null => false, :options => "AUTO_INCREMENT"
-
     User.find(:all, :conditions => "token is not null").each do |user|
       UserToken.create(:user_id => user.id, :token => user.token, :expiry => 1.week.from_now)
     end
index 51a4d1376233c28ed88494b24f3c7eca71ee5a30..74d85d195629d7ad731eb54e28956f15c4b176eb 100644 (file)
@@ -1,6 +1,6 @@
 class TileTracepoints < ActiveRecord::Migration
   def self.up
-    add_column "gps_points", "tile", :integer, :null => false, :unsigned => true
+    add_column "gps_points", "tile", :four_byte_unsigned
     add_index "gps_points", ["tile"], :name => "points_tile_idx"
     remove_index "gps_points", :name => "points_idx"
 
index 3a50cc9b05ff0372041681b8a16d2e12973556e5..dc4755ac3bbffeb4c53031bd70c24940437664d1 100644 (file)
@@ -33,10 +33,12 @@ class TileNodes < ActiveRecord::Migration
   end
 
   def self.up
+    remove_index "current_nodes", :name => "current_nodes_timestamp_idx"
+
     rename_table "current_nodes", "current_nodes_v5"
 
     create_table "current_nodes", innodb_table do |t|
-      t.column "id",        :bigint,   :limit => 64,                 :null => false
+      t.column "id",        :bigint_pk_64,                           :null => false
       t.column "latitude",  :integer,                                :null => false
       t.column "longitude", :integer,                                :null => false
       t.column "user_id",   :bigint,   :limit => 20,                 :null => false
@@ -46,17 +48,17 @@ class TileNodes < ActiveRecord::Migration
       t.column "tile",      :integer,                                :null => false
     end
 
-    add_primary_key "current_nodes", ["id"]
     add_index "current_nodes", ["timestamp"], :name => "current_nodes_timestamp_idx"
     add_index "current_nodes", ["tile"], :name => "current_nodes_tile_idx"
 
-    change_column "current_nodes", "id", :bigint, :limit => 64, :null => false, :options => "AUTO_INCREMENT"
-    change_column "current_nodes", "tile", :integer, :null => false, :unsigned => true
+    change_column "current_nodes", "tile", :four_byte_unsigned
 
     upgrade_table "current_nodes_v5", "current_nodes", Node
     
     drop_table "current_nodes_v5"
 
+    remove_index "nodes", :name=> "nodes_uid_idx"
+    remove_index "nodes", :name=> "nodes_timestamp_idx"
     rename_table "nodes", "nodes_v5"
 
     create_table "nodes", myisam_table do |t|
@@ -74,7 +76,7 @@ class TileNodes < ActiveRecord::Migration
     add_index "nodes", ["timestamp"], :name => "nodes_timestamp_idx"
     add_index "nodes", ["tile"], :name => "nodes_tile_idx"
 
-    change_column "nodes", "tile", :integer, :null => false, :unsigned => true
+    change_column "nodes", "tile", :four_byte_unsigned
 
     upgrade_table "nodes_v5", "nodes", OldNode
 
@@ -85,7 +87,7 @@ class TileNodes < ActiveRecord::Migration
     rename_table "current_nodes", "current_nodes_v6"
 
     create_table "current_nodes", innodb_table do |t|
-      t.column "id",        :bigint,   :limit => 64,                 :null => false
+      t.column "id",        :bigint_pk_64,                           :null => false
       t.column "latitude",  :double,                                 :null => false
       t.column "longitude", :double,                                 :null => false
       t.column "user_id",   :bigint,   :limit => 20,                 :null => false
@@ -94,12 +96,9 @@ class TileNodes < ActiveRecord::Migration
       t.column "timestamp", :datetime,                               :null => false
     end
 
-    add_primary_key "current_nodes", ["id"]
     add_index "current_nodes", ["latitude", "longitude"], :name => "current_nodes_lat_lon_idx"
     add_index "current_nodes", ["timestamp"], :name => "current_nodes_timestamp_idx"
 
-    change_column "current_nodes", "id", :bigint, :limit => 64, :null => false, :options => "AUTO_INCREMENT"
-
     downgrade_table "current_nodes_v6", "current_nodes"
 
     drop_table "current_nodes_v6"
index a30642e32a801127e576c21c4a7db0e7c581db61..c265fc3adef1507c8d4e672acf8fcb98a2fc0520 100644 (file)
@@ -11,7 +11,7 @@ class AddRelations < ActiveRecord::Migration
       t.column "member_role", :string
     end
     # enums work like strings but are more efficient
-    execute "alter table current_relation_members change column member_type member_type enum('node','way','relation');"
+    alter_column_nwr_enum :current_relation_members, :member_type
 
     add_primary_key "current_relation_members", ["id", "member_type", "member_id", "member_role"]
     add_index "current_relation_members", ["member_type", "member_id"], :name => "current_relation_members_member_idx"
@@ -24,18 +24,15 @@ class AddRelations < ActiveRecord::Migration
     end
 
     add_index "current_relation_tags", ["id"], :name => "current_relation_tags_id_idx"
-    execute "CREATE FULLTEXT INDEX `current_relation_tags_v_idx` ON `current_relation_tags` (`v`)"
+    add_fulltext_index "current_relation_tags", "v"
 
     create_table "current_relations", innodb_table do |t|
-      t.column "id",        :bigint,   :limit => 64, :null => false
+      t.column "id",        :bigint_pk_64,           :null => false
       t.column "user_id",   :bigint,   :limit => 20, :null => false
       t.column "timestamp", :datetime, :null => false
       t.column "visible",   :boolean,  :null => false
     end
 
-    add_primary_key "current_relations", ["id"]
-    change_column "current_relations", "id", :bigint, :limit => 64, :null => false, :options => "AUTO_INCREMENT"
-
     create_table "relation_members", myisam_table do |t|
       t.column "id",          :bigint,  :limit => 64, :default => 0, :null => false
       t.column "member_type", :string, :limit => 11, :null => false
@@ -44,7 +41,7 @@ class AddRelations < ActiveRecord::Migration
       t.column "version",     :bigint,  :limit => 20, :default => 0, :null => false
     end
 
-    execute "alter table relation_members change column member_type member_type enum('node','way','relation');" 
+    alter_column_nwr_enum :relation_members, :member_type 
     add_primary_key "relation_members", ["id", "version", "member_type", "member_id", "member_role"]
     add_index "relation_members", ["member_type", "member_id"], :name => "relation_members_member_idx"
 
@@ -68,7 +65,7 @@ class AddRelations < ActiveRecord::Migration
     add_primary_key "relations", ["id", "version"]
     add_index "relations", ["timestamp"], :name => "relations_timestamp_idx"
     
-    change_column "relations", "version", :bigint, :limit => 20, :null => false, :options => "AUTO_INCREMENT"
+    change_column "relations", "version", :bigint_auto_20
   end
 
 
index be07d851ae96cfd4918b5ebb4d2c3d2130384ff8..43019a938ff96738d322d39361e6677632871528 100644 (file)
@@ -1,7 +1,7 @@
 class DiaryComments < ActiveRecord::Migration
   def self.up
     create_table "diary_comments", myisam_table do |t|
-      t.column "id",             :bigint,   :limit => 20, :null => false
+      t.column "id",             :bigint_pk,              :null => false
       t.column "diary_entry_id", :bigint,   :limit => 20, :null => false
       t.column "user_id",        :bigint,   :limit => 20, :null => false
       t.column "body",           :text,                   :null => false
@@ -9,10 +9,8 @@ class DiaryComments < ActiveRecord::Migration
       t.column "updated_at",     :datetime,               :null => false
     end
 
-    add_primary_key "diary_comments", ["id"]
     add_index "diary_comments", ["diary_entry_id", "id"], :name => "diary_comments_entry_id_idx", :unique => true
 
-    change_column "diary_comments", "id", :bigint, :limit => 20, :null => false, :options => "AUTO_INCREMENT"
   end
 
   def self.down
index b8af4bf6ad5e7b6c519ba19c2b0f1228d95cd227..a0180970109a8ea212b013c359e75f3832009080 100644 (file)
@@ -1,7 +1,7 @@
 class AddEmailValid < ActiveRecord::Migration
   def self.up
     add_column "users", "email_valid", :boolean, :default => false, :null => false
-    User.update_all("email_valid = active")
+    User.update_all("email_valid = (active != 0)") #email_valid is :boolean, but active is :integer. "email_valid = active" (see r11802 or earlier) will fail for stricter dbs than mysql
   end
 
   def self.down
index d870dfffdae477c646136afc48a9650bbd2f3eab..869f24c3755c12eb792259d8d53e45594f2371dc 100644 (file)
@@ -1,7 +1,7 @@
 class AddUserVisible < ActiveRecord::Migration
   def self.up
     add_column "users", "visible", :boolean, :default => true, :null => false
-    User.update_all("visible = 1")
+    User.update_all(:visible => true)
   end
 
   def self.down
index 3606bd6704e5b51e7ae3c00d704f8259882905ff..5205b99b0403feaab7c65ba199637b253014004b 100644 (file)
@@ -1,19 +1,14 @@
 class CreateAcls < ActiveRecord::Migration
   def self.up
     create_table "acls", myisam_table do |t|
-      t.column "id",      :integer, :null => false
-      t.column "address", :integer, :null => false
-      t.column "netmask", :integer, :null => false
-      t.column "k",       :string,  :null => false
+      t.column "id",      :integer_pk, :null => false
+      t.column "address", :inet,       :null => false
+      t.column "netmask", :inet,       :null => false
+      t.column "k",       :string,     :null => false
       t.column "v",       :string
     end
 
-    add_primary_key "acls", ["id"]
     add_index "acls", ["k"], :name => "acls_k_idx"
-
-    change_column "acls", "id", :integer, :null => false, :options => "AUTO_INCREMENT"
-    change_column "acls", "address", :integer, :null => false, :unsigned => true
-    change_column "acls", "netmask", :integer, :null => false, :unsigned => true
   end
 
   def self.down
diff --git a/db/migrate/019_add_timestamp_indexes.rb b/db/migrate/019_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/020_populate_node_tags_and_remove.rb b/db/migrate/020_populate_node_tags_and_remove.rb
new file mode 100644 (file)
index 0000000..dc048f1
--- /dev/null
@@ -0,0 +1,60 @@
+class PopulateNodeTagsAndRemove < ActiveRecord::Migration
+  def self.up
+    have_nodes = select_value("SELECT count(*) FROM current_nodes").to_i != 0
+
+    if have_nodes
+      prefix = File.join Dir.tmpdir, "020_populate_node_tags_and_remove.#{$$}."
+
+      cmd = "db/migrate/020_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/020_populate_node_tags_and_remove_helper.c b/db/migrate/020_populate_node_tags_and_remove_helper.c
new file mode 100644 (file)
index 0000000..c41ea33
--- /dev/null
@@ -0,0 +1,241 @@
+#include <mysql.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+static void exit_mysql_err(MYSQL *mysql) {
+  const char *err = mysql_error(mysql);
+  if (err) {
+    fprintf(stderr, "019_populate_node_tags_and_remove_helper: MySQL error: %s\n", err);
+  } else {
+    fprintf(stderr, "019_populate_node_tags_and_remove_helper: MySQL error\n");
+  }
+  abort();
+  exit(EXIT_FAILURE);
+}
+
+static void write_csv_col(FILE *f, const char *str, char end) {
+  char *out = (char *) malloc(2 * strlen(str) + 4);
+  char *o = out;
+  size_t len;
+
+  *(o++) = '\"';
+  for (; *str; str++) {
+    if (*str == '\0') {
+      break;
+    } else if (*str == '\"') {
+      *(o++) = '\"';
+      *(o++) = '\"';
+    } else {
+      *(o++) = *str;
+    }
+  }
+  *(o++) = '\"';
+  *(o++) = end;
+  *(o++) = '\0';
+
+  len = strlen(out);
+  if (fwrite(out, len, 1, f) != 1) {
+    perror("fwrite");
+    exit(EXIT_FAILURE);
+  }
+
+  free(out);
+}
+
+static void unescape(char *str) {
+  char *i = str, *o = str, tmp;
+
+  while (*i) {
+    if (*i == '\\') {
+      i++;
+      switch (tmp = *i++) {
+        case 's': *o++ = ';'; break;
+        case 'e': *o++ = '='; break;
+        case '\\': *o++ = '\\'; break;
+        default: *o++ = tmp; break;
+      }
+    } else {
+      *o++ = *i++;
+    }
+  }
+}
+
+static int read_node_tags(char **tags, char **k, char **v) {
+  if (!**tags) return 0;
+  char *i = strchr(*tags, ';');
+  if (!i) i = *tags + strlen(*tags);
+  char *j = strchr(*tags, '=');
+  *k = *tags;
+  if (j && j < i) {
+    *v = j + 1;
+  } else {
+    *v = i;
+  }
+  *tags = *i ? i + 1 : i;
+  *i = '\0';
+  if (j) *j = '\0';
+
+  unescape(*k);
+  unescape(*v);
+
+  return 1;
+}
+
+struct data {
+  MYSQL *mysql;
+  size_t version_size;
+  uint16_t *version;
+};
+
+static void proc_nodes(struct data *d, const char *tbl, FILE *out, FILE *out_tags, int hist) {
+  MYSQL_RES *res;
+  MYSQL_ROW row;
+  char query[256];
+
+  snprintf(query, sizeof(query),  "SELECT id, latitude, longitude, "
+      "user_id, visible, tags, timestamp, tile FROM %s", tbl);
+  if (mysql_query(d->mysql, query))
+    exit_mysql_err(d->mysql);
+
+  res = mysql_use_result(d->mysql);
+  if (!res) exit_mysql_err(d->mysql);
+
+  while ((row = mysql_fetch_row(res))) {
+    unsigned long id = strtoul(row[0], NULL, 10);
+    uint32_t version;
+
+    if (id >= d->version_size) {
+      fprintf(stderr, "preallocated nodes size exceeded");
+      abort();
+    }
+
+    if (hist) {
+      version = ++(d->version[id]);
+
+      fprintf(out, "\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",\"%u\"\n",
+        row[0], row[1], row[2], row[3], row[4], row[6], row[7], version);
+    } else {
+      /*fprintf(out, "\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",\"%s\"\n",
+       row[0], row[1], row[2], row[3], row[4], row[6], row[7]);*/
+    }
+
+    char *tags_it = row[5], *k, *v;
+    while (read_node_tags(&tags_it, &k, &v)) {
+      if (hist) {
+        fprintf(out_tags, "\"%s\",\"%u\",", row[0], version);
+      } else {
+        fprintf(out_tags, "\"%s\",", row[0]);
+      }
+
+      write_csv_col(out_tags, k, ',');
+      write_csv_col(out_tags, v, '\n');
+    }
+  }
+  if (mysql_errno(d->mysql)) exit_mysql_err(d->mysql);
+
+  mysql_free_result(res);
+}
+
+static size_t select_size(MYSQL *mysql, const char *q) {
+  MYSQL_RES *res;
+  MYSQL_ROW row;
+  size_t ret;
+
+  if (mysql_query(mysql, q))
+    exit_mysql_err(mysql);
+
+  res = mysql_store_result(mysql);
+  if (!res) exit_mysql_err(mysql);
+
+  row = mysql_fetch_row(res);
+  if (!row) exit_mysql_err(mysql);
+
+  if (row[0]) {
+    ret = strtoul(row[0], NULL, 10);
+  } else {
+    ret = 0;
+  }
+
+  mysql_free_result(res);
+
+  return ret;
+}
+
+static MYSQL *connect_to_mysql(char **argv) {
+  MYSQL *mysql = mysql_init(NULL);
+  if (!mysql) exit_mysql_err(mysql);
+
+  if (!mysql_real_connect(mysql, argv[1], argv[2], argv[3], argv[4],
+      argv[5][0] ? atoi(argv[5]) : 0, argv[6][0] ? argv[6] : NULL, 0))
+    exit_mysql_err(mysql);
+
+  if (mysql_set_character_set(mysql, "utf8"))
+    exit_mysql_err(mysql);
+
+  return mysql;
+}
+
+static void open_file(FILE **f, char *fn) {
+  *f = fopen(fn, "w+");
+  if (!*f) {
+    perror("fopen");
+    exit(EXIT_FAILURE);
+  }
+}
+
+int main(int argc, char **argv) {
+  size_t prefix_len;
+  FILE *current_nodes, *current_node_tags, *nodes, *node_tags;
+  char *tempfn;
+  struct data data, *d = &data;
+
+  if (argc != 8) {
+    printf("Usage: 019_populate_node_tags_and_remove_helper host user passwd database port socket prefix\n");
+    exit(EXIT_FAILURE);
+  }
+
+  d->mysql = connect_to_mysql(argv);
+
+  d->version_size = 1 + select_size(d->mysql, "SELECT max(id) FROM current_nodes");
+  d->version = (uint16_t *) malloc(sizeof(uint16_t) * d->version_size);
+  if (!d->version) {
+    perror("malloc");
+    abort();
+    exit(EXIT_FAILURE);
+  }
+  memset(d->version, 0, sizeof(uint16_t) * d->version_size);
+
+  prefix_len = strlen(argv[7]);
+  tempfn = (char *) malloc(prefix_len + 32);
+  strcpy(tempfn, argv[7]);
+
+  strcpy(tempfn + prefix_len, "current_nodes");
+  open_file(&current_nodes, tempfn);
+
+  strcpy(tempfn + prefix_len, "current_node_tags");
+  open_file(&current_node_tags, tempfn);
+
+  strcpy(tempfn + prefix_len, "nodes");
+  open_file(&nodes, tempfn);
+
+  strcpy(tempfn + prefix_len, "node_tags");
+  open_file(&node_tags, tempfn);
+
+  free(tempfn);
+
+  proc_nodes(d, "nodes", nodes, node_tags, 1);
+  proc_nodes(d, "current_nodes", current_nodes, current_node_tags, 0);
+
+  free(d->version);
+
+  mysql_close(d->mysql);
+
+  fclose(current_nodes);
+  fclose(current_node_tags);
+  fclose(nodes);
+  fclose(node_tags);
+
+  exit(EXIT_SUCCESS);
+}
diff --git a/db/migrate/021_move_to_innodb.rb b/db/migrate/021_move_to_innodb.rb
new file mode 100644 (file)
index 0000000..da0488c
--- /dev/null
@@ -0,0 +1,45 @@
+class MoveToInnodb < ActiveRecord::Migration
+  @@conv_tables = ['nodes', 'ways', 'way_tags', 'way_nodes',
+    'current_way_tags', 'relation_members',
+    'relations', 'relation_tags', 'current_relation_tags']
+
+  @@ver_tbl = ['nodes', 'ways', 'relations']
+
+  def self.up
+    remove_index :current_way_tags, :name=> :current_way_tags_v_idx
+    remove_index :current_relation_tags, :name=> :current_relation_tags_v_idx
+
+    @@ver_tbl.each { |tbl|
+      change_column tbl, "version", :bigint, :limit => 20, :null => false
+    }
+
+    @@conv_tables.each { |tbl|
+      change_engine (tbl, "InnoDB")
+    }
+
+    @@ver_tbl.each { |tbl|
+      add_column "current_#{tbl}", "version", :bigint, :limit => 20, :null => false
+      # As the initial version of all nodes, ways and relations is 0, we set the 
+      # current version to something less so that we can update the version in 
+      # batches of 10000
+      tbl.classify.constantize.update_all("version=-1")
+      while tbl.classify.constantize.count(:conditions => {:version => -1}) > 0
+        tbl.classify.constantize.update_all("version=(SELECT max(version) FROM #{tbl} WHERE #{tbl}.id = current_#{tbl}.id)", {:version => -1}, :limit => 10000)
+      end
+     # execute "UPDATE current_#{tbl} SET version = " +
+      #  "(SELECT max(version) FROM #{tbl} WHERE #{tbl}.id = current_#{tbl}.id)"
+        # The above update causes a MySQL error:
+        # -- add_column("current_nodes", "version", :bigint, {:null=>false, :limit=>20})
+        # -> 1410.9152s
+        # -- execute("UPDATE current_nodes SET version = (SELECT max(version) FROM nodes WHERE nodes.id = current_nodes.id)")
+        # rake aborted!
+        # Mysql::Error: The total number of locks exceeds the lock table size: UPDATE current_nodes SET version = (SELECT max(version) FROM nodes WHERE nodes.id = current_nodes.id)
+
+        # The above rails version will take longer, however will no run out of locks
+    }
+  end
+
+  def self.down
+    raise IrreversibleMigration.new
+  end
+end
diff --git a/db/migrate/022_key_constraints.rb b/db/migrate/022_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/023_add_changesets.rb b/db/migrate/023_add_changesets.rb
new file mode 100644 (file)
index 0000000..e0cf390
--- /dev/null
@@ -0,0 +1,46 @@
+class AddChangesets < ActiveRecord::Migration
+  @@conv_user_tables = ['current_nodes',
+  'current_relations', 'current_ways', 'nodes', 'relations', 'ways' ]
+  
+  def self.up
+    create_table "changesets", innodb_table do |t|
+      t.column "id",             :bigint_pk,              :null => false
+      t.column "user_id",        :bigint,   :limit => 20, :null => false
+      t.column "created_at",     :datetime,               :null => false
+      t.column "open",           :boolean,                :null => false, :default => true
+      t.column "min_lat",        :integer,                :null => true
+      t.column "max_lat",        :integer,                :null => true
+      t.column "min_lon",        :integer,                :null => true
+      t.column "max_lon",        :integer,                :null => true
+    end
+
+    create_table "changeset_tags", innodb_table do |t|
+      t.column "id", :bigint, :limit => 64, :null => false
+      t.column "k",  :string, :default => "", :null => false
+      t.column "v",  :string, :default => "", :null => false
+    end
+
+    add_index "changeset_tags", ["id"], :name => "changeset_tags_id_idx"
+    
+    #
+    # Initially we will have one changeset for every user containing 
+    # all edits up to the API change,  
+    # all the changesets will have the id of the user that made them.
+    # We need to generate a changeset for each user in the database
+    execute "INSERT INTO changesets (id, user_id, created_at, open)" + 
+      "SELECT id, id, creation_time, false from users;"
+
+    @@conv_user_tables.each { |tbl|
+      rename_column tbl, :user_id, :changeset_id
+      #foreign keys too
+      add_foreign_key tbl, [:changeset_id], :changesets, [:id]
+    }
+  end
+
+  def self.down
+    # It's not easy to generate the user ids from the changesets
+    raise IrreversibleMigration.new
+    #drop_table "changesets"
+    #drop_table "changeset_tags"
+  end
+end
diff --git a/db/migrate/024_order_relation_members.rb b/db/migrate/024_order_relation_members.rb
new file mode 100644 (file)
index 0000000..5500edf
--- /dev/null
@@ -0,0 +1,33 @@
+class OrderRelationMembers < ActiveRecord::Migration
+  def self.up
+    # add sequence column. rails won't let us define an ordering here,
+    # as defaults must be constant.
+    add_column(:relation_members, :sequence_id, :integer,
+               :default => 0, :null => false)
+
+    # update the sequence column with default (partial) ordering by 
+    # element ID. the sequence ID is a smaller int type, so we can't
+    # just copy the member_id.
+    execute("update relation_members set sequence_id = mod(member_id, 16384)")
+
+    # need to update the primary key to include the sequence number, 
+    # otherwise the primary key will barf when we have repeated members.
+    # mysql barfs on this anyway, so we need a single command. this may
+    # not work in postgres... needs testing.
+    alter_primary_key("relation_members", [:id, :version, :member_type, :member_id, :member_role, :sequence_id])
+
+    # do the same for the current tables
+    add_column(:current_relation_members, :sequence_id, :integer,
+               :default => 0, :null => false)
+    execute("update current_relation_members set sequence_id = mod(member_id, 16384)")
+    alter_primary_key("current_relation_members", [:id, :member_type, :member_id, :member_role, :sequence_id])
+  end
+
+  def self.down
+    alter_primary_key("current_relation_members", [:id, :member_type, :member_id, :member_role])
+    remove_column :relation_members, :sequence_id
+
+    alter_primary_key("relation_members", [:id, :version, :member_type, :member_id, :member_role])
+    remove_column :current_relation_members, :sequence_id
+  end
+end
diff --git a/db/migrate/025_add_end_time_to_changesets.rb b/db/migrate/025_add_end_time_to_changesets.rb
new file mode 100644 (file)
index 0000000..4941b72
--- /dev/null
@@ -0,0 +1,34 @@
+class AddEndTimeToChangesets < ActiveRecord::Migration
+  def self.up
+    # swap the boolean closed-or-not for a time when the changeset will
+    # close or has closed.
+    add_column(:changesets, :closed_at, :datetime, :null => false)
+    
+    # it appears that execute will only accept string arguments, so
+    # this is an ugly, ugly hack to get some sort of mysql/postgres
+    # independence. now i have to go wash my brain with bleach.
+    execute("update changesets set closed_at=(now()-#{interval_constant('1 hour')}) where open=(1=0)")
+    execute("update changesets set closed_at=(now()+#{interval_constant('1 hour')}) where open=(1=1)")
+
+    # remove the open column as it is unnecessary now and denormalises 
+    # the table.
+    remove_column :changesets, :open
+
+    # add a column to keep track of the number of changes in a changeset.
+    # could probably work out how many changes there are here, but i'm not
+    # sure its actually important.
+    add_column(:changesets, :num_changes, :integer, 
+               :null => false, :default => 0)
+  end
+
+  def self.down
+    # in the reverse direction, we can look at the closed_at to figure out
+    # if changesets are closed or not.
+    add_column(:changesets, :open, :boolean, :null => false, :default => true)
+    execute("update changesets set open=(closed_at > now())")
+    remove_column :changesets, :closed_at
+
+    # remove the column for tracking number of changes
+    remove_column :changesets, :num_changes
+  end
+end
index 129a6f24b58bb7a79f78f5f07dcaa1fc58e646ec..6c4e6b0fcf38ebdef9a39fe7d05b5f0ff2132124 100644 (file)
@@ -1,7 +1,12 @@
-This is the OpenStreetMap rails server codebase. Documentation is currently extremely incomplete. Please help by writing docs and moving any SQL you see to use models etc.
+This is the OpenStreetMap rails server codebase. Documentation is currently
+extremely incomplete. Please help by writing docs and moving any SQL you
+see to use models etc.
 
 =INSTALL
 
+Full information is available at 
+http://wiki.openstreetmap.org/index.php/Rails
+
 * Get rails working (http://www.rubyonrails.org/)
 * Make your db (see db/README)
 * Install ruby libxml bindings:
@@ -18,14 +23,17 @@ This is the OpenStreetMap rails server codebase. Documentation is currently extr
 
 See
 
-http://wiki.openstreetmap.org/index.php/REST#Changes_in_the_upcoming_0.4_API
+The information about the next version of the protocol API 0.6 is available at 
+http://wiki.openstreetmap.org/index.php/OSM_Protocol_Version_0.6
+http://wiki.openstreetmap.org/index.php/REST
 
 =HACKING
 
 * Log in to your site (proably localhost:3000)
-* Create a user and confirm it
-* You want to play with the API (probably at http://localhost:3000/api/0.5/node/create etc)
-* Lots of tests are needed to test the API.
+* Create a user and confirm it (by setting the active flag to true in the users table of the database
+* You want to play with the API (probably at http://localhost:3000/api/0.6/node/create etc)
+* Lots of tests are needed to test the API. To run the tests use 
+    rake test
 * Lots of little things to make the site work like the old one.
 
 =Bugs
diff --git a/lib/consistency_validations.rb b/lib/consistency_validations.rb
new file mode 100644 (file)
index 0000000..4f38815
--- /dev/null
@@ -0,0 +1,45 @@
+module ConsistencyValidations
+  # Generic checks that are run for the updates and deletes of
+  # node, ways and relations. This code is here to avoid duplication, 
+  # and allow the extention of the checks without having to modify the
+  # code in 6 places for all the updates and deletes. Some of these tests are 
+  # needed for creates, but are currently not run :-( 
+  # This will throw an exception if there is an inconsistency
+  def check_consistency(old, new, user)
+    if new.version != old.version
+      raise OSM::APIVersionMismatchError.new(new.id, new.class.to_s, new.version, old.version)
+    elsif new.changeset.nil?
+      raise OSM::APIChangesetMissingError.new
+    elsif new.changeset.user_id != user.id
+      raise OSM::APIUserChangesetMismatchError.new
+    elsif not new.changeset.is_open?
+      raise OSM::APIChangesetAlreadyClosedError.new(new.changeset)
+    end
+  end
+  
+  # This is similar to above, just some validations don't apply
+  def check_create_consistency(new, user)
+    if new.changeset.nil?
+      raise OSM::APIChangesetMissingError.new
+    elsif new.changeset.user_id != user.id
+      raise OSM::APIUserChangesetMismatchError.new
+    elsif not new.changeset.is_open?
+      raise OSM::APIChangesetAlreadyClosedError.new(new.changeset)
+    end
+  end
+
+  ##
+  # subset of consistency checks which should be applied to almost
+  # all the changeset controller's writable methods.
+  def check_changeset_consistency(changeset, user)
+    # check user credentials - only the user who opened a changeset
+    # may alter it.
+    if changeset.nil?
+      raise OSM::APIChangesetMissingError.new
+    elsif user.id != changeset.user_id 
+      raise OSM::APIUserChangesetMismatchError.new
+    elsif not changeset.is_open?
+      raise OSM::APIChangesetAlreadyClosedError.new(changeset)
+    end
+  end
+end
diff --git a/lib/diff_reader.rb b/lib/diff_reader.rb
new file mode 100644 (file)
index 0000000..217e930
--- /dev/null
@@ -0,0 +1,224 @@
+##
+# DiffReader reads OSM diffs and applies them to the database.
+#
+# Uses the streaming LibXML "Reader" interface to cut down on memory
+# usage, so hopefully we can process fairly large diffs.
+class DiffReader
+  include ConsistencyValidations
+
+  # maps each element type to the model class which handles it
+  MODELS = { 
+    "node"     => Node, 
+    "way"      => Way, 
+    "relation" => Relation
+  }
+
+  ##
+  # Construct a diff reader by giving it a bunch of XML +data+ to parse
+  # in OsmChange format. All diffs must be limited to a single changeset
+  # given in +changeset+.
+  def initialize(data, changeset)
+    @reader = XML::Reader.string(data)
+    @changeset = changeset
+  end
+
+  ##
+  # Reads the next element from the XML document. Checks the return value
+  # and throws an exception if an error occurred.
+  def read_or_die
+    # NOTE: XML::Reader#read returns false for EOF and raises an
+    # exception if an error occurs.
+    begin
+      @reader.read
+    rescue LibXML::XML::Error => ex
+      raise OSM::APIBadXMLError.new("changeset", xml, ex.message)
+    end
+  end
+
+  ##
+  # An element-block mapping for using the LibXML reader interface. 
+  #
+  # Since a lot of LibXML reader usage is boilerplate iteration through
+  # elements, it would be better to DRY and do this in a block. This
+  # could also help with error handling...?
+  def with_element
+    # if the start element is empty then don't do any processing, as
+    # there won't be any child elements to process!
+    unless @reader.empty_element?
+      # read the first element
+      read_or_die
+
+      while @reader.node_type != 15 do # end element
+        # because we read elements in DOM-style to reuse their DOM
+        # parsing code, we don't always read an element on each pass
+        # as the call to @reader.next in the innermost loop will take
+        # care of that for us.
+        if @reader.node_type == 1 # element
+          yield @reader.name
+        else
+          read_or_die
+        end
+      end 
+    end
+    read_or_die
+  end
+
+  ##
+  # An element-block mapping for using the LibXML reader interface. 
+  #
+  # Since a lot of LibXML reader usage is boilerplate iteration through
+  # elements, it would be better to DRY and do this in a block. This
+  # could also help with error handling...?
+  def with_model
+    with_element do |model_name|
+      model = MODELS[model_name]
+      raise "Unexpected element type #{model_name}, " +
+        "expected node, way, relation." if model.nil?
+      yield model, @reader.expand
+      @reader.next
+    end
+  end
+
+  ##
+  # Checks a few invariants. Others are checked in the model methods
+  # such as save_ and delete_with_history.
+  def check(model, xml, new)
+    raise OSM::APIBadXMLError.new(model, xml) if new.nil?
+    unless new.changeset_id == @changeset.id 
+      raise OSM::APIChangesetMismatchError.new(new.changeset_id, @changeset.id)
+    end
+  end
+
+  ##
+  # Consume the XML diff and try to commit it to the database. This code
+  # is *not* transactional, so code which calls it should ensure that the
+  # appropriate transaction block is in place.
+  #
+  # On a failure to meet preconditions (e.g: optimistic locking fails) 
+  # an exception subclassing OSM::APIError will be thrown.
+  def commit
+
+    # data structure used for mapping placeholder IDs to real IDs
+    node_ids, way_ids, rel_ids = {}, {}, {}
+    ids = { :node => node_ids, :way => way_ids, :relation => rel_ids}
+
+    # take the first element and check that it is an osmChange element
+    @reader.read
+    raise APIBadUserInput.new("Document element should be 'osmChange'.") if @reader.name != 'osmChange'
+
+    result = OSM::API.new.get_xml_doc
+    result.root.name = "diffResult"
+
+    # loop at the top level, within the <osmChange> element
+    with_element do |action_name|
+      if action_name == 'create'
+        # create a new element. this code is agnostic of the element type
+        # because all the elements support the methods that we're using.
+        with_model do |model, xml|
+          new = model.from_xml_node(xml, true)
+          check(model, xml, new)
+
+          # when this element is saved it will get a new ID, so we save it
+          # to produce the mapping which is sent to other elements.
+          placeholder_id = xml['id'].to_i
+          raise OSM::APIBadXMLError.new(model, xml) if placeholder_id.nil?
+
+          # check if the placeholder ID has been given before and throw
+          # an exception if it has - we can't create the same element twice.
+          model_sym = model.to_s.downcase.to_sym
+          raise OSM::APIBadUserInput.new("Placeholder IDs must be unique for created elements.") if ids[model_sym].include? placeholder_id
+
+          # some elements may have placeholders for other elements in the
+          # diff, so we must fix these before saving the element.
+          new.fix_placeholders!(ids)
+
+          # create element given user
+          new.create_with_history(@changeset.user)
+          
+          # save placeholder => allocated ID map
+          ids[model_sym][placeholder_id] = new.id
+
+          # add the result to the document we're building for return.
+          xml_result = XML::Node.new model.to_s.downcase
+          xml_result["old_id"] = placeholder_id.to_s
+          xml_result["new_id"] = new.id.to_s
+          xml_result["new_version"] = new.version.to_s
+          result.root << xml_result
+        end
+        
+      elsif action_name == 'modify'
+        # modify an existing element. again, this code doesn't directly deal
+        # with types, but uses duck typing to handle them transparently.
+        with_model do |model, xml|
+          # get the new element from the XML payload
+          new = model.from_xml_node(xml, false)
+          check(model, xml, new)
+
+          # if the ID is a placeholder then map it to the real ID
+          model_sym = model.to_s.downcase.to_sym
+          is_placeholder = ids[model_sym].include? new.id
+          id = is_placeholder ? ids[model_sym][new.id] : new.id
+
+          # and the old one from the database
+          old = model.find(id)
+
+          new.fix_placeholders!(ids)
+          old.update_from(new, @changeset.user)
+
+          xml_result = XML::Node.new model.to_s.downcase
+          # oh, the irony... the "new" element actually contains the "old" ID
+          # a better name would have been client/server, but anyway...
+          xml_result["old_id"] = new.id.to_s
+          xml_result["new_id"] = id.to_s 
+          # version is updated in "old" through the update, so we must not
+          # return new.version here but old.version!
+          xml_result["new_version"] = old.version.to_s
+          result.root << xml_result
+        end
+
+      elsif action_name == 'delete'
+        # delete action. this takes a payload in API 0.6, so we need to do
+        # most of the same checks that are done for the modify.
+        with_model do |model, xml|
+          # delete doesn't have to contain a full payload, according to
+          # the wiki docs, so we just extract the things we need.
+          new_id = xml['id'].to_i
+          raise API::APIBadXMLError.new(model, xml, "ID attribute is required") if new_id.nil?
+
+          # if the ID is a placeholder then map it to the real ID
+          model_sym = model.to_s.downcase.to_sym
+          is_placeholder = ids[model_sym].include? new_id
+          id = is_placeholder ? ids[model_sym][new_id] : new_id
+
+          # build the "new" element by modifying the existing one
+          new = model.find(id)
+          new.changeset_id = xml['changeset'].to_i
+          new.version = xml['version'].to_i
+          check(model, xml, new)
+
+          # fetch the matching old element from the DB
+          old = model.find(id)
+
+          # can a delete have placeholders under any circumstances?
+          # if a way is modified, then deleted is that a valid diff?
+          new.fix_placeholders!(ids)
+          old.delete_with_history!(new, @changeset.user)
+
+          xml_result = XML::Node.new model.to_s.downcase
+          # oh, the irony... the "new" element actually contains the "old" ID
+          # a better name would have been client/server, but anyway...
+          xml_result["old_id"] = new_id.to_s
+          result.root << xml_result
+        end
+
+      else
+        # no other actions to choose from, so it must be the users fault!
+        raise OSM::APIChangesetActionInvalid.new(action_name)
+      end
+    end
+
+    # return the XML document to be rendered back to the client
+    return result
+  end
+
+end
index f1a923c42c1e48b3b0de75d2e67b744bdecea23a..2740eab0c5472da4c76d95128c5f8253dd440cbb 100644 (file)
@@ -1,4 +1,9 @@
 module GeoRecord
+  # This scaling factor is used to convert between the float lat/lon that is 
+  # returned by the API, and the integer lat/lon equivalent that is stored in
+  # the database.
+  SCALE = 10000000
+  
   def self.included(base)
     base.extend(ClassMethods)
   end
@@ -20,21 +25,21 @@ module GeoRecord
   end
 
   def lat=(l)
-    self.latitude = (l * 10000000).round
+    self.latitude = (l * SCALE).round
   end
 
   def lon=(l)
-    self.longitude = (l * 10000000).round
+    self.longitude = (l * SCALE).round
   end
 
   # Return WGS84 latitude
   def lat
-    return self.latitude.to_f / 10000000
+    return self.latitude.to_f / SCALE
   end
 
   # Return WGS84 longitude
   def lon
-    return self.longitude.to_f / 10000000
+    return self.longitude.to_f / SCALE
   end
 
 private
index 9b39c9342353fd28a693d1052a45bce99048fe07..153d65780da8f80025a3e06dee05fa60603b9888 100644 (file)
@@ -1,10 +1,11 @@
 module MapBoundary
+  # Take an array of length 4, and return the min_lon, min_lat, max_lon and 
+  # max_lat within their respective boundaries.
   def sanitise_boundaries(bbox)
-    min_lon = [bbox[0].to_f,-180].max
-    min_lat = [bbox[1].to_f,-90].max
-    max_lon = [bbox[2].to_f,+180].min
-    max_lat = [bbox[3].to_f,+90].min
-
+    min_lon = [[bbox[0].to_f,-180].max,180].min
+    min_lat = [[bbox[1].to_f,-90].max,90].min
+    max_lon = [[bbox[2].to_f,+180].min,-180].max
+    max_lat = [[bbox[3].to_f,+90].min,-90].max
     return min_lon, min_lat, max_lon, max_lat
   end
 
@@ -17,6 +18,7 @@ module MapBoundary
       raise("The minimum latitude must be less than the maximum latitude, but it wasn't")
     end
     unless min_lon >= -180 && min_lat >= -90 && max_lon <= 180 && max_lat <= 90
+      # Due to sanitize_boundaries, it is highly unlikely we'll actually get here
       raise("The latitudes must be between -90 and 90, and longitudes between -180 and 180")
     end
 
index 1d32d175d77d0fb85055f8f60c19dff7cc86de2c..392060b6015a8cdb34d5fcfa6172e8a215b05eb4 100644 (file)
@@ -1,6 +1,10 @@
 module ActiveRecord
   module ConnectionAdapters
     module SchemaStatements
+      def quote_column_names(column_name)
+        Array(column_name).map { |e| quote_column_name(e) }.join(", ")
+      end
+
       def add_primary_key(table_name, column_name, options = {})
         column_names = Array(column_name)
         quoted_column_names = column_names.map { |e| quote_column_name(e) }.join(", ")
@@ -11,6 +15,12 @@ module ActiveRecord
         execute "ALTER TABLE #{table_name} DROP PRIMARY KEY"
       end
 
+      def add_foreign_key(table_name, column_name, reftbl, refcol = nil)
+        execute "ALTER TABLE #{table_name} ADD " +
+         "FOREIGN KEY (#{quote_column_names(column_name)}) " +
+         "REFERENCES #{reftbl} (#{quote_column_names(refcol || column_name)})"
+      end
+
       alias_method :old_options_include_default?, :options_include_default?
 
       def options_include_default?(options)
@@ -28,12 +38,22 @@ module ActiveRecord
     end
 
     class MysqlAdapter
-      alias_method :old_native_database_types, :native_database_types
+      if MysqlAdapter.public_instance_methods(false).include?('native_database_types')
+        alias_method :old_native_database_types, :native_database_types
+      end
 
       def native_database_types
         types = old_native_database_types
         types[:bigint] = { :name => "bigint", :limit => 20 }
         types[:double] = { :name => "double" }
+        types[:integer_pk] = { :name => "integer DEFAULT NULL auto_increment PRIMARY KEY" }
+        types[:bigint_pk] = { :name => "bigint(20) DEFAULT NULL auto_increment PRIMARY KEY" }
+        types[:bigint_pk_64] = { :name => "bigint(64) DEFAULT NULL auto_increment PRIMARY KEY" }
+        types[:bigint_auto_64] = { :name => "bigint(64) DEFAULT NULL auto_increment" }
+        types[:bigint_auto_11] = { :name => "bigint(11) DEFAULT NULL auto_increment" }
+        types[:bigint_auto_20] = { :name => "bigint(20) DEFAULT NULL auto_increment" }
+        types[:four_byte_unsigned] = { :name=> "integer unsigned" }
+        types[:inet] = { :name=> "integer unsigned" }
         types
       end
 
@@ -58,6 +78,86 @@ module ActiveRecord
       def innodb_table
         return { :id => false, :force => true, :options => "ENGINE=InnoDB" }
       end
+
+      def innodb_option
+        return "ENGINE=InnoDB"
+      end
+
+      def change_engine (table_name, engine)
+        execute "ALTER TABLE #{table_name} ENGINE = #{engine}"
+      end
+
+      def add_fulltext_index (table_name, column)
+        execute "CREATE FULLTEXT INDEX `#{table_name}_#{column}_idx` ON `#{table_name}` (`#{column}`)"
+      end
+
+      def alter_column_nwr_enum (table_name, column)
+        execute "alter table #{table_name} change column #{column} #{column} enum('Node','Way','Relation');"
+      end
+
+      def alter_primary_key(table_name, new_columns)
+        execute("alter table #{table_name} drop primary key, add primary key (#{new_columns.join(',')})")
+      end
+
+      def interval_constant(interval)
+        "'#{interval}'"
+      end
+    end
+
+    class PostgreSQLAdapter
+      if PostgreSQLAdapter.public_instance_methods(false).include?('native_database_types')
+        alias_method :old_native_database_types, :native_database_types
+      end
+
+      def native_database_types
+        types = old_native_database_types
+        types[:double] = { :name => "double precision" }
+        types[:integer_pk] = { :name => "serial PRIMARY KEY" }
+        types[:bigint_pk] = { :name => "bigserial PRIMARY KEY" }
+        types[:bigint_pk_64] = { :name => "bigserial PRIMARY KEY" }
+        types[:bigint_auto_64] = { :name => "bigint" } #fixme: need autoincrement?
+        types[:bigint_auto_11] = { :name => "bigint" } #fixme: need autoincrement?
+        types[:bigint_auto_20] = { :name => "bigint" } #fixme: need autoincrement?
+        types[:four_byte_unsigned] = { :name => "bigint" } # meh
+        types[:inet] = { :name=> "inet" }
+        types
+      end
+
+      def myisam_table
+        return { :id => false, :force => true, :options => ""}
+      end
+
+      def innodb_table
+        return { :id => false, :force => true, :options => ""}
+      end
+
+      def innodb_option
+        return ""
+      end
+      def change_engine (table_name, engine)
+      end
+
+      def add_fulltext_index (table_name, column)
+        execute "CREATE INDEX #{table_name}_#{column}_idx on #{table_name} (#{column})"
+      end
+
+      def alter_column_nwr_enum (table_name, column)
+        response = select_one("select count(*) as count from pg_type where typname = 'nwr_enum'")
+        if response['count'] == "0" #yep, as a string
+          execute "create type nwr_enum as ENUM ('Node', 'Way', 'Relation')"
+        end
+        execute        "alter table #{table_name} drop #{column}"
+        execute "alter table #{table_name} add #{column} nwr_enum"
+      end
+
+      def alter_primary_key(table_name, new_columns)
+        execute "alter table #{table_name} drop constraint #{table_name}_pkey; alter table #{table_name} add primary key (#{new_columns.join(',')})"
+      end
+
+      def interval_constant(interval)
+        "'#{interval}'::interval"
+      end
     end
   end
 end
index 365ddf68ef080bc0aef2e72dce292584edfbbaab..f372979a8d784bf99a49b14746e1753ee7b74927 100644 (file)
@@ -10,18 +10,157 @@ module OSM
 
   # The base class for API Errors.
   class APIError < RuntimeError
+    def render_opts
+      { :text => "Generic API Error", :status => :internal_server_error, :content_type => "text/plain" }
+    end
   end
 
   # Raised when an API object is not found.
   class APINotFoundError < APIError
+    def render_opts
+      { :text => "The API wasn't found", :status => :not_found, :content_type => "text/plain" }
+    end
   end
 
   # Raised when a precondition to an API action fails sanity check.
   class APIPreconditionFailedError < APIError
+    def initialize(message = "")
+      @message = message
+    end
+    
+    def render_opts
+      { :text => "Precondition failed: #{@message}", :status => :precondition_failed, :content_type => "text/plain" }
+    end
   end
 
   # Raised when to delete an already-deleted object.
   class APIAlreadyDeletedError < APIError
+    def render_opts
+      { :text => "The object has already been deleted", :status => :gone, :content_type => "text/plain" }
+    end
+  end
+
+  # Raised when the user logged in isn't the same as the changeset
+  class APIUserChangesetMismatchError < APIError
+    def render_opts
+      { :text => "The user doesn't own that changeset", :status => :conflict, :content_type => "text/plain" }
+    end
+  end
+
+  # Raised when the changeset provided is already closed
+  class APIChangesetAlreadyClosedError < APIError
+    def initialize(changeset)
+      @changeset = changeset
+    end
+
+    attr_reader :changeset
+    
+    def render_opts
+      { :text => "The changeset #{@changeset.id} was closed at #{@changeset.closed_at}.", :status => :conflict, :content_type => "text/plain" }
+    end
+  end
+  
+  # Raised when a change is expecting a changeset, but the changeset doesn't exist
+  class APIChangesetMissingError < APIError
+    def render_opts
+      { :text => "You need to supply a changeset to be able to make a change", :status => :conflict, :content_type => "text/plain" }
+    end
+  end
+
+  # Raised when a diff is uploaded containing many changeset IDs which don't match
+  # the changeset ID that the diff was uploaded to.
+  class APIChangesetMismatchError < APIError
+    def initialize(provided, allowed)
+      @provided, @allowed = provided, allowed
+    end
+    
+    def render_opts
+      { :text => "Changeset mismatch: Provided #{@provided} but only " +
+      "#{@allowed} is allowed.", :status => :conflict, :content_type => "text/plain" }
+    end
+  end
+  
+  # Raised when a diff upload has an unknown action. You can only have create,
+  # modify, or delete
+  class APIChangesetActionInvalid < APIError
+    def initialize(provided)
+      @provided = provided
+    end
+    
+    def render_opts
+      { :text => "Unknown action #{@provided}, choices are create, modify, delete.",
+      :status => :bad_request, :content_type => "text/plain" }
+    end
+  end
+
+  # Raised when bad XML is encountered which stops things parsing as
+  # they should.
+  class APIBadXMLError < APIError
+    def initialize(model, xml, message="")
+      @model, @xml, @message = model, xml, message
+    end
+
+    def render_opts
+      { :text => "Cannot parse valid #{@model} from xml string #{@xml}. #{@message}",
+      :status => :bad_request, :content_type => "text/plain" }
+    end
+  end
+
+  # Raised when the provided version is not equal to the latest in the db.
+  class APIVersionMismatchError < APIError
+    def initialize(id, type, provided, latest)
+      @id, @type, @provided, @latest = id, type, provided, latest
+    end
+
+    attr_reader :provided, :latest, :id, :type
+
+    def render_opts
+      { :text => "Version mismatch: Provided " + provided.to_s +
+        ", server had: " + latest.to_s + " of " + type + " " + id.to_s, 
+        :status => :conflict, :content_type => "text/plain" }
+    end
+  end
+
+  # raised when a two tags have a duplicate key string in an element.
+  # this is now forbidden by the API.
+  class APIDuplicateTagsError < APIError
+    def initialize(type, id, tag_key)
+      @type, @id, @tag_key = type, id, tag_key
+    end
+
+    attr_reader :type, :id, :tag_key
+
+    def render_opts
+      { :text => "Element #{@type}/#{@id} has duplicate tags with key #{@tag_key}.",
+        :status => :bad_request, :content_type => "text/plain" }
+    end
+  end
+  
+  # Raised when a way has more than the configured number of way nodes.
+  # This prevents ways from being to long and difficult to work with
+  class APITooManyWayNodesError < APIError
+    def initialize(provided, max)
+      @provided, @max = provided, max
+    end
+    
+    attr_reader :provided, :max
+    
+    def render_opts
+      { :text => "You tried to add #{provided} nodes to the way, however only #{max} are allowed",
+        :status => :bad_request, :content_type => "text/plain" }
+    end
+  end
+
+  ##
+  # raised when user input couldn't be parsed
+  class APIBadUserInput < APIError
+    def initialize(message)
+      @message = message
+    end
+
+    def render_opts
+      { :text => @message, :content_type => "text/plain", :status => :bad_request }
+    end
   end
 
   # Helper methods for going to/from mercator and lat/lng.
@@ -190,7 +329,7 @@ module OSM
       doc.encoding = XML::Encoding::UTF_8
       root = XML::Node.new 'osm'
       root['version'] = API_VERSION
-      root['generator'] = 'OpenStreetMap server'
+      root['generator'] = GENERATOR
       doc.root = root
       return doc
     end
diff --git a/lib/tags.rb b/lib/tags.rb
deleted file mode 100644 (file)
index 2095b09..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-module Tags
-  def self.join(tags)
-    joined = tags.collect { |k,v| "#{escape_string(k)}=#{escape_string(v)}" }.join(';')
-    joined = '' if joined.nil?
-    return joined
-  end
-
-  def self.escape_string(tag)
-    return tag.gsub(/[;=\\]/) { |v| escape_char(v) }
-  end
-
-  def self.escape_char(v)
-    case v
-      when ';' then return '\\s'
-      when '=' then return '\\e'
-    end
-    return '\\\\'
-  end
-
-  def self.split(tags)
-    tags.split(';').each do |tag|
-      key,val = tag.split('=').collect { |s| s.strip }
-      key = '' if key.nil?
-      val = '' if val.nil?
-      if key != '' && val != ''
-        yield unescape_string(key),unescape_string(val)
-      end
-    end
-  end
-
-  def self.unescape_string(tag)
-    return tag.gsub(/\\[se\\]/) { |v| unescape_char(v) }
-  end
-
-  def self.unescape_char(v)
-    case v
-      when '\\s' then return ';'
-      when '\\e' then return '='
-    end
-    return '\\'
-  end
-end
diff --git a/lib/tasks/populate_node_tags.rake b/lib/tasks/populate_node_tags.rake
deleted file mode 100644 (file)
index 86747cf..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-namespace 'db' do
-  desc 'Populate the node_tags table'
-  task :node_tags  do
-    require File.dirname(__FILE__) + '/../../config/environment'
-
-    node_count = Node.count
-    limit = 1000 #the number of nodes to grab in one go
-    offset = 0   
-
-    while offset < node_count
-        Node.find(:all, :limit => limit, :offset => offset).each do |node|
-        seq_id = 1
-        node.tags.split(';').each do |tag|
-          nt = NodeTag.new
-          nt.id = node.id
-          nt.k = tag.split('=')[0] || ''
-          nt.v = tag.split('=')[1] || ''
-          nt.sequence_id = seq_id 
-          nt.save! || raise
-          seq_id += 1
-        end
-
-        version = 1 #version refers to one set of histories
-        node.old_nodes.find(:all, :order => 'timestamp asc').each do |old_node|
-        sequence_id = 1 #sequence_id refers to the sequence of node tags within a history
-        old_node.tags.split(';').each do |tag|
-          ont = OldNodeTag.new
-          ont.id = node.id #the id of the node tag
-          ont.k = tag.split('=')[0] || ''
-          ont.v = tag.split('=')[1] || ''
-          ont.version = version
-          ont.sequence_id = sequence_id
-          ont.save! || raise
-          sequence_id += 1
-          end     
-        version += 1
-        end
-      end
-    offset += limit
-    end
-  end
-end
diff --git a/lib/validators.rb b/lib/validators.rb
new file mode 100644 (file)
index 0000000..095fb7a
--- /dev/null
@@ -0,0 +1,32 @@
+module ActiveRecord
+  module Validations
+    module ClassMethods
+      
+      # error message when invalid UTF-8 is detected
+      @@invalid_utf8_message = " is invalid UTF-8"
+
+      ##
+      # validation method to be included like any other validations methods
+      # in the models definitions. this one checks that the named attribute
+      # is a valid UTF-8 format string.
+      def validates_as_utf8(*attrs)
+        validates_each(attrs) do |record, attr, value|
+          record.errors.add(attr, @@invalid_utf8_message) unless valid_utf8? value
+        end
+      end    
+      
+      ##
+      # Checks that a string is valid UTF-8 by trying to convert it to UTF-8
+      # using the iconv library, which is in the standard library.
+      def valid_utf8?(str)
+        return true if str.nil?
+        Iconv.conv("UTF-8", "UTF-8", str)
+        return true
+
+      rescue
+        return false
+      end  
+      
+    end
+  end
+end
index 0e1845619d8d06bf998b2b2aff7c2241c7abd009..16abbfca0e03f0483709dcbf6e08b06319ac42e5 100644 (file)
@@ -2,7 +2,11 @@
    "http://www.w3.org/TR/html4/loose.dtd">
 <html>
 <body>
-  <h1>File not found</h1>
-  <p>Change this error message for pages not found in public/404.html</p>
+  <img src="http://www.openstreetmap.org/images/osm_logo.png" style="float:left; margin:10px">
+  <div style="float:left;">
+    <h1>File not found</h1>  
+    <p>Couldn't find a file/directory/API operation by that name on the OpenStreetMap server (HTTP 404)</p>
+    <p>Feel free to <a href="http://wiki.openstreetmap.org/index.php/Contact" title="Various contact channels explained">contact</a> the OpenStreetMap community if you have found a broken link / bug. Make a note of the exact URL of your request.</p>
+  </div>
 </body>
 </html>
\ No newline at end of file
index ab95f74c4661b6ebc19908a47d1ce39f4b66087a..552024a2db5cd8829bd528c5ade2022b1564ff91 100644 (file)
@@ -2,7 +2,12 @@
    "http://www.w3.org/TR/html4/loose.dtd">
 <html>
 <body>
-  <h1>Application error</h1>
-  <p>Change this error message for exceptions thrown outside of an action (like in Dispatcher setups or broken Ruby code) in public/500.html</p>
+  <img src="http://www.openstreetmap.org/images/osm_logo.png" style="float:left; margin:10px">
+  <div style="float:left;">
+    <h1>Application error</h1>
+    <p>The OpenStreetMap server encountered an unexpected condition that prevented it from fulfilling the request (HTTP 500)</p>
+    <p>Feel free to <a href="http://wiki.openstreetmap.org/index.php/Contact" title="Various contact channels explained">contact</a> the OpenStreetMap community if your problem persists. Make a note of the exact URL / post data of your request.</p>
+    <p>This may be a problem in our Ruby On Rails code. 500 ocurrs with exceptions thrown outside of an action (like in Dispatcher setups or broken Ruby code)</p>
+  </div>
 </body>
 </html>
\ No newline at end of file
index c80ed2242c8147db90177925af3db63eacee971f..6e012b3d30f4cd0128d58654e63d20b1a02813be 100644 (file)
@@ -1,6 +1,7 @@
 var epsg4326 = new OpenLayers.Projection("EPSG:4326");
 var map;
 var markers;
+var vectors;
 var popup;
 
 var nonamekeys = {
@@ -13,6 +14,8 @@ var nonamekeys = {
 };
 
 OpenLayers._getScriptLocation = function () {
+  // Should really have this file as an erb, so that this can return 
+  // the real rails root
    return "/openlayers/";
 }
 
@@ -71,6 +74,7 @@ function createMap(divName, options) {
    map.addLayer(maplint);
 
    var numZoomLevels = Math.max(mapnik.numZoomLevels, osmarender.numZoomLevels);
+
    markers = new OpenLayers.Layer.Markers("Markers", {
       displayInLayerSwitcher: false,
       numZoomLevels: numZoomLevels,
@@ -80,7 +84,7 @@ function createMap(divName, options) {
       projection: "EPSG:900913"
    });
    map.addLayer(markers);
-
+   
    return map;
 }
 
@@ -104,6 +108,26 @@ function addMarkerToMap(position, icon, description) {
    return marker;
 }
 
+function addBoxToMap(boxbounds) {
+   if(!vectors) {
+     // Be aware that IE requires Vector layers be initialised on page load, and not under deferred script conditions
+     vectors = new OpenLayers.Layer.Vector("Box Layer", {
+        displayInLayerSwitcher: false
+     });
+     map.addLayer(vectors);
+   }
+   var geometry = boxbounds.toGeometry().transform(epsg4326, map.getProjectionObject());
+   var box = new OpenLayers.Feature.Vector(geometry, {}, {
+      strokeWidth: 2,
+      strokeColor: '#ee9900',
+      fillOpacity: 0
+   });
+   
+   vectors.addFeatures(box);
+
+   return box;
+}
+
 function openMapPopup(marker, description) {
    closeMapPopup();
 
@@ -127,11 +151,16 @@ function removeMarkerFromMap(marker){
    markers.removeMarker(marker);
 }
 
+function removeBoxFromMap(box){
+   vectors.removeFeature(box);
+}
+
 function getMapCenter(center, zoom) {
    return map.getCenter().clone().transform(map.getProjectionObject(), epsg4326);
 }
 
 function setMapCenter(center, zoom) {
+   zoom = parseInt(zoom);
    var numzoom = map.getNumZoomLevels();
    if (zoom >= numzoom) zoom = numzoom - 1;
    map.setCenter(center.clone().transform(epsg4326, map.getProjectionObject()), zoom);
@@ -141,7 +170,7 @@ function setMapExtent(extent) {
    map.zoomToExtent(extent.clone().transform(epsg4326, map.getProjectionObject()));
 }
 
-function getMapExtent(extent) {
+function getMapExtent() {
    return map.getExtent().clone().transform(map.getProjectionObject(), epsg4326);
 }
 
index 95c0b938e3938fe89675fbabd2d55302ccef78b2..06b4152df9a1267cef030762145d5c2946e117c1 100644 (file)
@@ -1,4 +1,6 @@
-function updatelinks(lon,lat,zoom,layers) {
+//Called as the user scrolls/zooms around.
+//Maniplate hrefs of the view tab and various other links
+function updatelinks(lon,lat,zoom,layers,extents) {
   var decimals = Math.pow(10, Math.floor(zoom/3));
   var node;
 
@@ -55,6 +57,28 @@ function updatelinks(lon,lat,zoom,layers) {
       node.style.fontStyle = 'italic';
     }
   }
+  
+  node = document.getElementById("historyanchor");
+  if (node) {
+    if (zoom >= 11) {
+      var args = new Object();
+      //set bbox param from 'extents' object
+      minlon = extents.left;
+      minlat = extents.bottom;
+      maxlon = extents.right;
+      maxlat = extents.top;
+      minlon = Math.round(minlon * decimals) / decimals;
+      minlat = Math.round(minlat * decimals) / decimals;
+      maxlon = Math.round(maxlon * decimals) / decimals;
+      maxlat = Math.round(maxlat * decimals) / decimals;
+      args.bbox = minlon + "," + minlat + "," + maxlon + "," + maxlat;
+      node.href = setArgs("history/", args);
+      node.style.fontStyle = 'normal';
+    } else {
+      node.href = 'javascript:alert("zoom in to see editing history");';
+      node.style.fontStyle = 'italic';
+    }
+  }
 }
 
 function getArgs(url) {
index 970835268ad19bc6e04b395bc836f4f2cba8d1f8..b4e86c20ff5923c355542a4309ec713457a4af1e 100644 (file)
Binary files a/public/potlatch/beep.mp3 and b/public/potlatch/beep.mp3 differ
index 9aa5735ac405af28c3afeceb7b4d96416a24148b..d638c379c71dcfec8931c320b128b16b17cb65fa 100755 (executable)
Binary files a/public/potlatch/potlatch.swf and b/public/potlatch/potlatch.swf differ
index 8fc05b01770ecefc28e80de7ec9f229900413327..0bab9c57ff4232d3f8a68638edfe0a75a0f587a3 100644 (file)
@@ -289,6 +289,12 @@ hides rule from IE5-Mac \*/
   font-size: 10px;
 }
 
+hr {
+  border: none;
+  background-color: #ccc;
+  color: #ccc;
+  height: 1px;
+}
 
 .gpxsummary {
   font-size: 12px;
@@ -531,6 +537,16 @@ input[type="submit"] {
   border: 1px solid black;
 }
 
+#accountForm td {
+       padding-bottom:10px;
+}
+
+.fieldName {
+       text-align:right;
+       font-weight:bold;
+}
+
+
 .nohome .location {
   display: none;
 }
@@ -543,9 +559,8 @@ input[type="submit"] {
   display: inline !important;
 }
 
-.editDescription {
-  height: 10ex;
-  width: 30em;
+.minorNote {
+       font-size:0.8em;
 }
 
 .nowrap {
index e5f82bba6152d37917261e8f6b4c01b4b553cf61..62c7e93a966eddfd229dc6ae3c14b01d831729c2 100755 (executable)
@@ -53,11 +53,14 @@ begin
 
   puts "<tr><th>GPX Files</th><td>#{day_count}</td><td>#{week_count}</td><td>#{month_count}</td></tr>"
 
-  day_count = OldNode.count(:user_id, :distinct => true, 
+  day_count = OldNode.count(:user_id, :distinct => true,
+                            :include => :changeset,
                             :conditions => "timestamp > NOW() - INTERVAL 1 DAY")
   week_count = OldNode.count(:user_id, :distinct => true, 
+                             :include => :changeset,
                              :conditions => "timestamp > NOW() - INTERVAL 7 DAY")
   month_count = OldNode.count(:user_id, :distinct => true, 
+                              :include => :changeset,
                               :conditions => "timestamp > NOW() - INTERVAL 28 DAY")
 
   puts "<tr><th>Nodes</th><td>#{day_count}</td><td>#{week_count}</td><td>#{month_count}</td></tr>"
@@ -69,11 +72,14 @@ begin
   puts "<tr><th>Day</th><th>Week</th><th>Month</th></tr>"
 
   day_users = OldNode.count(:conditions => "timestamp > NOW() - INTERVAL 1 DAY",
-                            :group => :user_id, :order => "count_all DESC")
+                            :include => :changeset, :group => :user_id,
+                            :order => "count_all DESC")
   week_users = OldNode.count(:conditions => "timestamp > NOW() - INTERVAL 7 DAY",
-                             :group => :user_id, :order => "count_all DESC", :limit => 60)
+                             :include => :changeset, :group => :user_id,
+                             :order => "count_all DESC", :limit => 60)
   month_users = OldNode.count(:conditions => "timestamp > NOW() - INTERVAL 28 DAY",
-                              :group => :user_id, :order => "count_all DESC", :limit => 60)
+                              :include => :changeset, :group => :user_id,
+                              :order => "count_all DESC", :limit => 60)
 
   SyncEnumerator.new(day_users, week_users, month_users).each do |row|
     puts "<tr>"
diff --git a/test/fixtures/changeset_tags.yml b/test/fixtures/changeset_tags.yml
new file mode 100644 (file)
index 0000000..34d2bf4
--- /dev/null
@@ -0,0 +1,4 @@
+changeset_1_tag_1: 
+  id: 1
+  k: created_by
+  v: test suite yml
diff --git a/test/fixtures/changesets.yml b/test/fixtures/changesets.yml
new file mode 100644 (file)
index 0000000..055381a
--- /dev/null
@@ -0,0 +1,71 @@
+# FIXME! all of these changesets need their bounding boxes set correctly!
+# 
+<% SCALE = 10000000 unless defined?(SCALE) %>
+
+# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
+normal_user_first_change:
+  id: 1
+  user_id: 1
+  created_at: "2007-01-01 00:00:00"
+  closed_at: <%= DateTime.now + Rational(1,24) %>
+  min_lon: <%= 1 * SCALE %>
+  min_lat: <%= 1 * SCALE %>
+  max_lon: <%= 5 * SCALE %>
+  max_lat: <%= 5 * SCALE %>
+  num_changes: 11
+  
+public_user_first_change:
+  id: 2
+  user_id: 2
+  created_at: "2008-05-01 01:23:45"
+  closed_at: <%= DateTime.now + Rational(1,24) %>
+  num_changes: 0
+
+normal_user_closed_change:
+  id: 3
+  user_id: 1
+  created_at: "2007-01-01 00:00:00"
+  closed_at: "2007-01-02 00:00:00"
+  num_changes: 0
+
+public_user_closed_change:
+  id: 7
+  user_id: 2
+  created_at: "2007-01-01 00:00:00"
+  closed_at: "2007-01-02 00:00:00"
+  num_changes: 0
+
+normal_user_version_change:
+  id: 4
+  user_id: 1
+  created_at: "2008-01-01 00:00:00"
+  closed_at: <%= DateTime.now + Rational(1,24) %>
+  min_lon: <%= 1 * SCALE %>
+  min_lat: <%= 1 * SCALE %>
+  max_lon: <%= 4 * SCALE %>
+  max_lat: <%= 4 * SCALE %>
+  num_changes: 8
+
+# changeset to contain all the invalid stuff that is in the
+# fixtures (nodes outside the world, etc...), but needs to have
+# a valid user...
+invalid_changeset:
+  id: 5
+  user_id: 3
+  created_at: "2008-01-01 00:00:00"
+  closed_at: "2008-01-02 00:00:00"
+  num_changes: 9
+
+# changeset which still has time remaining, but has been closed
+# by containing too many elements.
+too_many_elements_changeset:
+  id: 6
+  user_id: 1
+  created_at: "2008-01-01 00:00:00"
+  closed_at: <%= DateTime.now + Rational(1,24) %>
+  min_lon: <%= 1 * SCALE %>
+  min_lat: <%= 1 * SCALE %>
+  max_lon: <%= 4 * SCALE %>
+  max_lat: <%= 4 * SCALE %>
+  num_changes: <%= Changeset::MAX_ELEMENTS + 1 %>
+
diff --git a/test/fixtures/current_node_tags.yml b/test/fixtures/current_node_tags.yml
new file mode 100644 (file)
index 0000000..1494daf
--- /dev/null
@@ -0,0 +1,29 @@
+t1:
+  id: 1
+  k: 'testvisible'
+  v: 'yes'
+
+t2:
+  id: 2
+  k: 'testused'
+  v: 'yes'
+
+t3:
+  id: 3
+  k: 'test'
+  v: 'yes'
+
+t4:
+  id: 4
+  k: 'test'
+  v: 'yes'
+
+nv_t1:
+  id: 15
+  k: 'testing'
+  v: 'added in node version 3'
+
+nv_t2:
+  id: 15
+  k: 'testing two'
+  v: 'modified in node version 4'
index dd3bd248772a314f52e0bf8c92a68b0b076fca0c..6f21fd47f30a1267004e5e2116652cd1fe4f9ae2 100644 (file)
 # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
+<% SCALE = 10000000 unless defined?(SCALE) %>
+
 visible_node:
   id: 1
-  latitude: 1
-  longitude: 1
-  user_id: 1
-  visible: 1
-  tags: test=yes
+  latitude: <%= 1*SCALE %>
+  longitude: <%= 1*SCALE %>
+  changeset_id: 1
+  visible: true
+  version: 1
+  tile: <%= QuadTile.tile_for_point(1,1) %>
   timestamp: 2007-01-01 00:00:00
 
 invisible_node:
   id: 2
-  latitude: 2
-  longitude: 2
-  user_id: 1
-  visible: 0
-  tags: test=yes
+  latitude: <%= 2*SCALE %>
+  longitude: <%= 2*SCALE %>
+  changeset_id: 1
+  visible: false
+  version: 1
+  tile: <%= QuadTile.tile_for_point(2,2) %>
   timestamp: 2007-01-01 00:00:00
 
 used_node_1:
   id: 3
-  latitude: 3
-  longitude: 3
-  user_id: 1
-  visible: 1
-  tags: test=yes
+  latitude: <%= 3*SCALE %>
+  longitude: <%= 3*SCALE %>
+  changeset_id: 1
+  visible: true
+  version: 1
+  tile: <%= QuadTile.tile_for_point(3,3) %>
   timestamp: 2007-01-01 00:00:00
 
 used_node_2:
   id: 4
-  latitude: 4
-  longitude: 4
-  user_id: 1
-  visible: 1
-  tags: test=yes
+  latitude: <%= 4*SCALE %>
+  longitude: <%= 4*SCALE %>
+  changeset_id: 1
+  visible: true
+  version: 1
+  tile: <%= QuadTile.tile_for_point(4,4) %>
   timestamp: 2007-01-01 00:00:00
 
 node_used_by_relationship:
   id: 5
-  latitude: 5
-  longitude: 5
-  user_id: 1
-  visible: 1
-  tags: test=yes
+  latitude: <%= 5*SCALE %>
+  longitude: <%= 5*SCALE %>
+  changeset_id: 1
+  visible: true
+  version: 1
+  tile: <%= QuadTile.tile_for_point(5,5) %>
+  timestamp: 2007-01-01 00:00:00
+  
+node_too_far_north:
+  id: 6
+  latitude: <%= 90.01*SCALE %>
+  longitude: <%= 6*SCALE %>
+  changeset_id: 5
+  visible: true
+  version: 1
+  tile: <%= QuadTile.tile_for_point(90.01,6) %>
+  timestamp: 2007-01-01 00:00:00
+  
+node_north_limit:
+  id: 11
+  latitude: <%= 90*SCALE %>
+  longitude: <%= 11*SCALE %>
+  changeset_id: 5
+  visible: true
+  version: 1
+  tile: <%= QuadTile.tile_for_point(90,11) %>
+  timestamp: 2008-07-08 14:50:00
+  
+node_too_far_south:
+  id: 7
+  latitude: <%= -90.01*SCALE %>
+  longitude: <%= 7*SCALE %>
+  changeset_id: 5
+  visible: true
+  version: 1
+  tile: <%= QuadTile.tile_for_point(-90.01,7) %>
+  timestamp: 2007-01-01 00:00:00
+  
+node_south_limit:
+  id: 12
+  latitude: <%= -90*SCALE %>
+  longitude: <%= 12*SCALE %>
+  changeset_id: 5
+  visible: true
+  version: 1
+  tile: <%= QuadTile.tile_for_point(-90,12) %>
+  timestamp: 2008-07-08 15:02:18
+  
+node_too_far_west:
+  id: 8
+  latitude: <%= 8*SCALE %>
+  longitude: <%= -180.01*SCALE %>
+  changeset_id: 5
+  visible: true
+  version: 1
+  tile: <%= QuadTile.tile_for_point(8,-180.01) %>
+  timestamp: 2007-01-01 00:00:00
+  
+node_west_limit:
+  id: 13
+  latitude: <%= 13*SCALE %>
+  longitude: <%= -180*SCALE %>
+  changeset_id: 5
+  visible: true
+  version: 1
+  tile: <%= QuadTile.tile_for_point(13,-180) %>
+  timestamp: 2008-07-08 15:17:37
+  
+node_too_far_east:
+  id: 9
+  latitude: <%= 9*SCALE %>
+  longitude: <%= 180.01*SCALE %>
+  changeset_id: 5
+  visible: true
+  version: 1
+  tile: <%= QuadTile.tile_for_point(9,180.01) %>
+  timestamp: 2007-01-01 00:00:00
+  
+node_east_limit:
+  id: 14
+  latitude: <%= 14*SCALE %>
+  longitude: <%= 180*SCALE %>
+  changeset_id: 5
+  visible: true
+  version: 1
+  tile: <%= QuadTile.tile_for_point(14,180) %>
+  timestamp: 2008-07-08 15:46:16
+  
+node_totally_wrong:
+  id: 10
+  latitude: <%= 200*SCALE %>
+  longitude: <%= 200*SCALE %>
+  changeset_id: 5
+  visible: true
+  version: 1
+  tile: <%= QuadTile.tile_for_point(200,200) %>
   timestamp: 2007-01-01 00:00:00
+  
+node_with_versions:
+  id: 15
+  latitude: <%= 1*SCALE %>
+  longitude: <%= 1*SCALE %>
+  changeset_id: 4
+  visible: true
+  version: 4
+  tile: <%= QuadTile.tile_for_point(1,1) %>
+  timestamp: 2008-01-01 00:04:00
index bddc8a0dd0cf0a77dbd0825194240c9e2eeff40d..f055371151e96483e074c98da66ab51094334742 100644 (file)
@@ -1,23 +1,29 @@
 t1:
   id: 1
   member_role: "some"
-  member_type: "way"
+  member_type: "Way"
   member_id: 3
 
 t2:
   id: 1
   member_role: "some"
-  member_type: "node"
+  member_type: "Node"
   member_id: 5
 
 t3:
   id: 1
   member_role: "some"
-  member_type: "relation"
+  member_type: "Relation"
   member_id: 3
 
 t4:
   id: 3
   member_role: "some"
-  member_type: "node"
+  member_type: "Node"
+  member_id: 5
+
+t5:
+  id: 2
+  member_role: "some"
+  member_type: "Node"
   member_id: 5
index aaf06a397d54a3321dda12eb054e1a3549e823d0..d2755bdfd4edbe8781399363233f671a5de91f98 100644 (file)
@@ -1,14 +1,14 @@
 t1:
   id: 1
-  k: test
-  v: yes
+  k: 'test'
+  v: 'yes'
 
 t2:
   id: 2
-  k: test
-  v: yes
+  k: 'test'
+  v: 'yes'
 
-t2:
+t3:
   id: 3
-  k: test
-  v: yes
+  k: 'test'
+  v: 'yes'
index c1f77d4282d36a316fe68159f08579ed611b53d9..165f1a21e032a7623a9735cfc9d50d8e2d9eef97 100644 (file)
@@ -1,17 +1,20 @@
 visible_relation:
   id: 1
-  user_id: 1
+  changeset_id: 1
   timestamp: 2007-01-01 00:00:00
-  visible: 1
+  visible: true
+  version: 1
 
 invisible_relation:
   id: 2
-  user_id: 1
+  changeset_id: 1
   timestamp: 2007-01-01 00:00:00
-  visible: 0
+  visible: false
+  version: 1
 
 used_relation:
   id: 3
-  user_id: 1
+  changeset_id: 1
   timestamp: 2007-01-01 00:00:00
-  visible: 1
+  visible: true
+  version: 1
index ce394edbeed760341fd4f4d97e7f387dc2cd875b..66aae0f20af7ab68653288d1f8c4c84b9b10c02b 100644 (file)
@@ -12,3 +12,8 @@ t3:
   id: 3
   node_id: 3
   sequence_id: 1
+
+t4:
+  id: 4
+  node_id: 15
+  sequence_id: 1
index 375247ea2ce24e4ffed0f179dead3828fd77fe15..c1ef21d573a2eecbb9ca46a9f066d129499ce73f 100644 (file)
@@ -1,15 +1,15 @@
 t1:
   id: 1
-  k: test
-  v: yes
+  k: 'test'
+  v: 'yes'
 
 t2:
   id: 2
-  k: test
-  v: yes
+  k: 'test'
+  v: 'yes'
 
 t3:
   id: 3
-  k: test
-  v: yes
+  k: 'test'
+  v: 'yes'
 
index b129d7f45eb7d1a026f361bd0d3b0c5fafb7d22d..51b585df057872c591f766ded4454151c7645a18 100644 (file)
@@ -1,18 +1,27 @@
 visible_way:
   id: 1
-  user_id: 1
+  changeset_id: 2
   timestamp: 2007-01-01 00:00:00
-  visible: 1
+  visible: true
+  version: 1
 
 invisible_way:
   id: 2
-  user_id: 1
+  changeset_id: 2
   timestamp: 2007-01-01 00:00:00
-  visible: 0
+  visible: false
+  version: 1
 
 used_way:
   id: 3
-  user_id: 1
+  changeset_id: 2
   timestamp: 2007-01-01 00:00:00
-  visible: 1
+  visible: true
+  version: 1
 
+way_with_versions:
+  id: 4
+  changeset_id: 4
+  timestamp: 2008-01-01 00:04:00
+  visible: true
+  version: 4
diff --git a/test/fixtures/diary_comments.yml b/test/fixtures/diary_comments.yml
new file mode 100644 (file)
index 0000000..8bb9f49
--- /dev/null
@@ -0,0 +1,7 @@
+comment_for_geo_post:
+  id: 1
+  diary_entry_id: 2
+  user_id: 2
+  body: Some comment text
+  created_at: "2008-11-08 09:45:34"
+  updated_at: "2008-11-08 10:34:34"
diff --git a/test/fixtures/diary_entries.yml b/test/fixtures/diary_entries.yml
new file mode 100644 (file)
index 0000000..5d07e5f
--- /dev/null
@@ -0,0 +1,21 @@
+normal_user_entry_1:
+  id: 1
+  user_id: 1
+  title: Diary Entry 1
+  body: This is the body of diary entry 1.
+  created_at: "2008-11-07 17:43:34"
+  updated_at: "2008-11-07 17:43:34"
+  latitude: 
+  longitude: 
+  language: 
+  
+normal_user_geo_entry:
+  id: 2
+  user_id: 1
+  title: Geo Entry 1
+  body: This is the body of a geo diary entry in London.
+  created_at: "2008-11-07 17:47:34"
+  updated_at: "2008-11-07 17:47:34"
+  latitude: 51.50763
+  longitude: -0.10781
+  language: 
diff --git a/test/fixtures/friends.yml b/test/fixtures/friends.yml
new file mode 100644 (file)
index 0000000..782f1e3
--- /dev/null
@@ -0,0 +1,4 @@
+normal_user_with_second_user:
+  id: 1
+  user_id: 1
+  friend_user_id: 2
diff --git a/test/fixtures/gps_points.yml b/test/fixtures/gps_points.yml
new file mode 100644 (file)
index 0000000..13ee355
--- /dev/null
@@ -0,0 +1,9 @@
+first_trace_1:
+  altitude: 134
+  trackid: 1
+  latitude: 1
+  longitude: 1
+  gpx_id: 1
+  timestamp: "2008-10-01 10:10:10"
+  tile: 1
+  
diff --git a/test/fixtures/gpx_file_tags.yml b/test/fixtures/gpx_file_tags.yml
new file mode 100644 (file)
index 0000000..d914bfb
--- /dev/null
@@ -0,0 +1,4 @@
+first_trace_1:
+  gpx_id: 1
+  tag: London
+  id: 1
diff --git a/test/fixtures/gpx_files.yml b/test/fixtures/gpx_files.yml
new file mode 100644 (file)
index 0000000..3ab74c8
--- /dev/null
@@ -0,0 +1,12 @@
+first_trace_file:
+  id: 1
+  user_id: 1
+  visible: true
+  name: Fist Trace.gpx
+  size:
+  latitude: 1
+  longitude: 1
+  timestamp: "2008-10-29 10:10:10"
+  public: true
+  description: This is a trace
+  inserted: true
index b49c4eb4e1e9d522e9424c822ce1ce80662e94cc..22fab186322bca351586e0297f3d89aa98b4b15a 100644 (file)
@@ -1,5 +1,16 @@
 # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
 one:
-  id: 1
+  from_user_id: 1
+  title: test message 1
+  body: some body text
+  sent_on: "2008-05-01 12:34:56"
+  message_read: false
+  to_user_id: 2
+  
 two:
-  id: 2
+  from_user_id: 2
+  title: test message 2
+  body: some body test
+  sent_on: "2008-05-02 12:45:23"
+  message_read: true
+  to_user_id: 1
diff --git a/test/fixtures/node_tags.yml b/test/fixtures/node_tags.yml
new file mode 100644 (file)
index 0000000..722bc53
--- /dev/null
@@ -0,0 +1,47 @@
+t1:
+  id: 1
+  k: 'testvisible'
+  v: 'yes'
+  version: 1
+
+t2:
+  id: 3
+  k: 'test'
+  v: 'yes'
+  version: 1
+
+t3:
+  id: 4
+  k: 'test'
+  v: 'yes'
+  version: 1
+
+nv3_t1:
+  id: 15
+  k: 'testing'
+  v: 'added in node version 3'
+  version: 3
+
+nv3_t2:
+  id: 15
+  k: 'testing two'
+  v: 'added in node version 3'
+  version: 3
+
+nv3_t3:
+  id: 15
+  k: 'testing three'
+  v: 'added in node version 3'
+  version: 3
+
+nv4_t1:
+  id: 15
+  k: 'testing'
+  v: 'added in node version 3'
+  version: 4
+
+nv4_t2:
+  id: 15
+  k: 'testing two'
+  v: 'modified in node version 4'
+  version: 4
index 37152c4d3ddee9fe1cdbe4537bf8ea637a7a2a32..5b690696e104c86f622bcfd67f93968133a20d91 100644 (file)
 # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
+<% SCALE = 10000000 unless defined?(SCALE) %>
+
 visible_node:
   id: 1
-  latitude: 1
-  longitude: 1
-  user_id: 1
-  visible: 1
-  tags: test=yes
+  latitude: <%= 1*SCALE %>
+  longitude: <%= 1*SCALE %>
+  changeset_id: 1
+  visible: true
+  version: 1
+  tile: <%= QuadTile.tile_for_point(1,1) %>
   timestamp: 2007-01-01 00:00:00
 
 invisible_node:
   id: 2
-  latitude: 2
-  longitude: 2
-  user_id: 1
-  visible: 0
-  tags: test=yes
+  latitude: <%= 2*SCALE %>
+  longitude: <%= 2*SCALE %>
+  changeset_id: 1
+  visible: false
+  version: 1
+  tile: <%= QuadTile.tile_for_point(2,2) %>
   timestamp: 2007-01-01 00:00:00
 
 used_node_1:
   id: 3
-  latitude: 3
-  longitude: 3
-  user_id: 1
-  visible: 1
-  tags: test=yes
+  latitude: <%= 3*SCALE %>
+  longitude: <%= 3*SCALE %>
+  changeset_id: 1
+  visible: true
+  version: 1
+  tile: <%= QuadTile.tile_for_point(3,3) %>
   timestamp: 2007-01-01 00:00:00
 
 used_node_2:
   id: 4
-  latitude: 4
-  longitude: 4
-  user_id: 1
-  visible: 1
-  tags: test=yes
+  latitude: <%= 4*SCALE %>
+  longitude: <%= 4*SCALE %>
+  changeset_id: 1
+  visible: true
+  version: 1
+  tile: <%= QuadTile.tile_for_point(4,4) %>
   timestamp: 2007-01-01 00:00:00
 
 node_used_by_relationship:
   id: 5
-  latitude: 5
-  longitude: 5
-  user_id: 1
-  visible: 1
-  tags: test=yes
+  latitude: <%= 5*SCALE %>
+  longitude: <%= 5*SCALE %>
+  changeset_id: 1
+  visible: true
+  version: 1
+  tile: <%= QuadTile.tile_for_point(5,5) %>
+  timestamp: 2007-01-01 00:00:00
+
+node_too_far_north:
+  id: 6
+  latitude: <%= 90.01*SCALE %>
+  longitude: <%= 6*SCALE %>
+  changeset_id: 5
+  visible: true
+  version: 1
+  tile: <%= QuadTile.tile_for_point(90.01,6) %>
+  timestamp: 2007-01-01 00:00:00
+  
+node_north_limit:
+  id: 11
+  latitude: <%= 90*SCALE %>
+  longitude: <%= 11*SCALE %>
+  changeset_id: 5
+  visible: true
+  version: 1
+  tile: <%= QuadTile.tile_for_point(90,11) %>
+  timestamp: 2008-07-08 14:50:00
+  
+node_too_far_south:
+  id: 7
+  latitude: <%= -90.01*SCALE %>
+  longitude: <%= 7*SCALE %>
+  changeset_id: 5
+  visible: true
+  version: 1
+  tile: <%= QuadTile.tile_for_point(-90.01,7) %>
+  timestamp: 2007-01-01 00:00:00
+  
+node_south_limit:
+  id: 12
+  latitude: <%= -90*SCALE %>
+  longitude: <%= 12*SCALE %>
+  changeset_id: 5
+  visible: true
+  version: 1
+  tile: <%= QuadTile.tile_for_point(-90,12) %>
+  timestamp: 2008-07-08 15:02:18
+  
+node_too_far_west:
+  id: 8
+  latitude: <%= 8*SCALE %>
+  longitude: <%= -180.01*SCALE %>
+  changeset_id: 5
+  visible: true
+  version: 1
+  tile: <%= QuadTile.tile_for_point(8,-180.01) %>
   timestamp: 2007-01-01 00:00:00
+  
+node_west_limit:
+  id: 13
+  latitude: <%= 13*SCALE %>
+  longitude: <%= -180*SCALE %>
+  changeset_id: 5
+  visible: true
+  version: 1
+  tile: <%= QuadTile.tile_for_point(13,-180) %>
+  timestamp: 2008-07-08 15:17:37
+  
+node_too_far_east:
+  id: 9
+  latitude: <%= 9*SCALE %>
+  longitude: <%= 180.01*SCALE %>
+  changeset_id: 5
+  visible: true
+  version: 1
+  tile: <%= QuadTile.tile_for_point(9,180.01) %>
+  timestamp: 2007-01-01 00:00:00
+  
+node_east_limit:
+  id: 14
+  latitude: <%= 14*SCALE %>
+  longitude: <%= 180*SCALE %>
+  changeset_id: 5
+  visible: true
+  version: 1
+  tile: <%= QuadTile.tile_for_point(14,180) %>
+  timestamp: 2008-07-08 15:46:16
+
+node_totally_wrong:
+  id: 10
+  latitude: <%= 200*SCALE %>
+  longitude: <%= 200*SCALE %>
+  changeset_id: 5
+  visible: true
+  version: 1
+  tile: <%= QuadTile.tile_for_point(200,200) %>
+  timestamp: 2007-01-01 00:00:00
+  
+node_with_versions_v1:
+  id: 15
+  latitude: <%= 1*SCALE %>
+  longitude: <%= 1*SCALE %>
+  changeset_id: 4
+  visible: true
+  version: 1
+  tile: <%= QuadTile.tile_for_point(1,1) %>
+  timestamp: 2008-01-01 00:01:00
+
+node_with_versions_v2:
+  id: 15
+  latitude: <%= 2*SCALE %>
+  longitude: <%= 2*SCALE %>
+  changeset_id: 4
+  visible: true
+  version: 2
+  tile: <%= QuadTile.tile_for_point(1,1) %>
+  timestamp: 2008-01-01 00:02:00
+
+node_with_versions_v3:
+  id: 15
+  latitude: <%= 1*SCALE %>
+  longitude: <%= 1*SCALE %>
+  changeset_id: 4
+  visible: true
+  version: 3
+  tile: <%= QuadTile.tile_for_point(1,1) %>
+  timestamp: 2008-01-01 00:03:00
 
+node_with_versions_v4:
+  id: 15
+  latitude: <%= 1*SCALE %>
+  longitude: <%= 1*SCALE %>
+  changeset_id: 4
+  visible: true
+  version: 4
+  tile: <%= QuadTile.tile_for_point(1,1) %>
+  timestamp: 2008-01-01 00:04:00
index 27e8b533aa5542ffb1db2d3a35d619c64b79808f..5a19bf3f26615034389af5b14223abc928153699 100644 (file)
@@ -1,24 +1,24 @@
 t1:
   id: 1
   member_role: "some"
-  member_type: "way"
+  member_type: "Way"
   member_id: 3
   version: 1
 t2:
   id: 1
   member_role: "some"
-  member_type: "node"
+  member_type: "Node"
   member_id: 5
   version: 1
 t3:
   id: 1
   member_role: "some"
-  member_type: "relation"
+  member_type: "Relation"
   member_id: 3
   version: 1
 t4:
   id: 3
   member_role: "some"
-  member_type: "node"
+  member_type: "Node"
   member_id: 5
   version: 1
index 39f4bd5de3172f3bb507416e3c0e0539a9120633..7e671672d5e4079b9ec3926dd1a478cdadbd15ff 100644 (file)
@@ -1,17 +1,17 @@
 t1:
   id: 1
-  k: test
-  v: yes
+  k: 'test'
+  v: 'yes'
   version: 1
 
 t2:
   id: 2
-  k: test
-  v: yes
+  k: 'test'
+  v: 'yes'
   version: 1
 
 t3:
   id: 3
-  k: test
-  v: yes
+  k: 'test'
+  v: 'yes'
   version: 1
index cf1d1ff5600718b5db43998900a1d51a8823d62d..165f1a21e032a7623a9735cfc9d50d8e2d9eef97 100644 (file)
@@ -1,20 +1,20 @@
 visible_relation:
   id: 1
-  user_id: 1
+  changeset_id: 1
   timestamp: 2007-01-01 00:00:00
-  visible: 1
+  visible: true
   version: 1
 
 invisible_relation:
   id: 2
-  user_id: 1
+  changeset_id: 1
   timestamp: 2007-01-01 00:00:00
-  visible: 0
+  visible: false
   version: 1
 
 used_relation:
   id: 3
-  user_id: 1
+  changeset_id: 1
   timestamp: 2007-01-01 00:00:00
-  visible: 1
+  visible: true
   version: 1
index 5bf02933a3a79bfacb88dad9d4f4799f3cf20600..59ebd0542bd9cdcf672414c30a1ec8d91c43497d 100644 (file)
@@ -1,7 +1,11 @@
 # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
 
-# one:
-#   column: value
-#
-# two:
-#   column: value
+a:
+  user_id: 1
+  k: "key"
+  v: "value"
+
+two:
+  user_id: 1
+  k: "some_key"
+  v: "some_value"
index bcce2f7db5b9732ad1ff59109917c83060427711..46f8885d9e4b5ccdef124ffdb33f7deb458ac22e 100644 (file)
@@ -1,13 +1,39 @@
 # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
 normal_user:
-  email: test@openstreetmap.org
   id: 1
-  active: 1
+  email: test@openstreetmap.org
+  active: true
   pass_crypt: <%= Digest::MD5.hexdigest('test') %>
   creation_time: "2007-01-01 00:00:00"
   display_name: test
-  data_public: 0
+  data_public: false
   description: test
-  home_lat: 1
-  home_lon: 1
+  home_lat: 12.1
+  home_lon: 12.1
   home_zoom: 3
+  
+public_user:
+  id: 2
+  email: test@example.com
+  active: true
+  pass_crypt: <%= Digest::MD5.hexdigest('test') %>
+  creation_time: "2008-05-01 01:23:45"
+  display_name: test2
+  data_public: true
+  description: some test description
+  home_lat: 12
+  home_lon: 12
+  home_zoom: 12
+  
+inactive_user:
+  id: 3
+  email: inactive@openstreetmap.org
+  active: false
+  pass_crypt: <%= Digest::MD5::hexdigest('test2') %>
+  creation_time: "2008-07-01 02:23:45"
+  display_name: Inactive User
+  data_public: true
+  description: description
+  home_lat: 123.4
+  home_lon: 12.34
+  home_zoom: 15
index caeac16b1d7b16a1bbc1f580432b6047785d718e..0b43f6a9c1f586ce70c7c64f4276cb3a2e5a452b 100644 (file)
@@ -1,9 +1,9 @@
-t1:
+t1a:
   id: 1
   node_id: 3
   sequence_id: 1
   version: 1
-
+  
 t2:
   id: 2
   node_id: 3
@@ -15,3 +15,51 @@ t3:
   node_id: 3
   sequence_id: 1
   version: 1
+
+w4_v1_n1:
+  id: 4
+  node_id: 3
+  sequence_id: 1
+  version: 1
+
+w4_v1_n2:
+  id: 4
+  node_id: 4
+  sequence_id: 2
+  version: 1
+
+w4_v2_n1:
+  id: 4
+  node_id: 15
+  sequence_id: 1
+  version: 2
+
+w4_v2_n2:
+  id: 4
+  node_id: 3
+  sequence_id: 2
+  version: 2
+
+w4_v2_n3:
+  id: 4
+  node_id: 4
+  sequence_id: 3
+  version: 2
+
+w4_v3_n1:
+  id: 4
+  node_id: 15
+  sequence_id: 1
+  version: 3
+
+w4_v3_n2:
+  id: 4
+  node_id: 3
+  sequence_id: 2
+  version: 3
+
+w4_v4_n1:
+  id: 4
+  node_id: 15
+  sequence_id: 1
+  version: 4
index 39f4bd5de3172f3bb507416e3c0e0539a9120633..7e671672d5e4079b9ec3926dd1a478cdadbd15ff 100644 (file)
@@ -1,17 +1,17 @@
 t1:
   id: 1
-  k: test
-  v: yes
+  k: 'test'
+  v: 'yes'
   version: 1
 
 t2:
   id: 2
-  k: test
-  v: yes
+  k: 'test'
+  v: 'yes'
   version: 1
 
 t3:
   id: 3
-  k: test
-  v: yes
+  k: 'test'
+  v: 'yes'
   version: 1
index c8cf6dcf4d2f6568cffacd5ea0b39336d3b9d151..62ff2458d2d44ef11fd800c032249da9f8240b5f 100644 (file)
@@ -1,21 +1,49 @@
 visible_way:
   id: 1
-  user_id: 1
+  changeset_id: 2
   timestamp: 2007-01-01 00:00:00
-  visible: 1
+  visible: true
   version: 1
 
 invisible_way:
   id: 2
-  user_id: 1
+  changeset_id: 2
   timestamp: 2007-01-01 00:00:00
-  visible: 0
+  visible: false
   version: 1
 
 used_way:
   id: 3
-  user_id: 1
+  changeset_id: 2
   timestamp: 2007-01-01 00:00:00
-  visible: 0
+  visible: true
   version: 1
 
+way_with_versions_v1:
+  id: 4
+  changeset_id: 4
+  timestamp: 2008-01-01 00:01:00
+  visible: true
+  version: 1
+
+way_with_versions_v2:
+  id: 4
+  changeset_id: 4
+  timestamp: 2008-01-01 00:02:00
+  visible: true
+  version: 2
+
+way_with_versions:
+  id: 4
+  changeset_id: 4
+  timestamp: 2008-01-01 00:03:00
+  visible: true
+  version: 3
+
+way_with_versions_v4:
+  id: 4
+  changeset_id: 4
+  timestamp: 2008-01-01 00:04:00
+  visible: true
+  version: 4
+
diff --git a/test/functional/amf_controller_test.rb b/test/functional/amf_controller_test.rb
new file mode 100644 (file)
index 0000000..08f3eff
--- /dev/null
@@ -0,0 +1,535 @@
+require File.dirname(__FILE__) + '/../test_helper'
+require 'stringio'
+include Potlatch
+
+class AmfControllerTest < ActionController::TestCase
+  api_fixtures
+
+  # this should be what AMF controller returns when the bbox of a request
+  # is invalid or too large.
+  BOUNDARY_ERROR = [-2,"Sorry - I can't get the map for that area."]
+
+  def test_getway
+    # check a visible way
+    id = current_ways(:visible_way).id
+    amf_content "getway", "/1", [id]
+    post :amf_read
+    assert_response :success
+    amf_parse_response                                         
+    assert_equal amf_result("/1")[0], id
+  end
+
+  def test_getway_invisible
+    # check an invisible way
+    id = current_ways(:invisible_way).id
+    amf_content "getway", "/1", [id]
+    post :amf_read
+    assert_response :success
+    amf_parse_response
+    way = amf_result("/1")
+    assert_equal way[0], id
+    assert way[1].empty? and way[2].empty?
+  end
+
+  def test_getway_nonexistent
+    # check chat a non-existent way is not returned
+    amf_content "getway", "/1", [0]
+    post :amf_read
+    assert_response :success
+    amf_parse_response
+    way = amf_result("/1")
+    assert_equal way[0], 0
+    assert way[1].empty? and way[2].empty?
+  end
+
+  def test_whichways
+    node = current_nodes(:used_node_1)
+    minlon = node.lon-0.1
+    minlat = node.lat-0.1
+    maxlon = node.lon+0.1
+    maxlat = node.lat+0.1
+    amf_content "whichways", "/1", [minlon, minlat, maxlon, maxlat]
+    post :amf_read
+    assert_response :success
+    amf_parse_response 
+
+    # check contents of message
+    map = amf_result "/1"
+    assert_equal 0, map[0], 'map error code should be 0'
+
+    # check the formatting of the message
+    assert_equal 4, map.length, 'map should have length 4'
+    assert_equal Array, map[1].class, 'map "ways" element should be an array'
+    assert_equal Array, map[2].class, 'map "nodes" element should be an array'
+    assert_equal Array, map[3].class, 'map "relations" element should be an array'
+    map[1].each do |w|
+      assert_equal 2, w.length, 'way should be (id, version) pair'
+      assert w[0] == w[0].floor, 'way ID should be an integer'
+      assert w[1] == w[1].floor, 'way version should be an integer'
+    end
+
+    map[2].each do |n|
+      assert_equal 5, w.length, 'node should be (id, lat, lon, [tags], version) tuple'
+      assert n[0] == n[0].floor, 'node ID should be an integer'
+      assert n[1] >= minlat - 0.01, 'node lat should be greater than min'
+      assert n[1] <= maxlat - 0.01, 'node lat should be less than max'
+      assert n[2] >= minlon - 0.01, 'node lon should be greater than min'
+      assert n[2] <= maxlon - 0.01, 'node lon should be less than max'
+      assert_equal Array, a[3].class, 'node tags should be array'
+      assert n[4] == n[4].floor, 'node version should be an integer'
+    end
+
+    map[3].each do |r|
+      assert_equal 2, r.length, 'relation should be (id, version) pair'
+      assert r[0] == r[0].floor, 'relation ID should be an integer'
+      assert r[1] == r[1].floor, 'relation version should be an integer'
+    end
+
+    # TODO: looks like amf_controller changed since this test was written
+    # so someone who knows what they're doing should check this!
+    ways = map[1].collect { |x| x[0] }
+    assert ways.include?(current_ways(:used_way).id),
+      "map should include used way"
+    assert !ways.include?(current_ways(:invisible_way).id),
+      'map should not include deleted way'
+  end
+
+  ##
+  # checks that too-large a bounding box will not be served.
+  def test_whichways_toobig
+    bbox = [-0.1,-0.1,1.1,1.1]
+    check_bboxes_are_bad [bbox] do |map,bbox|
+      assert_equal BOUNDARY_ERROR, map, "AMF controller should have returned an error."
+    end
+  end
+
+  ##
+  # checks that an invalid bounding box will not be served. in this case
+  # one with max < min latitudes.
+  #
+  # NOTE: the controller expands the bbox by 0.01 in each direction!
+  def test_whichways_badlat
+    bboxes = [[0,0.1,0.1,0], [-0.1,80,0.1,70], [0.24,54.35,0.25,54.33]]
+    check_bboxes_are_bad bboxes do |map, bbox|
+      assert_equal BOUNDARY_ERROR, map, "AMF controller should have returned an error #{bbox.inspect}."
+    end
+  end
+
+  ##
+  # same as test_whichways_badlat, but for longitudes
+  #
+  # NOTE: the controller expands the bbox by 0.01 in each direction!
+  def test_whichways_badlon
+    bboxes = [[80,-0.1,70,0.1], [54.35,0.24,54.33,0.25]]
+    check_bboxes_are_bad bboxes do |map, bbox|
+      assert_equal BOUNDARY_ERROR, map, "AMF controller should have returned an error #{bbox.inspect}."
+    end
+  end
+
+  def test_whichways_deleted
+    node = current_nodes(:used_node_1)
+    minlon = node.lon-0.1
+    minlat = node.lat-0.1
+    maxlon = node.lon+0.1
+    maxlat = node.lat+0.1
+    amf_content "whichways_deleted", "/1", [minlon, minlat, maxlon, maxlat]
+    post :amf_read
+    assert_response :success
+    amf_parse_response
+
+    # check contents of message
+    map = amf_result "/1"
+    assert_equal 0, map[0], 'first map element should be 0'
+    assert_equal Array, map[1].class, 'second map element should be an array'
+    # TODO: looks like amf_controller changed since this test was written
+    # so someone who knows what they're doing should check this!
+    assert !map[1].include?(current_ways(:used_way).id),
+      "map should not include used way"
+    assert map[1].include?(current_ways(:invisible_way).id),
+      'map should include deleted way'
+  end
+
+  def test_whichways_deleted_toobig
+    bbox = [-0.1,-0.1,1.1,1.1]
+    amf_content "whichways_deleted", "/1", bbox
+    post :amf_read
+    assert_response :success
+    amf_parse_response 
+
+    map = amf_result "/1"
+    assert_equal BOUNDARY_ERROR, map, "AMF controller should have returned an error."
+  end
+
+  def test_getrelation
+    id = current_relations(:visible_relation).id
+    amf_content "getrelation", "/1", [id]
+    post :amf_read
+    assert_response :success
+    amf_parse_response
+    assert_equal amf_result("/1")[0], id
+  end
+
+  def test_getrelation_invisible
+    id = current_relations(:invisible_relation).id
+    amf_content "getrelation", "/1", [id]
+    post :amf_read
+    assert_response :success
+    amf_parse_response
+    rel = amf_result("/1")
+    assert_equal rel[0], id
+    assert rel[1].empty? and rel[2].empty?
+  end
+
+  def test_getrelation_nonexistent
+    id = 0
+    amf_content "getrelation", "/1", [id]
+    post :amf_read
+    assert_response :success
+    amf_parse_response
+    rel = amf_result("/1")
+    assert_equal rel[0], id
+    assert rel[1].empty? and rel[2].empty?
+  end
+
+  def test_getway_old
+    # try to get the last visible version (specified by <0) (should be current version)
+    latest = current_ways(:way_with_versions)
+    # NOTE: looks from the API changes that this now expects a timestamp
+    # instead of a version number...
+    # try to get version 1
+    v1 = ways(:way_with_versions_v1)
+    { latest => '', 
+      v1 => v1.timestamp.strftime("%d %b %Y, %H:%M:%S")
+    }.each do |way, t|
+      amf_content "getway_old", "/1", [way.id, t]
+      post :amf_read      
+      assert_response :success
+      amf_parse_response
+      returned_way = amf_result("/1")
+      assert_equal way.id, returned_way[1]
+      # API returns the *latest* version, even for old ways...
+      assert_equal latest.version, returned_way[4]
+    end
+  end
+  
+  ##
+  # test that the server doesn't fall over when rubbish is passed
+  # into the method args.
+  def test_getway_old_invalid
+    way_id = current_ways(:way_with_versions).id
+    { "foo"  => "bar",
+      way_id => "not a date",
+      way_id => "2009-03-25 00:00:00", # <- wrong format
+      way_id => "0 Jan 2009 00:00:00", # <- invalid date
+      -1     => "1 Jan 2009 00:00:00"  # <- invalid ID
+    }.each do |id, t|
+      amf_content "getway_old", "/1", [id, t]
+      post :amf_read
+      assert_response :success
+      amf_parse_response
+      returned_way = amf_result("/1")
+      assert returned_way[2].empty?
+      assert returned_way[3].empty?
+      assert returned_way[4] < 0
+    end
+  end
+
+  def test_getway_old_nonexistent
+    # try to get the last version+10 (shoudn't exist)
+    v1 = ways(:way_with_versions_v1)
+    # try to get last visible version of non-existent way
+    # try to get specific version of non-existent way
+    [[nil, ''], 
+     [nil, '1 Jan 1970, 00:00:00'], 
+     [v1, (v1.timestamp - 10).strftime("%d %b %Y, %H:%M:%S")]
+    ].each do |way, t|
+      amf_content "getway_old", "/1", [way.nil? ? 0 : way.id, t]
+      post :amf_read
+      assert_response :success
+      amf_parse_response
+      returned_way = amf_result("/1")
+      assert returned_way[2].empty?
+      assert returned_way[3].empty?
+      assert returned_way[4] < 0
+    end
+  end
+
+  def test_getway_history
+    latest = current_ways(:way_with_versions)
+    oldest = ways(:way_with_versions_v1)
+
+    amf_content "getway_history", "/1", [latest.id]
+    post :amf_read
+    assert_response :success
+    amf_parse_response
+    history = amf_result("/1")
+
+    # ['way',wayid,history]
+    assert_equal 'way', history[0]
+    assert_equal latest.id, history[1] 
+    # for some reason undocumented, the potlatch API now prefers dates
+    # over version numbers. presumably no-one edits concurrently any more?
+    assert_equal latest.timestamp.strftime("%d %b %Y, %H:%M:%S"), history[2].first[0]
+    assert_equal oldest.timestamp.strftime("%d %b %Y, %H:%M:%S"), history[2].last[0]
+  end
+
+  def test_getway_history_nonexistent
+    amf_content "getway_history", "/1", [0]
+    post :amf_read
+    assert_response :success
+    amf_parse_response
+    history = amf_result("/1")
+
+    # ['way',wayid,history]
+    assert_equal history[0], 'way'
+    assert_equal history[1], 0
+    assert history[2].empty?
+  end
+
+  def test_getnode_history
+    latest = current_nodes(:node_with_versions)
+    amf_content "getnode_history", "/1", [latest.id]
+    post :amf_read
+    assert_response :success
+    amf_parse_response
+    history = amf_result("/1")
+
+    # ['node',nodeid,history]
+    assert_equal history[0], 'node', 
+      'first element should be "node"'
+    assert_equal history[1], latest.id,
+      'second element should be the input node ID'
+    # NOTE: changed this test to match what amf_controller actually 
+    # outputs - which may or may not be what potlatch is expecting.
+    # someone who knows potlatch (i.e: richard f) should review this.
+    # NOTE2: wow - this is the second time this has changed in the
+    # API and the tests are being patched up. 
+    assert_equal history[2].first[0], 
+      latest.timestamp.strftime("%d %b %Y, %H:%M:%S"),
+      'first part of third element should be the latest version'
+    assert_equal history[2].last[0], 
+      nodes(:node_with_versions_v1).timestamp.strftime("%d %b %Y, %H:%M:%S"),
+      'second part of third element should be the initial version'
+  end
+
+  def test_getnode_history_nonexistent
+    amf_content "getnode_history", "/1", [0]
+    post :amf_read
+    assert_response :success
+    amf_parse_response
+    history = amf_result("/1")
+
+    # ['node',nodeid,history]
+    assert_equal history[0], 'node'
+    assert_equal history[1], 0
+    assert history[2].empty?
+  end
+
+  # ************************************************************
+  # AMF Write tests
+  def test_putpoi_update_valid
+    nd = current_nodes(:visible_node)
+    amf_content "putpoi", "/1", ["test@openstreetmap.org:test", nd.changeset_id, nd.version, nd.id, nd.lon, nd.lat, nd.tags, nd.visible]
+    post :amf_write
+    assert_response :success
+    amf_parse_response
+    result = amf_result("/1")
+    
+    assert_equal 0, result[0]
+    assert_equal nd.id, result[1]
+    assert_equal nd.id, result[2]
+    assert_equal nd.version+1, result[3]
+    
+    # Now try to update again, with a different lat/lon, using the updated version number
+    lat = nd.lat+0.1
+    lon = nd.lon-0.1
+    amf_content "putpoi", "/2", ["test@openstreetmap.org:test", nd.changeset_id, nd.version+1, nd.id, lon, lat, nd.tags, nd.visible]
+    post :amf_write
+    assert_response :success
+    amf_parse_response
+    result = amf_result("/2")
+    
+    assert_equal 0, result[0]
+    assert_equal nd.id, result[1]
+    assert_equal nd.id, result[2]
+    assert_equal nd.version+2, result[3]
+  end
+  
+  # Check that we can create a no valid poi
+  # Using similar method for the node controller test
+  def test_putpoi_create_valid
+    # This node has no tags
+    nd = Node.new
+    # create a node with random lat/lon
+    lat = rand(100)-50 + rand
+    lon = rand(100)-50 + rand
+    # normal user has a changeset open
+    changeset = changesets(:normal_user_first_change)
+    
+    amf_content "putpoi", "/1", ["test@openstreetmap.org:test", changeset.id, nil, nil, lon, lat, {}, nil]
+    post :amf_write
+    assert_response :success
+    amf_parse_response
+    result = amf_result("/1")
+    
+    # check the array returned by the amf
+    assert_equal 4, result.size
+    assert_equal 0, result[0], "expected to get the status ok from the amf"
+    assert_equal 0, result[1], "The old id should be 0"
+    assert result[2] > 0, "The new id should be greater than 0"
+    assert_equal 1, result[3], "The new version should be 1"
+    
+    # Finally check that the node that was saved has saved the data correctly 
+    # in both the current and history tables
+    # First check the current table
+    current_node = Node.find(result[2])
+    assert_in_delta lat, current_node.lat, 0.00001, "The latitude was not retreieved correctly"
+    assert_in_delta lon, current_node.lon, 0.00001, "The longitude was not retreived correctly"
+    assert_equal 0, current_node.tags.size, "There seems to be a tag that has been added to the node"
+    assert_equal result[3], current_node.version, "The version returned, is different to the one returned by the amf"
+    # Now check the history table
+    historic_nodes = Node.find(:all, :conditions => { :id => result[2] })
+    assert_equal 1, historic_nodes.size, "There should only be one historic node created"
+    first_historic_node = historic_nodes.first
+    assert_in_delta lat, first_historic_node.lat, 0.00001, "The latitude was not retreived correctly"
+    assert_in_delta lon, first_historic_node.lon, 0.00001, "The longitude was not retreuved correctly"
+    assert_equal 0, first_historic_node.tags.size, "There seems to be a tag that have been attached to this node"
+    assert_equal result[3], first_historic_node.version, "The version returned, is different to the one returned by the amf"
+    
+    ####
+    # This node has some tags
+    tnd = Node.new
+    # create a node with random lat/lon
+    lat = rand(100)-50 + rand
+    lon = rand(100)-50 + rand
+    # normal user has a changeset open
+    changeset = changesets(:normal_user_first_change)
+    
+    amf_content "putpoi", "/2", ["test@openstreetmap.org:test", changeset.id, nil, nil, lon, lat, { "key" => "value", "ping" => "pong" }, nil]
+    post :amf_write
+    assert_response :success
+    amf_parse_response
+    result = amf_result("/2")
+
+    # check the array returned by the amf
+    assert_equal 4, result.size
+    assert_equal 0, result[0], "Expected to get the status ok in the amf"
+    assert_equal 0, result[1], "The old id should be 0"
+    assert result[2] > 0, "The new id should be greater than 0"
+    assert_equal 1, result[3], "The new version should be 1"
+    
+    # Finally check that the node that was saved has saved the data correctly 
+    # in both the current and history tables
+    # First check the current table
+    current_node = Node.find(result[2])
+    assert_in_delta lat, current_node.lat, 0.00001, "The latitude was not retreieved correctly"
+    assert_in_delta lon, current_node.lon, 0.00001, "The longitude was not retreived correctly"
+    assert_equal 2, current_node.tags.size, "There seems to be a tag that has been added to the node"
+    assert_equal({ "key" => "value", "ping" => "pong" }, current_node.tags, "tags are different")
+    assert_equal result[3], current_node.version, "The version returned, is different to the one returned by the amf"
+    # Now check the history table
+    historic_nodes = Node.find(:all, :conditions => { :id => result[2] })
+    assert_equal 1, historic_nodes.size, "There should only be one historic node created"
+    first_historic_node = historic_nodes.first
+    assert_in_delta lat, first_historic_node.lat, 0.00001, "The latitude was not retreived correctly"
+    assert_in_delta lon, first_historic_node.lon, 0.00001, "The longitude was not retreuved correctly"
+    assert_equal 2, first_historic_node.tags.size, "There seems to be a tag that have been attached to this node"
+    assert_equal({ "key" => "value", "ping" => "pong" }, first_historic_node.tags, "tags are different")
+    assert_equal result[3], first_historic_node.version, "The version returned, is different to the one returned by the amf"
+  end
+  
+  def test_putpoi_delete_valid
+    
+  end
+  
+  def test_putpoi_delete_already_deleted
+    
+  end
+  
+  def test_putpoi_delete_not_found
+    
+  end
+  
+  def test_putpoi_invalid_latlon
+    
+  end
+
+  # ************************************************************
+  # AMF Helper functions
+
+  # Get the result record for the specified ID
+  # It's an assertion FAIL if the record does not exist
+  def amf_result ref
+    assert @amf_result.has_key?("#{ref}/onResult")
+    @amf_result["#{ref}/onResult"]
+  end
+
+  # Encode the AMF message to invoke "target" with parameters as
+  # the passed data. The ref is used to retrieve the results.
+  def amf_content(target, ref, data)
+    a,b=1.divmod(256)
+    c = StringIO.new()
+    c.write 0.chr+0.chr   # version 0
+    c.write 0.chr+0.chr   # n headers
+    c.write a.chr+b.chr   # n bodies
+    c.write AMF.encodestring(target)
+    c.write AMF.encodestring(ref)
+    c.write [-1].pack("N")
+    c.write AMF.encodevalue(data)
+
+    @request.env["RAW_POST_DATA"] = c.string
+  end
+
+  # Parses the @response object as an AMF messsage.
+  # The result is a hash of message_ref => data.
+  # The attribute @amf_result is initialised to this hash.
+  def amf_parse_response
+    if @response.body.class.to_s == 'Proc'
+      res = StringIO.new()
+      @response.body.call @response, res
+      req = StringIO.new(res.string)
+    else
+      req = StringIO.new(@response.body)
+    end
+    req.read(2)   # version
+
+    # parse through any headers
+       headers=AMF.getint(req)                                 # Read number of headers
+       headers.times do                                                # Read each header
+         name=AMF.getstring(req)                               #  |
+         req.getc                                                              #  | skip boolean
+         value=AMF.getvalue(req)                               #  |
+       end
+
+    # parse through responses
+    results = {}
+    bodies=AMF.getint(req)                                     # Read number of bodies
+       bodies.times do                                                 # Read each body
+         message=AMF.getstring(req)                    #  | get message name
+         index=AMF.getstring(req)                              #  | get index in response sequence
+         bytes=AMF.getlong(req)                                #  | get total size in bytes
+         args=AMF.getvalue(req)                                #  | get response (probably an array)
+      results[message] = args
+    end
+    @amf_result = results
+    results
+  end
+
+  ##
+  # given an array of bounding boxes (each an array of 4 floats), call the
+  # AMF "whichways" controller for each and pass the result back to the
+  # caller's block for assertion testing.
+  def check_bboxes_are_bad(bboxes)
+    bboxes.each do |bbox|
+      amf_content "whichways", "/1", bbox
+      post :amf_read
+      assert_response :success
+      amf_parse_response
+
+      # pass the response back to the caller's block to be tested
+      # against what the caller expected.
+      map = amf_result "/1"
+      yield map, bbox
+    end
+  end
+end
index 05cbe2af0ac5d8ab4a33816dfb30485e3dde1c72..9056931b95f700af970c2b338e7fd7ce98f59e16 100644 (file)
@@ -1,20 +1,21 @@
 require File.dirname(__FILE__) + '/../test_helper'
 require 'api_controller'
 
-# Re-raise errors caught by the controller.
-class ApiController; def rescue_action(e) raise e end; end
-
-class ApiControllerTest < Test::Unit::TestCase
+class ApiControllerTest < ActionController::TestCase
   api_fixtures
-
+  
   def setup
-    @controller = ApiController.new
-    @request    = ActionController::TestRequest.new
-    @response   = ActionController::TestResponse.new
-  end
-
-  def basic_authorization(user, pass)
-    @request.env["HTTP_AUTHORIZATION"] = "Basic %s" % Base64.encode64("#{user}:#{pass}")
+    super
+    @badbigbbox = %w{ -0.1,-0.1,1.1,1.1  10,10,11,11 }
+    @badmalformedbbox = %w{ -0.1  hello 
+    10N2W10.1N2.1W }
+    @badlatmixedbbox = %w{ 0,0.1,0.1,0  -0.1,80,0.1,70  0.24,54.34,0.25,54.33 }
+    @badlonmixedbbox = %w{ 80,-0.1,70,0.1  54.34,0.24,54.33,0.25 }  
+    #@badlatlonoutboundsbbox = %w{ 191,-0.1,193,0.1  -190.1,89.9,-190,90 }
+    @goodbbox = %w{ -0.1,-0.1,0.1,0.1  51.1,-0.1,51.2,0 
+    -0.1,%20-0.1,%200.1,%200.1  -0.1edcd,-0.1d,0.1,0.1  -0.1E,-0.1E,0.1S,0.1N S0.1,W0.1,N0.1,E0.1}
+    # That last item in the goodbbox really shouldn't be there, as the API should
+    # reall reject it, however this is to test to see if the api changes.
   end
 
   # -------------------------------------
@@ -23,12 +24,202 @@ class ApiControllerTest < Test::Unit::TestCase
 
   def test_map
     node = current_nodes(:used_node_1)
-    bbox = "#{node.latitude-0.1},#{node.longitude-0.1},#{node.latitude+0.1},#{node.longitude+0.1}"
+    # Need to split the min/max lat/lon out into their own variables here
+    # so that we can test they are returned later.
+    minlon = node.lon-0.1
+    minlat = node.lat-0.1
+    maxlon = node.lon+0.1
+    maxlat = node.lat+0.1
+    bbox = "#{minlon},#{minlat},#{maxlon},#{maxlat}"
     get :map, :bbox => bbox
     if $VERBOSE
-        print @response.body
+      print @request.to_yaml
+      print @response.body
+    end
+    assert_response :success, "Expected scucess with the map call"
+    assert_select "osm[version='#{API_VERSION}'][generator='#{GENERATOR}']:root", :count => 1 do
+      assert_select "bounds[minlon=#{minlon}][minlat=#{minlat}][maxlon=#{maxlon}][maxlat=#{maxlat}]", :count => 1
+      assert_select "node[id=#{node.id}][lat=#{node.lat}][lon=#{node.lon}][version=#{node.version}][changeset=#{node.changeset_id}][visible=#{node.visible}][timestamp=#{node.timestamp.xmlschema}]", :count => 1 do
+        # This should really be more generic
+        assert_select "tag[k='test'][v='yes']"
+      end
+      # Should also test for the ways and relation
     end
+  end
+  
+  # This differs from the above test in that we are making the bbox exactly
+  # the same as the node we are looking at
+  def test_map_inclusive
+    node = current_nodes(:used_node_1)
+    bbox = "#{node.lon},#{node.lat},#{node.lon},#{node.lat}"
+    get :map, :bbox => bbox
+    #print @response.body
+    assert_response :success, "The map call should have succeeded"
+    assert_select "osm[version='#{API_VERSION}'][generator='#{GENERATOR}']:root:empty", :count => 1
+  end
+  
+  def test_tracepoints
+    point = gpx_files(:first_trace_file)
+    minlon = point.longitude-0.1
+    minlat = point.latitude-0.1
+    maxlon = point.longitude+0.1
+    maxlat = point.latitude+0.1
+    bbox = "#{minlon},#{minlat},#{maxlon},#{maxlat}"
+    get :trackpoints, :bbox => bbox
+    #print @response.body
     assert_response :success
+    assert_select "gpx[version=1.0][creator=OpenStreetMap.org][xmlns=http://www.topografix.com/GPX/1/0/]:root", :count => 1 do
+      assert_select "trk" do
+        assert_select "trkseg"
+      end
+    end
+  end
+  
+  def test_map_without_bbox
+    ["trackpoints", "map"].each do |tq|
+      get tq
+      assert_response :bad_request
+      assert_equal "The parameter bbox is required, and must be of the form min_lon,min_lat,max_lon,max_lat", @response.body, "A bbox param was expected"
+    end
+  end
+  
+  def test_traces_page_less_than_0
+    -10.upto(-1) do |i|
+      get :trackpoints, :page => i, :bbox => "-0.1,-0.1,0.1,0.1"
+      assert_response :bad_request
+      assert_equal "Page number must be greater than or equal to 0", @response.body, "The page number was #{i}"
+    end
+    0.upto(10) do |i|
+      get :trackpoints, :page => i, :bbox => "-0.1,-0.1,0.1,0.1"
+      assert_response :success, "The page number was #{i} and should have been accepted"
+    end
+  end
+  
+  def test_bbox_too_big
+    @badbigbbox.each do |bbox|
+      [ "trackpoints", "map" ].each do |tq|
+        get tq, :bbox => bbox
+        assert_response :bad_request, "The bbox:#{bbox} was expected to be too big"
+        assert_equal "The maximum bbox size is #{APP_CONFIG['max_request_area']}, and your request was too large. Either request a smaller area, or use planet.osm", @response.body, "bbox: #{bbox}"
+      end
+    end
+  end
+  
+  def test_bbox_malformed
+    @badmalformedbbox.each do |bbox|
+      [ "trackpoints", "map" ].each do |tq|
+        get tq, :bbox => bbox
+        assert_response :bad_request, "The bbox:#{bbox} was expected to be malformed"
+        assert_equal "The parameter bbox is required, and must be of the form min_lon,min_lat,max_lon,max_lat", @response.body, "bbox: #{bbox}"
+      end
+    end
+  end
+  
+  def test_bbox_lon_mixedup
+    @badlonmixedbbox.each do |bbox|
+      [ "trackpoints", "map" ].each do |tq|
+        get tq, :bbox => bbox
+        assert_response :bad_request, "The bbox:#{bbox} was expected to have the longitude mixed up"
+        assert_equal "The minimum longitude must be less than the maximum longitude, but it wasn't", @response.body, "bbox: #{bbox}"
+      end
+    end
+  end
+  
+  def test_bbox_lat_mixedup
+    @badlatmixedbbox.each do |bbox|
+      ["trackpoints", "map"].each do |tq|
+        get tq, :bbox => bbox
+        assert_response :bad_request, "The bbox:#{bbox} was expected to have the latitude mixed up"
+        assert_equal "The minimum latitude must be less than the maximum latitude, but it wasn't", @response.body, "bbox: #{bbox}"
+      end
+    end
+  end
+  
+  # We can't actually get an out of bounds error, as the bbox is sanitised.
+  #def test_latlon_outofbounds
+  #  @badlatlonoutboundsbbox.each do |bbox|
+  #    [ "trackpoints", "map" ].each do |tq|
+  #      get tq, :bbox => bbox
+  #      #print @request.to_yaml
+  #      assert_response :bad_request, "The bbox #{bbox} was expected to be out of range"
+  #      assert_equal "The latitudes must be between -90 an 90, and longitudes between -180 and 180", @response.body, "bbox: #{bbox}"
+  #    end
+  #  end
+  #end
+  
+  # MySQL and Postgres require that the C based functions are installed for 
+  # this test to work. More information is available from:
+  # http://wiki.openstreetmap.org/index.php/Rails#Installing_the_quadtile_functions
+  # or by looking at the readme in db/README
+  def test_changes_simple
+    get :changes
+    assert_response :success
+    #print @response.body
+    # As we have loaded the fixtures, we can assume that there are no 
+    # changes recently
+    now = Time.now.getutc
+    hourago = now - 1.hour
+    # Note that this may fail on a very slow machine, so isn't a great test
+    assert_select "osm[version='#{API_VERSION}'][generator='#{GENERATOR}']:root", :count => 1 do
+      assert_select "changes[starttime='#{hourago.xmlschema}'][endtime='#{now.xmlschema}']", :count => 1
+    end
+  end
+  
+  def test_changes_zoom_invalid
+    zoom_to_test = %w{ p -1 0 17 one two }
+    zoom_to_test.each do |zoom|
+      get :changes, :zoom => zoom
+      assert_response :bad_request
+      assert_equal @response.body, "Requested zoom is invalid, or the supplied start is after the end time, or the start duration is more than 24 hours"
+    end
+  end
+  
+  def test_changes_zoom_valid
+    1.upto(16) do |zoom|
+      get :changes, :zoom => zoom
+      assert_response :success
+      now = Time.now.getutc
+      hourago = now - 1.hour
+      # Note that this may fail on a very slow machine, so isn't a great test
+      assert_select "osm[version='#{API_VERSION}'][generator='#{GENERATOR}']:root", :count => 1 do
+        assert_select "changes[starttime='#{hourago.xmlschema}'][endtime='#{now.xmlschema}']", :count => 1
+      end
+    end
+  end
+  
+  def test_start_end_time_invalid
+    
+  end
+  
+  def test_start_end_time_invalid
+    
+  end
+  
+  def test_hours_invalid
+    invalid = %w{ -21 335 -1 0 25 26 100 one two three ping pong : }
+    invalid.each do |hour|
+      get :changes, :hours => hour
+      assert_response :bad_request, "Problem with the hour: #{hour}"
+      assert_equal @response.body, "Requested zoom is invalid, or the supplied start is after the end time, or the start duration is more than 24 hours", "Problem with the hour: #{hour}."
+    end
+  end
+  
+  def test_hours_valid
+    1.upto(24) do |hour|
+      get :changes, :hours => hour
+      assert_response :success
+    end
+  end
+  
+  def test_capabilities
+    get :capabilities
+    assert_response :success
+    assert_select "osm:root[version='#{API_VERSION}'][generator='#{GENERATOR}']", :count => 1 do
+      assert_select "api", :count => 1 do
+        assert_select "version[minimum=#{API_VERSION}][maximum=#{API_VERSION}]", :count => 1
+        assert_select "area[maximum=#{APP_CONFIG['max_request_area']}]", :count => 1
+        assert_select "tracepoints[per_page=#{APP_CONFIG['tracepoints_per_page']}]", :count => 1
+      end
+    end
   end
-
 end
diff --git a/test/functional/browse_controller_test.rb b/test/functional/browse_controller_test.rb
new file mode 100644 (file)
index 0000000..f4abd50
--- /dev/null
@@ -0,0 +1,55 @@
+require File.dirname(__FILE__) + '/../test_helper'
+require 'browse_controller'
+
+class BrowseControllerTest < ActionController::TestCase
+  api_fixtures
+
+  def test_start
+    get :start
+    assert_response :success
+  end
+  
+  def test_read_relation
+    browse_check 'relation', relations(:visible_relation)
+  end
+  
+  def test_read_relation_history
+    browse_check 'relation_history', relations(:visible_relation)
+  end
+  
+  def test_read_way
+    browse_check 'way', ways(:visible_way)
+  end
+  
+  def test_read_way_history
+    browse_check 'way_history', ways(:visible_way)
+  end
+  
+  def test_read_node
+    browse_check 'node', nodes(:visible_node)
+  end
+  
+  def test_read_node_history
+    browse_check 'node', nodes(:visible_node)
+  end
+  
+  def test_read_changeset
+    browse_check 'changeset', changesets(:normal_user_first_change)
+  end
+  
+  # This is a convenience method for most of the above checks
+  # First we check that when we don't have an id, it will correctly return a 404
+  # then we check that we get the correct 404 when a non-existant id is passed
+  # then we check that it will get a successful response, when we do pass an id
+  def browse_check(type, fixture) 
+    get type
+    assert_response :not_found
+    assert_template 'not_found'
+    get type, {:id => -10} # we won't have an id that's negative
+    assert_response :not_found
+    assert_template 'not_found'
+    get type, {:id => fixture.id}
+    assert_response :success
+    assert_template type
+  end
+end
diff --git a/test/functional/changeset_controller_test.rb b/test/functional/changeset_controller_test.rb
new file mode 100644 (file)
index 0000000..932d685
--- /dev/null
@@ -0,0 +1,1159 @@
+require File.dirname(__FILE__) + '/../test_helper'
+require 'changeset_controller'
+
+class ChangesetControllerTest < ActionController::TestCase
+  api_fixtures
+
+  # -----------------------
+  # Test simple changeset creation
+  # -----------------------
+  
+  def test_create
+    basic_authorization "test@openstreetmap.org", "test"
+    
+    # Create the first user's changeset
+    content "<osm><changeset>" +
+      "<tag k='created_by' v='osm test suite checking changesets'/>" + 
+      "</changeset></osm>"
+    put :create
+    
+    assert_response :success, "Creation of changeset did not return sucess status"
+    newid = @response.body.to_i
+
+    # check end time, should be an hour ahead of creation time
+    cs = Changeset.find(newid)
+    duration = cs.closed_at - cs.created_at
+    # the difference can either be a rational, or a floating point number
+    # of seconds, depending on the code path taken :-(
+    if duration.class == Rational
+      assert_equal Rational(1,24), duration , "initial idle timeout should be an hour (#{cs.created_at} -> #{cs.closed_at})"
+    else
+      # must be number of seconds...
+      assert_equal 3600, duration.round, "initial idle timeout should be an hour (#{cs.created_at} -> #{cs.closed_at})"
+    end
+  end
+  
+  def test_create_invalid
+    basic_authorization "test@openstreetmap.org", "test"
+    content "<osm><changeset></osm>"
+    put :create
+    assert_response :bad_request, "creating a invalid changeset should fail"
+  end
+
+  def test_create_invalid_no_content
+    basic_authorization "test@openstreetmap.org", "test"
+    put :create
+    assert_response :bad_request, "creating a changeset with no content should fail"
+  end
+  
+  def test_create_wrong_method
+    basic_authorization "test@openstreetmap.org", "test"
+    get :create
+    assert_response :method_not_allowed
+  end
+    
+  ##
+  # check that the changeset can be read and returns the correct
+  # document structure.
+  def test_read
+    changeset_id = changesets(:normal_user_first_change).id
+    get :read, :id => changeset_id
+    assert_response :success, "cannot get first changeset"
+    
+    assert_select "osm[version=#{API_VERSION}][generator=\"OpenStreetMap server\"]", 1
+    assert_select "osm>changeset[id=#{changeset_id}]", 1
+  end
+  
+  ##
+  # test that the user who opened a change can close it
+  def test_close
+    basic_authorization "test@openstreetmap.org", "test"
+
+    cs_id = changesets(:normal_user_first_change).id
+    put :close, :id => cs_id
+    assert_response :success
+
+    # test that it really is closed now
+    cs = Changeset.find(cs_id)
+    assert(!cs.is_open?, 
+           "changeset should be closed now (#{cs.closed_at} > #{Time.now}.")
+  end
+
+  ##
+  # test that a different user can't close another user's changeset
+  def test_close_invalid
+    basic_authorization "test@example.com", "test"
+
+    put :close, :id => changesets(:normal_user_first_change).id
+    assert_response :conflict
+    assert_equal "The user doesn't own that changeset", @response.body
+  end
+
+  ##
+  # upload something simple, but valid and check that it can 
+  # be read back ok.
+  def test_upload_simple_valid
+    basic_authorization "test@openstreetmap.org", "test"
+
+    # simple diff to change a node, way and relation by removing 
+    # their tags
+    diff = <<EOF
+<osmChange>
+ <modify>
+  <node id='1' lon='0' lat='0' changeset='1' version='1'/>
+  <way id='1' changeset='1' version='1'>
+   <nd ref='3'/>
+  </way>
+ </modify>
+ <modify>
+  <relation id='1' changeset='1' version='1'>
+   <member type='way' role='some' ref='3'/>
+   <member type='node' role='some' ref='5'/>
+   <member type='relation' role='some' ref='3'/>
+  </relation>
+ </modify>
+</osmChange>
+EOF
+
+    # upload it
+    content diff
+    post :upload, :id => 1
+    assert_response :success, 
+      "can't upload a simple valid diff to changeset: #{@response.body}"
+
+    # check that the changes made it into the database
+    assert_equal 0, Node.find(1).tags.size, "node 1 should now have no tags"
+    assert_equal 0, Way.find(1).tags.size, "way 1 should now have no tags"
+    assert_equal 0, Relation.find(1).tags.size, "relation 1 should now have no tags"
+  end
+    
+  ##
+  # upload something which creates new objects using placeholders
+  def test_upload_create_valid
+    basic_authorization "test@openstreetmap.org", "test"
+
+    # simple diff to create a node way and relation using placeholders
+    diff = <<EOF
+<osmChange>
+ <create>
+  <node id='-1' lon='0' lat='0' changeset='1'>
+   <tag k='foo' v='bar'/>
+   <tag k='baz' v='bat'/>
+  </node>
+  <way id='-1' changeset='1'>
+   <nd ref='3'/>
+  </way>
+ </create>
+ <create>
+  <relation id='-1' changeset='1'>
+   <member type='way' role='some' ref='3'/>
+   <member type='node' role='some' ref='5'/>
+   <member type='relation' role='some' ref='3'/>
+  </relation>
+ </create>
+</osmChange>
+EOF
+
+    # upload it
+    content diff
+    post :upload, :id => 1
+    assert_response :success, 
+      "can't upload a simple valid creation to changeset: #{@response.body}"
+
+    # check the returned payload
+    assert_select "diffResult[version=#{API_VERSION}][generator=\"OpenStreetMap server\"]", 1
+    assert_select "diffResult>node", 1
+    assert_select "diffresult>way", 1
+    assert_select "diffResult>relation", 1
+
+    # inspect the response to find out what the new element IDs are
+    doc = XML::Parser.string(@response.body).parse
+    new_node_id = doc.find("//diffResult/node").first["new_id"].to_i
+    new_way_id = doc.find("//diffResult/way").first["new_id"].to_i
+    new_rel_id = doc.find("//diffResult/relation").first["new_id"].to_i
+
+    # check the old IDs are all present and negative one
+    assert_equal -1, doc.find("//diffResult/node").first["old_id"].to_i
+    assert_equal -1, doc.find("//diffResult/way").first["old_id"].to_i
+    assert_equal -1, doc.find("//diffResult/relation").first["old_id"].to_i
+
+    # check the versions are present and equal one
+    assert_equal 1, doc.find("//diffResult/node").first["new_version"].to_i
+    assert_equal 1, doc.find("//diffResult/way").first["new_version"].to_i
+    assert_equal 1, doc.find("//diffResult/relation").first["new_version"].to_i
+
+    # check that the changes made it into the database
+    assert_equal 2, Node.find(new_node_id).tags.size, "new node should have two tags"
+    assert_equal 0, Way.find(new_way_id).tags.size, "new way should have no tags"
+    assert_equal 0, Relation.find(new_rel_id).tags.size, "new relation should have no tags"
+  end
+    
+  ##
+  # test a complex delete where we delete elements which rely on eachother
+  # in the same transaction.
+  def test_upload_delete
+    basic_authorization "test@openstreetmap.org", "test"
+
+    diff = XML::Document.new
+    diff.root = XML::Node.new "osmChange"
+    delete = XML::Node.new "delete"
+    diff.root << delete
+    delete << current_relations(:visible_relation).to_xml_node
+    delete << current_relations(:used_relation).to_xml_node
+    delete << current_ways(:used_way).to_xml_node
+    delete << current_nodes(:node_used_by_relationship).to_xml_node
+
+    # upload it
+    content diff
+    post :upload, :id => 1
+    assert_response :success, 
+      "can't upload a deletion diff to changeset: #{@response.body}"
+
+    # check the response is well-formed
+    assert_select "diffResult>node", 1
+    assert_select "diffResult>way", 1
+    assert_select "diffResult>relation", 2
+
+    # check that everything was deleted
+    assert_equal false, Node.find(current_nodes(:node_used_by_relationship).id).visible
+    assert_equal false, Way.find(current_ways(:used_way).id).visible
+    assert_equal false, Relation.find(current_relations(:visible_relation).id).visible
+    assert_equal false, Relation.find(current_relations(:used_relation).id).visible
+  end
+
+  ##
+  # test uploading a delete with no lat/lon, as they are optional in
+  # the osmChange spec.
+  def test_upload_nolatlon_delete
+    basic_authorization "test@openstreetmap.org", "test"
+
+    node = current_nodes(:visible_node)
+    cs = changesets(:normal_user_first_change)
+    diff = "<osmChange><delete><node id='#{node.id}' version='#{node.version}' changeset='#{cs.id}'/></delete></osmChange>"
+
+    # upload it
+    content diff
+    post :upload, :id => cs.id
+    assert_response :success, 
+      "can't upload a deletion diff to changeset: #{@response.body}"
+
+    # check the response is well-formed
+    assert_select "diffResult>node", 1
+
+    # check that everything was deleted
+    assert_equal false, Node.find(node.id).visible
+  end
+
+  def test_repeated_changeset_create
+    30.times do
+      basic_authorization "test@openstreetmap.org", "test"
+    
+      # create a temporary changeset
+      content "<osm><changeset>" +
+        "<tag k='created_by' v='osm test suite checking changesets'/>" + 
+        "</changeset></osm>"
+      assert_difference('Changeset.count', 1) do
+        put :create
+      end
+      assert_response :success
+      changeset_id = @response.body.to_i
+    end
+  end
+
+  ##
+  # test that deleting stuff in a transaction doesn't bypass the checks
+  # to ensure that used elements are not deleted.
+  def test_upload_delete_invalid
+    basic_authorization "test@openstreetmap.org", "test"
+
+    diff = XML::Document.new
+    diff.root = XML::Node.new "osmChange"
+    delete = XML::Node.new "delete"
+    diff.root << delete
+    delete << current_relations(:visible_relation).to_xml_node
+    delete << current_ways(:used_way).to_xml_node
+    delete << current_nodes(:node_used_by_relationship).to_xml_node
+
+    # upload it
+    content diff
+    post :upload, :id => 1
+    assert_response :precondition_failed, 
+      "shouldn't be able to upload a invalid deletion diff: #{@response.body}"
+
+    # check that nothing was, in fact, deleted
+    assert_equal true, Node.find(current_nodes(:node_used_by_relationship).id).visible
+    assert_equal true, Way.find(current_ways(:used_way).id).visible
+    assert_equal true, Relation.find(current_relations(:visible_relation).id).visible
+  end
+
+  ##
+  # upload something which creates new objects and inserts them into
+  # existing containers using placeholders.
+  def test_upload_complex
+    basic_authorization "test@openstreetmap.org", "test"
+
+    # simple diff to create a node way and relation using placeholders
+    diff = <<EOF
+<osmChange>
+ <create>
+  <node id='-1' lon='0' lat='0' changeset='1'>
+   <tag k='foo' v='bar'/>
+   <tag k='baz' v='bat'/>
+  </node>
+ </create>
+ <modify>
+  <way id='1' changeset='1' version='1'>
+   <nd ref='-1'/>
+   <nd ref='3'/>
+  </way>
+  <relation id='1' changeset='1' version='1'>
+   <member type='way' role='some' ref='3'/>
+   <member type='node' role='some' ref='-1'/>
+   <member type='relation' role='some' ref='3'/>
+  </relation>
+ </modify>
+</osmChange>
+EOF
+
+    # upload it
+    content diff
+    post :upload, :id => 1
+    assert_response :success, 
+      "can't upload a complex diff to changeset: #{@response.body}"
+
+    # check the returned payload
+    assert_select "diffResult[version=#{API_VERSION}][generator=\"#{GENERATOR}\"]", 1
+    assert_select "diffResult>node", 1
+    assert_select "diffResult>way", 1
+    assert_select "diffResult>relation", 1
+
+    # inspect the response to find out what the new element IDs are
+    doc = XML::Parser.string(@response.body).parse
+    new_node_id = doc.find("//diffResult/node").first["new_id"].to_i
+
+    # check that the changes made it into the database
+    assert_equal 2, Node.find(new_node_id).tags.size, "new node should have two tags"
+    assert_equal [new_node_id, 3], Way.find(1).nds, "way nodes should match"
+    Relation.find(1).members.each do |type,id,role|
+      if type == 'node'
+        assert_equal new_node_id, id, "relation should contain new node"
+      end
+    end
+  end
+    
+  ##
+  # create a diff which references several changesets, which should cause
+  # a rollback and none of the diff gets committed
+  def test_upload_invalid_changesets
+    basic_authorization "test@openstreetmap.org", "test"
+
+    # simple diff to create a node way and relation using placeholders
+    diff = <<EOF
+<osmChange>
+ <modify>
+  <node id='1' lon='0' lat='0' changeset='1' version='1'/>
+  <way id='1' changeset='1' version='1'>
+   <nd ref='3'/>
+  </way>
+ </modify>
+ <modify>
+  <relation id='1' changeset='1' version='1'>
+   <member type='way' role='some' ref='3'/>
+   <member type='node' role='some' ref='5'/>
+   <member type='relation' role='some' ref='3'/>
+  </relation>
+ </modify>
+ <create>
+  <node id='-1' lon='0' lat='0' changeset='4'>
+   <tag k='foo' v='bar'/>
+   <tag k='baz' v='bat'/>
+  </node>
+ </create>
+</osmChange>
+EOF
+    # cache the objects before uploading them
+    node = current_nodes(:visible_node)
+    way = current_ways(:visible_way)
+    rel = current_relations(:visible_relation)
+
+    # upload it
+    content diff
+    post :upload, :id => 1
+    assert_response :conflict, 
+      "uploading a diff with multiple changsets should have failed"
+
+    # check that objects are unmodified
+    assert_nodes_are_equal(node, Node.find(1))
+    assert_ways_are_equal(way, Way.find(1))
+  end
+    
+  ##
+  # upload multiple versions of the same element in the same diff.
+  def test_upload_multiple_valid
+    basic_authorization "test@openstreetmap.org", "test"
+
+    # change the location of a node multiple times, each time referencing
+    # the last version. doesn't this depend on version numbers being
+    # sequential?
+    diff = <<EOF
+<osmChange>
+ <modify>
+  <node id='1' lon='0' lat='0' changeset='1' version='1'/>
+  <node id='1' lon='1' lat='0' changeset='1' version='2'/>
+  <node id='1' lon='1' lat='1' changeset='1' version='3'/>
+  <node id='1' lon='1' lat='2' changeset='1' version='4'/>
+  <node id='1' lon='2' lat='2' changeset='1' version='5'/>
+  <node id='1' lon='3' lat='2' changeset='1' version='6'/>
+  <node id='1' lon='3' lat='3' changeset='1' version='7'/>
+  <node id='1' lon='9' lat='9' changeset='1' version='8'/>
+ </modify>
+</osmChange>
+EOF
+
+    # upload it
+    content diff
+    post :upload, :id => 1
+    assert_response :success, 
+      "can't upload multiple versions of an element in a diff: #{@response.body}"
+    
+    # check the response is well-formed. its counter-intuitive, but the
+    # API will return multiple elements with the same ID and different
+    # version numbers for each change we made.
+    assert_select "diffResult>node", 8
+  end
+
+  ##
+  # upload multiple versions of the same element in the same diff, but
+  # keep the version numbers the same.
+  def test_upload_multiple_duplicate
+    basic_authorization "test@openstreetmap.org", "test"
+
+    diff = <<EOF
+<osmChange>
+ <modify>
+  <node id='1' lon='0' lat='0' changeset='1' version='1'/>
+  <node id='1' lon='1' lat='1' changeset='1' version='1'/>
+ </modify>
+</osmChange>
+EOF
+
+    # upload it
+    content diff
+    post :upload, :id => 1
+    assert_response :conflict, 
+      "shouldn't be able to upload the same element twice in a diff: #{@response.body}"
+  end
+
+  ##
+  # try to upload some elements without specifying the version
+  def test_upload_missing_version
+    basic_authorization "test@openstreetmap.org", "test"
+
+    diff = <<EOF
+<osmChange>
+ <modify>
+  <node id='1' lon='1' lat='1' changeset='1'/>
+ </modify>
+</osmChange>
+EOF
+
+    # upload it
+    content diff
+    post :upload, :id => 1
+    assert_response :bad_request, 
+      "shouldn't be able to upload an element without version: #{@response.body}"
+  end
+  
+  ##
+  # try to upload with commands other than create, modify, or delete
+  def test_action_upload_invalid
+    basic_authorization "test@openstreetmap.org", "test"
+    
+    diff = <<EOF
+<osmChange>
+  <ping>
+    <node id='1' lon='1' lat='1' changeset='1' />
+  </ping>
+</osmChange>
+EOF
+  content diff
+  post :upload, :id => 1
+  assert_response :bad_request, "Shouldn't be able to upload a diff with the action ping"
+  assert_equal @response.body, "Unknown action ping, choices are create, modify, delete."
+  end
+
+  ##
+  # upload a valid changeset which has a mixture of whitespace
+  # to check a bug reported by ivansanchez (#1565).
+  def test_upload_whitespace_valid
+    basic_authorization "test@openstreetmap.org", "test"
+
+    diff = <<EOF
+<osmChange>
+ <modify><node id='1' lon='0' lat='0' changeset='1' 
+  version='1'></node>
+  <node id='1' lon='1' lat='1' changeset='1' version='2'><tag k='k' v='v'/></node></modify>
+ <modify>
+  <relation id='1' changeset='1' version='1'><member 
+   type='way' role='some' ref='3'/><member 
+    type='node' role='some' ref='5'/>
+   <member type='relation' role='some' ref='3'/>
+  </relation>
+ </modify></osmChange>
+EOF
+
+    # upload it
+    content diff
+    post :upload, :id => 1
+    assert_response :success, 
+      "can't upload a valid diff with whitespace variations to changeset: #{@response.body}"
+
+    # check the response is well-formed
+    assert_select "diffResult>node", 2
+    assert_select "diffResult>relation", 1
+
+    # check that the changes made it into the database
+    assert_equal 1, Node.find(1).tags.size, "node 1 should now have one tag"
+    assert_equal 0, Relation.find(1).tags.size, "relation 1 should now have no tags"
+  end
+
+  ##
+  # upload a valid changeset which has a mixture of whitespace
+  # to check a bug reported by ivansanchez.
+  def test_upload_reuse_placeholder_valid
+    basic_authorization "test@openstreetmap.org", "test"
+
+    diff = <<EOF
+<osmChange>
+ <create>
+  <node id='-1' lon='0' lat='0' changeset='1'>
+   <tag k="foo" v="bar"/>
+  </node>
+ </create>
+ <modify>
+  <node id='-1' lon='1' lat='1' changeset='1' version='1'/>
+ </modify>
+ <delete>
+  <node id='-1' lon='2' lat='2' changeset='1' version='2'/>
+ </delete>
+</osmChange>
+EOF
+
+    # upload it
+    content diff
+    post :upload, :id => 1
+    assert_response :success, 
+      "can't upload a valid diff with re-used placeholders to changeset: #{@response.body}"
+
+    # check the response is well-formed
+    assert_select "diffResult>node", 3
+    assert_select "diffResult>node[old_id=-1]", 3
+  end
+
+  ##
+  # test what happens if a diff upload re-uses placeholder IDs in an
+  # illegal way.
+  def test_upload_placeholder_invalid
+    basic_authorization "test@openstreetmap.org", "test"
+
+    diff = <<EOF
+<osmChange>
+ <create>
+  <node id='-1' lon='0' lat='0' changeset='1' version='1'/>
+  <node id='-1' lon='1' lat='1' changeset='1' version='1'/>
+  <node id='-1' lon='2' lat='2' changeset='1' version='2'/>
+ </create>
+</osmChange>
+EOF
+
+    # upload it
+    content diff
+    post :upload, :id => 1
+    assert_response :bad_request, 
+      "shouldn't be able to re-use placeholder IDs"
+  end
+
+  ##
+  # test what happens if a diff is uploaded containing only a node
+  # move.
+  def test_upload_node_move
+    basic_authorization "test@openstreetmap.org", "test"
+
+    content "<osm><changeset>" +
+      "<tag k='created_by' v='osm test suite checking changesets'/>" + 
+      "</changeset></osm>"
+    put :create
+    assert_response :success
+    changeset_id = @response.body.to_i
+
+    old_node = current_nodes(:visible_node)
+
+    diff = XML::Document.new
+    diff.root = XML::Node.new "osmChange"
+    modify = XML::Node.new "modify"
+    xml_old_node = old_node.to_xml_node
+    xml_old_node["lat"] = (2.0).to_s
+    xml_old_node["lon"] = (2.0).to_s
+    xml_old_node["changeset"] = changeset_id.to_s
+    modify << xml_old_node
+    diff.root << modify
+
+    # upload it
+    content diff
+    post :upload, :id => changeset_id
+    assert_response :success, 
+      "diff should have uploaded OK"
+
+    # check the bbox
+    changeset = Changeset.find(changeset_id)
+    assert_equal 1*SCALE, changeset.min_lon, "min_lon should be 1 degree"
+    assert_equal 2*SCALE, changeset.max_lon, "max_lon should be 2 degrees"
+    assert_equal 1*SCALE, changeset.min_lat, "min_lat should be 1 degree"
+    assert_equal 2*SCALE, changeset.max_lat, "max_lat should be 2 degrees"
+  end
+
+  ##
+  # test what happens if a diff is uploaded adding a node to a way.
+  def test_upload_way_extend
+    basic_authorization "test@openstreetmap.org", "test"
+
+    content "<osm><changeset>" +
+      "<tag k='created_by' v='osm test suite checking changesets'/>" + 
+      "</changeset></osm>"
+    put :create
+    assert_response :success
+    changeset_id = @response.body.to_i
+
+    old_way = current_ways(:visible_way)
+
+    diff = XML::Document.new
+    diff.root = XML::Node.new "osmChange"
+    modify = XML::Node.new "modify"
+    xml_old_way = old_way.to_xml_node
+    nd_ref = XML::Node.new "nd"
+    nd_ref["ref"] = current_nodes(:visible_node).id.to_s
+    xml_old_way << nd_ref
+    xml_old_way["changeset"] = changeset_id.to_s
+    modify << xml_old_way
+    diff.root << modify
+
+    # upload it
+    content diff
+    post :upload, :id => changeset_id
+    assert_response :success, 
+      "diff should have uploaded OK"
+
+    # check the bbox
+    changeset = Changeset.find(changeset_id)
+    assert_equal 1*SCALE, changeset.min_lon, "min_lon should be 1 degree"
+    assert_equal 3*SCALE, changeset.max_lon, "max_lon should be 3 degrees"
+    assert_equal 1*SCALE, changeset.min_lat, "min_lat should be 1 degree"
+    assert_equal 3*SCALE, changeset.max_lat, "max_lat should be 3 degrees"
+  end
+
+  ##
+  # test for more issues in #1568
+  def test_upload_empty_invalid
+    basic_authorization "test@openstreetmap.org", "test"
+
+    [ "<osmChange/>",
+      "<osmChange></osmChange>",
+      "<osmChange><modify/></osmChange>",
+      "<osmChange><modify></modify></osmChange>"
+    ].each do |diff|
+      # upload it
+      content diff
+      post :upload, :id => 1
+      assert_response(:success, "should be able to upload " +
+                      "empty changeset: " + diff)
+    end
+  end
+
+  ##
+  # when we make some simple changes we get the same changes back from the 
+  # diff download.
+  def test_diff_download_simple
+    basic_authorization(users(:normal_user).email, "test")
+
+    # create a temporary changeset
+    content "<osm><changeset>" +
+      "<tag k='created_by' v='osm test suite checking changesets'/>" + 
+      "</changeset></osm>"
+    put :create
+    assert_response :success
+    changeset_id = @response.body.to_i
+
+    # add a diff to it
+    diff = <<EOF
+<osmChange>
+ <modify>
+  <node id='1' lon='0' lat='0' changeset='#{changeset_id}' version='1'/>
+  <node id='1' lon='1' lat='0' changeset='#{changeset_id}' version='2'/>
+  <node id='1' lon='1' lat='1' changeset='#{changeset_id}' version='3'/>
+  <node id='1' lon='1' lat='2' changeset='#{changeset_id}' version='4'/>
+  <node id='1' lon='2' lat='2' changeset='#{changeset_id}' version='5'/>
+  <node id='1' lon='3' lat='2' changeset='#{changeset_id}' version='6'/>
+  <node id='1' lon='3' lat='3' changeset='#{changeset_id}' version='7'/>
+  <node id='1' lon='9' lat='9' changeset='#{changeset_id}' version='8'/>
+ </modify>
+</osmChange>
+EOF
+
+    # upload it
+    content diff
+    post :upload, :id => changeset_id
+    assert_response :success, 
+      "can't upload multiple versions of an element in a diff: #{@response.body}"
+    
+    get :download, :id => changeset_id
+    assert_response :success
+
+    assert_select "osmChange", 1
+    assert_select "osmChange>modify", 8
+    assert_select "osmChange>modify>node", 8
+  end
+  
+  ##
+  # culled this from josm to ensure that nothing in the way that josm
+  # is formatting the request is causing it to fail.
+  #
+  # NOTE: the error turned out to be something else completely!
+  def test_josm_upload
+    basic_authorization(users(:normal_user).email, "test")
+
+    # create a temporary changeset
+    content "<osm><changeset>" +
+      "<tag k='created_by' v='osm test suite checking changesets'/>" + 
+      "</changeset></osm>"
+    put :create
+    assert_response :success
+    changeset_id = @response.body.to_i
+
+    diff = <<OSM
+<osmChange version="0.6" generator="JOSM">
+<create version="0.6" generator="JOSM">
+  <node id='-1' visible='true' changeset='#{changeset_id}' lat='51.49619982187321' lon='-0.18722061869438314' />
+  <node id='-2' visible='true' changeset='#{changeset_id}' lat='51.496359883909605' lon='-0.18653093576241928' />
+  <node id='-3' visible='true' changeset='#{changeset_id}' lat='51.49598132358285' lon='-0.18719613290981638' />
+  <node id='-4' visible='true' changeset='#{changeset_id}' lat='51.4961591711078' lon='-0.18629015888084607' />
+  <node id='-5' visible='true' changeset='#{changeset_id}' lat='51.49582126021711' lon='-0.18708186591517145' />
+  <node id='-6' visible='true' changeset='#{changeset_id}' lat='51.49591018437858' lon='-0.1861432441734455' />
+  <node id='-7' visible='true' changeset='#{changeset_id}' lat='51.49560784152179' lon='-0.18694719410005425' />
+  <node id='-8' visible='true' changeset='#{changeset_id}' lat='51.49567389979617' lon='-0.1860289771788006' />
+  <node id='-9' visible='true' changeset='#{changeset_id}' lat='51.49543761398892' lon='-0.186820684213126' />
+  <way id='-10' action='modiy' visible='true' changeset='#{changeset_id}'>
+    <nd ref='-1' />
+    <nd ref='-2' />
+    <nd ref='-3' />
+    <nd ref='-4' />
+    <nd ref='-5' />
+    <nd ref='-6' />
+    <nd ref='-7' />
+    <nd ref='-8' />
+    <nd ref='-9' />
+    <tag k='highway' v='residential' />
+    <tag k='name' v='Foobar Street' />
+  </way>
+</create>
+</osmChange>
+OSM
+
+    # upload it
+    content diff
+    post :upload, :id => changeset_id
+    assert_response :success, 
+      "can't upload a diff from JOSM: #{@response.body}"
+    
+    get :download, :id => changeset_id
+    assert_response :success
+
+    assert_select "osmChange", 1
+    assert_select "osmChange>create>node", 9
+    assert_select "osmChange>create>way", 1
+    assert_select "osmChange>create>way>nd", 9
+    assert_select "osmChange>create>way>tag", 2
+  end
+
+  ##
+  # when we make some complex changes we get the same changes back from the 
+  # diff download.
+  def test_diff_download_complex
+    basic_authorization(users(:normal_user).email, "test")
+
+    # create a temporary changeset
+    content "<osm><changeset>" +
+      "<tag k='created_by' v='osm test suite checking changesets'/>" + 
+      "</changeset></osm>"
+    put :create
+    assert_response :success
+    changeset_id = @response.body.to_i
+
+    # add a diff to it
+    diff = <<EOF
+<osmChange>
+ <delete>
+  <node id='1' lon='0' lat='0' changeset='#{changeset_id}' version='1'/>
+ </delete>
+ <create>
+  <node id='-1' lon='9' lat='9' changeset='#{changeset_id}' version='0'/>
+  <node id='-2' lon='8' lat='9' changeset='#{changeset_id}' version='0'/>
+  <node id='-3' lon='7' lat='9' changeset='#{changeset_id}' version='0'/>
+ </create>
+ <modify>
+  <node id='3' lon='20' lat='15' changeset='#{changeset_id}' version='1'/>
+  <way id='1' changeset='#{changeset_id}' version='1'>
+   <nd ref='3'/>
+   <nd ref='-1'/>
+   <nd ref='-2'/>
+   <nd ref='-3'/>
+  </way>
+ </modify>
+</osmChange>
+EOF
+
+    # upload it
+    content diff
+    post :upload, :id => changeset_id
+    assert_response :success, 
+      "can't upload multiple versions of an element in a diff: #{@response.body}"
+    
+    get :download, :id => changeset_id
+    assert_response :success
+
+    assert_select "osmChange", 1
+    assert_select "osmChange>create", 3
+    assert_select "osmChange>delete", 1
+    assert_select "osmChange>modify", 2
+    assert_select "osmChange>create>node", 3
+    assert_select "osmChange>delete>node", 1 
+    assert_select "osmChange>modify>node", 1
+    assert_select "osmChange>modify>way", 1
+  end
+
+  ##
+  # check that the bounding box of a changeset gets updated correctly
+  def test_changeset_bbox
+    basic_authorization "test@openstreetmap.org", "test"
+
+    # create a new changeset
+    content "<osm><changeset/></osm>"
+    put :create
+    assert_response :success, "Creating of changeset failed."
+    changeset_id = @response.body.to_i
+    
+    # add a single node to it
+    with_controller(NodeController.new) do
+      content "<osm><node lon='1' lat='2' changeset='#{changeset_id}'/></osm>"
+      put :create
+      assert_response :success, "Couldn't create node."
+    end
+
+    # get the bounding box back from the changeset
+    get :read, :id => changeset_id
+    assert_response :success, "Couldn't read back changeset."
+    assert_select "osm>changeset[min_lon=1.0]", 1
+    assert_select "osm>changeset[max_lon=1.0]", 1
+    assert_select "osm>changeset[min_lat=2.0]", 1
+    assert_select "osm>changeset[max_lat=2.0]", 1
+
+    # add another node to it
+    with_controller(NodeController.new) do
+      content "<osm><node lon='2' lat='1' changeset='#{changeset_id}'/></osm>"
+      put :create
+      assert_response :success, "Couldn't create second node."
+    end
+
+    # get the bounding box back from the changeset
+    get :read, :id => changeset_id
+    assert_response :success, "Couldn't read back changeset for the second time."
+    assert_select "osm>changeset[min_lon=1.0]", 1
+    assert_select "osm>changeset[max_lon=2.0]", 1
+    assert_select "osm>changeset[min_lat=1.0]", 1
+    assert_select "osm>changeset[max_lat=2.0]", 1
+
+    # add (delete) a way to it, which contains a point at (3,3)
+    with_controller(WayController.new) do
+      content update_changeset(current_ways(:visible_way).to_xml,
+                               changeset_id)
+      put :delete, :id => current_ways(:visible_way).id
+      assert_response :success, "Couldn't delete a way."
+    end
+
+    # get the bounding box back from the changeset
+    get :read, :id => changeset_id
+    assert_response :success, "Couldn't read back changeset for the third time."
+    # note that the 3.1 here is because of the bbox overexpansion
+    assert_select "osm>changeset[min_lon=1.0]", 1
+    assert_select "osm>changeset[max_lon=3.1]", 1
+    assert_select "osm>changeset[min_lat=1.0]", 1
+    assert_select "osm>changeset[max_lat=3.1]", 1    
+  end
+
+  ##
+  # test that the changeset :include method works as it should
+  def test_changeset_include
+    basic_authorization "test@openstreetmap.org", "test"
+
+    # create a new changeset
+    content "<osm><changeset/></osm>"
+    put :create
+    assert_response :success, "Creating of changeset failed."
+    changeset_id = @response.body.to_i
+
+    # NOTE: the include method doesn't over-expand, like inserting
+    # a real method does. this is because we expect the client to 
+    # know what it is doing!
+    check_after_include(changeset_id,  1,  1, [ 1,  1,  1,  1])
+    check_after_include(changeset_id,  3,  3, [ 1,  1,  3,  3])
+    check_after_include(changeset_id,  4,  2, [ 1,  1,  4,  3])
+    check_after_include(changeset_id,  2,  2, [ 1,  1,  4,  3])
+    check_after_include(changeset_id, -1, -1, [-1, -1,  4,  3])
+    check_after_include(changeset_id, -2,  5, [-2, -1,  4,  5])
+  end
+
+  ##
+  # test the query functionality of changesets
+  def test_query
+    get :query, :bbox => "-10,-10, 10, 10"
+    assert_response :success, "can't get changesets in bbox"
+    assert_changesets [1,4,6]
+
+    get :query, :bbox => "4.5,4.5,4.6,4.6"
+    assert_response :success, "can't get changesets in bbox"
+    assert_changesets [1]
+
+    # can't get changesets of user 1 without authenticating
+    get :query, :user => users(:normal_user).id
+    assert_response :not_found, "shouldn't be able to get changesets by non-public user"
+
+    # but this should work
+    basic_authorization "test@openstreetmap.org", "test"
+    get :query, :user => users(:normal_user).id
+    assert_response :success, "can't get changesets by user"
+    assert_changesets [1,3,4,6]
+
+    get :query, :user => users(:normal_user).id, :open => true
+    assert_response :success, "can't get changesets by user and open"
+    assert_changesets [1,4]
+
+    get :query, :time => '2007-12-31'
+    assert_response :success, "can't get changesets by time-since"
+    assert_changesets [1,2,4,5,6]
+
+    get :query, :time => '2008-01-01T12:34Z'
+    assert_response :success, "can't get changesets by time-since with hour"
+    assert_changesets [1,2,4,5,6]
+
+    get :query, :time => '2007-12-31T23:59Z,2008-01-01T00:01Z'
+    assert_response :success, "can't get changesets by time-range"
+    assert_changesets [1,4,5,6]
+
+    get :query, :open => 'true'
+    assert_response :success, "can't get changesets by open-ness"
+    assert_changesets [1,2,4]
+  end
+
+  ##
+  # check that errors are returned if garbage is inserted 
+  # into query strings
+  def test_query_invalid
+    [ "abracadabra!",
+      "1,2,3,F",
+      ";drop table users;"
+      ].each do |bbox|
+      get :query, :bbox => bbox
+      assert_response :bad_request, "'#{bbox}' isn't a bbox"
+    end
+
+    [ "now()",
+      "00-00-00",
+      ";drop table users;",
+      ",",
+      "-,-"
+      ].each do |time|
+      get :query, :time => time
+      assert_response :bad_request, "'#{time}' isn't a valid time range"
+    end
+
+    [ "me",
+      "foobar",
+      "-1",
+      "0"
+      ].each do |uid|
+      get :query, :user => uid
+      assert_response :bad_request, "'#{uid}' isn't a valid user ID"
+    end
+  end
+
+  ##
+  # check updating tags on a changeset
+  def test_changeset_update
+    changeset = changesets(:normal_user_first_change)
+    new_changeset = changeset.to_xml
+    new_tag = XML::Node.new "tag"
+    new_tag['k'] = "tagtesting"
+    new_tag['v'] = "valuetesting"
+    new_changeset.find("//osm/changeset").first << new_tag
+    content new_changeset
+
+    # try without any authorization
+    put :update, :id => changeset.id
+    assert_response :unauthorized
+
+    # try with the wrong authorization
+    basic_authorization "test@example.com", "test"
+    put :update, :id => changeset.id
+    assert_response :conflict
+
+    # now this should work...
+    basic_authorization "test@openstreetmap.org", "test"
+    put :update, :id => changeset.id
+    assert_response :success
+
+    assert_select "osm>changeset[id=#{changeset.id}]", 1
+    assert_select "osm>changeset>tag", 2
+    assert_select "osm>changeset>tag[k=tagtesting][v=valuetesting]", 1
+  end
+  
+  ##
+  # check that a user different from the one who opened the changeset
+  # can't modify it.
+  def test_changeset_update_invalid
+    basic_authorization "test@example.com", "test"
+
+    changeset = changesets(:normal_user_first_change)
+    new_changeset = changeset.to_xml
+    new_tag = XML::Node.new "tag"
+    new_tag['k'] = "testing"
+    new_tag['v'] = "testing"
+    new_changeset.find("//osm/changeset").first << new_tag
+
+    content new_changeset
+    put :update, :id => changeset.id
+    assert_response :conflict
+  end
+
+  ##
+  # check that a changeset can contain a certain max number of changes.
+  def test_changeset_limits
+    basic_authorization "test@openstreetmap.org", "test"
+
+    # open a new changeset
+    content "<osm><changeset/></osm>"
+    put :create
+    assert_response :success, "can't create a new changeset"
+    cs_id = @response.body.to_i
+
+    # start the counter just short of where the changeset should finish.
+    offset = 10
+    # alter the database to set the counter on the changeset directly, 
+    # otherwise it takes about 6 minutes to fill all of them.
+    changeset = Changeset.find(cs_id)
+    changeset.num_changes = Changeset::MAX_ELEMENTS - offset
+    changeset.save!
+
+    with_controller(NodeController.new) do
+      # create a new node
+      content "<osm><node changeset='#{cs_id}' lat='0.0' lon='0.0'/></osm>"
+      put :create
+      assert_response :success, "can't create a new node"
+      node_id = @response.body.to_i
+
+      get :read, :id => node_id
+      assert_response :success, "can't read back new node"
+      node_doc = XML::Parser.string(@response.body).parse
+      node_xml = node_doc.find("//osm/node").first
+
+      # loop until we fill the changeset with nodes
+      offset.times do |i|
+        node_xml['lat'] = rand.to_s
+        node_xml['lon'] = rand.to_s
+        node_xml['version'] = (i+1).to_s
+
+        content node_doc
+        put :update, :id => node_id
+        assert_response :success, "attempt #{i} should have succeeded"
+      end
+
+      # trying again should fail
+      node_xml['lat'] = rand.to_s
+      node_xml['lon'] = rand.to_s
+      node_xml['version'] = offset.to_s
+      
+      content node_doc
+      put :update, :id => node_id
+      assert_response :conflict, "final attempt should have failed"
+    end
+
+    changeset = Changeset.find(cs_id)
+    assert_equal Changeset::MAX_ELEMENTS + 1, changeset.num_changes
+
+    # check that the changeset is now closed as well
+    assert(!changeset.is_open?, 
+           "changeset should have been auto-closed by exceeding " + 
+           "element limit.")
+  end
+  
+  # This should display the last 20 changesets closed.
+  def test_list
+    @changesets = Changeset.find(:all, :order => "created_at DESC", :conditions => ['min_lat IS NOT NULL'], :limit=> 20)
+    assert @changesets.size <= 20
+    get :list
+    assert_response :success
+    assert_template "list"
+    # Now check that all 20 (or however many were returned) changesets are in the html
+    assert_select "h1", :text => "Recent Changes", :count => 1
+    assert_select "table[id='keyvalue'] tr", :count => @changesets.size + 1
+    @changesets.each do |changeset|
+      # FIXME this test needs rewriting - test for table contents
+    end
+  end
+  
+  #------------------------------------------------------------
+  # utility functions
+  #------------------------------------------------------------
+
+  ##
+  # boilerplate for checking that certain changesets exist in the
+  # output.
+  def assert_changesets(ids)
+    assert_select "osm>changeset", ids.size
+    ids.each do |id|
+      assert_select "osm>changeset[id=#{id}]", 1
+    end
+  end
+
+  ##
+  # call the include method and assert properties of the bbox
+  def check_after_include(changeset_id, lon, lat, bbox)
+    content "<osm><node lon='#{lon}' lat='#{lat}'/></osm>"
+    post :expand_bbox, :id => changeset_id
+    assert_response :success, "Setting include of changeset failed: #{@response.body}"
+
+    # check exactly one changeset
+    assert_select "osm>changeset", 1
+    assert_select "osm>changeset[id=#{changeset_id}]", 1
+
+    # check the bbox
+    doc = XML::Parser.string(@response.body).parse
+    changeset = doc.find("//osm/changeset").first
+    assert_equal bbox[0], changeset['min_lon'].to_f, "min lon"
+    assert_equal bbox[1], changeset['min_lat'].to_f, "min lat"
+    assert_equal bbox[2], changeset['max_lon'].to_f, "max lon"
+    assert_equal bbox[3], changeset['max_lat'].to_f, "max lat"
+  end
+
+  ##
+  # update the changeset_id of a way element
+  def update_changeset(xml, changeset_id)
+    xml_attr_rewrite(xml, 'changeset', changeset_id)
+  end
+
+  ##
+  # update an attribute in a way element
+  def xml_attr_rewrite(xml, name, value)
+    xml.find("//osm/way").first[name] = value.to_s
+    return xml
+  end
+
+end
diff --git a/test/functional/changeset_tag_controller_test.rb b/test/functional/changeset_tag_controller_test.rb
new file mode 100644 (file)
index 0000000..db9710e
--- /dev/null
@@ -0,0 +1,8 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class ChangesetTagControllerTest < ActionController::TestCase
+  # Replace this with your real tests.
+  def test_truth
+    assert true
+  end
+end
diff --git a/test/functional/diary_entry_controller_test.rb b/test/functional/diary_entry_controller_test.rb
new file mode 100644 (file)
index 0000000..10fb908
--- /dev/null
@@ -0,0 +1,162 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class DiaryEntryControllerTest < ActionController::TestCase
+  fixtures :users, :diary_entries, :diary_comments
+
+  def test_showing_new_diary_entry
+    get :new
+    assert_response :redirect
+    assert_redirected_to :controller => :user, :action => "login", :referer => "/diary_entry/new"
+    # Now pretend to login by using the session hash, with the 
+    # id of the person we want to login as through session(:user)=user.id
+    get(:new, nil, {'user' => users(:normal_user).id})
+    assert_response :success
+    #print @response.body
+    
+    #print @response.to_yaml
+    assert_select "html:root", :count => 1 do
+      assert_select "head", :count => 1 do
+        assert_select "title", :text => /New diary entry/, :count => 1
+      end
+      assert_select "body", :count => 1 do
+        assert_select "div#content", :count => 1 do
+          assert_select "h1", "New diary entry", :count => 1
+          # We don't care about the layout, we just care about the form fields
+          # that are available
+          assert_select "form[action='/diary_entry/new']", :count => 1 do
+            assert_select "input[id=diary_entry_title][name='diary_entry[title]']", :count => 1
+            assert_select "textarea#diary_entry_body[name='diary_entry[body]']", :count => 1
+            assert_select "input#latitude[name='diary_entry[latitude]'][type=text]", :count => 1
+            assert_select "input#longitude[name='diary_entry[longitude]'][type=text]", :count => 1
+            assert_select "input[name=commit][type=submit][value=Save]", :count => 1
+          end
+        end
+      end
+    end
+        
+  end
+  
+  def test_editing_diary_entry
+    # Make sure that you are redirected to the login page when you are 
+    # not logged in, without and with the id of the entry you want to edit
+    get :edit
+    assert_response :redirect
+    assert_redirected_to :controller => :user, :action => "login", :referer => "/diary_entry/edit"
+    
+    get :edit, :id => diary_entries(:normal_user_entry_1).id
+    assert_response :redirect
+    assert_redirected_to :controller => :user, :action => "login", :referer => "/diary_entry/edit"
+    
+    # Verify that you get a not found error, when you don't pass an id
+    get(:edit, nil, {'user' => users(:normal_user).id})
+    assert_response :not_found
+    assert_select "html:root", :count => 1 do
+      assert_select "body", :count => 1 do
+        assert_select "div#content", :count => 1 do
+          assert_select "h2", :text => "No entry with the id:", :count => 1 
+        end
+      end
+    end
+    
+    # Now pass the id, and check that you can edit it, when using the same 
+    # user as the person who created the entry
+    get(:edit, {:id => diary_entries(:normal_user_entry_1).id}, {'user' => users(:normal_user).id})
+    assert_response :success
+    assert_select "html:root", :count => 1 do
+      assert_select "head", :count => 1 do
+        assert_select "title", :text => /Edit diary entry/, :count => 1
+      end
+      assert_select "body", :count => 1 do
+        assert_select "div#content", :count => 1 do 
+          assert_select "h1", :text => /Edit diary entry/, :count => 1
+          assert_select "form[action='/diary_entry/#{diary_entries(:normal_user_entry_1).id}/edit'][method=post]", :count => 1 do
+            assert_select "input#diary_entry_title[name='diary_entry[title]'][value='#{diary_entries(:normal_user_entry_1).title}']", :count => 1
+            assert_select "textarea#diary_entry_body[name='diary_entry[body]']", :text => diary_entries(:normal_user_entry_1).body, :count => 1
+            assert_select "input#latitude[name='diary_entry[latitude]']", :count => 1
+            assert_select "input#longitude[name='diary_entry[longitude]']", :count => 1
+            assert_select "input[name=commit][type=submit][value=Save]", :count => 1
+            assert_select "input", :count => 4
+          end
+        end
+      end
+    end
+    
+    # Now lets see if you can edit the diary entry
+    new_title = "New Title"
+    new_body = "This is a new body for the diary entry"
+    new_latitude = "1.1"
+    new_longitude = "2.2"
+    post(:edit, {:id => diary_entries(:normal_user_entry_1).id, 'commit' => 'save', 
+      'diary_entry'=>{'title' => new_title, 'body' => new_body, 'latitude' => new_latitude, 'longitude' => new_longitude} },
+         {'user' => users(:normal_user).id})
+    assert_response :redirect
+    assert_redirected_to :action => :view, :id => diary_entries(:normal_user_entry_1).id
+    
+    # Now check that the new data is rendered, when logged in
+    get :view, {:id => diary_entries(:normal_user_entry_1).id, :display_name => 'test'}, {'user' => users(:normal_user).id}
+    assert_response :success
+    assert_template 'diary_entry/view'
+    assert_select "html:root", :count => 1 do
+      assert_select "head", :count => 1 do
+        assert_select "title", :text => /Users' diaries | /, :count => 1
+      end
+      assert_select "body", :count => 1 do
+        assert_select "div#content", :count => 1 do
+          assert_select "h2", :text => /#{users(:normal_user).display_name}'s diary/, :count => 1
+          assert_select "b", :text => /#{new_title}/, :count => 1
+          # This next line won't work if the text has been run through the htmlize function
+          # due to formatting that could be introduced
+          assert_select "p", :text => /#{new_body}/, :count => 1
+          assert_select "span.latitude", :text => new_latitude, :count => 1
+          assert_select "span.longitude", :text => new_longitude, :count => 1
+          # As we're not logged in, check that you cannot edit
+          #print @response.body
+          assert_select "a[href='/user/#{users(:normal_user).display_name}/diary/#{diary_entries(:normal_user_entry_1).id}/edit']", :text => "Edit this entry", :count => 1
+        end
+      end
+    end
+    
+    # and when not logged in as the user who wrote the entry
+    get :view, {:id => diary_entries(:normal_user_entry_1).id, :display_name => 'test'}, {'user' => users(:public_user).id}
+    assert_response :success
+    assert_template 'diary_entry/view'
+    assert_select "html:root", :count => 1 do
+      assert_select "head", :count => 1 do
+        assert_select "title", :text => /Users' diaries | /, :count => 1
+      end
+      assert_select "body", :count => 1 do
+        assert_select "div#content", :count => 1 do
+          assert_select "h2", :text => /#{users(:normal_user).display_name}'s diary/, :count => 1
+          assert_select "b", :text => /#{new_title}/, :count => 1
+          # This next line won't work if the text has been run through the htmlize function
+          # due to formatting that could be introduced
+          assert_select "p", :text => /#{new_body}/, :count => 1
+          assert_select "span.latitude", :text => new_latitude, :count => 1
+          assert_select "span.longitude", :text => new_longitude, :count => 1
+          # As we're not logged in, check that you cannot edit
+          assert_select "a[href='/user/#{users(:normal_user).display_name}/diary/#{diary_entries(:normal_user_entry_1).id}/edit']", :text => "Edit this entry", :count => 0
+        end
+      end
+    end
+    #print @response.body
+    
+  end
+  
+  def test_editing_creating_diary_comment
+    
+  end
+  
+  def test_listing_diary_entries
+    
+  end
+  
+  def test_rss
+    get :rss
+    assert :success
+    
+  end
+  
+  def test_viewing_diary_entry
+    
+  end
+end
diff --git a/test/functional/export_controller_test.rb b/test/functional/export_controller_test.rb
new file mode 100644 (file)
index 0000000..8a97941
--- /dev/null
@@ -0,0 +1,8 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class ExportControllerTest < ActionController::TestCase
+  # Replace this with your real tests.
+  def test_truth
+    assert true
+  end
+end
diff --git a/test/functional/friend_controller_test.rb b/test/functional/friend_controller_test.rb
new file mode 100644 (file)
index 0000000..d1f0e7d
--- /dev/null
@@ -0,0 +1,8 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class FriendControllerTest < ActionController::TestCase
+  # Replace this with your real tests.
+  def test_truth
+    assert true
+  end
+end
index 3faadc7404aab06ec9fb202d3505bb7175d633a5..f63fe518d0aa04925c04f91b2e45ad905f145013 100644 (file)
@@ -1,15 +1,7 @@
 require File.dirname(__FILE__) + '/../test_helper'
 require 'geocoder_controller'
 
-# Re-raise errors caught by the controller.
-class GeocoderController; def rescue_action(e) raise e end; end
-
-class GeocoderControllerTest < Test::Unit::TestCase
-  def setup
-    @controller = GeocoderController.new
-    @request    = ActionController::TestRequest.new
-    @response   = ActionController::TestResponse.new
-  end
+class GeocoderControllerTest < ActionController::TestCase
 
   # Replace this with your real tests.
   def test_truth
index 54c8a18d12952265bd681b8fdbf00ce2999dc9e1..96f509cb0208b3548753e4fd5f351bdf65822b4c 100644 (file)
@@ -1,15 +1,7 @@
 require File.dirname(__FILE__) + '/../test_helper'
 require 'message_controller'
 
-# Re-raise errors caught by the controller.
-class MessageController; def rescue_action(e) raise e end; end
-
-class MessageControllerTest < Test::Unit::TestCase
-  def setup
-    @controller = MessageController.new
-    @request    = ActionController::TestRequest.new
-    @response   = ActionController::TestResponse.new
-  end
+class MessageControllerTest < ActionController::TestCase
 
   # Replace this with your real tests.
   def test_truth
index a380eeb208313f08672104595eef0d188ec72e06..8d019bf7902fa549ec259b9061f71676dc5dbc49 100644 (file)
@@ -1,28 +1,23 @@
 require File.dirname(__FILE__) + '/../test_helper'
-require 'node_controller'
 
-# Re-raise errors caught by the controller.
-class NodeController; def rescue_action(e) raise e end; end
-
-class NodeControllerTest < Test::Unit::TestCase
+class NodeControllerTest < ActionController::TestCase
   api_fixtures
 
-  def setup
-    @controller = NodeController.new
-    @request    = ActionController::TestRequest.new
-    @response   = ActionController::TestResponse.new
-  end
-
   def test_create
     # cannot read password from fixture as it is stored as MD5 digest
-    basic_authorization("test@openstreetmap.org", "test");  
+    basic_authorization(users(:normal_user).email, "test")
+    
     # create a node with random lat/lon
     lat = rand(100)-50 + rand
     lon = rand(100)-50 + rand
-    content("<osm><node lat='#{lat}' lon='#{lon}' /></osm>")
+    # normal user has a changeset open, so we'll use that.
+    changeset = changesets(:normal_user_first_change)
+    # create a minimal xml file
+    content("<osm><node lat='#{lat}' lon='#{lon}' changeset='#{changeset.id}'/></osm>")
     put :create
     # hope for success
     assert_response :success, "node upload did not return success status"
+
     # read id of created node and search for it
     nodeid = @response.body
     checknode = Node.find(nodeid)
@@ -30,10 +25,36 @@ class NodeControllerTest < Test::Unit::TestCase
     # compare values
     assert_in_delta lat * 10000000, checknode.latitude, 1, "saved node does not match requested latitude"
     assert_in_delta lon * 10000000, checknode.longitude, 1, "saved node does not match requested longitude"
-    assert_equal users(:normal_user).id, checknode.user_id, "saved node does not belong to user that created it"
+    assert_equal changesets(:normal_user_first_change).id, checknode.changeset_id, "saved node does not belong to changeset that it was created in"
     assert_equal true, checknode.visible, "saved node is not visible"
   end
 
+  def test_create_invalid_xml
+    # Initial setup
+    basic_authorization(users(:normal_user).email, "test")
+    # normal user has a changeset open, so we'll use that.
+    changeset = changesets(:normal_user_first_change)
+    lat = 3.434
+    lon = 3.23
+    
+    # test that the upload is rejected when no lat is supplied
+    # create a minimal xml file
+    content("<osm><node lon='#{lon}' changeset='#{changeset.id}'/></osm>")
+    put :create
+    # hope for success
+    assert_response :bad_request, "node upload did not return bad_request status"
+    assert_equal 'Cannot parse valid node from xml string <node lon="3.23" changeset="1"/>. lat missing', @response.body
+
+    # test that the upload is rejected when no lon is supplied
+    # create a minimal xml file
+    content("<osm><node lat='#{lat}' changeset='#{changeset.id}'/></osm>")
+    put :create
+    # hope for success
+    assert_response :bad_request, "node upload did not return bad_request status"
+    assert_equal 'Cannot parse valid node from xml string <node lat="3.434" changeset="1"/>. lon missing', @response.body
+
+  end
+
   def test_read
     # check that a visible node is returned properly
     get :read, :id => current_nodes(:visible_node).id
@@ -51,19 +72,36 @@ class NodeControllerTest < Test::Unit::TestCase
   # this tests deletion restrictions - basic deletion is tested in the unit
   # tests for node!
   def test_delete
-
     # first try to delete node without auth
     delete :delete, :id => current_nodes(:visible_node).id
     assert_response :unauthorized
 
     # now set auth
-    basic_authorization("test@openstreetmap.org", "test");  
+    basic_authorization(users(:normal_user).email, "test");  
 
-    # this should work
+    # try to delete with an invalid (closed) changeset
+    content update_changeset(current_nodes(:visible_node).to_xml,
+                             changesets(:normal_user_closed_change).id)
+    delete :delete, :id => current_nodes(:visible_node).id
+    assert_response :conflict
+
+    # try to delete with an invalid (non-existent) changeset
+    content update_changeset(current_nodes(:visible_node).to_xml,0)
+    delete :delete, :id => current_nodes(:visible_node).id
+    assert_response :conflict
+
+    # valid delete now takes a payload
+    content(nodes(:visible_node).to_xml)
     delete :delete, :id => current_nodes(:visible_node).id
     assert_response :success
 
+    # valid delete should return the new version number, which should
+    # be greater than the old version number
+    assert @response.body.to_i > current_nodes(:visible_node).version,
+       "delete request should return a new version number for node"
+
     # this won't work since the node is already deleted
+    content(nodes(:invisible_node).to_xml)
     delete :delete, :id => current_nodes(:invisible_node).id
     assert_response :gone
 
@@ -71,17 +109,174 @@ class NodeControllerTest < Test::Unit::TestCase
     delete :delete, :id => 0
     assert_response :not_found
 
-    # this won't work since the node is in use
+    ## these test whether nodes which are in-use can be deleted:
+    # in a way...
+    content(nodes(:used_node_1).to_xml)
     delete :delete, :id => current_nodes(:used_node_1).id
-    assert_response :precondition_failed
+    assert_response :precondition_failed,
+       "shouldn't be able to delete a node used in a way (#{@response.body})"
+
+    # in a relation...
+    content(nodes(:node_used_by_relationship).to_xml)
+    delete :delete, :id => current_nodes(:node_used_by_relationship).id
+    assert_response :precondition_failed,
+       "shouldn't be able to delete a node used in a relation (#{@response.body})"
+  end
+
+  ##
+  # tests whether the API works and prevents incorrect use while trying
+  # to update nodes.
+  def test_update
+    # try and update a node without authorisation
+    # first try to delete node without auth
+    content current_nodes(:visible_node).to_xml
+    put :update, :id => current_nodes(:visible_node).id
+    assert_response :unauthorized
+    
+    # setup auth
+    basic_authorization(users(:normal_user).email, "test")
+
+    ## trying to break changesets
+
+    # try and update in someone else's changeset
+    content update_changeset(current_nodes(:visible_node).to_xml,
+                             changesets(:second_user_first_change).id)
+    put :update, :id => current_nodes(:visible_node).id
+    assert_response :conflict, "update with other user's changeset should be rejected"
+
+    # try and update in a closed changeset
+    content update_changeset(current_nodes(:visible_node).to_xml,
+                             changesets(:normal_user_closed_change).id)
+    put :update, :id => current_nodes(:visible_node).id
+    assert_response :conflict, "update with closed changeset should be rejected"
+
+    # try and update in a non-existant changeset
+    content update_changeset(current_nodes(:visible_node).to_xml, 0)
+    put :update, :id => current_nodes(:visible_node).id
+    assert_response :conflict, "update with changeset=0 should be rejected"
+
+    ## try and submit invalid updates
+    content xml_attr_rewrite(current_nodes(:visible_node).to_xml, 'lat', 91.0);
+    put :update, :id => current_nodes(:visible_node).id
+    assert_response :bad_request, "node at lat=91 should be rejected"
+
+    content xml_attr_rewrite(current_nodes(:visible_node).to_xml, 'lat', -91.0);
+    put :update, :id => current_nodes(:visible_node).id
+    assert_response :bad_request, "node at lat=-91 should be rejected"
+    
+    content xml_attr_rewrite(current_nodes(:visible_node).to_xml, 'lon', 181.0);
+    put :update, :id => current_nodes(:visible_node).id
+    assert_response :bad_request, "node at lon=181 should be rejected"
+
+    content xml_attr_rewrite(current_nodes(:visible_node).to_xml, 'lon', -181.0);
+    put :update, :id => current_nodes(:visible_node).id
+    assert_response :bad_request, "node at lon=-181 should be rejected"
+
+    ## next, attack the versioning
+    current_node_version = current_nodes(:visible_node).version
+
+    # try and submit a version behind
+    content xml_attr_rewrite(current_nodes(:visible_node).to_xml, 
+                             'version', current_node_version - 1);
+    put :update, :id => current_nodes(:visible_node).id
+    assert_response :conflict, "should have failed on old version number"
+    
+    # try and submit a version ahead
+    content xml_attr_rewrite(current_nodes(:visible_node).to_xml, 
+                             'version', current_node_version + 1);
+    put :update, :id => current_nodes(:visible_node).id
+    assert_response :conflict, "should have failed on skipped version number"
+
+    # try and submit total crap in the version field
+    content xml_attr_rewrite(current_nodes(:visible_node).to_xml, 
+                             'version', 'p1r4t3s!');
+    put :update, :id => current_nodes(:visible_node).id
+    assert_response :conflict, 
+       "should not be able to put 'p1r4at3s!' in the version field"
+    
+    ## finally, produce a good request which should work
+    content current_nodes(:visible_node).to_xml
+    put :update, :id => current_nodes(:visible_node).id
+    assert_response :success, "a valid update request failed"
   end
 
+  ##
+  # test adding tags to a node
+  def test_duplicate_tags
+    # setup auth
+    basic_authorization(users(:normal_user).email, "test")
+
+    # add an identical tag to the node
+    tag_xml = XML::Node.new("tag")
+    tag_xml['k'] = current_node_tags(:t1).k
+    tag_xml['v'] = current_node_tags(:t1).v
+
+    # add the tag into the existing xml
+    node_xml = current_nodes(:visible_node).to_xml
+    node_xml.find("//osm/node").first << tag_xml
+
+    # try and upload it
+    content node_xml
+    put :update, :id => current_nodes(:visible_node).id
+    assert_response :bad_request, 
+      "adding duplicate tags to a node should fail with 'bad request'"
+    assert_equal "Element node/#{current_nodes(:visible_node).id} has duplicate tags with key #{current_node_tags(:t1).k}.", @response.body
+  end
+
+  # test whether string injection is possible
+  def test_string_injection
+    basic_authorization(users(:normal_user).email, "test")
+    changeset_id = changesets(:normal_user_first_change).id
+
+    # try and put something into a string that the API might 
+    # use unquoted and therefore allow code injection...
+    content "<osm><node lat='0' lon='0' changeset='#{changeset_id}'>" +
+      '<tag k="#{@user.inspect}" v="0"/>' +
+      '</node></osm>'
+    put :create
+    assert_response :success
+    nodeid = @response.body
+
+    # find the node in the database
+    checknode = Node.find(nodeid)
+    assert_not_nil checknode, "node not found in data base after upload"
+    
+    # and grab it using the api
+    get :read, :id => nodeid
+    assert_response :success
+    apinode = Node.from_xml(@response.body)
+    assert_not_nil apinode, "downloaded node is nil, but shouldn't be"
+    
+    # check the tags are not corrupted
+    assert_equal checknode.tags, apinode.tags
+    assert apinode.tags.include?('#{@user.inspect}')
+  end
 
   def basic_authorization(user, pass)
     @request.env["HTTP_AUTHORIZATION"] = "Basic %s" % Base64.encode64("#{user}:#{pass}")
   end
 
   def content(c)
-    @request.env["RAW_POST_DATA"] = c
+    @request.env["RAW_POST_DATA"] = c.to_s
+  end
+
+  ##
+  # update the changeset_id of a node element
+  def update_changeset(xml, changeset_id)
+    xml_attr_rewrite(xml, 'changeset', changeset_id)
+  end
+
+  ##
+  # update an attribute in the node element
+  def xml_attr_rewrite(xml, name, value)
+    xml.find("//osm/node").first[name] = value.to_s
+    return xml
+  end
+
+  ##
+  # parse some xml
+  def xml_parse(xml)
+    parser = XML::Parser.string(xml)
+    parser.parse
   end
 end
diff --git a/test/functional/old_node_controller_test.rb b/test/functional/old_node_controller_test.rb
new file mode 100644 (file)
index 0000000..f1328e6
--- /dev/null
@@ -0,0 +1,133 @@
+require File.dirname(__FILE__) + '/../test_helper'
+require 'old_node_controller'
+
+class OldNodeControllerTest < ActionController::TestCase
+  api_fixtures
+
+  #
+  # TODO: test history
+  #
+
+  ##
+  # test the version call by submitting several revisions of a new node
+  # to the API and ensuring that later calls to version return the 
+  # matching versions of the object.
+  def test_version
+    basic_authorization(users(:normal_user).email, "test")
+    changeset_id = changesets(:normal_user_first_change).id
+
+    # setup a simple XML node
+    xml_doc = current_nodes(:visible_node).to_xml
+    xml_node = xml_doc.find("//osm/node").first
+    nodeid = current_nodes(:visible_node).id
+
+    # keep a hash of the versions => string, as we'll need something
+    # to test against later
+    versions = Hash.new
+
+    # save a version for later checking
+    versions[xml_node['version']] = xml_doc.to_s
+
+    # randomly move the node about
+    20.times do 
+      # move the node somewhere else
+      xml_node['lat'] = precision(rand * 180 -  90).to_s
+      xml_node['lon'] = precision(rand * 360 - 180).to_s
+      with_controller(NodeController.new) do
+        content xml_doc
+        put :update, :id => nodeid
+        assert_response :success
+        xml_node['version'] = @response.body.to_s
+      end
+      # save a version for later checking
+      versions[xml_node['version']] = xml_doc.to_s
+    end
+
+    # add a bunch of random tags
+    30.times do 
+      xml_tag = XML::Node.new("tag")
+      xml_tag['k'] = random_string
+      xml_tag['v'] = random_string
+      xml_node << xml_tag
+      with_controller(NodeController.new) do
+        content xml_doc
+        put :update, :id => nodeid
+        assert_response :success,
+        "couldn't update node #{nodeid} (#{@response.body})"
+        xml_node['version'] = @response.body.to_s
+      end
+      # save a version for later checking
+      versions[xml_node['version']] = xml_doc.to_s
+    end
+
+    # check all the versions
+    versions.keys.each do |key|
+      get :version, :id => nodeid, :version => key.to_i
+
+      assert_response :success,
+         "couldn't get version #{key.to_i} of node #{nodeid}"
+
+      check_node = Node.from_xml(versions[key])
+      api_node = Node.from_xml(@response.body.to_s)
+
+      assert_nodes_are_equal check_node, api_node
+    end
+  end
+
+  ##
+  # Test that getting the current version is identical to picking
+  # that version with the version URI call.
+  def test_current_version
+    check_current_version(current_nodes(:visible_node))
+    check_current_version(current_nodes(:used_node_1))
+    check_current_version(current_nodes(:used_node_2))
+    check_current_version(current_nodes(:node_used_by_relationship))
+    check_current_version(current_nodes(:node_with_versions))
+  end
+  
+  def check_current_version(node_id)
+    # get the current version of the node
+    current_node = with_controller(NodeController.new) do
+      get :read, :id => node_id
+      assert_response :success, "cant get current node #{node_id}" 
+      Node.from_xml(@response.body)
+    end
+    assert_not_nil current_node, "getting node #{node_id} returned nil"
+
+    # get the "old" version of the node from the old_node interface
+    get :version, :id => node_id, :version => current_node.version
+    assert_response :success, "cant get old node #{node_id}, v#{current_node.version}" 
+    old_node = Node.from_xml(@response.body)
+
+    # check the nodes are the same
+    assert_nodes_are_equal current_node, old_node
+  end
+
+  ##
+  # returns a 16 character long string with some nasty characters in it.
+  # this ought to stress-test the tag handling as well as the versioning.
+  def random_string
+    letters = [['!','"','$','&',';','@'],
+               ('a'..'z').to_a,
+               ('A'..'Z').to_a,
+               ('0'..'9').to_a].flatten
+    (1..16).map { |i| letters[ rand(letters.length) ] }.join
+  end
+
+  ##
+  # truncate a floating point number to the scale that it is stored in
+  # the database. otherwise rounding errors can produce failing unit
+  # tests when they shouldn't.
+  def precision(f)
+    return (f * GeoRecord::SCALE).round.to_f / GeoRecord::SCALE
+  end
+
+  def basic_authorization(user, pass)
+    @request.env["HTTP_AUTHORIZATION"] = "Basic %s" % Base64.encode64("#{user}:#{pass}")
+  end
+
+  def content(c)
+    @request.env["RAW_POST_DATA"] = c.to_s
+  end
+
+end
index b8bf464b6030fd180dced5c4f75dc88efdfed56e..a52211e2e15485ee28c7acb1eacfacf538601c18 100644 (file)
@@ -1,22 +1,12 @@
 require File.dirname(__FILE__) + '/../test_helper'
 require 'old_relation_controller'
 
-# Re-raise errors caught by the controller.
-#class OldRelationController; def rescue_action(e) raise e end; end
-
-class OldRelationControllerTest < Test::Unit::TestCase
+class OldRelationControllerTest < ActionController::TestCase
   api_fixtures
 
-  def setup
-    @controller = OldRelationController.new
-    @request    = ActionController::TestRequest.new
-    @response   = ActionController::TestResponse.new
-  end
-
   # -------------------------------------
   # Test reading old relations.
   # -------------------------------------
-
   def test_history
     # check that a visible relations is returned properly
     get :history, :id => relations(:visible_relation).id
index 374ea7dc2de42bbba429d9c2946d24b0d601ccf5..31da1d2c784bb18d6598cbc52a486b77760343d7 100644 (file)
@@ -1,31 +1,89 @@
 require File.dirname(__FILE__) + '/../test_helper'
 require 'old_way_controller'
 
-# Re-raise errors caught by the controller.
-class OldWayController; def rescue_action(e) raise e end; end
-
-class OldWayControllerTest < Test::Unit::TestCase
+class OldWayControllerTest < ActionController::TestCase
   api_fixtures
 
-  def setup
-    @controller = OldWayController.new
-    @request    = ActionController::TestRequest.new
-    @response   = ActionController::TestResponse.new
-  end
-
   # -------------------------------------
   # Test reading old ways.
   # -------------------------------------
 
-  def test_history
+  def test_history_visible
     # check that a visible way is returned properly
     get :history, :id => ways(:visible_way).id
     assert_response :success
-
+  end
+  
+  def test_history_invisible
+    # check that an invisible way's history is returned properly
+    get :history, :id => ways(:invisible_way).id
+    assert_response :success
+  end
+  
+  def test_history_invalid
     # check chat a non-existent way is not returned
     get :history, :id => 0
     assert_response :not_found
+  end
+  
+  ##
+  # check that we can retrieve versions of a way
+  def test_version
+    check_current_version(current_ways(:visible_way).id)
+    check_current_version(current_ways(:used_way).id)
+    check_current_version(current_ways(:way_with_versions).id)
+  end
+
+  ##
+  # check that returned history is the same as getting all 
+  # versions of a way from the api.
+  def test_history_equals_versions
+    check_history_equals_versions(current_ways(:visible_way).id)
+    check_history_equals_versions(current_ways(:used_way).id)
+    check_history_equals_versions(current_ways(:way_with_versions).id)
+  end
+
+  ##
+  # check that the current version of a way is equivalent to the
+  # version which we're getting from the versions call.
+  def check_current_version(way_id)
+    # get the current version
+    current_way = with_controller(WayController.new) do
+      get :read, :id => way_id
+      assert_response :success, "can't get current way #{way_id}"
+      Way.from_xml(@response.body)
+    end
+    assert_not_nil current_way, "getting way #{way_id} returned nil"
+
+    # get the "old" version of the way from the version method
+    get :version, :id => way_id, :version => current_way.version
+    assert_response :success, "can't get old way #{way_id}, v#{current_way.version}"
+    old_way = Way.from_xml(@response.body)
+
+    # check that the ways are identical
+    assert_ways_are_equal current_way, old_way
+  end
+
+  ##
+  # look at all the versions of the way in the history and get each version from
+  # the versions call. check that they're the same.
+  def check_history_equals_versions(way_id)
+    get :history, :id => way_id
+    assert_response :success, "can't get way #{way_id} from API"
+    history_doc = XML::Parser.string(@response.body).parse
+    assert_not_nil history_doc, "parsing way #{way_id} history failed"
+
+    history_doc.find("//osm/way").each do |way_doc|
+      history_way = Way.from_xml_node(way_doc)
+      assert_not_nil history_way, "parsing way #{way_id} version failed"
 
+      get :version, :id => way_id, :version => history_way.version
+      assert_response :success, "couldn't get way #{way_id}, v#{history_way.version}"
+      version_way = Way.from_xml(@response.body)
+      assert_not_nil version_way, "failed to parse #{way_id}, v#{history_way.version}"
+      
+      assert_ways_are_equal history_way, version_way
+    end
   end
 
 end
index 202a015a87f737459b13ec30a4055e9dc5c67a37..d2dacd79c2408e15f2f0c53c2498b6f0ea226c42 100644 (file)
@@ -1,28 +1,8 @@
 require File.dirname(__FILE__) + '/../test_helper'
 require 'relation_controller'
 
-# Re-raise errors caught by the controller.
-class RelationController; def rescue_action(e) raise e end; end
-
-class RelationControllerTest < Test::Unit::TestCase
+class RelationControllerTest < ActionController::TestCase
   api_fixtures
-  fixtures :relations, :current_relations, :relation_members, :current_relation_members, :relation_tags, :current_relation_tags
-  set_fixture_class :current_relations => :Relation
-  set_fixture_class :relations => :OldRelation
-
-  def setup
-    @controller = RelationController.new
-    @request    = ActionController::TestRequest.new
-    @response   = ActionController::TestResponse.new
-  end
-
-  def basic_authorization(user, pass)
-    @request.env["HTTP_AUTHORIZATION"] = "Basic %s" % Base64.encode64("#{user}:#{pass}")
-  end
-
-  def content(c)
-    @request.env["RAW_POST_DATA"] = c
-  end
 
   # -------------------------------------
   # Test reading relations.
@@ -40,31 +20,49 @@ class RelationControllerTest < Test::Unit::TestCase
     # check chat a non-existent relation is not returned
     get :read, :id => 0
     assert_response :not_found
+  end
 
-    # check the "relations for node" mode
-    get :relations_for_node, :id => current_nodes(:node_used_by_relationship).id
-    assert_response :success
-    # FIXME check whether this contains the stuff we want!
-    if $VERBOSE
-        print @response.body
-    end
+  ##
+  # check that all relations containing a particular node, and no extra
+  # relations, are returned from the relations_for_node call.
+  def test_relations_for_node
+    check_relations_for_element(:relations_for_node, "node", 
+                                current_nodes(:node_used_by_relationship).id,
+                                [ :visible_relation, :used_relation ])
+  end
 
-    # check the "relations for way" mode
-    get :relations_for_way, :id => current_ways(:used_way).id
-    assert_response :success
-    # FIXME check whether this contains the stuff we want!
-    if $VERBOSE
-        print @response.body
-    end
+  def test_relations_for_way
+    check_relations_for_element(:relations_for_way, "way",
+                                current_ways(:used_way).id,
+                                [ :visible_relation ])
+  end
 
+  def test_relations_for_relation
+    check_relations_for_element(:relations_for_relation, "relation",
+                                current_relations(:used_relation).id,
+                                [ :visible_relation ])
+  end
+
+  def check_relations_for_element(method, type, id, expected_relations)
     # check the "relations for relation" mode
-    get :relations_for_relation, :id => current_relations(:used_relation).id
+    get method, :id => id
     assert_response :success
-    # FIXME check whether this contains the stuff we want!
-    if $VERBOSE
-        print @response.body
+
+    # count one osm element
+    assert_select "osm[version=#{API_VERSION}][generator=\"OpenStreetMap server\"]", 1
+
+    # we should have only the expected number of relations
+    assert_select "osm>relation", expected_relations.size
+
+    # and each of them should contain the node we originally searched for
+    expected_relations.each do |r|
+      relation_id = current_relations(r).id
+      assert_select "osm>relation#?", relation_id
+      assert_select "osm>relation#?>member[type=\"#{type}\"][ref=#{id}]", relation_id
     end
+  end
 
+  def test_full
     # check the "full" mode
     get :full, :id => current_relations(:visible_relation).id
     assert_response :success
@@ -80,9 +78,12 @@ class RelationControllerTest < Test::Unit::TestCase
 
   def test_create
     basic_authorization "test@openstreetmap.org", "test"
+    
+    # put the relation in a dummy fixture changset
+    changeset_id = changesets(:normal_user_first_change).id
 
     # create an relation without members
-    content "<osm><relation><tag k='test' v='yes' /></relation></osm>"
+    content "<osm><relation changeset='#{changeset_id}'><tag k='test' v='yes' /></relation></osm>"
     put :create
     # hope for success
     assert_response :success, 
@@ -97,7 +98,9 @@ class RelationControllerTest < Test::Unit::TestCase
         "saved relation contains members but should not"
     assert_equal checkrelation.tags.length, 1, 
         "saved relation does not contain exactly one tag"
-    assert_equal users(:normal_user).id, checkrelation.user_id, 
+    assert_equal changeset_id, checkrelation.changeset.id,
+        "saved relation does not belong in the changeset it was assigned to"
+    assert_equal users(:normal_user).id, checkrelation.changeset.user_id, 
         "saved relation does not belong to user that created it"
     assert_equal true, checkrelation.visible, 
         "saved relation is not visible"
@@ -106,10 +109,46 @@ class RelationControllerTest < Test::Unit::TestCase
     assert_response :success
 
 
+    ###
     # create an relation with a node as member
+    # This time try with a role attribute in the relation
+    nid = current_nodes(:used_node_1).id
+    content "<osm><relation changeset='#{changeset_id}'>" +
+      "<member  ref='#{nid}' type='node' role='some'/>" +
+      "<tag k='test' v='yes' /></relation></osm>"
+    put :create
+    # hope for success
+    assert_response :success, 
+        "relation upload did not return success status"
+    # read id of created relation and search for it
+    relationid = @response.body
+    checkrelation = Relation.find(relationid)
+    assert_not_nil checkrelation, 
+        "uploaded relation not found in data base after upload"
+    # compare values
+    assert_equal checkrelation.members.length, 1, 
+        "saved relation does not contain exactly one member"
+    assert_equal checkrelation.tags.length, 1, 
+        "saved relation does not contain exactly one tag"
+    assert_equal changeset_id, checkrelation.changeset.id,
+        "saved relation does not belong in the changeset it was assigned to"
+    assert_equal users(:normal_user).id, checkrelation.changeset.user_id, 
+        "saved relation does not belong to user that created it"
+    assert_equal true, checkrelation.visible, 
+        "saved relation is not visible"
+    # ok the relation is there but can we also retrieve it?
+    
+    get :read, :id => relationid
+    assert_response :success
+    
+    
+    ###
+    # create an relation with a node as member, this time test that we don't 
+    # need a role attribute to be included
     nid = current_nodes(:used_node_1).id
-    content "<osm><relation><member type='node' ref='#{nid}' role='some'/>" +
-        "<tag k='test' v='yes' /></relation></osm>"
+    content "<osm><relation changeset='#{changeset_id}'>" +
+      "<member  ref='#{nid}' type='node'/>"+
+      "<tag k='test' v='yes' /></relation></osm>"
     put :create
     # hope for success
     assert_response :success, 
@@ -124,7 +163,9 @@ class RelationControllerTest < Test::Unit::TestCase
         "saved relation does not contain exactly one member"
     assert_equal checkrelation.tags.length, 1, 
         "saved relation does not contain exactly one tag"
-    assert_equal users(:normal_user).id, checkrelation.user_id, 
+    assert_equal changeset_id, checkrelation.changeset.id,
+        "saved relation does not belong in the changeset it was assigned to"
+    assert_equal users(:normal_user).id, checkrelation.changeset.user_id, 
         "saved relation does not belong to user that created it"
     assert_equal true, checkrelation.visible, 
         "saved relation is not visible"
@@ -133,12 +174,14 @@ class RelationControllerTest < Test::Unit::TestCase
     get :read, :id => relationid
     assert_response :success
 
+    ###
     # create an relation with a way and a node as members
     nid = current_nodes(:used_node_1).id
     wid = current_ways(:used_way).id
-    content "<osm><relation><member type='node' ref='#{nid}' role='some'/>" +
-        "<member type='way' ref='#{wid}' role='other'/>" +
-        "<tag k='test' v='yes' /></relation></osm>"
+    content "<osm><relation changeset='#{changeset_id}'>" +
+      "<member type='node' ref='#{nid}' role='some'/>" +
+      "<member type='way' ref='#{wid}' role='other'/>" +
+      "<tag k='test' v='yes' /></relation></osm>"
     put :create
     # hope for success
     assert_response :success, 
@@ -153,7 +196,9 @@ class RelationControllerTest < Test::Unit::TestCase
         "saved relation does not have exactly two members"
     assert_equal checkrelation.tags.length, 1, 
         "saved relation does not contain exactly one tag"
-    assert_equal users(:normal_user).id, checkrelation.user_id, 
+    assert_equal changeset_id, checkrelation.changeset.id,
+        "saved relation does not belong in the changeset it was assigned to"
+    assert_equal users(:normal_user).id, checkrelation.changeset.user_id, 
         "saved relation does not belong to user that created it"
     assert_equal true, checkrelation.visible, 
         "saved relation is not visible"
@@ -170,21 +215,45 @@ class RelationControllerTest < Test::Unit::TestCase
   def test_create_invalid
     basic_authorization "test@openstreetmap.org", "test"
 
+    # put the relation in a dummy fixture changset
+    changeset_id = changesets(:normal_user_first_change).id
+
     # create a relation with non-existing node as member
-    content "<osm><relation><member type='node' ref='0'/><tag k='test' v='yes' /></relation></osm>"
+    content "<osm><relation changeset='#{changeset_id}'>" +
+      "<member type='node' ref='0'/><tag k='test' v='yes' />" +
+      "</relation></osm>"
     put :create
     # expect failure
     assert_response :precondition_failed, 
         "relation upload with invalid node did not return 'precondition failed'"
   end
 
+  # -------------------------------------
+  # Test creating a relation, with some invalid XML
+  # -------------------------------------
+  def test_create_invalid_xml
+    basic_authorization "test@openstreetmap.org", "test"
+    
+    # put the relation in a dummy fixture changeset that works
+    changeset_id = changesets(:normal_user_first_change).id
+    
+    # create some xml that should return an error
+    content "<osm><relation changeset='#{changeset_id}'>" +
+    "<member type='type' ref='#{current_nodes(:used_node_1).id}' role=''/>" +
+    "<tag k='tester' v='yep'/></relation></osm>"
+    put :create
+    # expect failure
+    assert_response :bad_request
+    assert_match(/Cannot parse valid relation from xml string/, @response.body)
+    assert_match(/The type is not allowed only, /, @response.body)
+  end
+  
+  
   # -------------------------------------
   # Test deleting relations.
   # -------------------------------------
   
   def test_delete
-  return true
-
     # first try to delete relation without auth
     delete :delete, :id => current_relations(:visible_relation).id
     assert_response :unauthorized
@@ -192,17 +261,278 @@ class RelationControllerTest < Test::Unit::TestCase
     # now set auth
     basic_authorization("test@openstreetmap.org", "test");  
 
-    # this should work
+    # this shouldn't work, as we should need the payload...
+    delete :delete, :id => current_relations(:visible_relation).id
+    assert_response :bad_request
+
+    # try to delete without specifying a changeset
+    content "<osm><relation id='#{current_relations(:visible_relation).id}'/></osm>"
+    delete :delete, :id => current_relations(:visible_relation).id
+    assert_response :bad_request
+    assert_match(/You are missing the required changeset in the relation/, @response.body)
+
+    # try to delete with an invalid (closed) changeset
+    content update_changeset(current_relations(:visible_relation).to_xml,
+                             changesets(:normal_user_closed_change).id)
+    delete :delete, :id => current_relations(:visible_relation).id
+    assert_response :conflict
+
+    # try to delete with an invalid (non-existent) changeset
+    content update_changeset(current_relations(:visible_relation).to_xml,0)
+    delete :delete, :id => current_relations(:visible_relation).id
+    assert_response :conflict
+
+    # this won't work because the relation is in-use by another relation
+    content(relations(:used_relation).to_xml)
+    delete :delete, :id => current_relations(:used_relation).id
+    assert_response :precondition_failed, 
+       "shouldn't be able to delete a relation used in a relation (#{@response.body})"
+
+    # this should work when we provide the appropriate payload...
+    content(relations(:visible_relation).to_xml)
     delete :delete, :id => current_relations(:visible_relation).id
     assert_response :success
 
+    # valid delete should return the new version number, which should
+    # be greater than the old version number
+    assert @response.body.to_i > current_relations(:visible_relation).version,
+       "delete request should return a new version number for relation"
+
     # this won't work since the relation is already deleted
+    content(relations(:invisible_relation).to_xml)
     delete :delete, :id => current_relations(:invisible_relation).id
     assert_response :gone
 
+    # this works now because the relation which was using this one 
+    # has been deleted.
+    content(relations(:used_relation).to_xml)
+    delete :delete, :id => current_relations(:used_relation).id
+    assert_response :success, 
+       "should be able to delete a relation used in an old relation (#{@response.body})"
+
     # this won't work since the relation never existed
     delete :delete, :id => 0
     assert_response :not_found
   end
 
+  ##
+  # when a relation's tag is modified then it should put the bounding
+  # box of all its members into the changeset.
+  def test_tag_modify_bounding_box
+    # in current fixtures, relation 5 contains nodes 3 and 5 (node 3
+    # indirectly via way 3), so the bbox should be [3,3,5,5].
+    check_changeset_modify([3,3,5,5]) do |changeset_id|
+      # add a tag to an existing relation
+      relation_xml = current_relations(:visible_relation).to_xml
+      relation_element = relation_xml.find("//osm/relation").first
+      new_tag = XML::Node.new("tag")
+      new_tag['k'] = "some_new_tag"
+      new_tag['v'] = "some_new_value"
+      relation_element << new_tag
+      
+      # update changeset ID to point to new changeset
+      update_changeset(relation_xml, changeset_id)
+      
+      # upload the change
+      content relation_xml
+      put :update, :id => current_relations(:visible_relation).id
+      assert_response :success, "can't update relation for tag/bbox test"
+    end
+  end
+
+  ##
+  # add a member to a relation and check the bounding box is only that
+  # element.
+  def test_add_member_bounding_box
+    check_changeset_modify([4,4,4,4]) do |changeset_id|
+      # add node 4 (4,4) to an existing relation
+      relation_xml = current_relations(:visible_relation).to_xml
+      relation_element = relation_xml.find("//osm/relation").first
+      new_member = XML::Node.new("member")
+      new_member['ref'] = current_nodes(:used_node_2).id.to_s
+      new_member['type'] = "node"
+      new_member['role'] = "some_role"
+      relation_element << new_member
+      
+      # update changeset ID to point to new changeset
+      update_changeset(relation_xml, changeset_id)
+      
+      # upload the change
+      content relation_xml
+      put :update, :id => current_relations(:visible_relation).id
+      assert_response :success, "can't update relation for add node/bbox test"
+    end
+  end
+  
+  ##
+  # remove a member from a relation and check the bounding box is 
+  # only that element.
+  def test_remove_member_bounding_box
+    check_changeset_modify([5,5,5,5]) do |changeset_id|
+      # remove node 5 (5,5) from an existing relation
+      relation_xml = current_relations(:visible_relation).to_xml
+      relation_xml.
+        find("//osm/relation/member[@type='node'][@ref='5']").
+        first.remove!
+      
+      # update changeset ID to point to new changeset
+      update_changeset(relation_xml, changeset_id)
+      
+      # upload the change
+      content relation_xml
+      put :update, :id => current_relations(:visible_relation).id
+      assert_response :success, "can't update relation for remove node/bbox test"
+    end
+  end
+  
+  ##
+  # check that relations are ordered
+  def test_relation_member_ordering
+    basic_authorization("test@openstreetmap.org", "test");  
+
+    doc_str = <<OSM
+<osm>
+ <relation changeset='1'>
+  <member ref='1' type='node' role='first'/>
+  <member ref='3' type='node' role='second'/>
+  <member ref='1' type='way' role='third'/>
+  <member ref='3' type='way' role='fourth'/>
+ </relation>
+</osm>
+OSM
+    doc = XML::Parser.string(doc_str).parse
+
+    content doc
+    put :create
+    assert_response :success, "can't create a relation: #{@response.body}"
+    relation_id = @response.body.to_i
+
+    # get it back and check the ordering
+    get :read, :id => relation_id
+    assert_response :success, "can't read back the relation: #{@response.body}"
+    check_ordering(doc, @response.body)
+
+    # insert a member at the front
+    new_member = XML::Node.new "member"
+    new_member['ref'] = 5.to_s
+    new_member['type'] = 'node'
+    new_member['role'] = 'new first'
+    doc.find("//osm/relation").first.child.prev = new_member
+    # update the version, should be 1?
+    doc.find("//osm/relation").first['id'] = relation_id.to_s
+    doc.find("//osm/relation").first['version'] = 1.to_s
+
+    # upload the next version of the relation
+    content doc
+    put :update, :id => relation_id
+    assert_response :success, "can't update relation: #{@response.body}"
+    new_version = @response.body.to_i
+
+    # get it back again and check the ordering again
+    get :read, :id => relation_id
+    assert_response :success, "can't read back the relation: #{@response.body}"
+    check_ordering(doc, @response.body)
+  end
+
+  ## 
+  # check that relations can contain duplicate members
+  def test_relation_member_duplicates
+    basic_authorization("test@openstreetmap.org", "test");  
+
+    doc_str = <<OSM
+<osm>
+ <relation changeset='1'>
+  <member ref='1' type='node' role='forward'/>
+  <member ref='3' type='node' role='forward'/>
+  <member ref='1' type='node' role='forward'/>
+  <member ref='3' type='node' role='forward'/>
+ </relation>
+</osm>
+OSM
+    doc = XML::Parser.string(doc_str).parse
+
+    content doc
+    put :create
+    assert_response :success, "can't create a relation: #{@response.body}"
+    relation_id = @response.body.to_i
+
+    # get it back and check the ordering
+    get :read, :id => relation_id
+    assert_response :success, "can't read back the relation: #{@response.body}"
+    check_ordering(doc, @response.body)
+  end
+
+  # ============================================================
+  # utility functions
+  # ============================================================
+
+  ##
+  # checks that the XML document and the string arguments have
+  # members in the same order.
+  def check_ordering(doc, xml)
+    new_doc = XML::Parser.string(xml).parse
+
+    doc_members = doc.find("//osm/relation/member").collect do |m|
+      [m['ref'].to_i, m['type'].to_sym, m['role']]
+    end
+
+    new_members = new_doc.find("//osm/relation/member").collect do |m|
+      [m['ref'].to_i, m['type'].to_sym, m['role']]
+    end
+
+    doc_members.zip(new_members).each do |d, n|
+      assert_equal d, n, "members are not equal - ordering is wrong? (#{doc}, #{xml})"
+    end
+  end
+
+  ##
+  # create a changeset and yield to the caller to set it up, then assert
+  # that the changeset bounding box is +bbox+.
+  def check_changeset_modify(bbox)
+    basic_authorization("test@openstreetmap.org", "test");  
+
+    # create a new changeset for this operation, so we are assured
+    # that the bounding box will be newly-generated.
+    changeset_id = with_controller(ChangesetController.new) do
+      content "<osm><changeset/></osm>"
+      put :create
+      assert_response :success, "couldn't create changeset for modify test"
+      @response.body.to_i
+    end
+
+    # go back to the block to do the actual modifies
+    yield changeset_id
+
+    # now download the changeset to check its bounding box
+    with_controller(ChangesetController.new) do
+      get :read, :id => changeset_id
+      assert_response :success, "can't re-read changeset for modify test"
+      assert_select "osm>changeset", 1
+      assert_select "osm>changeset[id=#{changeset_id}]", 1
+      assert_select "osm>changeset[min_lon=#{bbox[0].to_f}]", 1
+      assert_select "osm>changeset[min_lat=#{bbox[1].to_f}]", 1
+      assert_select "osm>changeset[max_lon=#{bbox[2].to_f}]", 1
+      assert_select "osm>changeset[max_lat=#{bbox[3].to_f}]", 1
+    end
+  end
+
+  ##
+  # update the changeset_id of a node element
+  def update_changeset(xml, changeset_id)
+    xml_attr_rewrite(xml, 'changeset', changeset_id)
+  end
+
+  ##
+  # update an attribute in the node element
+  def xml_attr_rewrite(xml, name, value)
+    xml.find("//osm/relation").first[name] = value.to_s
+    return xml
+  end
+
+  ##
+  # parse some xml
+  def xml_parse(xml)
+    parser = XML::Parser.string(xml)
+    parser.parse
+  end
 end
diff --git a/test/functional/search_controller_test.rb b/test/functional/search_controller_test.rb
new file mode 100644 (file)
index 0000000..a213253
--- /dev/null
@@ -0,0 +1,8 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class SearchControllerTest < ActionController::TestCase
+  # Replace this with your real tests.
+  def test_truth
+    assert true
+  end
+end
diff --git a/test/functional/site_controller_test.rb b/test/functional/site_controller_test.rb
new file mode 100644 (file)
index 0000000..39a6464
--- /dev/null
@@ -0,0 +1,8 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class SiteControllerTest < ActionController::TestCase
+  # Replace this with your real tests.
+  def test_truth
+    assert true
+  end
+end
diff --git a/test/functional/swf_controller_test.rb b/test/functional/swf_controller_test.rb
new file mode 100644 (file)
index 0000000..862d3a8
--- /dev/null
@@ -0,0 +1,8 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class SwfControllerTest < ActionController::TestCase
+  # Replace this with your real tests.
+  def test_truth
+    assert true
+  end
+end
diff --git a/test/functional/trace_controller_test.rb b/test/functional/trace_controller_test.rb
new file mode 100644 (file)
index 0000000..6b46dbc
--- /dev/null
@@ -0,0 +1,8 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class TraceControllerTest < ActionController::TestCase
+  # Replace this with your real tests.
+  def test_truth
+    assert true
+  end
+end
diff --git a/test/functional/user_controller_test.rb b/test/functional/user_controller_test.rb
new file mode 100644 (file)
index 0000000..d68f1f8
--- /dev/null
@@ -0,0 +1,48 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class UserControllerTest < ActionController::TestCase
+  fixtures :users
+  
+  # The user creation page loads
+  def test_user_create
+    get :new
+    assert_response :success
+    
+    assert_select "html:root", :count => 1 do
+      assert_select "head", :count => 1 do
+        assert_select "title", :text => /create account/, :count => 1
+      end
+      assert_select "body", :count => 1 do
+        assert_select "div#content", :count => 1 do
+          assert_select "form[action='/user/save'][method=post]", :count => 1 do
+            assert_select "input[id=user_email]", :count => 1
+            assert_select "input[id=user_email_confirmation]", :count => 1
+            assert_select "input[id=user_display_name]", :count => 1
+            assert_select "input[id=user_pass_crypt][type=password]", :count => 1
+            assert_select "input[id=user_pass_crypt_confirmation][type=password]", :count => 1
+            assert_select "input[type=submit][value=Signup]", :count => 1
+          end
+        end
+      end
+    end
+  end
+  
+  # Check that the user account page will display and contains some relevant
+  # information for the user
+  def test_view_user_account
+    get :view
+    assert_response :not_found
+    
+    get :view, {:display_name => "test"}
+    assert_response :success
+  end
+  
+  def test_user_api_details
+    get :api_details
+    assert_response :unauthorized
+    
+    basic_authorization(users(:normal_user).email, "test")
+    get :api_details
+    assert_response :success
+  end
+end
index 7ff64b30ebb7a5150674b80ea335409bdd06e23d..714f45c5ddb82456a9eb8fc71bc24cb1d313ecf0 100644 (file)
@@ -1,8 +1,25 @@
 require File.dirname(__FILE__) + '/../test_helper'
 
 class UserPreferenceControllerTest < ActionController::TestCase
-  # Replace this with your real tests.
-  def test_truth
-    assert true
+  fixtures :users, :user_preferences
+  
+  def test_read
+    # first try without auth
+    get :read
+    assert_response :unauthorized, "should be authenticated"
+    
+    # now set the auth
+    basic_authorization("test@openstreetmap.org", "test")
+    
+    get :read
+    assert_response :success
+    assert_select "osm:root" do
+      assert_select "preferences", :count => 1 do
+        assert_select "preference", :count => 2
+        assert_select "preference[k=\"#{user_preferences(:a).k}\"][v=\"#{user_preferences(:a).v}\"]", :count => 1
+        assert_select "preference[k=\"#{user_preferences(:two).k}\"][v=\"#{user_preferences(:two).v}\"]", :count => 1
+      end
+    end
   end
+
 end
index 933dfb542edc9be778923735378a9052439b0de0..9c32ae90400508d7d3654fec1021833fd4ae6627 100644 (file)
@@ -1,26 +1,9 @@
 require File.dirname(__FILE__) + '/../test_helper'
 require 'way_controller'
 
-# Re-raise errors caught by the controller.
-class WayController; def rescue_action(e) raise e end; end
-
-class WayControllerTest < Test::Unit::TestCase
+class WayControllerTest < ActionController::TestCase
   api_fixtures
 
-  def setup
-    @controller = WayController.new
-    @request    = ActionController::TestRequest.new
-    @response   = ActionController::TestResponse.new
-  end
-
-  def basic_authorization(user, pass)
-    @request.env["HTTP_AUTHORIZATION"] = "Basic %s" % Base64.encode64("#{user}:#{pass}")
-  end
-
-  def content(c)
-    @request.env["RAW_POST_DATA"] = c
-  end
-
   # -------------------------------------
   # Test reading ways.
   # -------------------------------------
@@ -37,18 +20,35 @@ class WayControllerTest < Test::Unit::TestCase
     # check chat a non-existent way is not returned
     get :read, :id => 0
     assert_response :not_found
+  end
 
-    # check the "ways for node" mode
-    get :ways_for_node, :id => current_nodes(:used_node_1).id
-    assert_response :success
-    # FIXME check whether this contains the stuff we want!
-    print @response.body
+  ##
+  # check the "full" mode
+  def test_full
+    Way.find(:all).each do |way|
+      get :full, :id => way.id
 
-    # check the "full" mode
-    get :full, :id => current_ways(:visible_way).id
-    assert_response :success
-    # FIXME check whether this contains the stuff we want!
-    print @response.body
+      # full call should say "gone" for non-visible ways...
+      unless way.visible
+        assert_response :gone
+        next
+      end
+
+      # otherwise it should say success
+      assert_response :success
+      
+      # Check the way is correctly returned
+      assert_select "osm way[id=#{way.id}][version=#{way.version}][visible=#{way.visible}]", 1
+      
+      # check that each node in the way appears once in the output as a 
+      # reference and as the node element. note the slightly dodgy assumption
+      # that nodes appear only once. this is currently the case with the
+      # fixtures, but it doesn't have to be.
+      way.nodes.each do |n|
+        assert_select "osm way nd[ref=#{n.id}]", 1
+        assert_select "osm node[id=#{n.id}][version=#{n.version}][lat=#{n.lat}][lon=#{n.lon}]", 1
+      end
+    end
   end
 
   # -------------------------------------
@@ -56,12 +56,37 @@ class WayControllerTest < Test::Unit::TestCase
   # -------------------------------------
 
   def test_create
+    ## First check that it fails when creating a way using a non-public user
+    nid1 = current_nodes(:used_node_1).id
+    nid2 = current_nodes(:used_node_2).id
+    basic_authorization users(:normal_user).email, "test"
+
+    # use the first user's open changeset
+    changeset_id = changesets(:normal_user_first_change).id
+    
+    # create a way with pre-existing nodes
+    content "<osm><way changeset='#{changeset_id}'>" +
+      "<nd ref='#{nid1}'/><nd ref='#{nid2}'/>" + 
+      "<tag k='test' v='yes' /></way></osm>"
+    put :create
+    # hope for success
+    assert_response :forbidden, 
+        "way upload did not return success status"
+    # read id of created way and search for it
+    wayid = @response.body
+
+    ## Now use a public user
     nid1 = current_nodes(:used_node_1).id
     nid2 = current_nodes(:used_node_2).id
-    basic_authorization "test@openstreetmap.org", "test"
+    basic_authorization users(:public_user).email, "test"
 
+    # use the first user's open changeset
+    changeset_id = changesets(:public_user_first_change).id
+    
     # create a way with pre-existing nodes
-    content "<osm><way><nd ref='#{nid1}'/><nd ref='#{nid2}'/><tag k='test' v='yes' /></way></osm>"
+    content "<osm><way changeset='#{changeset_id}'>" +
+      "<nd ref='#{nid1}'/><nd ref='#{nid2}'/>" + 
+      "<tag k='test' v='yes' /></way></osm>"
     put :create
     # hope for success
     assert_response :success, 
@@ -78,7 +103,9 @@ class WayControllerTest < Test::Unit::TestCase
         "saved way does not contain the right node on pos 0"
     assert_equal checkway.nds[1], nid2, 
         "saved way does not contain the right node on pos 1"
-    assert_equal users(:normal_user).id, checkway.user_id, 
+    assert_equal checkway.changeset_id, changeset_id,
+        "saved way does not belong to the correct changeset"
+    assert_equal users(:public_user).id, checkway.changeset.user_id, 
         "saved way does not belong to user that created it"
     assert_equal true, checkway.visible, 
         "saved way is not visible"
@@ -89,21 +116,70 @@ class WayControllerTest < Test::Unit::TestCase
   # -------------------------------------
 
   def test_create_invalid
-    basic_authorization "test@openstreetmap.org", "test"
+    ## First test with a private user to make sure that they are not authorized
+    basic_authorization users(:normal_user).email, "test"
+
+    # use the first user's open changeset
+    open_changeset_id = changesets(:normal_user_first_change).id
+    closed_changeset_id = changesets(:normal_user_closed_change).id
+    nid1 = current_nodes(:used_node_1).id
+
+    # create a way with non-existing node
+    content "<osm><way changeset='#{open_changeset_id}'>" + 
+      "<nd ref='0'/><tag k='test' v='yes' /></way></osm>"
+    put :create
+    # expect failure
+    assert_response :forbidden, 
+    "way upload with invalid node using a private user did not return 'forbidden'"
+
+    # create a way with no nodes
+    content "<osm><way changeset='#{open_changeset_id}'>" +
+      "<tag k='test' v='yes' /></way></osm>"
+    put :create
+    # expect failure
+    assert_response :forbidden, 
+    "way upload with no node using a private userdid not return 'forbidden'"
+
+    # create a way inside a closed changeset
+    content "<osm><way changeset='#{closed_changeset_id}'>" +
+      "<nd ref='#{nid1}'/></way></osm>"
+    put :create
+    # expect failure
+    assert_response :forbidden, 
+    "way upload to closed changeset with a private user did not return 'forbidden'"    
+
+    
+    ## Now test with a public user
+    basic_authorization users(:public_user).email, "test"
+
+    # use the first user's open changeset
+    open_changeset_id = changesets(:public_user_first_change).id
+    closed_changeset_id = changesets(:public_user_closed_change).id
+    nid1 = current_nodes(:used_node_1).id
 
     # create a way with non-existing node
-    content "<osm><way><nd ref='0'/><tag k='test' v='yes' /></way></osm>"
+    content "<osm><way changeset='#{open_changeset_id}'>" + 
+      "<nd ref='0'/><tag k='test' v='yes' /></way></osm>"
     put :create
     # expect failure
     assert_response :precondition_failed, 
         "way upload with invalid node did not return 'precondition failed'"
 
     # create a way with no nodes
-    content "<osm><way><tag k='test' v='yes' /></way></osm>"
+    content "<osm><way changeset='#{open_changeset_id}'>" +
+      "<tag k='test' v='yes' /></way></osm>"
     put :create
     # expect failure
     assert_response :precondition_failed, 
         "way upload with no node did not return 'precondition failed'"
+
+    # create a way inside a closed changeset
+    content "<osm><way changeset='#{closed_changeset_id}'>" +
+      "<nd ref='#{nid1}'/></way></osm>"
+    put :create
+    # expect failure
+    assert_response :conflict, 
+        "way upload to closed changeset did not return 'conflict'"    
   end
 
   # -------------------------------------
@@ -111,25 +187,282 @@ class WayControllerTest < Test::Unit::TestCase
   # -------------------------------------
   
   def test_delete
-
     # first try to delete way without auth
     delete :delete, :id => current_ways(:visible_way).id
     assert_response :unauthorized
 
+    # now set auth using the private user
+    basic_authorization(users(:normal_user).email, "test");  
+
+    # this shouldn't work as with the 0.6 api we need pay load to delete
+    delete :delete, :id => current_ways(:visible_way).id
+    assert_response :forbidden
+    
+    # Now try without having a changeset
+    content "<osm><way id='#{current_ways(:visible_way).id}'></osm>"
+    delete :delete, :id => current_ways(:visible_way).id
+    assert_response :forbidden
+    
+    # try to delete with an invalid (closed) changeset
+    content update_changeset(current_ways(:visible_way).to_xml,
+                             changesets(:normal_user_closed_change).id)
+    delete :delete, :id => current_ways(:visible_way).id
+    assert_response :forbidden
+
+    # try to delete with an invalid (non-existent) changeset
+    content update_changeset(current_ways(:visible_way).to_xml,0)
+    delete :delete, :id => current_ways(:visible_way).id
+    assert_response :forbidden
+
+    # Now try with a valid changeset
+    content current_ways(:visible_way).to_xml
+    delete :delete, :id => current_ways(:visible_way).id
+    assert_response :forbidden
+
+    # check the returned value - should be the new version number
+    # valid delete should return the new version number, which should
+    # be greater than the old version number
+    #assert @response.body.to_i > current_ways(:visible_way).version,
+    #   "delete request should return a new version number for way"
+
+    # this won't work since the way is already deleted
+    content current_ways(:invisible_way).to_xml
+    delete :delete, :id => current_ways(:invisible_way).id
+    assert_response :forbidden
+
+    # this shouldn't work as the way is used in a relation
+    content current_ways(:used_way).to_xml
+    delete :delete, :id => current_ways(:used_way).id
+    assert_response :forbidden, 
+    "shouldn't be able to delete a way used in a relation (#{@response.body}), when done by a private user"
+
+    # this won't work since the way never existed
+    delete :delete, :id => 0
+    assert_response :forbidden
+
+    
+    ### Now check with a public user
     # now set auth
-    basic_authorization("test@openstreetmap.org", "test");  
+    basic_authorization(users(:public_user).email, "test");  
+
+    # this shouldn't work as with the 0.6 api we need pay load to delete
+    delete :delete, :id => current_ways(:visible_way).id
+    assert_response :bad_request
+    
+    # Now try without having a changeset
+    content "<osm><way id='#{current_ways(:visible_way).id}'></osm>"
+    delete :delete, :id => current_ways(:visible_way).id
+    assert_response :bad_request
+    
+    # try to delete with an invalid (closed) changeset
+    content update_changeset(current_ways(:visible_way).to_xml,
+                             changesets(:public_user_closed_change).id)
+    delete :delete, :id => current_ways(:visible_way).id
+    assert_response :conflict
+
+    # try to delete with an invalid (non-existent) changeset
+    content update_changeset(current_ways(:visible_way).to_xml,0)
+    delete :delete, :id => current_ways(:visible_way).id
+    assert_response :conflict
 
-    # this should work
+    # Now try with a valid changeset
+    content current_ways(:visible_way).to_xml
     delete :delete, :id => current_ways(:visible_way).id
     assert_response :success
 
+    # check the returned value - should be the new version number
+    # valid delete should return the new version number, which should
+    # be greater than the old version number
+    assert @response.body.to_i > current_ways(:visible_way).version,
+       "delete request should return a new version number for way"
+
     # this won't work since the way is already deleted
+    content current_ways(:invisible_way).to_xml
     delete :delete, :id => current_ways(:invisible_way).id
     assert_response :gone
 
+    # this shouldn't work as the way is used in a relation
+    content current_ways(:used_way).to_xml
+    delete :delete, :id => current_ways(:used_way).id
+    assert_response :precondition_failed, 
+       "shouldn't be able to delete a way used in a relation (#{@response.body})"
+
     # this won't work since the way never existed
     delete :delete, :id => 0
     assert_response :not_found
   end
 
+  # ------------------------------------------------------------
+  # test tags handling
+  # ------------------------------------------------------------
+
+  ##
+  # Try adding a duplicate of an existing tag to a way
+  def test_add_duplicate_tags
+    ## Try with the non-public user
+    # setup auth
+    basic_authorization(users(:normal_user).email, "test")
+
+    # add an identical tag to the way
+    tag_xml = XML::Node.new("tag")
+    tag_xml['k'] = current_way_tags(:t1).k
+    tag_xml['v'] = current_way_tags(:t1).v
+
+    # add the tag into the existing xml
+    way_xml = current_ways(:visible_way).to_xml
+    way_xml.find("//osm/way").first << tag_xml
+
+    # try and upload it
+    content way_xml
+    put :update, :id => current_ways(:visible_way).id
+    assert_response :forbidden, 
+    "adding a duplicate tag to a way for a non-public should fail with 'forbidden'"
+
+    ## Now try with the public user
+    # setup auth
+    basic_authorization(users(:public_user).email, "test")
+
+    # add an identical tag to the way
+    tag_xml = XML::Node.new("tag")
+    tag_xml['k'] = current_way_tags(:t1).k
+    tag_xml['v'] = current_way_tags(:t1).v
+
+    # add the tag into the existing xml
+    way_xml = current_ways(:visible_way).to_xml
+    way_xml.find("//osm/way").first << tag_xml
+
+    # try and upload it
+    content way_xml
+    put :update, :id => current_ways(:visible_way).id
+    assert_response :bad_request, 
+       "adding a duplicate tag to a way should fail with 'bad request'"
+    assert_equal "Element way/#{current_ways(:visible_way).id} has duplicate tags with key #{current_way_tags(:t1).k}.", @response.body
+  end
+
+  ##
+  # Try adding a new duplicate tags to a way
+  def test_new_duplicate_tags
+    ## First test with the non-public user so should be rejected
+    # setup auth
+    basic_authorization(users(:normal_user).email, "test")
+
+    # create duplicate tag
+    tag_xml = XML::Node.new("tag")
+    tag_xml['k'] = "i_am_a_duplicate"
+    tag_xml['v'] = "foobar"
+
+    # add the tag into the existing xml
+    way_xml = current_ways(:visible_way).to_xml
+
+    # add two copies of the tag
+    way_xml.find("//osm/way").first << tag_xml.copy(true) << tag_xml
+
+    # try and upload it
+    content way_xml
+    put :update, :id => current_ways(:visible_way).id
+    assert_response :forbidden, 
+    "adding new duplicate tags to a way using a non-public user should fail with 'forbidden'"
+    
+    ## Now test with the public user
+    # setup auth
+    basic_authorization(users(:public_user).email, "test")
+
+    # create duplicate tag
+    tag_xml = XML::Node.new("tag")
+    tag_xml['k'] = "i_am_a_duplicate"
+    tag_xml['v'] = "foobar"
+
+    # add the tag into the existing xml
+    way_xml = current_ways(:visible_way).to_xml
+
+    # add two copies of the tag
+    way_xml.find("//osm/way").first << tag_xml.copy(true) << tag_xml
+
+    # try and upload it
+    content way_xml
+    put :update, :id => current_ways(:visible_way).id
+    assert_response :bad_request, 
+       "adding new duplicate tags to a way should fail with 'bad request'"
+    assert_equal "Element way/#{current_ways(:visible_way).id} has duplicate tags with key i_am_a_duplicate.", @response.body
+    
+  end
+
+  ##
+  # Try adding a new duplicate tags to a way.
+  # But be a bit subtle - use unicode decoding ambiguities to use different
+  # binary strings which have the same decoding.
+  def test_invalid_duplicate_tags
+    ## First make sure that you can't with a non-public user
+    # setup auth
+    basic_authorization(users(:normal_user).email, "test")
+
+    # add the tag into the existing xml
+    way_str = "<osm><way changeset='1'>"
+    way_str << "<tag k='addr:housenumber' v='1'/>"
+    way_str << "<tag k='addr:housenumber' v='2'/>"
+    way_str << "</way></osm>";
+
+    # try and upload it
+    content way_str
+    put :create
+    assert_response :forbidden, 
+    "adding new duplicate tags to a way with a non-public user should fail with 'forbidden'"
+    
+    ## Now do it with a public user
+    # setup auth
+    basic_authorization(users(:public_user).email, "test")
+
+    # add the tag into the existing xml
+    way_str = "<osm><way changeset='1'>"
+    way_str << "<tag k='addr:housenumber' v='1'/>"
+    way_str << "<tag k='addr:housenumber' v='2'/>"
+    way_str << "</way></osm>";
+
+    # try and upload it
+    content way_str
+    put :create
+    assert_response :bad_request, 
+    "adding new duplicate tags to a way should fail with 'bad request'"
+    assert_equal "Element way/ has duplicate tags with key addr:housenumber.", @response.body
+  end
+
+  ##
+  # test that a call to ways_for_node returns all ways that contain the node
+  # and none that don't.
+  def test_ways_for_node
+    # in current fixtures ways 1 and 3 all use node 3. ways 2 and 4 
+    # *used* to use it but doesn't.
+    get :ways_for_node, :id => current_nodes(:used_node_1).id
+    assert_response :success
+    ways_xml = XML::Parser.string(@response.body).parse
+    assert_not_nil ways_xml, "failed to parse ways_for_node response"
+
+    # check that the set of IDs match expectations
+    expected_way_ids = [ current_ways(:visible_way).id,
+                         current_ways(:used_way).id
+                       ]
+    found_way_ids = ways_xml.find("//osm/way").collect { |w| w["id"].to_i }
+    assert_equal expected_way_ids, found_way_ids,
+      "expected ways for node #{current_nodes(:used_node_1).id} did not match found"
+    
+    # check the full ways to ensure we're not missing anything
+    expected_way_ids.each do |id|
+      way_xml = ways_xml.find("//osm/way[@id=#{id}]").first
+      assert_ways_are_equal(Way.find(id),
+                            Way.from_xml_node(way_xml))
+    end
+  end
+
+  ##
+  # update the changeset_id of a node element
+  def update_changeset(xml, changeset_id)
+    xml_attr_rewrite(xml, 'changeset', changeset_id)
+  end
+
+  ##
+  # update an attribute in the node element
+  def xml_attr_rewrite(xml, name, value)
+    xml.find("//osm/way").first[name] = value.to_s
+    return xml
+  end
 end
diff --git a/test/integration/user_creation_test.rb b/test/integration/user_creation_test.rb
new file mode 100644 (file)
index 0000000..5f664b5
--- /dev/null
@@ -0,0 +1,10 @@
+require 'test_helper'
+
+class UserCreationTest < ActionController::IntegrationTest
+  fixtures :users
+
+  def test_create_user_duplicate
+    get '/user/new'
+    assert_response :success
+  end
+end
diff --git a/test/integration/user_diaries_test.rb b/test/integration/user_diaries_test.rb
new file mode 100644 (file)
index 0000000..98ea045
--- /dev/null
@@ -0,0 +1,52 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class UserDiariesTest < ActionController::IntegrationTest
+  fixtures :users, :diary_entries
+
+  # Test the creation of a diary entry, making sure that you are redirected to 
+  # login page when not logged in
+  def test_showing_create_diary_entry
+    get_via_redirect '/user/test/diary/new'
+    # We should now be at the login page
+    assert_response :success
+    assert_template 'user/login'
+    # We can now login
+    post  '/login', {'user[email]' => "test@openstreetmap.org", 'user[password]' => "test", :referer => '/user/test/diary/new'}
+    assert_response :redirect
+    #print @response.body
+    # Check that there is some payload alerting the user to the redirect
+    # and allowing them to get to the page they are being directed to
+    assert_select "html:root" do
+      assert_select "body" do
+        assert_select "a[href='http://www.example.com/user/test/diary/new']"
+      end
+    end
+    # Required due to a bug in the rails testing framework
+    # http://markmail.org/message/wnslvi5xv5moqg7g
+    @html_document = nil
+    follow_redirect!
+    
+    assert_response :success
+    assert_template 'diary_entry/edit'
+    #print @response.body
+    #print @html_document.to_yaml
+
+    # We will make sure that the form exists here, full 
+    # assert testing of the full form should be done in the
+    # functional tests rather than this integration test
+    # There are some things that are specific to the integratio
+    # that need to be tested, which can't be tested in the functional tests
+    assert_select "html:root" do
+      assert_select "body" do
+        assert_select "div#content" do
+          assert_select "h1", "New diary entry" 
+          assert_select "form[action='/user/#{users(:normal_user).display_name}/diary/new']" do
+            assert_select "input[id=diary_entry_title]"
+          end
+        end
+      end
+    end
+    
+    
+  end
+end
index b1d7a8fcc280dec9ac39ddbbad7c90de4adf0c2c..8c270c6820e34fbe0673295ffb0e2d8e2f0fb9c7 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
@@ -24,33 +25,102 @@ class Test::Unit::TestCase
   # then set this back to true.
   self.use_instantiated_fixtures  = false
 
+
   # Load standard fixtures needed to test API methods
   def self.api_fixtures
-    fixtures :users
+    #print "setting up the api_fixtures"
+    fixtures :users, :changesets, :changeset_tags
 
     fixtures :current_nodes, :nodes
-    set_fixture_class :current_nodes => :Node
-    set_fixture_class :nodes => :OldNode
-
-    fixtures :current_ways, :current_way_nodes, :current_way_tags
-    set_fixture_class :current_ways => :Way
-    set_fixture_class :current_way_nodes => :WayNode
-    set_fixture_class :current_way_tags => :WayTag
-
-    fixtures :ways, :way_nodes, :way_tags
-    set_fixture_class :ways => :OldWay
-    set_fixture_class :way_nodes => :OldWayNode
-    set_fixture_class :way_tags => :OldWayTag
-
-    fixtures :current_relations, :current_relation_members, :current_relation_tags
-    set_fixture_class :current_relations => :Relation
-    set_fixture_class :current_relation_members => :RelationMember
-    set_fixture_class :current_relation_tags => :RelationTag
-
-    fixtures :relations, :relation_members, :relation_tags
-    set_fixture_class :relations => :OldRelation
-    set_fixture_class :relation_members => :OldRelationMember
-    set_fixture_class :relation_tags => :OldRelationTag
+    set_fixture_class :current_nodes => 'Node'
+    set_fixture_class :nodes => 'OldNode'
+
+    fixtures  :current_node_tags,:node_tags
+    set_fixture_class :current_node_tags => 'NodeTag'
+    set_fixture_class :node_tags => 'OldNodeTag'
+
+    fixtures :current_ways
+    set_fixture_class :current_ways => 'Way'
+
+    fixtures :current_way_nodes, :current_way_tags
+    set_fixture_class :current_way_nodes => 'WayNode'
+    set_fixture_class :current_way_tags => 'WayTag'
+
+    fixtures :ways
+    set_fixture_class :ways => 'OldWay'
+
+    fixtures :way_nodes, :way_tags
+    set_fixture_class :way_nodes => 'OldWayNode'
+    set_fixture_class :way_tags => 'OldWayTag'
+
+    fixtures :current_relations
+    set_fixture_class :current_relations => 'Relation'
+
+    fixtures :current_relation_members, :current_relation_tags
+    set_fixture_class :current_relation_members => 'RelationMember'
+    set_fixture_class :current_relation_tags => 'RelationTag'
+
+    fixtures :relations
+    set_fixture_class :relations => 'OldRelation'
+
+    fixtures :relation_members, :relation_tags
+    set_fixture_class :relation_members => 'OldRelationMember'
+    set_fixture_class :relation_tags => 'OldRelationTag'
+    
+    fixtures :gpx_files, :gps_points, :gpx_file_tags
+    set_fixture_class :gpx_files => 'Trace'
+    set_fixture_class :gps_points => 'Tracepoint'
+    set_fixture_class :gpx_file_tags => 'Tracetag'
+  end
+
+  ##
+  # takes a block which is executed in the context of a different 
+  # ActionController instance. this is used so that code can call methods
+  # on the node controller whilst testing the old_node controller.
+  def with_controller(new_controller)
+    controller_save = @controller
+    begin
+      @controller = new_controller
+      yield
+    ensure
+      @controller = controller_save
+    end
+  end
+
+  ##
+  # for some reason assert_equal a, b fails when the ways are actually
+  # equal, so this method manually checks the fields...
+  def assert_ways_are_equal(a, b)
+    assert_not_nil a, "first way is not allowed to be nil"
+    assert_not_nil b, "second way #{a.id} is not allowed to be nil"
+    assert_equal a.id, b.id, "way IDs"
+    assert_equal a.changeset_id, b.changeset_id, "changeset ID on way #{a.id}"
+    assert_equal a.visible, b.visible, "visible on way #{a.id}, #{a.visible.inspect} != #{b.visible.inspect}"
+    assert_equal a.version, b.version, "version on way #{a.id}"
+    assert_equal a.tags, b.tags, "tags on way #{a.id}"
+    assert_equal a.nds, b.nds, "node references on way #{a.id}"
+  end
+
+  ##
+  # for some reason a==b is false, but there doesn't seem to be any 
+  # difference between the nodes, so i'm checking all the attributes 
+  # manually and blaming it on ActiveRecord
+  def assert_nodes_are_equal(a, b)
+    assert_equal a.id, b.id, "node IDs"
+    assert_equal a.latitude, b.latitude, "latitude on node #{a.id}"
+    assert_equal a.longitude, b.longitude, "longitude on node #{a.id}"
+    assert_equal a.changeset_id, b.changeset_id, "changeset ID on node #{a.id}"
+    assert_equal a.visible, b.visible, "visible on node #{a.id}"
+    assert_equal a.version, b.version, "version on node #{a.id}"
+    assert_equal a.tags, b.tags, "tags on node #{a.id}"
+  end
+
+  def basic_authorization(user, pass)
+    @request.env["HTTP_AUTHORIZATION"] = "Basic %s" % Base64.encode64("#{user}:#{pass}")
+  end
+
+  def content(c)
+    @request.env["RAW_POST_DATA"] = c.to_s
   end
 
   # Add more helper methods to be used by all tests here...
diff --git a/test/unit/changeset_tag_test.rb b/test/unit/changeset_tag_test.rb
new file mode 100644 (file)
index 0000000..c89f426
--- /dev/null
@@ -0,0 +1,70 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class ChangesetTagTest < Test::Unit::TestCase
+  api_fixtures
+
+  def test_changeset_tag_count
+    assert_equal 1, ChangesetTag.count
+  end
+  
+  def test_length_key_valid
+    key = "k"
+    (0..255).each do |i|
+      tag = ChangesetTag.new
+      tag.id = 1
+      tag.k = key*i
+      tag.v = "v"
+      assert_valid tag
+    end
+  end
+  
+  def test_length_value_valid
+    val = "v"
+    (0..255).each do |i|
+      tag = ChangesetTag.new
+      tag.id = 1
+      tag.k = "k"
+      tag.v = val*i
+      assert_valid tag
+    end
+  end
+  
+  def test_length_key_invalid
+    ["k"*256].each do |k|
+      tag = ChangesetTag.new
+      tag.id = 1
+      tag.k = k
+      tag.v = "v"
+      assert !tag.valid?, "Key #{k} should be too long"
+      assert tag.errors.invalid?(:k)
+    end
+  end
+  
+  def test_length_value_invalid
+    ["v"*256].each do |v|
+      tag = ChangesetTag.new
+      tag.id = 1
+      tag.k = "k"
+      tag.v = v
+      assert !tag.valid?, "Value #{v} should be too long"
+      assert tag.errors.invalid?(:v)
+    end
+  end
+  
+  def test_empty_tag_invalid
+    tag = ChangesetTag.new
+    assert !tag.valid?, "Empty tag should be invalid"
+    assert tag.errors.invalid?(:id)
+  end
+  
+  def test_uniqueness
+    tag = ChangesetTag.new
+    tag.id = changeset_tags(:changeset_1_tag_1).id
+    tag.k = changeset_tags(:changeset_1_tag_1).k
+    tag.v = changeset_tags(:changeset_1_tag_1).v
+    assert tag.new_record?
+    assert !tag.valid?
+    assert_raise(ActiveRecord::RecordInvalid) {tag.save!}
+    assert tag.new_record?
+  end
+end
diff --git a/test/unit/changeset_test.rb b/test/unit/changeset_test.rb
new file mode 100644 (file)
index 0000000..bc7abe0
--- /dev/null
@@ -0,0 +1,10 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class ChangesetTest < Test::Unit::TestCase
+  api_fixtures
+  
+  def test_changeset_count
+    assert_equal 7, Changeset.count
+  end
+  
+end
diff --git a/test/unit/diary_comment_test.rb b/test/unit/diary_comment_test.rb
new file mode 100644 (file)
index 0000000..8e2bbf3
--- /dev/null
@@ -0,0 +1,11 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class DiaryCommentTest < Test::Unit::TestCase
+  api_fixtures
+  fixtures :diary_comments
+  
+  def test_diary_comment_count
+    assert_equal 1, DiaryComment.count
+  end
+  
+end
diff --git a/test/unit/diary_entry_test.rb b/test/unit/diary_entry_test.rb
new file mode 100644 (file)
index 0000000..e28e03a
--- /dev/null
@@ -0,0 +1,33 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class DiaryEntryTest < Test::Unit::TestCase
+  api_fixtures
+  fixtures :diary_entries
+  
+  def test_diary_entry_count
+    assert_equal 2, DiaryEntry.count
+  end
+  
+  def test_diary_entry_validations
+    diary_entry_valid({})
+    diary_entry_valid({:title => ''}, false)
+    diary_entry_valid({:title => 'a'*255})
+    diary_entry_valid({:title => 'a'*256}, false)
+    diary_entry_valid({:body => ''}, false)
+    diary_entry_valid({:latitude => 90})
+    diary_entry_valid({:latitude => 90.00001}, false)
+    diary_entry_valid({:latitude => -90})
+    diary_entry_valid({:latitude => -90.00001}, false)
+    diary_entry_valid({:longitude => 180})
+    diary_entry_valid({:longitude => 180.00001}, false)
+    diary_entry_valid({:longitude => -180})
+    diary_entry_valid({:longitude => -180.00001}, false)
+  end
+  
+  def diary_entry_valid(attrs, result = true)
+    entry = diary_entries(:normal_user_entry_1).clone
+    entry.attributes = attrs
+    assert_equal result, entry.valid?
+  end
+  
+end
diff --git a/test/unit/friend_test.rb b/test/unit/friend_test.rb
new file mode 100644 (file)
index 0000000..8c93146
--- /dev/null
@@ -0,0 +1,11 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class FriendTest < Test::Unit::TestCase
+  api_fixtures
+  fixtures :friends
+
+  def test_friend_count
+    assert_equal 1, Friend.count
+  end
+  
+end
index 8804fe003b6c54f794acfa8f56aaec792d44b08b..a732aa58633d67cc1ad98fca66cbae1a48b3d0ed 100644 (file)
@@ -1,10 +1,97 @@
 require File.dirname(__FILE__) + '/../test_helper'
 
 class MessageTest < Test::Unit::TestCase
+  api_fixtures
   fixtures :messages
 
-  # Replace this with your real tests.
-  def test_truth
-    assert true
+  EURO = "\xe2\x82\xac" #euro symbol
+
+  # This needs to be updated when new fixtures are added
+  # or removed.
+  def test_check_message_count
+    assert_equal 2, Message.count
+  end
+
+  def test_check_empty_message_fails
+    message = Message.new
+    assert !message.valid?
+    assert message.errors.invalid?(:title)
+    assert message.errors.invalid?(:body)
+    assert message.errors.invalid?(:sent_on)
+    assert true, message.message_read
+  end
+  
+  def test_validating_msgs
+    message = messages(:one)
+    assert message.valid?
+    massage = messages(:two)
+    assert message.valid?
+  end
+  
+  def test_invalid_send_recipient
+    message = messages(:one)
+    message.sender = nil
+    message.recipient = nil
+    assert !message.valid?
+
+    assert_raise(ActiveRecord::RecordNotFound) { User.find(0) }
+    message.from_user_id = 0
+    message.to_user_id = 0
+    assert_raise(ActiveRecord::RecordInvalid) {message.save!}
+  end
+
+  def test_utf8_roundtrip
+    (1..255).each do |i|
+      assert_message_ok('c', i)
+      assert_message_ok(EURO, i)
+    end
+  end
+
+  def test_length_oversize
+    assert_raise(ActiveRecord::RecordInvalid) { make_message('c', 256).save! }
+    assert_raise(ActiveRecord::RecordInvalid) { make_message(EURO, 256).save! }
   end
+
+  def test_invalid_utf8
+    # See e.g http://en.wikipedia.org/wiki/UTF-8 for byte sequences
+    # FIXME - Invalid Unicode characters can still be encoded into "valid" utf-8 byte sequences - maybe check this too?
+    invalid_sequences = ["\xC0",         # always invalid utf8
+                         "\xC2\x4a",     # 2-byte multibyte identifier, followed by plain ASCII
+                         "\xC2\xC2",     # 2-byte multibyte identifier, followed by another one
+                         "\x4a\x82",     # plain ASCII, followed by multibyte continuation
+                         "\x82\x82",     # multibyte continuations without multibyte identifier
+                         "\xe1\x82\x4a", # three-byte identifier, contination and (incorrectly) plain ASCII
+                        ]
+    invalid_sequences.each do |char|
+      begin
+        # create a message and save to the database
+        msg = make_message(char, 1)
+        # if the save throws, thats fine and the test should pass, as we're
+        # only testing invalid sequences anyway.
+        msg.save! 
+
+        # get the saved message back and check that it is identical - i.e: 
+        # its OK to accept invalid UTF-8 as long as we return it unmodified.
+        db_msg = msg.class.find(msg.id)
+        assert_equal char, db_msg.title, "Database silently truncated message title"
+
+      rescue ActiveRecord::RecordInvalid
+        # because we only test invalid sequences it is OK to barf on them
+      end
+    end
+  end  
+
+  def make_message(char, count)
+    message = messages(:one)
+    message.title = char * count
+    return message
+  end
+
+  def assert_message_ok(char, count)
+    message = make_message(char, count)
+    assert message.save!
+    response = message.class.find(message.id) # stand by for some über-generalisation...
+    assert_equal char * count, response.title, "message with #{count} #{char} chars (i.e. #{char.length*count} bytes) fails"
+  end
+
 end
diff --git a/test/unit/node_tag_test.rb b/test/unit/node_tag_test.rb
new file mode 100644 (file)
index 0000000..bd7d9f6
--- /dev/null
@@ -0,0 +1,80 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class NodeTagTest < Test::Unit::TestCase
+  api_fixtures
+  
+  def test_tag_count
+    assert_equal 6, NodeTag.count
+    node_tag_count(:visible_node, 1)
+    node_tag_count(:invisible_node, 1)
+    node_tag_count(:used_node_1, 1)
+    node_tag_count(:used_node_2, 1)
+    node_tag_count(:node_with_versions, 2)
+  end
+  
+  def node_tag_count (node, count)
+    nod = current_nodes(node)
+    assert_equal count, nod.node_tags.count
+  end
+  
+  def test_length_key_valid
+    key = "k"
+    (0..255).each do |i|
+      tag = NodeTag.new
+      tag.id = current_node_tags(:t1).id
+      tag.k = key*i
+      tag.v = "v"
+      assert_valid tag
+    end
+  end
+  
+  def test_length_value_valid
+    val = "v"
+    (0..255).each do |i|
+      tag = NodeTag.new
+      tag.id = current_node_tags(:t1).id
+      tag.k = "k"
+      tag.v = val*i
+      assert_valid tag
+    end
+  end
+  
+  def test_length_key_invalid
+    ["k"*256].each do |i|
+      tag = NodeTag.new
+      tag.id = current_node_tags(:t1).id
+      tag.k = i
+      tag.v = "v"
+      assert !tag.valid?, "Key should be too long"
+      assert tag.errors.invalid?(:k)
+    end
+  end
+  
+  def test_length_value_invalid
+    ["k"*256].each do |i|
+      tag = NodeTag.new
+      tag.id = current_node_tags(:t1).id
+      tag.k = "k"
+      tag.v = i
+      assert !tag.valid?, "Value should be too long"
+      assert tag.errors.invalid?(:v)
+    end
+  end
+  
+  def test_empty_node_tag_invalid
+    tag = NodeTag.new
+    assert !tag.valid?, "Empty tag should be invalid"
+    assert tag.errors.invalid?(:id)
+  end
+  
+  def test_uniqueness
+    tag = NodeTag.new
+    tag.id = current_node_tags(:t1).id
+    tag.k = current_node_tags(:t1).k
+    tag.v = current_node_tags(:t1).v
+    assert tag.new_record?
+    assert !tag.valid?
+    assert_raise(ActiveRecord::RecordInvalid) {tag.save!}
+    assert tag.new_record?
+  end
+end
index 95321b5cf0cb6d2e8803c484de0069163faa7c41..51cf82d123d1670cb0ef7f1704ec325d4831166d 100644 (file)
@@ -1,25 +1,91 @@
 require File.dirname(__FILE__) + '/../test_helper'
 
 class NodeTest < Test::Unit::TestCase
-  fixtures :current_nodes, :nodes, :users
-  set_fixture_class :current_nodes => :Node
-  set_fixture_class :nodes => :OldNode
+  api_fixtures
 
+  def test_node_too_far_north
+         invalid_node_test(:node_too_far_north)
+  end
+  
+  def test_node_north_limit
+    valid_node_test(:node_north_limit)
+  end
+  
+  def test_node_too_far_south
+    invalid_node_test(:node_too_far_south)
+  end
+  
+  def test_node_south_limit
+    valid_node_test(:node_south_limit)
+  end
+  
+  def test_node_too_far_west
+    invalid_node_test(:node_too_far_west)
+  end
+  
+  def test_node_west_limit
+    valid_node_test(:node_west_limit)
+  end
+  
+  def test_node_too_far_east
+    invalid_node_test(:node_too_far_east)
+  end
+  
+  def test_node_east_limit
+    valid_node_test(:node_east_limit)
+  end
+  
+  def test_totally_wrong
+    invalid_node_test(:node_totally_wrong)
+  end
+  
+  # This helper method will check to make sure that a node is within the world, and
+  # has the the same lat, lon and timestamp than what was put into the db by 
+  # the fixture
+  def valid_node_test(nod)
+    node = current_nodes(nod)
+    dbnode = Node.find(node.id)
+    assert_equal dbnode.lat, node.latitude.to_f/SCALE
+    assert_equal dbnode.lon, node.longitude.to_f/SCALE
+    assert_equal dbnode.changeset_id, node.changeset_id
+    assert_equal dbnode.timestamp, node.timestamp
+    assert_equal dbnode.version, node.version
+    assert_equal dbnode.visible, node.visible
+    #assert_equal node.tile, QuadTile.tile_for_point(node.lat, node.lon)
+    assert_valid node
+  end
+  
+  # This helper method will check to make sure that a node is outwith the world, 
+  # and has the same lat, lon and timesamp than what was put into the db by the
+  # fixture
+  def invalid_node_test(nod)
+    node = current_nodes(nod)
+    dbnode = Node.find(node.id)
+    assert_equal dbnode.lat, node.latitude.to_f/SCALE
+    assert_equal dbnode.lon, node.longitude.to_f/SCALE
+    assert_equal dbnode.changeset_id, node.changeset_id
+    assert_equal dbnode.timestamp, node.timestamp
+    assert_equal dbnode.version, node.version
+    assert_equal dbnode.visible, node.visible
+    #assert_equal node.tile, QuadTile.tile_for_point(node.lat, node.lon)
+    assert_equal false, dbnode.valid?
+  end
+  
+  # Check that you can create a node and store it
   def test_create
     node_template = Node.new(:latitude => 12.3456,
                              :longitude => 65.4321,
-                             :user_id => users(:normal_user).id,
-                             :visible => 1,
-                             :tags => "")
-    assert node_template.save_with_history!
+                             :changeset_id => changesets(:normal_user_first_change).id,
+                             :visible => 1, 
+                             :version => 1)
+    assert node_template.create_with_history(users(:normal_user))
 
     node = Node.find(node_template.id)
     assert_not_nil node
     assert_equal node_template.latitude, node.latitude
     assert_equal node_template.longitude, node.longitude
-    assert_equal node_template.user_id, node.user_id
+    assert_equal node_template.changeset_id, node.changeset_id
     assert_equal node_template.visible, node.visible
-    assert_equal node_template.tags, node.tags
     assert_equal node_template.timestamp.to_i, node.timestamp.to_i
 
     assert_equal OldNode.find(:all, :conditions => [ "id = ?", node_template.id ]).length, 1
@@ -27,14 +93,14 @@ class NodeTest < Test::Unit::TestCase
     assert_not_nil old_node
     assert_equal node_template.latitude, old_node.latitude
     assert_equal node_template.longitude, old_node.longitude
-    assert_equal node_template.user_id, old_node.user_id
+    assert_equal node_template.changeset_id, old_node.changeset_id
     assert_equal node_template.visible, old_node.visible
     assert_equal node_template.tags, old_node.tags
     assert_equal node_template.timestamp.to_i, old_node.timestamp.to_i
   end
 
   def test_update
-    node_template = Node.find(1)
+    node_template = Node.find(current_nodes(:visible_node).id)
     assert_not_nil node_template
 
     assert_equal OldNode.find(:all, :conditions => [ "id = ?", node_template.id ]).length, 1
@@ -43,16 +109,16 @@ class NodeTest < Test::Unit::TestCase
 
     node_template.latitude = 12.3456
     node_template.longitude = 65.4321
-    node_template.tags = "updated=yes"
-    assert node_template.save_with_history!
+    #node_template.tags = "updated=yes"
+    assert node_template.update_from(old_node_template, users(:normal_user))
 
     node = Node.find(node_template.id)
     assert_not_nil node
     assert_equal node_template.latitude, node.latitude
     assert_equal node_template.longitude, node.longitude
-    assert_equal node_template.user_id, node.user_id
+    assert_equal node_template.changeset_id, node.changeset_id
     assert_equal node_template.visible, node.visible
-    assert_equal node_template.tags, node.tags
+    #assert_equal node_template.tags, node.tags
     assert_equal node_template.timestamp.to_i, node.timestamp.to_i
 
     assert_equal OldNode.find(:all, :conditions => [ "id = ?", node_template.id ]).length, 2
@@ -61,30 +127,29 @@ class NodeTest < Test::Unit::TestCase
     assert_not_nil old_node
     assert_equal node_template.latitude, old_node.latitude
     assert_equal node_template.longitude, old_node.longitude
-    assert_equal node_template.user_id, old_node.user_id
+    assert_equal node_template.changeset_id, old_node.changeset_id
     assert_equal node_template.visible, old_node.visible
-    assert_equal node_template.tags, old_node.tags
+    #assert_equal node_template.tags, old_node.tags
     assert_equal node_template.timestamp.to_i, old_node.timestamp.to_i
   end
 
   def test_delete
-    node_template = Node.find(1)
+    node_template = Node.find(current_nodes(:visible_node))
     assert_not_nil node_template
 
     assert_equal OldNode.find(:all, :conditions => [ "id = ?", node_template.id ]).length, 1
     old_node_template = OldNode.find(:first, :conditions => [ "id = ?", node_template.id ])
     assert_not_nil old_node_template
 
-    node_template.visible = 0
-    assert node_template.save_with_history!
+    assert node_template.delete_with_history!(old_node_template, users(:normal_user))
 
     node = Node.find(node_template.id)
     assert_not_nil node
     assert_equal node_template.latitude, node.latitude
     assert_equal node_template.longitude, node.longitude
-    assert_equal node_template.user_id, node.user_id
+    assert_equal node_template.changeset_id, node.changeset_id
     assert_equal node_template.visible, node.visible
-    assert_equal node_template.tags, node.tags
+    #assert_equal node_template.tags, node.tags
     assert_equal node_template.timestamp.to_i, node.timestamp.to_i
 
     assert_equal OldNode.find(:all, :conditions => [ "id = ?", node_template.id ]).length, 2
@@ -93,9 +158,9 @@ class NodeTest < Test::Unit::TestCase
     assert_not_nil old_node
     assert_equal node_template.latitude, old_node.latitude
     assert_equal node_template.longitude, old_node.longitude
-    assert_equal node_template.user_id, old_node.user_id
+    assert_equal node_template.changeset_id, old_node.changeset_id
     assert_equal node_template.visible, old_node.visible
-    assert_equal node_template.tags, old_node.tags
+    #assert_equal node_template.tags, old_node.tags
     assert_equal node_template.timestamp.to_i, old_node.timestamp.to_i
   end
 end
diff --git a/test/unit/old_node_tag_test.rb b/test/unit/old_node_tag_test.rb
new file mode 100644 (file)
index 0000000..9696776
--- /dev/null
@@ -0,0 +1,76 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class OldNodeTest < Test::Unit::TestCase
+  api_fixtures
+
+  def test_old_node_tag_count
+    assert_equal 8, OldNodeTag.count, "Unexpected number of fixtures loaded."
+  end
+  
+  def test_length_key_valid
+    key = "k"
+    (0..255).each do |i|
+      tag = OldNodeTag.new
+      tag.id = node_tags(:t1).id
+      tag.version = node_tags(:t1).version
+      tag.k = key*i
+      tag.v = "v"
+      assert_valid tag
+    end
+  end
+  
+  def test_length_value_valid
+    val = "v"
+    (0..255).each do |i|
+      tag = OldNodeTag.new
+      tag.id = node_tags(:t1).id
+      tag.version = node_tags(:t1).version
+      tag.k = "k"
+      tag.v = val*i
+      assert_valid tag
+    end
+  end
+  
+  def test_length_key_invalid
+    ["k"*256].each do |i|
+      tag = OldNodeTag.new
+      tag.id = node_tags(:t1).id
+      tag.version = node_tags(:t1).version
+      tag.k = i
+      tag.v = "v", "Key should be too long"
+      assert !tag.valid?
+      assert tag.errors.invalid?(:k)
+    end
+  end
+  
+  def test_length_value_invalid
+    ["k"*256].each do |i|
+      tag = OldNodeTag.new
+      tag.id = node_tags(:t1).id
+      tag.version = node_tags(:t1).version
+      tag.k = "k"
+      tag.v = i
+      assert !tag.valid?, "Value should be too long"
+      assert tag.errors.invalid?(:v)
+    end
+  end
+  
+  def test_empty_old_node_tag_invalid
+    tag = OldNodeTag.new
+    assert !tag.valid?, "Empty tag should be invalid"
+    assert tag.errors.invalid?(:id)
+    assert tag.errors.invalid?(:version)
+  end
+  
+  def test_uniqueness
+    tag = OldNodeTag.new
+    tag.id = node_tags(:t1).id
+    tag.version = node_tags(:t1).version
+    tag.k = node_tags(:t1).k
+    tag.v = node_tags(:t1).v
+    assert tag.new_record?
+    assert !tag.valid?
+    assert_raise(ActiveRecord::RecordInvalid) {tag.save!}
+    assert tag.new_record?
+  end
+end
diff --git a/test/unit/old_node_test.rb b/test/unit/old_node_test.rb
new file mode 100644 (file)
index 0000000..4915e40
--- /dev/null
@@ -0,0 +1,75 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class OldNodeTest < Test::Unit::TestCase
+  api_fixtures
+
+  def test_node_too_far_north
+         invalid_node_test(:node_too_far_north)
+  end
+  
+  def test_node_north_limit
+    valid_node_test(:node_north_limit)
+  end
+  
+  def test_node_too_far_south
+    invalid_node_test(:node_too_far_south)
+  end
+  
+  def test_node_south_limit
+    valid_node_test(:node_south_limit)
+  end
+  
+  def test_node_too_far_west
+    invalid_node_test(:node_too_far_west)
+  end
+  
+  def test_node_west_limit
+    valid_node_test(:node_west_limit)
+  end
+  
+  def test_node_too_far_east
+    invalid_node_test(:node_too_far_east)
+  end
+  
+  def test_node_east_limit
+    valid_node_test(:node_east_limit)
+  end
+  
+  def test_totally_wrong
+    invalid_node_test(:node_totally_wrong)
+  end
+  
+  # This helper method will check to make sure that a node is within the world, and
+  # has the the same lat, lon and timestamp than what was put into the db by 
+  # the fixture
+  def valid_node_test(nod)
+    node = nodes(nod)
+    dbnode = Node.find(node.id)
+    assert_equal dbnode.lat, node.latitude.to_f/SCALE
+    assert_equal dbnode.lon, node.longitude.to_f/SCALE
+    assert_equal dbnode.changeset_id, node.changeset_id
+    assert_equal dbnode.version, node.version
+    assert_equal dbnode.visible, node.visible
+    assert_equal dbnode.timestamp, node.timestamp
+    #assert_equal node.tile, QuadTile.tile_for_point(nodes(nod).lat, nodes(nod).lon)
+    assert_valid node
+  end
+  
+  # This helpermethod will check to make sure that a node is outwith the world, 
+  # and has the same lat, lon and timesamp than what was put into the db by the
+  # fixture
+  def invalid_node_test(nod)
+    node = nodes(nod)
+    dbnode = Node.find(node.id)
+    assert_equal dbnode.lat, node.latitude.to_f/SCALE
+    assert_equal dbnode.lon, node.longitude.to_f/SCALE
+    assert_equal dbnode.changeset_id, node.changeset_id
+    assert_equal dbnode.version, node.version
+    assert_equal dbnode.visible, node.visible
+    assert_equal dbnode.timestamp, node.timestamp
+    #assert_equal node.tile, QuadTile.tile_for_point(nodes(nod).lat, nodes(nod).lon)
+    assert_equal false, node.valid?
+  end
+  
+
+end
diff --git a/test/unit/old_relation_tag_test.rb b/test/unit/old_relation_tag_test.rb
new file mode 100644 (file)
index 0000000..aee2901
--- /dev/null
@@ -0,0 +1,75 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class OldRelationTagTest < Test::Unit::TestCase
+  api_fixtures
+  
+  def test_tag_count
+    assert_equal 3, OldRelationTag.count
+  end
+  
+  def test_length_key_valid
+    key = "k"
+    (0..255).each do |i|
+      tag = OldRelationTag.new
+      tag.id = relation_tags(:t1).id
+      tag.version = 1
+      tag.k = key*i
+      tag.v = "v"
+      assert_valid tag
+    end
+  end
+  
+  def test_length_value_valid
+    val = "v"
+    (0..255).each do |i|
+      tag = OldRelationTag.new
+      tag.id = relation_tags(:t1).id
+      tag.version = 1
+      tag.k = "k"
+      tag.v = val*i
+      assert_valid tag
+    end
+  end
+  
+  def test_length_key_invalid
+    ["k"*256].each do |i|
+      tag = OldRelationTag.new
+      tag.id = relation_tags(:t1).id
+      tag.version = 1
+      tag.k = i
+      tag.v = "v"
+      assert !tag.valid?, "Key should be too long"
+      assert tag.errors.invalid?(:k)
+    end
+  end
+  
+  def test_length_value_invalid
+    ["k"*256].each do |i|
+      tag = OldRelationTag.new
+      tag.id = relation_tags(:t1).id
+      tag.version = 1
+      tag.k = "k"
+      tag.v = i
+      assert !tag.valid?, "Value should be too long"
+      assert tag.errors.invalid?(:v)
+    end
+  end
+  
+  def test_empty_node_tag_invalid
+    tag = OldRelationTag.new
+    assert !tag.valid?, "Empty tag should be invalid"
+    assert tag.errors.invalid?(:id)
+  end
+  
+  def test_uniqueness
+    tag = OldRelationTag.new
+    tag.id = relation_tags(:t1).id
+    tag.version = relation_tags(:t1).version
+    tag.k = relation_tags(:t1).k
+    tag.v = relation_tags(:t1).v
+    assert tag.new_record?
+    assert !tag.valid?
+    assert_raise(ActiveRecord::RecordInvalid) {tag.save!}
+    assert tag.new_record?
+  end
+end
diff --git a/test/unit/old_way_tag_test.rb b/test/unit/old_way_tag_test.rb
new file mode 100644 (file)
index 0000000..b2724cb
--- /dev/null
@@ -0,0 +1,75 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class WayTagTest < Test::Unit::TestCase
+  api_fixtures
+  
+  def test_tag_count
+    assert_equal 3, OldWayTag.count
+  end
+  
+  def test_length_key_valid
+    key = "k"
+    (0..255).each do |i|
+      tag = OldWayTag.new
+      tag.id = way_tags(:t1).id
+      tag.version = 1
+      tag.k = key*i
+      tag.v = "v"
+      assert_valid tag
+    end
+  end
+  
+  def test_length_value_valid
+    val = "v"
+    (0..255).each do |i|
+      tag = OldWayTag.new
+      tag.id = way_tags(:t1).id
+      tag.version = 1
+      tag.k = "k"
+      tag.v = val*i
+      assert_valid tag
+    end
+  end
+  
+  def test_length_key_invalid
+    ["k"*256].each do |i|
+      tag = OldWayTag.new
+      tag.id = way_tags(:t1).id
+      tag.version = 1
+      tag.k = i
+      tag.v = "v"
+      assert !tag.valid?, "Key should be too long"
+      assert tag.errors.invalid?(:k)
+    end
+  end
+  
+  def test_length_value_invalid
+    ["k"*256].each do |i|
+      tag = OldWayTag.new
+      tag.id = way_tags(:t1).id
+      tag.version = 1
+      tag.k = "k"
+      tag.v = i
+      assert !tag.valid?, "Value should be too long"
+      assert tag.errors.invalid?(:v)
+    end
+  end
+  
+  def test_empty_node_tag_invalid
+    tag = OldNodeTag.new
+    assert !tag.valid?, "Empty tag should be invalid"
+    assert tag.errors.invalid?(:id)
+  end
+  
+  def test_uniqueness
+    tag = OldWayTag.new
+    tag.id = way_tags(:t1).id
+    tag.version = way_tags(:t1).version
+    tag.k = way_tags(:t1).k
+    tag.v = way_tags(:t1).v
+    assert tag.new_record?
+    assert !tag.valid?
+    assert_raise(ActiveRecord::RecordInvalid) {tag.save!}
+    assert tag.new_record?
+  end
+end
diff --git a/test/unit/relation_member_test.rb b/test/unit/relation_member_test.rb
new file mode 100644 (file)
index 0000000..f0590ef
--- /dev/null
@@ -0,0 +1,10 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class RelationMemberTest < Test::Unit::TestCase
+  api_fixtures
+  
+  def test_relation_member_count
+    assert_equal 5, RelationMember.count
+  end
+  
+end
diff --git a/test/unit/relation_tag_test.rb b/test/unit/relation_tag_test.rb
new file mode 100644 (file)
index 0000000..5c008fc
--- /dev/null
@@ -0,0 +1,70 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class RelationTagTest < Test::Unit::TestCase
+  api_fixtures
+  
+  def test_relation_tag_count
+    assert_equal 3, RelationTag.count
+  end
+  
+  def test_length_key_valid
+    key = "k"
+    (0..255).each do |i|
+      tag = RelationTag.new
+      tag.id = 1
+      tag.k = key*i
+      tag.v = "v"
+      assert_valid tag
+    end
+  end
+  
+  def test_length_value_valid
+    val = "v"
+    (0..255).each do |i|
+      tag = RelationTag.new
+      tag.id = 1
+      tag.k = "k"
+      tag.v = val*i
+      assert_valid tag
+    end
+  end
+  
+  def test_length_key_invalid
+    ["k"*256].each do |i|
+      tag = RelationTag.new
+      tag.id = 1
+      tag.k = i
+      tag.v = "v"
+      assert !tag.valid?, "Key #{i} should be too long"
+      assert tag.errors.invalid?(:k)
+    end
+  end
+  
+  def test_length_value_invalid
+    ["v"*256].each do |i|
+      tag = RelationTag.new
+      tag.id = 1
+      tag.k = "k"
+      tag.v = i
+      assert !tag.valid?, "Value #{i} should be too long"
+      assert tag.errors.invalid?(:v)
+    end
+  end
+  
+  def test_empty_tag_invalid
+    tag = RelationTag.new
+    assert !tag.valid?, "Empty relation tag should be invalid"
+    assert tag.errors.invalid?(:id)
+  end
+  
+  def test_uniquness
+    tag = RelationTag.new
+    tag.id = current_relation_tags(:t1).id
+    tag.k = current_relation_tags(:t1).k
+    tag.v = current_relation_tags(:t1).v
+    assert tag.new_record?
+    assert !tag.valid?
+    assert_raise(ActiveRecord::RecordInvalid) {tag.save!}
+    assert tag.new_record?
+  end
+end
diff --git a/test/unit/relation_test.rb b/test/unit/relation_test.rb
new file mode 100644 (file)
index 0000000..36aad7c
--- /dev/null
@@ -0,0 +1,10 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class RelationTest < Test::Unit::TestCase
+  api_fixtures
+  
+  def test_relation_count
+    assert_equal 3, Relation.count
+  end
+  
+end
diff --git a/test/unit/trace_test.rb b/test/unit/trace_test.rb
new file mode 100644 (file)
index 0000000..6380039
--- /dev/null
@@ -0,0 +1,10 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class TraceTest < Test::Unit::TestCase
+  api_fixtures
+  
+  def test_trace_count
+    assert_equal 1, Trace.count
+  end
+  
+end
diff --git a/test/unit/tracepoint_test.rb b/test/unit/tracepoint_test.rb
new file mode 100644 (file)
index 0000000..80534c2
--- /dev/null
@@ -0,0 +1,10 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class TracepointTest < Test::Unit::TestCase
+  api_fixtures
+  
+  def test_tracepoint_count
+    assert_equal 1, Tracepoint.count
+  end
+  
+end
diff --git a/test/unit/tracetag_test.rb b/test/unit/tracetag_test.rb
new file mode 100644 (file)
index 0000000..c61913f
--- /dev/null
@@ -0,0 +1,10 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class TracetagTest < Test::Unit::TestCase
+  api_fixtures
+  
+  def test_tracetag_count
+    assert_equal 1, Tracetag.count
+  end
+  
+end
index bd4e800150c89d8200c20855ee78919cfeff4b98..df9e1e9b277ebddcea902703154458c3e7433b3d 100644 (file)
@@ -1,8 +1,55 @@
 require File.dirname(__FILE__) + '/../test_helper'
 
 class UserPreferenceTest < ActiveSupport::TestCase
-  # Replace this with your real tests.
-  def test_truth
-    assert true
+  api_fixtures
+  fixtures :user_preferences
+
+  # This checks to make sure that there are two user preferences
+  # stored in the test database.
+  # This test needs to be updated for every addition/deletion from
+  # the fixture file
+  def test_check_count
+    assert_equal 2, UserPreference.count
+  end
+
+  # Checks that you cannot add a new preference, that is a duplicate
+  def test_add_duplicate_preference
+    up = user_preferences(:a)
+    newUP = UserPreference.new
+    newUP.user = users(:normal_user)
+    newUP.k = up.k
+    newUP.v = "some other value"
+    assert_not_equal newUP.v, up.v
+    assert_raise (ActiveRecord::StatementInvalid) {newUP.save}
   end
+  
+  def test_check_valid_length
+    key = "k"
+    val = "v"
+    (1..255).each do |i|
+      up = UserPreference.new
+      up.user = users(:normal_user)
+      up.k = key*i
+      up.v = val*i
+      assert up.valid?
+      assert up.save!
+      resp = UserPreference.find(up.id)
+      assert_equal key*i, resp.k, "User preference with #{i} #{key} chars (i.e. #{key.length*i} bytes) fails"
+      assert_equal val*i, resp.v, "User preference with #{i} #{val} chars (i.e. #{val.length*i} bytes) fails"
+    end
+  end
+  
+  def test_check_invalid_length
+    key = "k"
+    val = "v"
+    [0,256].each do |i|
+      up = UserPreference.new
+      up.user = users(:normal_user)
+      up.k = key*i
+      up.v = val*i
+      assert_equal false, up.valid?
+      assert_raise(ActiveRecord::RecordInvalid) {up.save!}
+    end
+  end
+
 end
index 5468f7a2d90fc88f295f8beb1cfc595699bfce10..02251b0eef865cde105883d110e950130d1d392d 100644 (file)
 require File.dirname(__FILE__) + '/../test_helper'
 
 class UserTest < Test::Unit::TestCase
-  fixtures :users
+  api_fixtures
+  fixtures :friends
 
-  # Replace this with your real tests.
-  def test_truth
-    assert true
+  def test_invalid_with_empty_attributes
+    user = User.new
+    assert !user.valid?
+    assert user.errors.invalid?(:email)
+    assert user.errors.invalid?(:pass_crypt)
+    assert user.errors.invalid?(:display_name)
+    assert user.errors.invalid?(:email)
+    assert !user.errors.invalid?(:home_lat)
+    assert !user.errors.invalid?(:home_lon)
+    assert !user.errors.invalid?(:home_zoom)
+  end
+  
+  def test_unique_email
+    new_user = User.new(:email => users(:normal_user).email,
+      :active => 1, 
+      :pass_crypt => Digest::MD5.hexdigest('test'),
+      :display_name => "new user",
+      :data_public => 1,
+      :description => "desc")
+    assert !new_user.save
+    assert_equal ActiveRecord::Errors.default_error_messages[:taken], new_user.errors.on(:email)
+  end
+  
+  def test_unique_display_name
+    new_user = User.new(:email => "tester@openstreetmap.org",
+      :active => 0,
+      :pass_crypt => Digest::MD5.hexdigest('test'),
+      :display_name => users(:normal_user).display_name, 
+      :data_public => 1,
+      :description => "desc")
+    assert !new_user.save
+    assert_equal ActiveRecord::Errors.default_error_messages[:taken], new_user.errors.on(:display_name)
+  end
+  
+  def test_email_valid
+    ok = %w{ a@s.com test@shaunmcdonald.me.uk hello_local@ping-d.ng 
+    test_local@openstreetmap.org test-local@example.com
+    輕觸搖晃的遊戲@ah.com も対応します@s.name }
+    bad = %w{ hi ht@ n@ @.com help@.me.uk help"hi.me.uk も対@応します }
+    
+    ok.each do |name|
+      user = users(:normal_user)
+      user.email = name
+      assert user.valid?, user.errors.full_messages
+    end
+    
+    bad.each do |name|
+      user = users(:normal_user)
+      user.email = name
+      assert !user.valid?, "#{name} is valid when it shouldn't be" 
+    end
+  end
+  
+  def test_display_name_length
+    user = users(:normal_user)
+    user.display_name = "123"
+    assert user.valid?, " should allow nil display name"
+    user.display_name = "12"
+    assert !user.valid?, "should not allow 2 char name"
+    user.display_name = ""
+    assert !user.valid?
+    user.display_name = nil
+    # Don't understand why it isn't allowing a nil value, 
+    # when the validates statements specifically allow it
+    # It appears the database does not allow null values
+    assert !user.valid?
+  end
+  
+  def test_display_name_valid
+    # Due to sanitisation in the view some of these that you might not 
+    # expact are allowed
+    # However, would they affect the xml planet dumps?
+    ok = [ "Name", "'me", "he\"", "#ping", "<hr>", "*ho", "\"help\"@", 
+           "vergrößern", "ルシステムにも対応します", "輕觸搖晃的遊戲" ]
+    # These need to be 3 chars in length, otherwise the length test above
+    # should be used.
+    bad = [ "<hr/>", "test@example.com", "s/f", "aa/", "aa;", "aa.",
+            "aa,", "aa?", "/;.,?", "も対応します/" ]
+    ok.each do |display_name|
+      user = users(:normal_user)
+      user.display_name = display_name
+      assert user.valid?, "#{display_name} is invalid, when it should be"
+    end
+    
+    bad.each do |display_name|
+      user = users(:normal_user)
+      user.display_name = display_name
+      assert !user.valid?, "#{display_name} is valid when it shouldn't be"
+      assert_equal "is invalid", user.errors.on(:display_name)
+    end
+  end
+  
+  def test_friend_with
+    assert_equal true, users(:normal_user).is_friends_with?(users(:public_user))
+    assert_equal false, users(:normal_user).is_friends_with?(users(:inactive_user))
+    assert_equal false, users(:public_user).is_friends_with?(users(:normal_user))
+    assert_equal false, users(:public_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(:public_user))
+  end
+  
+  def test_users_nearby
+    # second user has their data public and is close by normal user
+    assert_equal [users(:public_user)], users(:normal_user).nearby
+    # second_user has normal user nearby, but normal user has their data private
+    assert_equal [], users(:public_user).nearby
+    # inactive_user has no user nearby
+    assert_equal [], users(:inactive_user).nearby
+  end
+  
+  def test_friends_with
+    # normal user is a friend of second user
+    # it should be a one way friend accossitation
+    assert_equal 1, Friend.count
+    norm = users(:normal_user)
+    sec = users(:public_user)
+    #friend = Friend.new
+    #friend.befriender = norm
+    #friend.befriendee = sec
+    #friend.save
+    assert_equal [sec], norm.nearby
+    assert_equal 1, norm.nearby.size
+    assert_equal 1, Friend.count
+    assert_equal true, norm.is_friends_with?(sec)
+    assert_equal false, sec.is_friends_with?(norm)
+    assert_equal false, users(:normal_user).is_friends_with?(users(:inactive_user))
+    assert_equal false, users(:public_user).is_friends_with?(users(:normal_user))
+    assert_equal false, users(:public_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(:public_user))
+    #Friend.delete(friend)
+    #assert_equal 0, Friend.count
   end
 end
diff --git a/test/unit/user_token_test.rb b/test/unit/user_token_test.rb
new file mode 100644 (file)
index 0000000..83a36f8
--- /dev/null
@@ -0,0 +1,11 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class UserTokenTest < Test::Unit::TestCase
+  api_fixtures
+  fixtures :user_tokens
+
+  def test_user_token_count
+    assert_equal 0, UserToken.count
+  end
+  
+end
diff --git a/test/unit/way_node_test.rb b/test/unit/way_node_test.rb
new file mode 100644 (file)
index 0000000..acc62d3
--- /dev/null
@@ -0,0 +1,10 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class WayNodeTest < Test::Unit::TestCase
+  api_fixtures
+  
+  def test_way_nodes_count
+    assert_equal 4, WayNode.count
+  end
+  
+end
diff --git a/test/unit/way_tag_test.rb b/test/unit/way_tag_test.rb
new file mode 100644 (file)
index 0000000..018263d
--- /dev/null
@@ -0,0 +1,70 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class WayTagTest < Test::Unit::TestCase
+  api_fixtures
+  
+  def test_way_tag_count
+    assert_equal 3, WayTag.count
+  end
+  
+  def test_length_key_valid
+    key = "k"
+    (0..255).each do |i|
+      tag = WayTag.new
+      tag.id = current_way_tags(:t1).id
+      tag.k = key*i
+      tag.v = current_way_tags(:t1).v
+      assert_valid tag
+    end
+  end
+  
+  def test_length_value_valid
+    val = "v"
+    (0..255).each do |i|
+      tag = WayTag.new
+      tag.id = current_way_tags(:t1).id
+      tag.k = "k"
+      tag.v = val*i
+      assert_valid tag
+    end
+  end
+  
+  def test_length_key_invalid
+    ["k"*256].each do |i|
+      tag = WayTag.new
+      tag.id = current_way_tags(:t1).id
+      tag.k = i
+      tag.v = "v"
+      assert !tag.valid?, "Key #{i} should be too long"
+      assert tag.errors.invalid?(:k)
+    end
+  end
+  
+  def test_length_value_invalid
+    ["v"*256].each do |i|
+      tag = WayTag.new
+      tag.id = current_way_tags(:t1).id
+      tag.k = "k"
+      tag.v = i
+      assert !tag.valid?, "Value #{i} should be too long"
+      assert tag.errors.invalid?(:v)
+    end
+  end
+  
+  def test_empty_tag_invalid
+    tag = WayTag.new
+    assert !tag.valid?, "Empty way tag should be invalid"
+    assert tag.errors.invalid?(:id)
+  end
+  
+  def test_uniqueness
+    tag = WayTag.new
+    tag.id = current_way_tags(:t1).id
+    tag.k = current_way_tags(:t1).k
+    tag.v = current_way_tags(:t1).v
+    assert tag.new_record?
+    assert !tag.valid?
+    assert_raise(ActiveRecord::RecordInvalid) {tag.save!}
+    assert tag.new_record?
+  end
+end
diff --git a/test/unit/way_test.rb b/test/unit/way_test.rb
new file mode 100644 (file)
index 0000000..55ab092
--- /dev/null
@@ -0,0 +1,39 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class WayTest < Test::Unit::TestCase
+  api_fixtures
+
+  # Check that we have the correct number of currnet ways in the db
+  # This will need to updated whenever the current_ways.yml is updated
+  def test_db_count
+    assert_equal 4, Way.count
+  end
+  
+  def test_bbox
+    node = current_nodes(:used_node_1)
+    [ :visible_way,
+      :invisible_way,
+      :used_way ].each do |way_symbol|
+      way = current_ways(way_symbol)
+      assert_equal node.bbox, way.bbox
+    end
+  end
+  
+  # Check that the preconditions fail when you are over the defined limit of 
+  # the maximum number of nodes in each way.
+  def test_max_nodes_per_way_limit
+    # Take one of the current ways and add nodes to it until we are near the limit
+    way = Way.find(current_ways(:visible_way).id)
+    assert way.valid?
+    # it already has 1 node
+    1.upto((APP_CONFIG['max_number_of_way_nodes'])/2) {
+      way.add_nd_num(current_nodes(:used_node_1).id)
+      way.add_nd_num(current_nodes(:used_node_2).id)
+    }
+    way.save
+    #print way.nds.size
+    assert way.valid?
+    way.add_nd_num(current_nodes(:visible_node).id)
+    assert way.valid?
+  end
+end