]> git.openstreetmap.org Git - rails.git/commitdiff
Resyncing from head 10895:11795
authorShaun McDonald <shaun@shaunmcdonald.me.uk>
Sat, 8 Nov 2008 11:52:58 +0000 (11:52 +0000)
committerShaun McDonald <shaun@shaunmcdonald.me.uk>
Sat, 8 Nov 2008 11:52:58 +0000 (11:52 +0000)
154 files changed:
app/controllers/api_controller.rb
app/controllers/application.rb
app/controllers/browse_controller.rb
app/controllers/changeset_controller.rb [new file with mode: 0644]
app/controllers/changeset_tag_controller.rb [new file with mode: 0644]
app/controllers/diary_entry_controller.rb
app/controllers/message_controller.rb
app/controllers/node_controller.rb
app/controllers/old_node_controller.rb
app/controllers/old_relation_controller.rb
app/controllers/old_way_controller.rb
app/controllers/relation_controller.rb
app/controllers/trace_controller.rb
app/controllers/user_controller.rb
app/controllers/way_controller.rb
app/models/changeset.rb [new file with mode: 0644]
app/models/changeset_tag.rb [new file with mode: 0644]
app/models/diary_entry.rb
app/models/message.rb
app/models/node.rb
app/models/node_tag.rb [new file with mode: 0644]
app/models/old_node.rb
app/models/old_node_tag.rb [new file with mode: 0644]
app/models/old_relation.rb
app/models/old_way.rb
app/models/relation.rb
app/models/trace.rb
app/models/tracetag.rb
app/models/user.rb
app/models/user_preference.rb
app/models/way.rb
app/views/browse/_changeset_details.rhtml [new file with mode: 0644]
app/views/browse/_common_details.rhtml
app/views/browse/changeset.rhtml [new file with mode: 0644]
app/views/browse/index.rhtml
app/views/browse/not_found.rhtml [new file with mode: 0644]
app/views/browse/start.rjs
app/views/layouts/site.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/routes.rb
db/migrate/001_create_osm_db.rb
db/migrate/017_add_timestamp_indexes.rb [new file with mode: 0644]
db/migrate/018_populate_node_tags_and_remove.rb [new file with mode: 0644]
db/migrate/018_populate_node_tags_and_remove_helper.c [new file with mode: 0644]
db/migrate/019_move_to_innodb.rb [new file with mode: 0644]
db/migrate/020_key_constraints.rb [new file with mode: 0644]
db/migrate/021_add_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/tasks/populate_node_tags.rake [deleted file]
lib/validators.rb [new file with mode: 0644]
public/javascripts/map.js
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_relations.yml
test/fixtures/current_way_nodes.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/gpx_file_tags.yml [new file with mode: 0644]
test/fixtures/gpx_files.yml [new file with mode: 0644]
test/fixtures/gpx_points.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/relations.yml
test/fixtures/user_preferences.yml
test/fixtures/users.yml
test/fixtures/way_nodes.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/way_controller_test.rb
test/test_helper.rb
test/unit/current_node_tag_test.rb [new file with mode: 0644]
test/unit/message_test.rb
test/unit/node_test.rb
test/unit/old_node_test.rb [new file with mode: 0644]
test/unit/user_preference_test.rb
test/unit/user_test.rb
test/unit/way_test.rb [new file with mode: 0644]
vendor/plugins/deadlock_retry/README [new file with mode: 0644]
vendor/plugins/deadlock_retry/Rakefile [new file with mode: 0644]
vendor/plugins/deadlock_retry/init.rb [new file with mode: 0644]
vendor/plugins/deadlock_retry/lib/deadlock_retry.rb [new file with mode: 0644]
vendor/plugins/deadlock_retry/test/deadlock_retry_test.rb [new file with mode: 0644]
vendor/plugins/file_column/CHANGELOG [new file with mode: 0644]
vendor/plugins/file_column/README [new file with mode: 0644]
vendor/plugins/file_column/Rakefile [new file with mode: 0644]
vendor/plugins/file_column/TODO [new file with mode: 0644]
vendor/plugins/file_column/init.rb [new file with mode: 0644]
vendor/plugins/file_column/lib/file_column.rb [new file with mode: 0644]
vendor/plugins/file_column/lib/file_column_helper.rb [new file with mode: 0644]
vendor/plugins/file_column/lib/file_compat.rb [new file with mode: 0644]
vendor/plugins/file_column/lib/magick_file_column.rb [new file with mode: 0644]
vendor/plugins/file_column/lib/rails_file_column.rb [new file with mode: 0644]
vendor/plugins/file_column/lib/test_case.rb [new file with mode: 0644]
vendor/plugins/file_column/lib/validations.rb [new file with mode: 0644]
vendor/plugins/file_column/test/abstract_unit.rb [new file with mode: 0644]
vendor/plugins/file_column/test/connection.rb [new file with mode: 0644]
vendor/plugins/file_column/test/file_column_helper_test.rb [new file with mode: 0644]
vendor/plugins/file_column/test/file_column_test.rb [new file with mode: 0755]
vendor/plugins/file_column/test/fixtures/entry.rb [new file with mode: 0644]
vendor/plugins/file_column/test/fixtures/invalid-image.jpg [new file with mode: 0644]
vendor/plugins/file_column/test/fixtures/kerb.jpg [new file with mode: 0644]
vendor/plugins/file_column/test/fixtures/mysql.sql [new file with mode: 0644]
vendor/plugins/file_column/test/fixtures/schema.rb [new file with mode: 0644]
vendor/plugins/file_column/test/fixtures/skanthak.png [new file with mode: 0644]
vendor/plugins/file_column/test/magick_test.rb [new file with mode: 0644]
vendor/plugins/file_column/test/magick_view_only_test.rb [new file with mode: 0644]
vendor/plugins/sql_session_store/LICENSE [new file with mode: 0644]
vendor/plugins/sql_session_store/README [new file with mode: 0755]
vendor/plugins/sql_session_store/Rakefile [new file with mode: 0755]
vendor/plugins/sql_session_store/generators/sql_session_store/USAGE [new file with mode: 0755]
vendor/plugins/sql_session_store/generators/sql_session_store/sql_session_store_generator.rb [new file with mode: 0755]
vendor/plugins/sql_session_store/generators/sql_session_store/templates/migration.rb [new file with mode: 0755]
vendor/plugins/sql_session_store/init.rb [new file with mode: 0755]
vendor/plugins/sql_session_store/install.rb [new file with mode: 0755]
vendor/plugins/sql_session_store/lib/mysql_session.rb [new file with mode: 0755]
vendor/plugins/sql_session_store/lib/oracle_session.rb [new file with mode: 0755]
vendor/plugins/sql_session_store/lib/postgresql_session.rb [new file with mode: 0755]
vendor/plugins/sql_session_store/lib/sql_session.rb [new file with mode: 0644]
vendor/plugins/sql_session_store/lib/sql_session_store.rb [new file with mode: 0755]
vendor/plugins/sql_session_store/lib/sqlite_session.rb [new file with mode: 0755]

index 6b36b41ae947e25fdfe035f5377e2cc652e0d3a5..0724a3712651cfd23e05fcd81c0fc38f24182be3 100644 (file)
@@ -11,12 +11,13 @@ class ApiController < ApplicationController
   @@count = COUNT
 
   # The maximum area you're allowed to request, in square degrees
-  MAX_REQUEST_AREA = 0.25
+  MAX_REQUEST_AREA = APP_CONFIG['max_request_area']
 
   # Number of GPS trace/trackpoints returned per-page
-  TRACEPOINTS_PER_PAGE = 5000
+  TRACEPOINTS_PER_PAGE = APP_CONFIG['tracepoints_per_page']
 
-  
+  # Get an XML response containing a list of tracepoints that have been uploaded
+  # within the specified bounding box, and in the specified page.
   def trackpoints
     @@count+=1
     #retrieve the page number
@@ -84,6 +85,15 @@ class ApiController < ApplicationController
     render :text => doc.to_s, :content_type => "text/xml"
   end
 
+  # This is probably the most common call of all. It is used for getting the 
+  # OSM data for a specified bounding box, usually for editing. First the
+  # bounding box (bbox) is checked to make sure that it is sane. All nodes 
+  # are searched, then all the ways that reference those nodes are found.
+  # All Nodes that are referenced by those ways are fetched and added to the list
+  # of nodes.
+  # Then all the relations that reference the already found nodes and ways are
+  # fetched. All the nodes and ways that are referenced by those ways are then 
+  # fetched. Finally all the xml is returned.
   def map
     GC.start
     @@count+=1
@@ -109,18 +119,19 @@ class ApiController < ApplicationController
       return
     end
 
-    @nodes = Node.find_by_area(min_lat, min_lon, max_lat, max_lon, :conditions => "visible = 1", :limit => APP_CONFIG['max_number_of_nodes']+1)
+    # FIXME um why is this area using a different order for the lat/lon from above???
+    @nodes = Node.find_by_area(min_lat, min_lon, max_lat, max_lon, :conditions => {:visible => true}, :limit => APP_CONFIG['max_number_of_nodes']+1)
     # get all the nodes, by tag not yet working, waiting for change from NickB
     # need to be @nodes (instance var) so tests in /spec can be performed
     #@nodes = Node.search(bbox, params[:tag])
 
     node_ids = @nodes.collect(&:id)
     if node_ids.length > APP_CONFIG['max_number_of_nodes']
-      report_error("You requested too many nodes (limit is 50,000). Either request a smaller area, or use planet.osm")
+      report_error("You requested too many nodes (limit is #{APP_CONFIG['max_number_of_nodes']}). Either request a smaller area, or use planet.osm")
       return
     end
     if node_ids.length == 0
-      render :text => "<osm version='0.5'></osm>", :content_type => "text/xml"
+      render :text => "<osm version='#{API_VERSION}' generator='#{GENERATOR}'></osm>", :content_type => "text/xml"
       return
     end
 
@@ -176,15 +187,15 @@ class ApiController < ApplicationController
       end
     end 
 
-    relations = Relation.find_for_nodes(visible_nodes.keys, :conditions => "visible = 1") +
-                Relation.find_for_ways(way_ids, :conditions => "visible = 1")
+    relations = Relation.find_for_nodes(visible_nodes.keys, :conditions => {:visible => true}) +
+                Relation.find_for_ways(way_ids, :conditions => {:visible => true})
 
     # we do not normally return the "other" partners referenced by an relation, 
     # e.g. if we return a way A that is referenced by relation X, and there's 
     # another way B also referenced, that is not returned. But we do make 
     # an exception for cases where an relation references another *relation*; 
     # in that case we return that as well (but we don't go recursive here)
-    relations += Relation.find_for_relations(relations.collect { |r| r.id }, :conditions => "visible = 1")
+    relations += Relation.find_for_relations(relations.collect { |r| r.id }, :conditions => {:visible => true})
 
     # this "uniq" may be slightly inefficient; it may be better to first collect and output
     # all node-related relations, then find the *not yet covered* way-related ones etc.
@@ -204,6 +215,8 @@ class ApiController < ApplicationController
     end
   end
 
+  # Get a list of the tiles that have changed within a specified time
+  # period
   def changes
     zoom = (params[:zoom] || '12').to_i
 
@@ -217,7 +230,7 @@ class ApiController < ApplicationController
     end
 
     if zoom >= 1 and zoom <= 16 and
-       endtime >= starttime and endtime - starttime <= 24.hours
+       endtime > starttime and endtime - starttime <= 24.hours
       mask = (1 << zoom) - 1
 
       tiles = Node.count(:conditions => ["timestamp BETWEEN ? AND ?", starttime, endtime],
@@ -245,21 +258,32 @@ class ApiController < ApplicationController
 
       render :text => doc.to_s, :content_type => "text/xml"
     else
-      render :nothing => true, :status => :bad_request
+      render :text => "Requested zoom is invalid, or the supplied start is after the end time, or the start duration is more than 24 hours", :status => :bad_request
     end
   end
 
+  # External apps that use the api are able to query the api to find out some 
+  # parameters of the API. It currently returns: 
+  # * minimum and maximum API versions that can be used.
+  # * maximum area that can be requested in a bbox request in square degrees
+  # * number of tracepoints that are returned in each tracepoints page
   def capabilities
     doc = OSM::API.new.get_xml_doc
 
     api = XML::Node.new 'api'
     version = XML::Node.new 'version'
-    version['minimum'] = '0.5';
-    version['maximum'] = '0.5';
+    version['minimum'] = "#{API_VERSION}";
+    version['maximum'] = "#{API_VERSION}";
     api << version
     area = XML::Node.new 'area'
     area['maximum'] = MAX_REQUEST_AREA.to_s;
     api << area
+    tracepoints = XML::Node.new 'tracepoints'
+    tracepoints['per_page'] = APP_CONFIG['tracepoints_per_page'].to_s
+    api << tracepoints
+    waynodes = XML::Node.new 'waynodes'
+    waynodes['maximum'] = APP_CONFIG['max_number_of_way_nodes'].to_s
+    api << waynodes
     
     doc.root << api
 
index ce13a6aa3a6ae625407d3ac7fe2eaa11f7ab6ed9..579c50e95a725217070beb7532d2880899636c5d 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
@@ -71,7 +71,7 @@ class ApplicationController < ActionController::Base
   #  phrase from that, we can also put the error message into the status
   #  message. For now, rails won't let us)
   def report_error(message)
-    render :nothing => true, :status => :bad_request
+    render :text => message, :status => :bad_request
     # Todo: some sort of escaping of problem characters in the message
     response.headers['Error'] = message
   end
@@ -82,6 +82,8 @@ private
   def get_auth_data 
     if request.env.has_key? 'X-HTTP_AUTHORIZATION'          # where mod_rewrite might have put it 
       authdata = request.env['X-HTTP_AUTHORIZATION'].to_s.split 
+    elsif request.env.has_key? 'REDIRECT_X_HTTP_AUTHORIZATION'          # mod_fcgi 
+      authdata = request.env['REDIRECT_X_HTTP_AUTHORIZATION'].to_s.split 
     elsif request.env.has_key? 'HTTP_AUTHORIZATION'         # regular location
       authdata = request.env['HTTP_AUTHORIZATION'].to_s.split
     end 
index f3a04519cbd362b5f8133a1c7c079d7ef9dda7ff..2c6c3dc5f6e26f42de0f5bc298407e8be71a5062 100644 (file)
@@ -17,14 +17,15 @@ class BrowseController < ApplicationController
      
       @name = @relation.tags['name'].to_s 
       if @name.length == 0:
-       @name = "#" + @relation.id.to_s
+          @name = "#" + @relation.id.to_s
       end
        
       @title = 'Relation | ' + (@name)
       @next = Relation.find(:first, :order => "id ASC", :conditions => [ "visible = true AND id > :id", { :id => @relation.id }] ) 
       @prev = Relation.find(:first, :order => "id DESC", :conditions => [ "visible = true AND id < :id", { :id => @relation.id }] ) 
     rescue ActiveRecord::RecordNotFound
-      render :nothing => true, :status => :not_found
+      @type = "relation"
+      render :action => "not_found", :status => :not_found
     end
   end
   
@@ -34,12 +35,13 @@ class BrowseController < ApplicationController
      
       @name = @relation.tags['name'].to_s 
       if @name.length == 0:
-       @name = "#" + @relation.id.to_s
+          @name = "#" + @relation.id.to_s
       end
        
       @title = 'Relation History | ' + (@name)
     rescue ActiveRecord::RecordNotFound
-      render :nothing => true, :status => :not_found
+      @type = "relation"
+      render :action => "not_found", :status => :not_found
     end
   end
   
@@ -49,14 +51,15 @@ class BrowseController < ApplicationController
      
       @name = @way.tags['name'].to_s 
       if @name.length == 0:
-       @name = "#" + @way.id.to_s
+          @name = "#" + @way.id.to_s
       end
        
       @title = 'Way | ' + (@name)
       @next = Way.find(:first, :order => "id ASC", :conditions => [ "visible = true AND id > :id", { :id => @way.id }] ) 
       @prev = Way.find(:first, :order => "id DESC", :conditions => [ "visible = true AND id < :id", { :id => @way.id }] ) 
     rescue ActiveRecord::RecordNotFound
-      render :nothing => true, :status => :not_found
+      @type = "way"
+      render :action => "not_found", :status => :not_found
     end
   end
   
@@ -66,12 +69,13 @@ class BrowseController < ApplicationController
      
       @name = @way.tags['name'].to_s 
       if @name.length == 0:
-       @name = "#" + @way.id.to_s
+          @name = "#" + @way.id.to_s
       end
        
       @title = 'Way History | ' + (@name)
     rescue ActiveRecord::RecordNotFound
-      render :nothing => true, :status => :not_found
+      @type = "way"
+      render :action => "not_found", :status => :not_found
     end
   end
 
@@ -81,14 +85,15 @@ class BrowseController < ApplicationController
      
       @name = @node.tags_as_hash['name'].to_s 
       if @name.length == 0:
-       @name = "#" + @node.id.to_s
+          @name = "#" + @node.id.to_s
       end
        
       @title = 'Node | ' + (@name)
       @next = Node.find(:first, :order => "id ASC", :conditions => [ "visible = true AND id > :id", { :id => @node.id }] ) 
       @prev = Node.find(:first, :order => "id DESC", :conditions => [ "visible = true AND id < :id", { :id => @node.id }] ) 
     rescue ActiveRecord::RecordNotFound
-      render :nothing => true, :status => :not_found
+      @type = "node"
+      render :action => "not_found", :status => :not_found
     end
   end
   
@@ -98,12 +103,26 @@ class BrowseController < ApplicationController
      
       @name = @node.tags_as_hash['name'].to_s 
       if @name.length == 0:
-       @name = "#" + @node.id.to_s
+          @name = "#" + @node.id.to_s
       end
        
       @title = 'Node History | ' + (@name)
     rescue ActiveRecord::RecordNotFound
-      render :nothing => true, :status => :not_found
+      @type = "way"
+      render :action => "not_found", :status => :not_found
+    end
+  end
+  
+  def changeset
+    begin
+      @changeset = Changeset.find(params[:id])
+      
+      @title = "Changeset | #{@changeset.id}"
+      @next = Changeset.find(:first, :order => "id ASC", :conditions => [ "id > :id", { :id => @changeset.id }] ) 
+      @prev = Changeset.find(:first, :order => "id DESC", :conditions => [ "id < :id", { :id => @changeset.id }] ) 
+    rescue ActiveRecord::RecordNotFound
+      @type = "changeset"
+      render :action => "not_found", :status => :not_found
     end
   end
 end
diff --git a/app/controllers/changeset_controller.rb b/app/controllers/changeset_controller.rb
new file mode 100644 (file)
index 0000000..29b5ef8
--- /dev/null
@@ -0,0 +1,330 @@
+# The ChangesetController is the RESTful interface to Changeset objects
+
+class ChangesetController < ApplicationController
+  require 'xml/libxml'
+  require 'diff_reader'
+
+  before_filter :authorize, :only => [:create, :update, :delete, :upload, :include]
+  before_filter :check_write_availability, :only => [:create, :update, :delete, :upload, :include]
+  before_filter :check_read_availability, :except => [:create, :update, :delete, :upload, :download, :query]
+  after_filter :compress_output
+
+  # Help methods for checking boundary sanity and area size
+  include MapBoundary
+
+  # Create a changeset from XML.
+  def create
+    if request.put?
+      cs = Changeset.from_xml(request.raw_post, true)
+
+      if cs
+        cs.user_id = @user.id
+        cs.save_with_tags!
+        render :text => cs.id.to_s, :content_type => "text/plain"
+      else
+        render :nothing => true, :status => :bad_request
+      end
+    else
+      render :nothing => true, :status => :method_not_allowed
+    end
+  end
+
+  def read
+    begin
+      changeset = Changeset.find(params[:id])
+      render :text => changeset.to_xml.to_s, :content_type => "text/xml"
+    rescue ActiveRecord::RecordNotFound
+      render :nothing => true, :status => :not_found
+    end
+  end
+  
+  def close 
+    begin
+      unless request.put?
+        render :nothing => true, :status => :method_not_allowed
+        return
+      end
+
+      changeset = Changeset.find(params[:id])
+
+      unless @user.id == changeset.user_id 
+        raise OSM::APIUserChangesetMismatchError 
+      end
+
+      changeset.open = false
+      changeset.save!
+      render :nothing => true
+    rescue ActiveRecord::RecordNotFound
+      render :nothing => true, :status => :not_found
+    end
+  end
+
+  ##
+  # insert a (set of) points into a changeset bounding box. this can only
+  # increase the size of the bounding box. this is a hint that clients can
+  # set either before uploading a large number of changes, or changes that
+  # the client (but not the server) knows will affect areas further away.
+  def include
+    # only allow POST requests, because although this method is
+    # idempotent, there is no "document" to PUT really...
+    if request.post?
+      cs = Changeset.find(params[:id])
+
+      # check user credentials - only the user who opened a changeset
+      # may alter it.
+      unless @user.id == cs.user_id 
+        raise OSM::APIUserChangesetMismatchError 
+      end
+
+      # keep an array of lons and lats
+      lon = Array.new
+      lat = Array.new
+
+      # the request is in pseudo-osm format... this is kind-of an
+      # abuse, maybe should change to some other format?
+      doc = XML::Parser.string(request.raw_post).parse
+      doc.find("//osm/node").each do |n|
+        lon << n['lon'].to_f * SCALE
+        lat << n['lat'].to_f * SCALE
+      end
+
+      # add the existing bounding box to the lon-lat array
+      lon << cs.min_lon unless cs.min_lon.nil?
+      lat << cs.min_lat unless cs.min_lat.nil?
+      lon << cs.max_lon unless cs.max_lon.nil?
+      lat << cs.max_lat unless cs.max_lat.nil?
+
+      # collapse the arrays to minimum and maximum
+      cs.min_lon, cs.min_lat, cs.max_lon, cs.max_lat = 
+        lon.min, lat.min, lon.max, lat.max
+
+      # save the larger bounding box and return the changeset, which
+      # will include the bigger bounding box.
+      cs.save!
+      render :text => cs.to_xml.to_s, :content_type => "text/xml"
+
+    else
+      render :nothing => true, :status => :method_not_allowed
+    end
+
+  rescue ActiveRecord::RecordNotFound
+    render :nothing => true, :status => :not_found
+  rescue OSM::APIError => ex
+    render ex.render_opts
+  end
+
+  ##
+  # Upload a diff in a single transaction.
+  #
+  # 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])
+
+    # access control - only the user who created a changeset may
+    # upload to it.
+    unless @user.id == changeset.user_id 
+      raise OSM::APIUserChangesetMismatchError 
+    end
+    
+    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'])
+    cond_merge conditions, conditions_user(params['user'])
+    cond_merge conditions, conditions_time(params['time'])
+    cond_merge conditions, conditions_open(params['open'])
+
+    # create the results document
+    results = OSM::API.new.get_xml_doc
+
+    # add all matching changesets to the XML results document
+    Changeset.find(:all, 
+                   :conditions => conditions, 
+                   :limit => 100,
+                   :order => 'created_at desc').each do |cs|
+      results.root << cs.to_xml_node
+    end
+
+    render :text => results.to_s, :content_type => "text/xml"
+
+  rescue ActiveRecord::RecordNotFound
+    render :nothing => true, :status => :not_found
+  rescue OSM::APIError => ex
+    render ex.render_opts
+  rescue String => s
+    render :text => s, :content_type => "text/plain", :status => :bad_request
+  end
+
+  ##
+  # merge two conditions
+  def cond_merge(a, b)
+    if a and b
+      a_str = a.shift
+      b_str = b.shift
+      return [ a_str + " and " + b_str ] + a + b
+    elsif a 
+      return a
+    else b
+      return b
+    end
+  end
+
+  ##
+  # if a bounding box was specified then parse it and do some sanity 
+  # checks. this is mostly the same as the map call, but without the 
+  # area restriction.
+  def conditions_bbox(bbox)
+    unless bbox.nil?
+      raise "Bounding box should be min_lon,min_lat,max_lon,max_lat" unless bbox.count(',') == 3
+      bbox = sanitise_boundaries(bbox.split(/,/))
+      raise "Minimum longitude should be less than maximum." unless bbox[0] <= bbox[2]
+      raise "Minimum latitude should be less than maximum." unless bbox[1] <= bbox[3]
+      return ['min_lon < ? and max_lon > ? and min_lat < ? and max_lat > ?',
+              bbox[2] * SCALE, bbox[0] * SCALE, bbox[3]* SCALE, bbox[1] * SCALE]
+    else
+      return nil
+    end
+  end
+
+  ##
+  # restrict changesets to those by a particular user
+  def conditions_user(user)
+    unless user.nil?
+      u = User.find(user.to_i)
+      raise OSM::APINotFoundError unless u.data_public?
+      return ['user_id = ?', u.id]
+    else
+      return nil
+    end
+  end
+
+  ##
+  # restrict changes to those during a particular time period
+  def conditions_time(time) 
+    unless time.nil?
+      # if there is a range, i.e: comma separated, then the first is 
+      # low, second is high - same as with bounding boxes.
+      if time.count(',') == 1
+        from, to = time.split(/,/).collect { |t| Date.parse(t) }
+        return ['created_at > ? and created_at < ?', from, to]
+      else
+        # if there is no comma, assume its a lower limit on time
+        return ['created_at > ?', Date.parse(time)]
+      end
+    else
+      return nil
+    end
+  rescue ArgumentError => ex
+    raise ex.message.to_s
+  end
+
+  ##
+  # restrict changes to those which are open
+  def conditions_open(open)
+    return open.nil? ? nil : ['open = ?', true]
+  end
+
+end
diff --git a/app/controllers/changeset_tag_controller.rb b/app/controllers/changeset_tag_controller.rb
new file mode 100644 (file)
index 0000000..3e8db3f
--- /dev/null
@@ -0,0 +1,9 @@
+class ChangesetTagController < ApplicationController
+  layout 'site'
+
+  def search
+    @tags = ChangesetTag.find(:all, :limit => 11, :conditions => ["match(v) against (?)", params[:query][:query].to_s] )
+  end
+
+
+end
index e0d6e44cdd8764f4aacc39bfcf8d664ca38ee83e..d9f5e4253fedab9391f3b869e11f017c72e6218c 100644 (file)
@@ -54,7 +54,7 @@ class DiaryEntryController < ApplicationController
 
   def list
     if params[:display_name]
-      @this_user = User.find_by_display_name(params[:display_name], :conditions => "visible = 1")
+      @this_user = User.find_by_display_name(params[:display_name], :conditions => {:visible => true})
 
       if @this_user
         @title = @this_user.display_name + "'s diary"
@@ -78,7 +78,7 @@ class DiaryEntryController < ApplicationController
 
   def rss
     if params[:display_name]
-      user = User.find_by_display_name(params[:display_name], :conditions => "visible = 1")
+      user = User.find_by_display_name(params[:display_name], :conditions => {:visible => true})
 
       if user
         @entries = DiaryEntry.find(:all, :conditions => ['user_id = ?', user.id], :order => 'created_at DESC', :limit => 20)
@@ -103,7 +103,7 @@ class DiaryEntryController < ApplicationController
   end
 
   def view
-    user = User.find_by_display_name(params[:display_name], :conditions => "visible = 1")
+    user = User.find_by_display_name(params[:display_name], :conditions => {:visible => true})
 
     if user
       @entry = DiaryEntry.find(:first, :conditions => ['user_id = ? AND id = ?', user.id, params[:id]])
index 85c0ac328f2fc0349bd733518f51dd343410a825..fc7a9101b12496084b96c054db873013874dbea3 100644 (file)
@@ -4,6 +4,9 @@ class MessageController < ApplicationController
   before_filter :authorize_web
   before_filter :require_user
 
+  # Allow the user to write a new message to another user. This action also 
+  # deals with the sending of that message to the other user when the user
+  # clicks send.
   def new
     @title = 'send message'
     if params[:message]
@@ -22,6 +25,7 @@ class MessageController < ApplicationController
     end
   end
 
+  # Allow the user to reply to another message.
   def reply
     message = Message.find(params[:message_id], :conditions => ["to_user_id = ? or from_user_id = ?", @user.id, @user.id ])
     @body = "On #{message.sent_on} #{message.sender.display_name} wrote:\n\n#{message.body.gsub(/^/, '> ')}" 
@@ -32,15 +36,17 @@ class MessageController < ApplicationController
     render :nothing => true, :status => :not_found
   end
 
+  # Show a message
   def read
     @title = 'read message'
     @message = Message.find(params[:message_id], :conditions => ["to_user_id = ? or from_user_id = ?", @user.id, @user.id ])
-    @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
   end
 
+  # Display the list of messages that have been sent to the user.
   def inbox
     @title = 'inbox'
     if @user and params[:display_name] == @user.display_name
@@ -49,6 +55,7 @@ class MessageController < ApplicationController
     end
   end
 
+  # Display the list of messages that the user has sent to other users.
   def outbox
     @title = 'outbox'
     if @user and params[:display_name] == @user.display_name
@@ -57,15 +64,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
index edc3675e58382fce0b8b5801a2e7180ab280cec2..9763be5d46597e8fa5b868769010a53d6dc4708f 100644 (file)
@@ -11,20 +11,21 @@ class NodeController < ApplicationController
 
   # Create a node from XML.
   def create
-    if request.put?
-      node = Node.from_xml(request.raw_post, true)
-
-      if node
-        node.user_id = @user.id
-        node.visible = true
-        node.save_with_history!
+    begin
+      if request.put?
+        node = Node.from_xml(request.raw_post, true)
 
-        render :text => node.id.to_s, :content_type => "text/plain"
+        if node
+          node.create_with_history @user
+          render :text => node.id.to_s, :content_type => "text/plain"
+        else
+          render :nothing => true, :status => :bad_request
+        end
       else
-        render :nothing => true, :status => :bad_request
+        render :nothing => true, :status => :method_not_allowed
       end
-    else
-      render :nothing => true, :status => :method_not_allowed
+    rescue OSM::APIError => ex
+      render ex.render_opts
     end
   end
 
@@ -32,7 +33,7 @@ class NodeController < ApplicationController
   def read
     begin
       node = Node.find(params[:id])
-      if node.visible
+      if node.visible?
         response.headers['Last-Modified'] = node.timestamp.rfc822
         render :text => node.to_xml.to_s, :content_type => "text/xml"
        else
@@ -42,7 +43,7 @@ class NodeController < ApplicationController
       render :nothing => true, :status => :not_found
     end
   end
-
+  
   # Update a node from given XML
   def update
     begin
@@ -50,45 +51,36 @@ 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
 
index e27898336361832ed193c9d1b3c8de04ddb14a41..56397625c967490fdd1b365e664bb94588ede6a7 100644 (file)
@@ -22,4 +22,21 @@ class OldNodeController < ApplicationController
       render :nothing => true, :status => :internal_server_error
     end
   end
+  
+  def version
+    begin
+      old_node = OldNode.find(:first, :conditions => {:id => params[:id], :version => params[:version]} )
+      
+      response.headers['Last-Modified'] = old_node.timestamp.rfc822
+
+      doc = OSM::API.new.get_xml_doc
+      doc.root << old_node.to_xml_node
+
+      render :text => doc.to_s, :content_type => "text/xml"
+    rescue ActiveRecord::RecordNotFound
+      render :nothing => true, :status => :not_found
+    rescue
+      render :nothing => true, :status => :internal_server_error
+    end
+  end
 end
index 0b5aa89be885f18c03cf83c46097778efc839610..84d0b0c902c53d84ce4fd09f942ff220a9b87455 100644 (file)
@@ -2,6 +2,7 @@ class OldRelationController < ApplicationController
   require 'xml/libxml'
 
   session :off
+  before_filter :check_read_availability
   after_filter :compress_output
 
   def history
@@ -20,4 +21,21 @@ class OldRelationController < ApplicationController
       render :nothing => true, :status => :internal_server_error
     end
   end
+  
+  def version
+    begin
+      old_relation = OldRelation.find(:first, :conditions => {:id => params[:id], :version => params[:version]} )
+      
+      response.headers['Last-Modified'] = old_relation.timestamp.rfc822
+
+      doc = OSM::API.new.get_xml_doc
+      doc.root << old_relation.to_xml_node
+
+      render :text => doc.to_s, :content_type => "text/xml"
+    rescue ActiveRecord::RecordNotFound
+      render :nothing => true, :status => :not_found
+    rescue
+      render :nothing => true, :status => :internetal_service_error
+    end
+  end
 end
index e72c97a0078022650a8484d08c070b7d67fed6c7..da4e26d67be706e07aebd6297b38838ce73f7813 100644 (file)
@@ -13,7 +13,7 @@ class OldWayController < ApplicationController
 
       way.old_ways.each do |old_way|
         doc.root << old_way.to_xml_node
-     end
+      end
 
       render :text => doc.to_s, :content_type => "text/xml"
     rescue ActiveRecord::RecordNotFound
@@ -22,4 +22,21 @@ class OldWayController < ApplicationController
       render :nothing => true, :status => :internal_server_error
     end
   end
+  
+  def version
+    begin
+      old_way = OldWay.find(:first, :conditions => {:id => params[:id], :version => params[:version]} )
+      
+      response.headers['Last-Modified'] = old_way.timestamp.rfc822
+      
+      doc = OSM::API.new.get_xml_doc
+      doc.root << old_way.to_xml_node
+      
+      render :text => doc.to_s, :content_type => "text/xml"
+    rescue ActiveRecord::RecordNotFound
+      render :nothing => true, :status => :not_found
+    rescue
+      render :nothing => true, :status => :internal_server_error
+    end
+  end
 end
index 2b1ba6c753c70df6579d381facebc7bd451be754..da5129467ff1e72caba2f35da5815804eb285008 100644 (file)
@@ -8,23 +8,21 @@ class RelationController < ApplicationController
   after_filter :compress_output
 
   def create
-    if request.put?
-      relation = Relation.from_xml(request.raw_post, true)
+    begin
+      if request.put?
+        relation = Relation.from_xml(request.raw_post, true)
 
-      if relation
-        if !relation.preconditions_ok?
-          render :text => "", :status => :precondition_failed
+        if relation
+          relation.create_with_history @user
+          render :text => relation.id.to_s, :content_type => "text/plain"
         else
-          relation.user_id = @user.id
-          relation.save_with_history!
-
-         render :text => relation.id.to_s, :content_type => "text/plain"
+          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
 
@@ -45,29 +43,21 @@ 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
 
@@ -75,26 +65,17 @@ class RelationController < ApplicationController
 #XXX check if member somewhere!
     begin
       relation = Relation.find(params[:id])
-
-      if relation.visible
-        if RelationMember.find(:first, :joins => "INNER JOIN current_relations ON current_relations.id=current_relation_members.id", :conditions => [ "visible = 1 AND member_type='relation' and member_id=?", params[:id]])
-          render :text => "", :status => :precondition_failed
-        else
-          relation.user_id = @user.id
-          relation.tags = []
-          relation.members = []
-          relation.visible = false
-          relation.save_with_history!
-
-          render :nothing => true
-        end
+      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
 
@@ -204,7 +185,7 @@ class RelationController < ApplicationController
     doc = OSM::API.new.get_xml_doc
 
     Relation.find(relationids).each do |relation|
-      doc.root << relation.to_xml_node
+      doc.root << relation.to_xml_node if relation.visible
     end
 
     render :text => doc.to_s, :content_type => "text/xml"
index 899df05dfc021d1d683012bfbb8b995f42579d04..d94280a6af080c469a2662212382e340e05c4387 100644 (file)
@@ -12,7 +12,7 @@ class TraceController < ApplicationController
     # from display name, pick up user id if one user's traces only
     display_name = params[:display_name]
     if target_user.nil? and !display_name.blank?
-      target_user = User.find(:first, :conditions => [ "visible = 1 and display_name = ?", display_name])
+      target_user = User.find(:first, :conditions => [ "visible = ? and display_name = ?", true, display_name])
     end
 
     # set title
@@ -33,15 +33,15 @@ class TraceController < ApplicationController
     # 4 - user's traces, not logged in as that user = all user's public traces
     if target_user.nil? # all traces
       if @user
-        conditions = ["(gpx_files.public = 1 OR gpx_files.user_id = ?)", @user.id] #1
+        conditions = ["(gpx_files.public = ? OR gpx_files.user_id = ?)", true, @user.id] #1
       else
-        conditions  = ["gpx_files.public = 1"] #2
+        conditions  = ["gpx_files.public = ?", true] #2
       end
     else
       if @user and @user == target_user
         conditions = ["gpx_files.user_id = ?", @user.id] #3 (check vs user id, so no join + can't pick up non-public traces by changing name)
       else
-        conditions = ["gpx_files.public = 1 AND gpx_files.user_id = ?", target_user.id] #4
+        conditions = ["gpx_files.public = ? AND gpx_files.user_id = ?", true, target_user.id] #4
       end
     end
     
@@ -51,7 +51,8 @@ class TraceController < ApplicationController
       conditions << @tag
     end
     
-    conditions[0] += " AND gpx_files.visible = 1"
+    conditions[0] += " AND gpx_files.visible = ?"
+    conditions << true
 
     @trace_pages, @traces = paginate(:traces,
                                      :include => [:user, :tags],
@@ -196,7 +197,7 @@ class TraceController < ApplicationController
   end
 
   def georss
-    conditions = ["gpx_files.public = 1"]
+    conditions = ["gpx_files.public = ?", true]
 
     if params[:display_name]
       conditions[0] += " AND users.display_name = ?"
index c658b201412a5bf2dda750e292458b38c945c1aa..b9ed5409658ac85a614124961f54ee1da77f6f12 100644 (file)
@@ -77,7 +77,7 @@ class UserController < ApplicationController
   def lost_password
     @title = 'lost password'
     if params[:user] and params[:user][:email]
-      user = User.find_by_email(params[:user][:email], :conditions => "visible = 1")
+      user = User.find_by_email(params[:user][:email], :conditions => {:visible => true})
 
       if user
         token = user.tokens.create
@@ -217,7 +217,7 @@ class UserController < ApplicationController
   end
 
   def view
-    @this_user = User.find_by_display_name(params[:display_name], :conditions => "visible = 1")
+    @this_user = User.find_by_display_name(params[:display_name], :conditions => {:visible => true})
 
     if @this_user
       @title = @this_user.display_name
@@ -230,7 +230,7 @@ class UserController < ApplicationController
   def make_friend
     if params[:display_name]     
       name = params[:display_name]
-      new_friend = User.find_by_display_name(name, :conditions => "visible = 1")
+      new_friend = User.find_by_display_name(name, :conditions => {:visible => true})
       friend = Friend.new
       friend.user_id = @user.id
       friend.friend_user_id = new_friend.id
@@ -252,7 +252,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 3b6491cf0b3ceda5ed90ee7e1da5e49579c6af21..5b0a632f75091a37bef05e2e95e6272351027da3 100644 (file)
@@ -8,23 +8,21 @@ class WayController < ApplicationController
   after_filter :compress_output
 
   def create
-    if request.put?
-      way = Way.from_xml(request.raw_post, true)
-
-      if way
-        if !way.preconditions_ok?
-          render :text => "", :status => :precondition_failed
-        else
-          way.user_id = @user.id
-          way.save_with_history!
+    begin
+      if request.put?
+        way = Way.from_xml(request.raw_post, true)
 
+        if way
+          way.create_with_history @user
           render :text => way.id.to_s, :content_type => "text/plain"
+        else
+          render :nothing => true, :status => :bad_request
         end
       else
-        render :nothing => true, :status => :bad_request
+        render :nothing => true, :status => :method_not_allowed
       end
-    else
-      render :nothing => true, :status => :method_not_allowed
+    rescue OSM::APIError => ex
+      render ex.render_opts
     end
   end
 
@@ -39,6 +37,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 +50,13 @@ class WayController < ApplicationController
       new_way = Way.from_xml(request.raw_post)
 
       if new_way and new_way.id == way.id
-        if !new_way.preconditions_ok?
-          render :text => "", :status => :precondition_failed
-        else
-          way.user_id = @user.id
-          way.tags = new_way.tags
-          way.nds = new_way.nds
-          way.visible = true
-          way.save_with_history!
-
-          render :nothing => true
-        end
+        way.update_from(new_way, @user)
+        render :text => way.version.to_s, :content_type => "text/plain"
       else
         render :nothing => true, :status => :bad_request
       end
+    rescue OSM::APIError => ex
+      render ex.render_opts
     rescue ActiveRecord::RecordNotFound
       render :nothing => true, :status => :not_found
     end
@@ -73,14 +66,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 +87,7 @@ class WayController < ApplicationController
 
       if way.visible
         nd_ids = way.nds + [-1]
-        nodes = Node.find(:all, :conditions => "visible = 1 AND id IN (#{nd_ids.join(',')})")
+        nodes = Node.find(:all, :conditions => ["visible = ? AND id IN (#{nd_ids.join(',')})", true])
 
         # Render
         doc = OSM::API.new.get_xml_doc
@@ -130,13 +125,19 @@ class WayController < ApplicationController
     end
   end
 
+  ##
+  # returns all the ways which are currently using the node given in the 
+  # :id parameter. note that this used to return deleted ways as well, but
+  # this seemed not to be the expected behaviour, so it was removed.
   def ways_for_node
-    wayids = WayNode.find(:all, :conditions => ['node_id = ?', params[:id]]).collect { |ws| ws.id[0] }.uniq
+    wayids = WayNode.find(:all, 
+                          :conditions => ['node_id = ?', params[:id]]
+                          ).collect { |ws| ws.id[0] }.uniq
 
     doc = OSM::API.new.get_xml_doc
 
     Way.find(wayids).each do |way|
-      doc.root << way.to_xml_node
+      doc.root << way.to_xml_node if way.visible
     end
 
     render :text => doc.to_s, :content_type => "text/xml"
diff --git a/app/models/changeset.rb b/app/models/changeset.rb
new file mode 100644 (file)
index 0000000..b00dfa8
--- /dev/null
@@ -0,0 +1,170 @@
+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 :user_id, :created_at
+  validates_inclusion_of :open, :in => [ true, false ]
+  
+  # over-expansion factor to use when updating the bounding box
+  EXPAND = 0.1
+
+  # Use a method like this, so that we can easily change how we
+  # determine whether a changeset is open, without breaking code in at 
+  # least 6 controllers
+  def is_open?
+    return open
+  end
+
+  def self.from_xml(xml, create=false)
+    begin
+      p = XML::Parser.new
+      p.string = xml
+      doc = p.parse
+
+      cs = Changeset.new
+
+      doc.find('//osm/changeset').each do |pt|
+        if create
+          cs.created_at = Time.now
+        end
+
+        pt.find('tag').each do |tag|
+          cs.add_tag_keyval(tag['k'], tag['v'])
+        end
+      end
+    rescue Exception => ex
+      cs = nil
+    end
+
+    return cs
+  end
+
+  ##
+  # returns the bounding box of the changeset. it is possible that some
+  # or all of the values will be nil, indicating that they are undefined.
+  def bbox
+    @bbox ||= [ min_lon, min_lat, max_lon, max_lat ]
+  end
+
+  ##
+  # expand the bounding box to include the given bounding box. also, 
+  # expand a little bit more in the direction of the expansion, so that
+  # further expansions may be unnecessary. this is an optimisation 
+  # suggested on the wiki page by kleptog.
+  def update_bbox!(array)
+    # ensure that bbox is cached and has no nils in it. if there are any
+    # nils, just use the bounding box update to write over them.
+    @bbox = bbox.zip(array).collect { |a, b| a.nil? ? b : a }
+
+    # FIXME - this looks nasty and violates DRY... is there any prettier 
+    # way to do this? 
+    @bbox[0] = array[0] + EXPAND * (@bbox[0] - @bbox[2]) if array[0] < @bbox[0]
+    @bbox[1] = array[1] + EXPAND * (@bbox[1] - @bbox[3]) if array[1] < @bbox[1]
+    @bbox[2] = array[2] + EXPAND * (@bbox[2] - @bbox[0]) if array[2] > @bbox[2]
+    @bbox[3] = array[3] + EXPAND * (@bbox[3] - @bbox[1]) if array[3] > @bbox[3]
+
+    # update active record. rails 2.1's dirty handling should take care of
+    # whether this object needs saving or not.
+    self.min_lon, self.min_lat, self.max_lon, self.max_lat = @bbox
+  end
+
+  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
+
+    Changeset.transaction do
+      # fixme update modified_at time?
+      # FIXME there is no modified_at time, should it be added
+      self.save!
+    end
+
+    ChangesetTag.transaction do
+      tags = self.tags
+      ChangesetTag.delete_all(['id = ?', self.id])
+
+      tags.each do |k,v|
+        tag = ChangesetTag.new
+        tag.k = k
+        tag.v = v
+        tag.id = self.id
+        tag.save!
+      end
+    end
+  end
+  
+  def to_xml
+    doc = OSM::API.new.get_xml_doc
+    doc.root << to_xml_node()
+    return doc
+  end
+  
+  def to_xml_node(user_display_name_cache = nil)
+    el1 = XML::Node.new 'changeset'
+    el1['id'] = self.id.to_s
+
+    user_display_name_cache = {} if user_display_name_cache.nil?
+
+    if user_display_name_cache and user_display_name_cache.key?(self.user_id)
+      # use the cache if available
+    elsif self.user.data_public?
+      user_display_name_cache[self.user_id] = self.user.display_name
+    else
+      user_display_name_cache[self.user_id] = nil
+    end
+
+    el1['user'] = user_display_name_cache[self.user_id] unless user_display_name_cache[self.user_id].nil?
+
+    self.tags.each do |k,v|
+      el2 = XML::Node.new('tag')
+      el2['k'] = k.to_s
+      el2['v'] = v.to_s
+      el1 << el2
+    end
+    
+    el1['created_at'] = self.created_at.xmlschema
+    el1['open'] = self.open.to_s
+
+    el1['min_lon'] = (bbox[0] / SCALE).to_s unless bbox[0].nil?
+    el1['min_lat'] = (bbox[1] / SCALE).to_s unless bbox[1].nil?
+    el1['max_lon'] = (bbox[2] / SCALE).to_s unless bbox[2].nil?
+    el1['max_lat'] = (bbox[3] / SCALE).to_s unless bbox[3].nil?
+    
+    # NOTE: changesets don't include the XML of the changes within them,
+    # they are just structures for tagging. to get the osmChange of a
+    # changeset, see the download method of the controller.
+
+    return el1
+  end
+end
diff --git a/app/models/changeset_tag.rb b/app/models/changeset_tag.rb
new file mode 100644 (file)
index 0000000..6298fbe
--- /dev/null
@@ -0,0 +1,5 @@
+class ChangesetTag < ActiveRecord::Base
+
+  belongs_to :changeset, :foreign_key => 'id'
+
+end
index dd1f9882a7a4726ffff14147292b1891a0584893..c20788fbbe7858b6056d5773ae2183a5941b0d2e 100644 (file)
@@ -5,6 +5,8 @@ class DiaryEntry < ActiveRecord::Base
                             :order => "diary_comments.id"
 
   validates_presence_of :title, :body
+  validates_length_of :title, :within => 1..255
+  validates_length_of :language, :within => 2..3
   validates_numericality_of :latitude, :allow_nil => true
   validates_numericality_of :longitude, :allow_nil => true
   validates_associated :user
index 97e411192b0df5cedbd89d7255f236ac7fb2cf35..464c5502837b56c5ffdf409ff32288f6b5779cac 100644 (file)
@@ -1,8 +1,12 @@
+require 'validators'
+
 class Message < ActiveRecord::Base
   belongs_to :sender, :class_name => "User", :foreign_key => :from_user_id
   belongs_to :recipient, :class_name => "User", :foreign_key => :to_user_id
 
-  validates_presence_of :title, :body, :sent_on
+  validates_presence_of :title, :body, :sent_on, :sender, :recipient
+  validates_length_of :title, :within => 1..255
   validates_inclusion_of :message_read, :in => [ true, false ]
   validates_associated :sender, :recipient
+  validates_as_utf8 :title
 end
index cec755f4765bfc35e9679256934512be093f74da..cf7aedae88c77692519022e11804678383a0bf6c 100644 (file)
@@ -2,21 +2,24 @@ class Node < ActiveRecord::Base
   require 'xml/libxml'
 
   include GeoRecord
+  include ConsistencyValidations
 
   set_table_name 'current_nodes'
-  
-  validates_presence_of :user_id, :timestamp
+
+  validates_presence_of :changeset_id, :timestamp
   validates_inclusion_of :visible, :in => [ true, false ]
   validates_numericality_of :latitude, :longitude
   validate :validate_position
 
-  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
 
@@ -50,7 +53,7 @@ class Node < ActiveRecord::Base
     #conditions = keys.join(' AND ')
  
     find_by_area(min_lat, min_lon, max_lat, max_lon,
-                    :conditions => 'visible = 1',
+                    :conditions => {:visible => true},
                     :limit => APP_CONFIG['max_number_of_nodes']+1)  
   end
 
@@ -60,83 +63,168 @@ class Node < ActiveRecord::Base
       p = XML::Parser.new
       p.string = xml
       doc = p.parse
-  
-      node = Node.new
 
       doc.find('//osm/node').each do |pt|
-        node.lat = pt['lat'].to_f
-        node.lon = pt['lon'].to_f
-
-        return nil unless node.in_world?
+        return Node.from_xml_node(pt, create)
+      end
+    rescue
+      return nil
+    end
+  end
 
-        unless create
-          if pt['id'] != '0'
-            node.id = pt['id'].to_i
-          end
-        end
+  def self.from_xml_node(pt, create=false)
+    node = Node.new
+    
+    node.lat = pt['lat'].to_f
+    node.lon = pt['lon'].to_f
+    node.changeset_id = pt['changeset'].to_i
 
-        node.visible = pt['visible'] and pt['visible'] == 'true'
+    return nil unless node.in_world?
 
-        if create
-          node.timestamp = Time.now
-        else
-          if pt['timestamp']
-            node.timestamp = Time.parse(pt['timestamp'])
-          end
-        end
+    # version must be present unless creating
+    return nil unless create or not pt['version'].nil?
+    node.version = create ? 0 : pt['version'].to_i
 
-        tags = []
+    unless create
+      if pt['id'] != '0'
+        node.id = pt['id'].to_i
+      end
+    end
 
-        pt.find('tag').each do |tag|
-          tags << [tag['k'],tag['v']]
-        end
+    # visible if it says it is, or as the default if the attribute
+    # is missing.
+    node.visible = pt['visible'].nil? or pt['visible'] == 'true'
 
-        node.tags = Tags.join(tags)
+    if create
+      node.timestamp = Time.now
+    else
+      if pt['timestamp']
+        node.timestamp = Time.parse(pt['timestamp'])
       end
-    rescue
-      node = nil
+    end
+
+    tags = []
+
+    pt.find('tag').each do |tag|
+      node.add_tag_key_val(tag['k'],tag['v'])
     end
 
     return node
   end
 
-  # Save this node with the appropriate OldNode object to represent it's history.
+  ##
+  # the bounding box around a node
+  def bbox
+    [ longitude, latitude, longitude, latitude ]
+  end
+
   def save_with_history!
+    t = Time.now
     Node.transaction do
-      self.timestamp = Time.now
+      self.version += 1
+      self.timestamp = t
       self.save!
+
+      # Create a NodeTag
+      tags = self.tags
+      NodeTag.delete_all(['id = ?', self.id])
+      tags.each do |k,v|
+        tag = NodeTag.new
+        tag.k = k 
+        tag.v = v 
+        tag.id = self.id
+        tag.save!
+      end 
+
+      # Create an OldNode
       old_node = OldNode.from_node(self)
-      old_node.save!
+      old_node.timestamp = t
+      old_node.save_with_dependencies!
+
+      # save the changeset in case of bounding box updates
+      changeset.save!
+    end
+  end
+
+  # Should probably be renamed delete_from to come in line with update
+  def delete_with_history!(new_node, user)
+    if self.visible
+      check_consistency(self, new_node, user)
+      if WayNode.find(:first, :joins => "INNER JOIN current_ways ON current_ways.id = current_way_nodes.id", :conditions => [ "current_ways.visible = ? 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
+    else
+      raise OSM::APIAlreadyDeletedError.new
     end
   end
 
-  # Turn this Node in to a complete OSM XML object with <osm> wrapper
+  def update_from(new_node, user)
+    check_consistency(self, new_node, user)
+
+    # update changeset with *old* position first
+    changeset.update_bbox!(bbox);
+
+    # FIXME logic needs to be double checked
+    self.changeset_id = new_node.changeset_id
+    self.latitude = new_node.latitude 
+    self.longitude = new_node.longitude
+    self.tags = new_node.tags
+    self.visible = true
+
+    # update changeset with *new* position
+    changeset.update_bbox!(bbox);
+
+    save_with_history!
+  end
+  
+  def create_with_history(user)
+    check_create_consistency(self, user)
+    self.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?
+    el1['user'] = user_display_name_cache[self.changeset.user_id] unless user_display_name_cache[self.changeset.user_id].nil?
 
-    Tags.split(self.tags) do |k,v|
+    self.tags.each do |k,v|
       el2 = XML::Node.new('tag')
       el2['k'] = k.to_s
       el2['v'] = v.to_s
@@ -148,12 +236,39 @@ 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
-    hash
+    @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 if @tags.include? k
+
+    @tags[k] = v
   end
+
+  ##
+  # dummy method to make the interfaces of node, way and relation
+  # more consistent.
+  def fix_placeholders!(id_map)
+    # nodes don't refer to anything, so there is nothing to do here
+  end
+
 end
diff --git a/app/models/node_tag.rb b/app/models/node_tag.rb
new file mode 100644 (file)
index 0000000..9795ff4
--- /dev/null
@@ -0,0 +1,5 @@
+class NodeTag < ActiveRecord::Base
+  set_table_name 'current_node_tags'
+
+  belongs_to :node, :foreign_key => 'id'
+end
index 76eab8427b2c570cce79846887706eb6c10923b6..91b5a1a8ea9024519a8372bf5b43a5220a9fdf04 100644 (file)
@@ -1,25 +1,20 @@
 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
 
-  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 +22,27 @@ 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
+    el1['user'] = self.changeset.user.display_name if self.changeset.user.data_public?
 
-    Tags.split(self.tags) do |k,v|
+    self.tags.each do |k,v|
       el2 = XML::Node.new('tag')
       el2['k'] = k.to_s
       el2['v'] = v.to_s
@@ -48,24 +51,58 @@ class OldNode < ActiveRecord::Base
 
     el1['visible'] = self.visible.to_s
     el1['timestamp'] = self.timestamp.xmlschema
+    el1['version'] = self.version.to_s
     return el1
   end
-  
-  def tags_as_hash
-    hash = {}
-    Tags.split(self.tags) do |k,v|
-      hash[k] = v
+
+  def save_with_dependencies!
+    save!
+    #not sure whats going on here
+    clear_aggregation_cache
+    clear_association_cache
+    #ok from here
+    @attributes.update(OldNode.find(:first, :conditions => ['id = ? AND timestamp = ? 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 
+    hash = {} 
+    Tags.split(self.tags) do |k,v| 
+      hash[k] = v 
+    end 
+    hash 
+  end 
+  # Pretend we're not in any ways 
+  def ways 
+    return [] 
+  end 
+  # Pretend we're not in any relations 
+  def containing_relation_members 
+    return [] 
+  end 
 end
diff --git a/app/models/old_node_tag.rb b/app/models/old_node_tag.rb
new file mode 100644 (file)
index 0000000..26a6c92
--- /dev/null
@@ -0,0 +1,7 @@
+class OldNodeTag < ActiveRecord::Base
+  belongs_to :user
+
+  set_table_name 'node_tags'
+
+
+end
index bac03c4d2eff6811a9dc4b5d2c32e3bb988e76b3..b7e7248d9bab19b246df2d97f5151d2ed90a7415 100644 (file)
@@ -1,14 +1,17 @@
 class OldRelation < ActiveRecord::Base
+  include ConsistencyValidations
+  
   set_table_name 'relations'
 
-  belongs_to :user
+  belongs_to :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
@@ -85,12 +88,20 @@ 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?
+    el1['user'] = self.changeset.user.display_name if self.changeset.user.data_public?
+    el1['version'] = self.version.to_s
+    el1['changeset'] = self.changeset_id.to_s
     
     self.old_members.each do |member|
       e = XML::Node.new 'member'
index 63265d6bf5c77814e90205cc4c0c5138a65a04c3..44155d05c8d58367d0656e62743d1be3dbef2e1d 100644 (file)
@@ -1,14 +1,17 @@
 class OldWay < ActiveRecord::Base
+  include ConsistencyValidations
+  
   set_table_name 'ways'
 
-  belongs_to :user
+  belongs_to :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 +96,9 @@ class OldWay < ActiveRecord::Base
     el1['id'] = self.id.to_s
     el1['visible'] = self.visible.to_s
     el1['timestamp'] = self.timestamp.xmlschema
-    el1['user'] = self.user.display_name if self.user.data_public?
+    el1['user'] = self.changeset.user.display_name if self.changeset.user.data_public?
+    el1['version'] = self.version.to_s
+    el1['changeset'] = self.changeset.id.to_s
     
     self.old_nodes.each do |nd| # FIXME need to make sure they come back in the right order
       e = XML::Node.new 'nd'
index c8516b58a3441c9f3b0ec38262d7628c8888d00f..be990e589a2135b0c88b714927ce6824bd29d8cb 100644 (file)
@@ -1,9 +1,11 @@
 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'
 
@@ -19,32 +21,39 @@ class Relation < ActiveRecord::Base
       p.string = xml
       doc = p.parse
 
-      relation = Relation.new
-
       doc.find('//osm/relation').each do |pt|
-        if !create and pt['id'] != '0'
-          relation.id = pt['id'].to_i
-        end
+       return Relation.from_xml_node(pt, create)
+      end
+    rescue
+      return nil
+    end
+  end
 
-        if create
-          relation.timestamp = Time.now
-          relation.visible = true
-        else
-          if pt['timestamp']
-            relation.timestamp = Time.parse(pt['timestamp'])
-          end
-        end
+  def self.from_xml_node(pt, create=false)
+    relation = Relation.new
 
-        pt.find('tag').each do |tag|
-          relation.add_tag_keyval(tag['k'], tag['v'])
-        end
+    if !create and pt['id'] != '0'
+      relation.id = pt['id'].to_i
+    end
 
-        pt.find('member').each do |member|
-          relation.add_member(member['type'], member['ref'], member['role'])
-        end
+    relation.version = pt['version']
+    relation.changeset_id = pt['changeset']
+
+    if create
+      relation.timestamp = Time.now
+      relation.visible = true
+    else
+      if pt['timestamp']
+        relation.timestamp = Time.parse(pt['timestamp'])
       end
-    rescue
-      relation = nil
+    end
+
+    pt.find('tag').each do |tag|
+      relation.add_tag_keyval(tag['k'], tag['v'])
+    end
+
+    pt.find('member').each do |member|
+      relation.add_member(member['type'], member['ref'], member['role'])
     end
 
     return relation
@@ -61,18 +70,20 @@ 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?
+    el1['user'] = user_display_name_cache[self.changeset.user_id] unless user_display_name_cache[self.changeset.user_id].nil?
 
     self.relation_members.each do |member|
       p=0
@@ -171,19 +182,52 @@ class Relation < ActiveRecord::Base
 
   def add_tag_keyval(k, v)
     @tags = Hash.new unless @tags
+
+    # duplicate tags are now forbidden, so we can't allow values
+    # in the hash to be overwritten.
+    raise OSM::APIDuplicateTagsError.new if @tags.include? k
+
     @tags[k] = v
   end
 
   def save_with_history!
     Relation.transaction do
+      # have to be a little bit clever here - to detect if any tags
+      # changed then we have to monitor their before and after state.
+      tags_changed = false
+
       t = Time.now
+      self.version += 1
       self.timestamp = t
       self.save!
 
       tags = self.tags
+      self.relation_tags.each do |old_tag|
+        key = old_tag.k
+        # if we can match the tags we currently have to the list
+        # of old tags, then we never set the tags_changed flag. but
+        # if any are different then set the flag and do the DB 
+        # update.
+        if tags.has_key? key 
+          # rails 2.1 dirty handling should take care of making this
+          # somewhat efficient... hopefully...
+          old_tag.v = tags[key]
+          tags_changed |= old_tag.changed?
+          old_tag.save!
+
+          # remove from the map, so that we can expect an empty map
+          # at the end if there are no new tags
+          tags.delete key
 
-      RelationTag.delete_all(['id = ?', self.id])
-
+        else
+          # this means a tag was deleted
+          tags_changed = true
+          RelationTag.delete_all ['id = ? and k = ?', self.id, old_tag.k]
+        end
+      end
+      # if there are left-over tags then they are new and will have to
+      # be added.
+      tags_changed |= (not tags.empty?)
       tags.each do |k,v|
         tag = RelationTag.new
         tag.k = k
@@ -192,25 +236,128 @@ class Relation < ActiveRecord::Base
         tag.save!
       end
 
-      members = self.members
-
-      RelationMember.delete_all(['id = ?', self.id])
+      # same pattern as before, but this time we're collecting the
+      # changed members in an array, as the bounding box updates for
+      # elements are per-element, not blanked on/off like for tags.
+      changed_members = Array.new
+      members = self.members_as_hash
+      relation_members.each do |old_member|
+        key = [old_member.member_id.to_s, old_member.member_type]
+        if members.has_key? key
+          # i'd love to rely on rails' dirty handling here, but the 
+          # relation members are always dirty because of the member_class
+          # handling.
+          if members[key] != old_member.member_role
+            old_member.member_role = members[key]
+            changed_members << key
+            old_member.save!
+          end
+          members.delete key
 
-      members.each do |n|
+        else
+          changed_members << key
+          RelationMember.delete_all ['id = ? and member_id = ? and member_type = ?', self.id, old_member.member_id, old_member.member_type]
+        end
+      end
+      # any remaining members must be new additions
+      changed_members += members.keys
+      members.each do |k,v|
         mem = RelationMember.new
         mem.id = self.id
-        mem.member_type = n[0];
-        mem.member_id = n[1];
-        mem.member_role = n[2];
+        mem.member_type = k[1];
+        mem.member_id = k[0];
+        mem.member_role = v;
         mem.save!
       end
 
       old_relation = OldRelation.from_relation(self)
       old_relation.timestamp = t
       old_relation.save_with_dependencies!
+
+      # update the bbox of the changeset and save it too.
+      # discussion on the mailing list gave the following definition for
+      # the bounding box update procedure of a relation:
+      #
+      # adding or removing nodes or ways from a relation causes them to be
+      # added to the changeset bounding box. adding a relation member or
+      # changing tag values causes all node and way members to be added to the
+      # bounding box. this is similar to how the map call does things and is
+      # reasonable on the assumption that adding or removing members doesn't
+      # materially change the rest of the relation.
+      any_relations = 
+        changed_members.collect { |id,type| type == "relation" }.
+        inject(false) { |b,s| b or s }
+
+      if tags_changed or any_relations
+        # add all non-relation bounding boxes to the changeset
+        # FIXME: check for tag changes along with element deletions and
+        # make sure that the deleted element's bounding box is hit.
+        self.members.each do |type, id, role|
+          if type != "relation"
+            update_changeset_element(type, id)
+          end
+        end
+      else
+        # add only changed members to the changeset
+        changed_members.each do |id, type|
+          update_changeset_element(type, id)
+        end
+      end
+
+      # save the (maybe updated) changeset bounding box
+      changeset.save!
+    end
+  end
+
+  ##
+  # updates the changeset bounding box to contain the bounding box of 
+  # the element with given +type+ and +id+. this only works with nodes
+  # and ways at the moment, as they're the only elements to respond to
+  # the :bbox call.
+  def update_changeset_element(type, id)
+    element = Kernel.const_get(type.capitalize).find(id)
+    changeset.update_bbox! element.bbox
+  end    
+
+  def delete_with_history!(new_relation, user)
+    if self.visible
+      check_consistency(self, new_relation, user)
+      if RelationMember.find(:first, :joins => "INNER JOIN current_relations ON current_relations.id=current_relation_members.id", :conditions => [ "visible = ? AND member_type='relation' and member_id=? ", true, self.id ])
+        raise OSM::APIPreconditionFailedError.new
+      else
+        self.changeset_id = new_relation.changeset_id
+        self.tags = {}
+        self.members = []
+        self.visible = false
+        save_with_history!
+      end
+    else
+      raise OSM::APIAlreadyDeletedError.new
     end
   end
 
+  def update_from(new_relation, user)
+    check_consistency(self, new_relation, user)
+    if !new_relation.preconditions_ok?
+      raise OSM::APIPreconditionFailedError.new
+    end
+    self.changeset_id = new_relation.changeset_id
+    self.tags = new_relation.tags
+    self.members = new_relation.members
+    self.visible = true
+    save_with_history!
+  end
+  
+  def create_with_history(user)
+    check_create_consistency(self, user)
+    if !self.preconditions_ok?
+      raise OSM::APIPreconditionFailedError.new
+    end
+    self.version = 0
+    self.visible = true
+    save_with_history!
+  end
+
   def preconditions_ok?
     # These are hastables that store an id in the index of all 
     # the nodes/way/relations that have already been added.
@@ -264,8 +411,37 @@ class Relation < ActiveRecord::Base
     return false
   end
 
+  ##
+  # members in a hash table [id,type] => role
+  def members_as_hash
+    h = Hash.new
+    members.each do |m|
+      # should be: h[[m.id, m.type]] = m.role, but someone prefers arrays
+      h[[m[1], m[0]]] = m[2]
+    end
+    return h
+  end
+
   # Temporary method to match interface to nodes
   def tags_as_hash
     return self.tags
   end
+
+  ##
+  # if any members are referenced by placeholder IDs (i.e: negative) then
+  # this calling this method will fix them using the map from placeholders 
+  # to IDs +id_map+. 
+  def fix_placeholders!(id_map)
+    self.members.map! do |type, id, role|
+      old_id = id.to_i
+      if old_id < 0
+        new_id = id_map[type.to_sym][old_id]
+        raise "invalid placeholder" if new_id.nil?
+        [type, new_id, role]
+      else
+        [type, id, role]
+      end
+    end
+  end
+
 end
index 10e867badc71381fc42ba6c93b4a7071f53ea827..1b44e218717cdea042dc30ce7f0359d719609e37 100644 (file)
@@ -3,6 +3,8 @@ class Trace < ActiveRecord::Base
 
   validates_presence_of :user_id, :name, :timestamp
   validates_presence_of :description, :on => :create
+  validates_length_of :name, :within => 1..255
+  validates_length_of :description, :within => 1..255
 #  validates_numericality_of :latitude, :longitude
   validates_inclusion_of :public, :inserted, :in => [ true, false]
   
index f1d5967d53dd5c38b75d591b792cf6a330cd69ad..f9833e141446125128a07ab5e03a6c21ba2f9bde 100644 (file)
@@ -2,6 +2,7 @@ class Tracetag < ActiveRecord::Base
   set_table_name 'gpx_file_tags'
 
   validates_format_of :tag, :with => /^[^\/;.,?]*$/
+  validates_length_of :tag, :within => 1..255
 
   belongs_to :trace, :foreign_key => 'gpx_id'
 end
index 9b5bfd59528538f1826e6fc993a26eafb4c7301e..80faf68e9137c7ba60d2f75ef6073ed2f598e7d3 100644 (file)
@@ -4,9 +4,9 @@ class User < ActiveRecord::Base
   has_many :traces
   has_many :diary_entries, :order => 'created_at DESC'
   has_many :messages, :foreign_key => :to_user_id, :order => 'sent_on DESC'
-  has_many :new_messages, :class_name => "Message", :foreign_key => :to_user_id, :conditions => "message_read = 0", :order => 'sent_on DESC'
+  has_many :new_messages, :class_name => "Message", :foreign_key => :to_user_id, :conditions => {:message_read => false}, :order => 'sent_on DESC'
   has_many :sent_messages, :class_name => "Message", :foreign_key => :from_user_id, :order => 'sent_on DESC'
-  has_many :friends, :include => :befriendee, :conditions => "users.visible = 1"
+  has_many :friends, :include => :befriendee, :conditions => ["users.visible = ?", true]
   has_many :tokens, :class_name => "UserToken"
   has_many :preferences, :class_name => "UserPreference"
 
@@ -15,8 +15,9 @@ class User < ActiveRecord::Base
   validates_confirmation_of :pass_crypt, :message => 'Password must match the confirmation password'
   validates_uniqueness_of :display_name, :allow_nil => true
   validates_uniqueness_of :email
-  validates_length_of :pass_crypt, :minimum => 8
-  validates_length_of :display_name, :minimum => 3, :allow_nil => true
+  validates_length_of :pass_crypt, :within => 8..255
+  validates_length_of :display_name, :within => 3..255, :allow_nil => true
+  validates_length_of :email, :within => 6..255
   validates_format_of :email, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i
   validates_format_of :display_name, :with => /^[^\/;.,?]*$/
   validates_numericality_of :home_lat, :allow_nil => true
@@ -80,7 +81,7 @@ class User < ActiveRecord::Base
     if self.home_lon and self.home_lat 
       gc = OSM::GreatCircle.new(self.home_lat, self.home_lon)
       bounds = gc.bounds(radius)
-      nearby = User.find(:all, :conditions => "visible = 1 and home_lat between #{bounds[:minlat]} and #{bounds[:maxlat]} and home_lon between #{bounds[:minlon]} and #{bounds[:maxlon]} and data_public = 1 and id != #{self.id}")
+      nearby = User.find(:all, :conditions => ["visible = ? and home_lat between #{bounds[:minlat]} and #{bounds[:maxlat]} and home_lon between #{bounds[:minlon]} and #{bounds[:maxlon]} and data_public = ? and id != #{self.id}", true, true])
       nearby.delete_if { |u| gc.distance(u.home_lat, u.home_lon) > radius }
       nearby.sort! { |u1,u2| gc.distance(u1.home_lat, u1.home_lon) <=> gc.distance(u2.home_lat, u2.home_lon) }
     else
index 3985a527ec5f62e83b53b948b72164a48789a08d..28ef40f1d5c8347597c9f9a790b4d8d002cb63ce 100644 (file)
@@ -1,6 +1,9 @@
 class UserPreference < ActiveRecord::Base
   set_primary_keys :user_id, :k
   belongs_to :user
+  
+  validates_length_of :k, :within => 1..255
+  validates_length_of :v, :within => 1..255
 
   # Turn this Node in to an XML Node without the <osm> wrapper.
   def to_xml_node
index 958944200df628054c67c6c0eb8e60105d99bb38..c9e695b32e0193855b5e2b0c0cdf5acc59cd08a8 100644 (file)
@@ -1,9 +1,14 @@
 class Way < ActiveRecord::Base
   require 'xml/libxml'
+  
+  include ConsistencyValidations
 
   set_table_name 'current_ways'
 
-  belongs_to :user
+  validates_presence_of :changeset_id, :timestamp
+  validates_inclusion_of :visible, :in => [ true, false ]
+  
+  belongs_to :changeset
 
   has_many :old_ways, :foreign_key => 'id', :order => 'version'
 
@@ -21,32 +26,41 @@ class Way < ActiveRecord::Base
       p.string = xml
       doc = p.parse
 
-      way = Way.new
-
       doc.find('//osm/way').each do |pt|
-        if !create and pt['id'] != '0'
-          way.id = pt['id'].to_i
-        end
+       return Way.from_xml_node(pt, create)
+      end
+    rescue
+      return nil
+    end
+  end
 
-        if create
-          way.timestamp = Time.now
-          way.visible = true
-        else
-          if pt['timestamp']
-            way.timestamp = Time.parse(pt['timestamp'])
-          end
-        end
+  def self.from_xml_node(pt, create=false)
+    way = Way.new
 
-        pt.find('tag').each do |tag|
-          way.add_tag_keyval(tag['k'], tag['v'])
-        end
+    if !create and pt['id'] != '0'
+      way.id = pt['id'].to_i
+    end
+    
+    way.version = pt['version']
+    way.changeset_id = pt['changeset']
 
-        pt.find('nd').each do |nd|
-          way.add_nd_num(nd['ref'])
-        end
+    if create
+      way.timestamp = Time.now
+      way.visible = true
+    else
+      if pt['timestamp']
+        way.timestamp = Time.parse(pt['timestamp'])
       end
-    rescue
-      way = nil
+      # if visible isn't present then it defaults to true
+      way.visible = (pt['visible'] or true)
+    end
+
+    pt.find('tag').each do |tag|
+      way.add_tag_keyval(tag['k'], tag['v'])
+    end
+
+    pt.find('nd').each do |nd|
+      way.add_nd_num(nd['ref'])
     end
 
     return way
@@ -74,18 +88,20 @@ 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?
+    el1['user'] = user_display_name_cache[self.changeset.user_id] unless user_display_name_cache[self.changeset.user_id].nil?
 
     # make sure nodes are output in sequence_id order
     ordered_nodes = []
@@ -155,22 +171,39 @@ class Way < ActiveRecord::Base
 
   def add_tag_keyval(k, v)
     @tags = Hash.new unless @tags
+
+    # duplicate tags are now forbidden, so we can't allow values
+    # in the hash to be overwritten.
+    raise OSM::APIDuplicateTagsError.new if @tags.include? k
+
     @tags[k] = v
   end
 
+  ##
+  # the integer coords (i.e: unscaled) bounding box of the way, assuming
+  # straight line segments.
+  def bbox
+    lons = nodes.collect { |n| n.longitude }
+    lats = nodes.collect { |n| n.latitude }
+    [ lons.min, lats.min, lons.max, lats.max ]
+  end
+
   def save_with_history!
     t = Time.now
 
+    # update the bounding box, but don't save it as the controller knows the 
+    # lifetime of the change better. note that this has to be done both before 
+    # and after the save, so that nodes from both versions are included in the 
+    # bbox.
+    changeset.update_bbox!(bbox) unless nodes.empty?
+
     Way.transaction do
+      self.version += 1
       self.timestamp = t
       self.save!
-    end
 
-    WayTag.transaction do
       tags = self.tags
-
       WayTag.delete_all(['id = ?', self.id])
-
       tags.each do |k,v|
         tag = WayTag.new
         tag.k = k
@@ -178,13 +211,9 @@ class Way < ActiveRecord::Base
         tag.id = self.id
         tag.save!
       end
-    end
 
-    WayNode.transaction do
       nds = self.nds
-
       WayNode.delete_all(['id = ?', self.id])
-
       sequence = 1
       nds.each do |n|
         nd = WayNode.new
@@ -193,15 +222,45 @@ class Way < ActiveRecord::Base
         nd.save!
         sequence += 1
       end
+
+      old_way = OldWay.from_way(self)
+      old_way.timestamp = t
+      old_way.save_with_dependencies!
+
+      # update and commit the bounding box, now that way nodes 
+      # have been updated and we're in a transaction.
+      changeset.update_bbox!(bbox) unless nodes.empty?
+      changeset.save!
+    end
+  end
+
+  def update_from(new_way, user)
+    check_consistency(self, new_way, user)
+    if !new_way.preconditions_ok?
+      raise OSM::APIPreconditionFailedError.new
     end
+    self.changeset_id = new_way.changeset_id
+    self.tags = new_way.tags
+    self.nds = new_way.nds
+    self.visible = true
+    save_with_history!
+  end
 
-    old_way = OldWay.from_way(self)
-    old_way.timestamp = t
-    old_way.save_with_dependencies!
+  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?
     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
@@ -211,18 +270,14 @@ class Way < ActiveRecord::Base
     return true
   end
 
-  # Delete the way and it's relations, but don't really delete it - set its visibility to false and update the history etc to maintain wiki-like functionality.
-  def delete_with_relations_and_history(user)
+  def delete_with_history!(new_way, user)
+    check_consistency(self, new_way, user)
     if self.visible
-         # FIXME
-         # this should actually delete the relations,
-         # not just throw a PreconditionFailed if it's a member of a relation!!
       if RelationMember.find(:first, :joins => "INNER JOIN current_relations ON current_relations.id=current_relation_members.id",
-                             :conditions => [ "visible = 1 AND member_type='way' and member_id=?", self.id])
+                             :conditions => [ "visible = ? AND member_type='way' and member_id=? ", true, self.id])
         raise OSM::APIPreconditionFailedError
-      # end FIXME
       else
-        self.user_id = user.id
+        self.changeset_id = new_way.changeset_id
         self.tags = []
         self.nds = []
         self.visible = false
@@ -234,6 +289,8 @@ class Way < ActiveRecord::Base
   end
 
   # delete a way and it's nodes that aren't part of other ways, with history
+
+  # FIXME: merge the potlatch code to delete the relations
   def delete_with_relations_and_nodes_and_history(user)
     # delete the nodes not used by other ways
     self.unshared_node_ids.each do |node_id|
@@ -243,9 +300,10 @@ class Way < ActiveRecord::Base
       n.save_with_history!
     end
     
+    # FIXME needs more information passed in so that the changeset can be updated
     self.user_id = user.id
 
-    self.delete_with_relations_and_history(user)
+    self.delete_with_history(user)
   end
 
   # Find nodes that belong to this way only
@@ -264,4 +322,21 @@ class Way < ActiveRecord::Base
   def tags_as_hash
     return self.tags
   end
+
+  ##
+  # if any referenced nodes are placeholder IDs (i.e: are negative) then
+  # this calling this method will fix them using the map from placeholders 
+  # to IDs +id_map+. 
+  def fix_placeholders!(id_map)
+    self.nds.map! do |node_id|
+      if node_id < 0
+        new_id = id_map[:node][node_id]
+        raise "invalid placeholder for #{node_id.inspect}: #{new_id.inspect}" if new_id.nil?
+        new_id
+      else
+        node_id
+      end
+    end
+  end
+
 end
diff --git a/app/views/browse/_changeset_details.rhtml b/app/views/browse/_changeset_details.rhtml
new file mode 100644 (file)
index 0000000..27335dd
--- /dev/null
@@ -0,0 +1,68 @@
+<table>
+
+  <tr>
+    <th>Created at:</th>
+    <td><%= h(changeset_details.created_at) %></td>
+  </tr>
+  
+  <% if changeset_details.user.data_public? %>
+    <tr>
+      <th>Belongs to:</th>
+      <td><%= link_to h(changeset_details.user.display_name), :controller => "user", :action => "view", :display_name => changeset_details.user.display_name %></td>
+    </tr>
+  <% end %>
+  
+  <% unless changeset_details.tags_as_hash.empty? %>
+    <tr valign="top">
+      <th>Tags:</th>
+      <td>
+        <table padding="0">
+          <%= render :partial => "tag", :collection => changeset_details.tags_as_hash %>
+        </table>
+      </td>
+    </tr>
+    <% end %>
+
+  <% unless changeset_details.old_nodes.empty? %>
+    <tr valign="top">
+      <th>Has the following nodes:</th>
+      <td>
+        <table padding="0">
+          <% changeset_details.old_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>
+  <% end %>
+  
+  <% unless changeset_details.old_ways.empty? %>
+    <tr valign="top">
+      <th>Has the following ways:</th>
+      <td>
+        <table padding="0">
+          <% changeset_details.old_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>      
+  <% end %>
+  
+  <% unless changeset_details.old_relations.empty? %>
+    <tr valign="top">
+      <th>Has the following relations:</th>
+      <td>
+        <table padding="0">
+          <% changeset_details.old_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>
+  <% end %>
+
+</table>
index ee5f22ceebee4990058fb8be17b10dbaf43a3c4e..1f9f9ffe677ed5609b1bef337c978a62dd96c4b5 100644 (file)
@@ -3,13 +3,23 @@
   <td><%= h(common_details.timestamp) %></td>
 </tr>
 
-<% if common_details.user.data_public %>
+<% if common_details.changeset.user.data_public? %>
   <tr>
     <th>Edited by:</th>
-    <td><%= link_to h(common_details.user.display_name), :controller => "user", :action => "view", :display_name => common_details.user.display_name %></td>
+    <td><%= link_to h(common_details.changeset.user.display_name), :controller => "user", :action => "view", :display_name => common_details.changeset.user.display_name %></td>
   </tr>
 <% end %>
 
+<tr>
+  <th>Version:</th>
+  <td><%= h(common_details.version) %></td>
+</tr>
+
+<tr>
+  <th>In changeset:</th>
+  <td><%= link_to common_details.changeset_id, :action => :changeset %></td>
+</tr>
+
 <% unless common_details.tags_as_hash.empty? %>
   <tr valign="top">
     <th>Tags:</th>
diff --git a/app/views/browse/changeset.rhtml b/app/views/browse/changeset.rhtml
new file mode 100644 (file)
index 0000000..9345eb0
--- /dev/null
@@ -0,0 +1,17 @@
+<table width="100%">
+  <tr>
+    <td>
+      <h2>Changeset: <%= h(@changeset.id) %></h2>
+    </td>
+    <td>
+      <%= render :partial => "navigation" %>
+    </td>
+  </tr>
+  <tr valign="top">
+    <td>
+    <%= render :partial => "changeset_details", :object => @changeset %>
+    <hr />
+      <%= link_to "Download XML", :controller => "changeset", :action => "read" %>
+    </td>
+  </tr>
+</table>
index 2cd5cc9da4c09c1f46655033c93042bc0e94bb5f..e9d830a1063d511fc43e0a81a2adcb172e70c128 100644 (file)
@@ -1,11 +1,11 @@
 <h2><%= @nodes.length %> Recently Changed Nodes</h2> 
-<ul>
+<ul id="recently_changed">
 <% @nodes.each do |node| 
    name = node.tags_as_hash['name'].to_s 
    if name.length == 0:
      name = "(No name)"
    end
-   name = name + " - " + node.id.to_s 
+   name = "#{name} - #{node.id} (#{node.version})"
 %>
    <li><%= link_to h(name), :action => "node", :id => node.id %></li>
 <% end %>
diff --git a/app/views/browse/not_found.rhtml b/app/views/browse/not_found.rhtml
new file mode 100644 (file)
index 0000000..1322a0a
--- /dev/null
@@ -0,0 +1 @@
+<p>Sorry, the <%= @type -%> with the id <%= params[:id] -%>, could not be found.</p>
index c17325ad19c6a94b1b87e245d39ee3fabc8e0f01..f38b1dc80937cb0c9f6355236fd87a2b50b01e49 100644 (file)
@@ -189,7 +189,7 @@ page << <<EOJ
     if (size > 0.25) {
       setStatus("Unable to load: Bounding box size of " + size + " is too large (must be smaller than 0.25)");
     } else {
-      loadGML("/api/0.5/map?bbox=" + projected.toBBOX());
+      loadGML("/api/#{API_VERSION}/map?bbox=" + projected.toBBOX());
     }
   }
 
@@ -393,7 +393,7 @@ page << <<EOJ
     this.link.href = "";
     this.link.innerHTML = "Wait...";
 
-    new Ajax.Request("/api/0.5/" + this.type + "/" + this.feature.osm_id + "/history", {
+    new Ajax.Request("/api/#{API_VERSION}/" + this.type + "/" + this.feature.osm_id + "/history", {
       onComplete: OpenLayers.Function.bind(displayHistory, this)
     });
 
index 7aa2db7b9432cdb5d44a90e7e38d6fa9e0866e33..cf7ad9fc811f839adc9fe2cf413831ebb6954996 100644 (file)
@@ -7,6 +7,7 @@
     <%= stylesheet_link_tag 'site' %>
     <%= stylesheet_link_tag 'print', :media => "print" %>
     <%= tag("link", { :rel => "search", :type => "application/opensearchdescription+xml", :title => "OpenStreetMap Search", :href => "/opensearch/osm.xml" }) %>
+    <%= tag("meta", { :name => "description", :content => "OpenStreetMap is the free wiki world map." }) %>
     <title>OpenStreetMap<%= ' | '+ h(@title) if @title %></title>
   </head>
   <body>
             <input type="hidden" name="encrypted" value="-----BEGIN PKCS7-----MIIHTwYJKoZIhvcNAQcEoIIHQDCCBzwCAQExggEwMIIBLAIBADCBlDCBjjELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtQYXlQYWwgSW5jLjETMBEGA1UECxQKbGl2ZV9jZXJ0czERMA8GA1UEAxQIbGl2ZV9hcGkxHDAaBgkqhkiG9w0BCQEWDXJlQHBheXBhbC5jb20CAQAwDQYJKoZIhvcNAQEBBQAEgYCsNDDDDa7OZFojBzDvG4HSPXOiJSO3VNuLoc8HGwsds3LsZYYtv4cPGw7Z/SoVVda+RELM+5FQn0D3Kv7hjA2Z6QdwEkFH2kDDlXCvyPt53ENHkQrzC1KOueRpimsQMH5hl03nvuVXij0hEYlMFqTH0UZr80vyczB+lJU6ZKYtrDELMAkGBSsOAwIaBQAwgcwGCSqGSIb3DQEHATAUBggqhkiG9w0DBwQIZa12CIRB0geAgahqF6Otz0oY0+Wg56fSuEpZvbUmNGEQznjWqBXkJqTkZT0jOwekOrlEi7bNEU8yVIie2u5L1gOhBDSl6rmgpxxVURSa4Jig5qiSioyK5baH6HjXVPQ+MDEWg1gZ4LtjYYtroZ8SBE/1eikQWmG7EOEgU62Vn/jqJJ77/mgS7mdEQhlEWYMiyJBZs35yCB/pK5FUxhZnrquL4sS+2QKHPPOGPDfRc/dnhMKgggOHMIIDgzCCAuygAwIBAgIBADANBgkqhkiG9w0BAQUFADCBjjELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtQYXlQYWwgSW5jLjETMBEGA1UECxQKbGl2ZV9jZXJ0czERMA8GA1UEAxQIbGl2ZV9hcGkxHDAaBgkqhkiG9w0BCQEWDXJlQHBheXBhbC5jb20wHhcNMDQwMjEzMTAxMzE1WhcNMzUwMjEzMTAxMzE1WjCBjjELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtQYXlQYWwgSW5jLjETMBEGA1UECxQKbGl2ZV9jZXJ0czERMA8GA1UEAxQIbGl2ZV9hcGkxHDAaBgkqhkiG9w0BCQEWDXJlQHBheXBhbC5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMFHTt38RMxLXJyO2SmS+Ndl72T7oKJ4u4uw+6awntALWh03PewmIJuzbALScsTS4sZoS1fKciBGoh11gIfHzylvkdNe/hJl66/RGqrj5rFb08sAABNTzDTiqqNpJeBsYs/c2aiGozptX2RlnBktH+SUNpAajW724Nv2Wvhif6sFAgMBAAGjge4wgeswHQYDVR0OBBYEFJaffLvGbxe9WT9S1wob7BDWZJRrMIG7BgNVHSMEgbMwgbCAFJaffLvGbxe9WT9S1wob7BDWZJRroYGUpIGRMIGOMQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxFDASBgNVBAoTC1BheVBhbCBJbmMuMRMwEQYDVQQLFApsaXZlX2NlcnRzMREwDwYDVQQDFAhsaXZlX2FwaTEcMBoGCSqGSIb3DQEJARYNcmVAcGF5cGFsLmNvbYIBADAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4GBAIFfOlaagFrl71+jq6OKidbWFSE+Q4FqROvdgIONth+8kSK//Y/4ihuE4Ymvzn5ceE3S/iBSQQMjyvb+s2TWbQYDwcp129OPIbD9epdr4tJOUNiSojw7BHwYRiPh58S1xGlFgHFXwrEBb3dgNbMUa+u4qectsMAXpVHnD9wIyfmHMYIBmjCCAZYCAQEwgZQwgY4xCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDQTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEUMBIGA1UEChMLUGF5UGFsIEluYy4xEzARBgNVBAsUCmxpdmVfY2VydHMxETAPBgNVBAMUCGxpdmVfYXBpMRwwGgYJKoZIhvcNAQkBFg1yZUBwYXlwYWwuY29tAgEAMAkGBSsOAwIaBQCgXTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0wNjA4MjYwODQ2NDdaMCMGCSqGSIb3DQEJBDEWBBTyC1ZchvuTMtcYeudPPSP/w8HiEDANBgkqhkiG9w0BAQEFAASBgJPpBf69pRAJfhzv/MfPiMncuq3TSlvpX7VtG9p4dXzSko4i2lWUDD72r5zdF2NwDgZ6avf630PutgpOzYJQ525If1xU2olc9DWI43UZTqY+FArgFuCJ8VnkPsy9mcbXPoSjLRqNwrsA2yoETxMISO3ASELzELJTJgpPk4bU57eZ-----END PKCS7-----" />
           </form>
 
-          <a href="http://creativecommons.org/licenses/by-sa/2.0/"><img src="/images/cc_button.png" border="0" alt="" /></a>
+          <%= link_to (image_tag "cc_button.png", :alt => "CC by-sa 2.0", :border => "0"), "http://creativecommons.org/licenses/by-sa/2.0/" %>
 
         </center>
       </div>
index 85ebe9f2152884b044c2f73c136ffaee0cd642fa..7400a7b9a916088639aa0a092198f9c3000d82f4 100644 (file)
@@ -3,8 +3,10 @@ standard_settings: &standard_settings
   max_request_area: 0.25
   # Number of GPS trace/trackpoints returned per-page
   tracepoints_per_page: 5000
-  # Maximum number of nodes
+  # Maximum number of nodes that will be returned by the api in a map request
   max_number_of_nodes: 50000
+  # Maximum number of nodes that can be in a way (checked on save)
+  max_number_of_way_nodes: 2000
  
 development:
   <<: *standard_settings
index b884f3b938fea8c5ea541e0d4af5fbbc16529dd7..cc3f9a1a5fb26e4fd5b2186728c3e4d5c6b45127 100644 (file)
@@ -16,6 +16,7 @@ development:
   username: openstreetmap
   password: openstreetmap
   host: localhost
+  encoding: utf8
 
 # Warning: The database defined as 'test' will be erased and
 # re-generated from your development database when you run 'rake'.
@@ -23,14 +24,15 @@ development:
 test:
   adapter: mysql
   database: osm_test
-  username: root
-  password:
+  username: osm_test
+  password: osm_test
   host: localhost
+  encoding: utf8
 
 production:
   adapter: mysql
-  database: openstreetmap
-  username: openstreetmap
-  password: openstreetmap
-  host: db.openstreetmap.org
-
+  database: osm
+  username: osm
+  password: osm
+  host: localhost
+  encoding: utf8
index e6af619eb82b03aaaf8400db39ea951dbba227b6..171bb6fc8e5d4b688cad375ac39574e953669259 100644 (file)
@@ -5,13 +5,16 @@
 ENV['RAILS_ENV'] ||= 'production'
 
 # Specifies gem version of Rails to use when vendor/rails is not present
-RAILS_GEM_VERSION = '2.0.2' unless defined? RAILS_GEM_VERSION
+RAILS_GEM_VERSION = '2.1.2' unless defined? RAILS_GEM_VERSION
 
 # Set the server URL
 SERVER_URL = ENV['OSM_SERVER_URL'] || 'www.openstreetmap.org'
 
+# Set the generator
+GENERATOR = ENV['OSM_SERVER_GENERATOR'] || 'OpenStreetMap server'
+
 # Application constants needed for routes.rb - must go before Initializer call
-API_VERSION = ENV['OSM_API_VERSION'] || '0.5'
+API_VERSION = ENV['OSM_API_VERSION'] || '0.6'
 
 # Set application status - possible settings are:
 #
@@ -37,6 +40,16 @@ Rails::Initializer.run do |config|
     config.frameworks -= [ :active_record ]
   end
 
+  # Specify gems that this application depends on. 
+  # They can then be installed with "rake gems:install" on new installations.
+  # config.gem "bj"
+  # config.gem "hpricot", :version => '0.6', :source => "http://code.whytheluckystiff.net"
+  # config.gem "aws-s3", :lib => "aws/s3"
+  config.gem 'composite_primary_keys', :version => '1.0.10'
+  config.gem 'libxml-ruby', :version => '>= 0.8.3', :lib => 'libxml'
+  config.gem 'rmagick', :lib => 'RMagick'
+  config.gem 'mysql'
+
   # Only load the plugins named here, in the order given. By default, all plugins 
   # in vendor/plugins are loaded in alphabetical order.
   # :all can be used as a placeholder for all plugins not explicitly named
@@ -63,6 +76,12 @@ Rails::Initializer.run do |config|
   # (create the session table with 'rake db:sessions:create')
   config.action_controller.session_store = :sql_session_store
 
+  # We will use the old style of migrations, rather than the newer
+  # timestamped migrations that were introduced with Rails 2.1, as
+  # it will be confusing to have the numbered and timestamped migrations
+  # together in the same folder.
+  config.active_record.timestamped_migrations = false
+
   # Use SQL instead of Active Record's schema dumper when creating the test database.
   # This is necessary if your schema can't be completely dumped by the schema dumper,
   # like if you have constraints or database-specific column types
index 09a451f9a336aa17352c3421db1cc593c593155d..85c9a6080ea865b51fee9a43e23a6969d709ffb4 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
diff --git a/config/initializers/composite_primary_keys.rb b/config/initializers/composite_primary_keys.rb
deleted file mode 100644 (file)
index 430bcfa..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-require 'rubygems'
-gem 'composite_primary_keys', '= 0.9.93'
-require 'composite_primary_keys'
index a1870dbab8b4aaaacfe5e5169bcb40e622f3dc11..3b5919f0fca8c0a65c7a0f48f75d099f275bc4fa 100644 (file)
@@ -1,7 +1,5 @@
-require 'rubygems'
-gem 'libxml-ruby', '>= 0.8.3'
-require 'libxml'
-
+# This is required otherwise libxml writes out memory errors to
+# the standard output and exits uncleanly 
 LibXML::XML::Parser.register_error_handler do |message|
   raise message
 end
index b7d570980de7876f3f344848e215527a289e74b2..91130b9b01d2d209a2213ddb39963dfe89972ed0 100644 (file)
@@ -1,10 +1,19 @@
 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", :controller => 'changeset', :action => 'read', :id => /\d+/
+  map.connect "api/#{API_VERSION}/changeset/:id/close", :controller => 'changeset', :action => 'close', :id =>/\d+/
+  
   map.connect "api/#{API_VERSION}/node/create", :controller => 'node', :action => 'create'
   map.connect "api/#{API_VERSION}/node/:id/ways", :controller => 'way', :action => 'ways_for_node', :id => /\d+/
   map.connect "api/#{API_VERSION}/node/:id/relations", :controller => 'relation', :action => 'relations_for_node', :id => /\d+/
   map.connect "api/#{API_VERSION}/node/:id/history", :controller => 'old_node', :action => 'history', :id => /\d+/
+  map.connect "api/#{API_VERSION}/node/:id/:version", :controller => 'old_node', :action => 'version', :id => /\d+/, :version => /\d+/
   map.connect "api/#{API_VERSION}/node/:id", :controller => 'node', :action => 'read', :id => /\d+/, :conditions => { :method => :get }
   map.connect "api/#{API_VERSION}/node/:id", :controller => 'node', :action => 'update', :id => /\d+/, :conditions => { :method => :put }
   map.connect "api/#{API_VERSION}/node/:id", :controller => 'node', :action => 'delete', :id => /\d+/, :conditions => { :method => :delete }
@@ -14,12 +23,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+/
@@ -56,6 +65,7 @@ ActionController::Routing::Routes.draw do |map|
   
   map.connect "api/#{API_VERSION}/amf/read", :controller =>'amf', :action =>'amf_read'
   map.connect "api/#{API_VERSION}/amf/write", :controller =>'amf', :action =>'amf_write'
+  map.connect "api/#{API_VERSION}/amf", :controller =>'amf', :action =>'talk'
   map.connect "api/#{API_VERSION}/swf/trackpoints", :controller =>'swf', :action =>'trackpoints'
   
   # Data browsing
@@ -67,6 +77,7 @@ ActionController::Routing::Routes.draw do |map|
   map.connect '/browse/node/:id/history', :controller => 'browse', :action => 'node_history', :id => /\d+/
   map.connect '/browse/relation/:id', :controller => 'browse', :action => 'relation', :id => /\d+/
   map.connect '/browse/relation/:id/history', :controller => 'browse', :action => 'relation_history', :id => /\d+/
+  map.connect '/browse/changeset/:id', :controller => 'browse', :action => 'changeset', :id => /\d+/
   
   # web site
 
index 2c80dd8adeac4c053a6ce5297d8849a1483d1671..689ca3c209986638319ba6a2b26d6a5be2e23f30 100644 (file)
@@ -110,7 +110,7 @@ class CreateOsmDb < ActiveRecord::Migration
     add_primary_key "gpx_file_tags", ["id"]
     add_index "gpx_file_tags", ["gpx_id"], :name => "gpx_file_tags_gpxid_idx"
 
-    change_column "gpx_file_tags", "id", :integer, :limit => 20, :null => false, :options => "AUTO_INCREMENT"
+    change_column "gpx_file_tags", "id", :integer, :null => false, :options => "AUTO_INCREMENT"
 
     create_table "gpx_files", myisam_table do |t|
       t.column "id",          :bigint,   :limit => 64,                   :null => false
diff --git a/db/migrate/017_add_timestamp_indexes.rb b/db/migrate/017_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/018_populate_node_tags_and_remove.rb b/db/migrate/018_populate_node_tags_and_remove.rb
new file mode 100644 (file)
index 0000000..f10bf16
--- /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, "017_populate_node_tags_and_remove.#{$$}."
+
+      cmd = "db/migrate/017_populate_node_tags_and_remove_helper"
+      src = "#{cmd}.c"
+      if not File.exists? cmd or File.mtime(cmd) < File.mtime(src) then 
+        system 'cc -O3 -Wall `mysql_config --cflags --libs` ' +
+          "#{src} -o #{cmd}" or fail
+      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/018_populate_node_tags_and_remove_helper.c b/db/migrate/018_populate_node_tags_and_remove_helper.c
new file mode 100644 (file)
index 0000000..83c1b17
--- /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, "018_populate_node_tags_and_remove_helper: MySQL error: %s\n", err);
+  } else {
+    fprintf(stderr, "018_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: 018_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/019_move_to_innodb.rb b/db/migrate/019_move_to_innodb.rb
new file mode 100644 (file)
index 0000000..d17da8f
--- /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
+    execute 'DROP INDEX current_way_tags_v_idx ON current_way_tags'
+    execute 'DROP INDEX current_relation_tags_v_idx ON current_relation_tags'
+
+    @@ver_tbl.each { |tbl|
+      change_column tbl, "version", :bigint, :limit => 20, :null => false
+    }
+
+    @@conv_tables.each { |tbl|
+      execute "ALTER TABLE #{tbl} ENGINE = InnoDB"
+    }
+
+    @@ver_tbl.each { |tbl|
+      add_column "current_#{tbl}", "version", :bigint, :limit => 20, :null => false
+      # 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/020_key_constraints.rb b/db/migrate/020_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/021_add_changesets.rb b/db/migrate/021_add_changesets.rb
new file mode 100644 (file)
index 0000000..772a5f2
--- /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 "user_id",        :bigint,   :limit => 20, :null => false
+      t.column "created_at",     :datetime,               :null => false
+      t.column "open",           :boolean,                :null => false, :default => true
+      t.column "min_lat",        :integer,                :null => true
+      t.column "max_lat",        :integer,                :null => true
+      t.column "min_lon",        :integer,                :null => true
+      t.column "max_lon",        :integer,                :null => true
+    end
+    add_column :changesets, :id, :bigint_pk
+
+    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, 0 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
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..8fd6c25
--- /dev/null
@@ -0,0 +1,30 @@
+module ConsistencyValidations
+  # Generic checks that are run for the updates and deletes of
+  # node, ways and relations. This code is here to avoid duplication, 
+  # and allow the extention of the checks without having to modify the
+  # code in 6 places for all the updates and deletes. Some of these tests are 
+  # needed for creates, but are currently not run :-( 
+  # This will throw an exception if there is an inconsistency
+  def check_consistency(old, new, user)
+    if new.version != old.version
+      raise OSM::APIVersionMismatchError.new(new.version, old.version)
+    elsif new.changeset.nil?
+      raise OSM::APIChangesetMissingError.new
+    elsif new.changeset.user_id != user.id
+      raise OSM::APIUserChangesetMismatchError.new
+    elsif not new.changeset.is_open?
+      raise OSM::APIChangesetAlreadyClosedError.new
+    end
+  end
+  
+  # This is similar to above, just some validations don't apply
+  def check_create_consistency(new, user)
+    if new.changeset.nil?
+      raise OSM::APIChangesetMissingError.new
+    elsif new.changeset.user_id != user.id
+      raise OSM::APIUserChangesetMismatchError.new
+    elsif not new.changeset.is_open?
+      raise OSM::APIChangesetAlreadyClosedError.new
+    end
+  end
+end
diff --git a/lib/diff_reader.rb b/lib/diff_reader.rb
new file mode 100644 (file)
index 0000000..d793f63
--- /dev/null
@@ -0,0 +1,166 @@
+##
+# DiffReader reads OSM diffs and applies them to the database.
+#
+# Uses the streaming LibXML "Reader" interface to cut down on memory
+# usage, so hopefully we can process fairly large diffs.
+class DiffReader
+  include ConsistencyValidations
+
+  # maps each element type to the model class which handles it
+  MODELS = { 
+    "node"     => Node, 
+    "way"      => Way, 
+    "relation" => Relation
+  }
+
+  ##
+  # Construct a diff reader by giving it a bunch of XML +data+ to parse
+  # in OsmChange format. All diffs must be limited to a single changeset
+  # given in +changeset+.
+  def initialize(data, changeset)
+    @reader = XML::Reader.new data
+    @changeset = changeset
+  end
+
+  ##
+  # An element-block mapping for using the LibXML reader interface. 
+  #
+  # Since a lot of LibXML reader usage is boilerplate iteration through
+  # elements, it would be better to DRY and do this in a block. This
+  # could also help with error handling...?
+  def with_element
+    # skip the first element, which is our opening element of the block
+    @reader.read
+    # loop over all elements. 
+    # NOTE: XML::Reader#read returns 0 for EOF and -1 for error.
+    while @reader.read == 1
+      break if @reader.node_type == 15 # end element
+      next unless @reader.node_type == 1 # element
+      yield @reader.name
+    end
+  end
+
+  ##
+  # An element-block mapping for using the LibXML reader interface. 
+  #
+  # Since a lot of LibXML reader usage is boilerplate iteration through
+  # elements, it would be better to DRY and do this in a block. This
+  # could also help with error handling...?
+  def with_model
+    with_element do |model_name|
+      model = MODELS[model_name]
+      raise "Unexpected element type #{model_name}, " +
+        "expected node, way, relation." if model.nil?
+      yield model, @reader.expand
+      @reader.next
+    end
+  end
+
+  ##
+  # Checks a few invariants. Others are checked in the model methods
+  # such as save_ and delete_with_history.
+  def check(model, xml, new)
+    raise OSM::APIBadXMLError.new(model, xml) if new.nil?
+    unless new.changeset_id == @changeset.id 
+      raise OSM::APIChangesetMismatchError.new(new.changeset_id, @changeset.id)
+    end
+  end
+
+  ##
+  # Consume the XML diff and try to commit it to the database. This code
+  # is *not* transactional, so code which calls it should ensure that the
+  # appropriate transaction block is in place.
+  #
+  # On a failure to meet preconditions (e.g: optimistic locking fails) 
+  # an exception subclassing OSM::APIError will be thrown.
+  def commit
+
+    node_ids, way_ids, rel_ids = {}, {}, {}
+    ids = { :node => node_ids, :way => way_ids, :relation => rel_ids}
+
+    result = OSM::API.new.get_xml_doc
+
+    # loop at the top level, within the <osmChange> element (although we
+    # don't actually check this...)
+    with_element do |action_name|
+      if action_name == 'create'
+        # create a new element. this code is agnostic of the element type
+        # because all the elements support the methods that we're using.
+        with_model do |model, xml|
+          new = model.from_xml_node(xml, true)
+          check(model, xml, new)
+
+          # when this element is saved it will get a new ID, so we save it
+          # to produce the mapping which is sent to other elements.
+          placeholder_id = xml['id'].to_i
+          raise OSM::APIBadXMLError.new(model, xml) if placeholder_id.nil?
+
+          # some elements may have placeholders for other elements in the
+          # diff, so we must fix these before saving the element.
+          new.fix_placeholders!(ids)
+
+          # create element given user
+          new.create_with_history(@changeset.user)
+          
+          # save placeholder => allocated ID map
+          ids[model.to_s.downcase.to_sym][placeholder_id] = new.id
+
+          # add the result to the document we're building for return.
+          xml_result = XML::Node.new model.to_s.downcase
+          xml_result["old_id"] = placeholder_id.to_s
+          xml_result["new_id"] = new.id.to_s
+          xml_result["new_version"] = new.version.to_s
+          result.root << xml_result
+        end
+        
+      elsif action_name == 'modify'
+        # modify an existing element. again, this code doesn't directly deal
+        # with types, but uses duck typing to handle them transparently.
+        with_model do |model, xml|
+          # get the new element from the XML payload
+          new = model.from_xml_node(xml, false)
+          check(model, xml, new)
+
+          # and the old one from the database
+          old = model.find(new.id)
+
+          new.fix_placeholders!(ids)
+          old.update_from(new, @changeset.user)
+
+          xml_result = XML::Node.new model.to_s.downcase
+          xml_result["old_id"] = old.id.to_s
+          xml_result["new_id"] = new.id.to_s
+          xml_result["new_version"] = new.version.to_s
+          result.root << xml_result
+        end
+
+      elsif action_name == 'delete'
+        # delete action. this takes a payload in API 0.6, so we need to do
+        # most of the same checks that are done for the modify.
+        with_model do |model, xml|
+          new = model.from_xml_node(xml, false)
+          check(model, xml, new)
+
+          old = model.find(new.id)
+
+          # can a delete have placeholders under any circumstances?
+          # if a way is modified, then deleted is that a valid diff?
+          new.fix_placeholders!(ids)
+          old.delete_with_history!(new, @changeset.user)
+
+          xml_result = XML::Node.new model.to_s.downcase
+          xml_result["old_id"] = old.id.to_s
+          result.root << xml_result
+        end
+
+      else
+        # no other actions to choose from, so it must be the users fault!
+        raise 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..68ff04615b48b4a3eee3cd90aaa3fd39c1da8d34 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)
@@ -34,6 +44,7 @@ module ActiveRecord
         types = old_native_database_types
         types[:bigint] = { :name => "bigint", :limit => 20 }
         types[:double] = { :name => "double" }
+        types[:bigint_pk] = { :name => "bigint(20) DEFAULT NULL auto_increment PRIMARY KEY" }
         types
       end
 
index 9c271607dc0160d1d7e5b2b78138dc04ed2dedf0..223e351f4c60a0e4790a6d45ee89f36541ede7f9 100644 (file)
@@ -10,6 +10,9 @@ module OSM
 
   # The base class for API Errors.
   class APIError < RuntimeError
+    def render_opts
+      { :text => "", :status => :internal_server_error }
+    end
   end
 
   # Raised when an API object is not found.
@@ -18,10 +21,120 @@ module OSM
 
   # Raised when a precondition to an API action fails sanity check.
   class APIPreconditionFailedError < APIError
+    def render_opts
+      { :text => "", :status => :precondition_failed }
+    end
   end
 
   # Raised when to delete an already-deleted object.
   class APIAlreadyDeletedError < APIError
+    def render_opts
+      { :text => "", :status => :gone }
+    end
+  end
+
+  # Raised when the user logged in isn't the same as the changeset
+  class APIUserChangesetMismatchError < APIError
+    def render_opts
+      { :text => "The user doesn't own that changeset", :status => :conflict }
+    end
+  end
+
+  # Raised when the changeset provided is already closed
+  class APIChangesetAlreadyClosedError < APIError
+    def render_opts
+      { :text => "The supplied changeset has already been closed", :status => :conflict }
+    end
+  end
+  
+  # Raised when a change is expecting a changeset, but the changeset doesn't exist
+  class APIChangesetMissingError < APIError
+    def render_opts
+      { :text => "You need to supply a changeset to be able to make a change", :status => :conflict }
+    end
+  end
+
+  # Raised when a diff is uploaded containing many changeset IDs which don't match
+  # the changeset ID that the diff was uploaded to.
+  class APIChangesetMismatchError < APIError
+    def initialize(provided, allowed)
+      @provided, @allowed = provided, allowed
+    end
+    
+    def render_opts
+      { :text => "Changeset mismatch: Provided #{@provided} but only " +
+        "#{@allowed} is allowed.", :status => :conflict }
+    end
+  end
+  
+  # Raised when a diff upload has an unknown action. You can only have create,
+  # modify, or delete
+  class APIChangesetActionInvalid < APIError
+    def initialize(provided)
+      @provided = provided
+    end
+    
+    def render_opts
+      { :text => "Unknown action #{@provided}, choices are create, modify, delete.",
+      :status => :bad_request }
+    end
+  end
+
+  # Raised when bad XML is encountered which stops things parsing as
+  # they should.
+  class APIBadXMLError < APIError
+    def initialize(model, xml)
+      @model, @xml = model, xml
+    end
+
+    def render_opts
+      { :text => "Cannot parse valid #{@model} from xml string #{@xml}",
+        :status => :bad_request }
+    end
+  end
+
+  # Raised when the provided version is not equal to the latest in the db.
+  class APIVersionMismatchError < APIError
+    def initialize(provided, latest)
+      @provided, @latest = provided, latest
+    end
+
+    attr_reader :provided, :latest
+
+    def render_opts
+      { :text => "Version mismatch: Provided " + provided.to_s +
+      ", server had: " + latest.to_s, :status => :conflict }
+    end
+  end
+
+  # raised when a two tags have a duplicate key string in an element.
+  # this is now forbidden by the API.
+  class APIDuplicateTagsError < APIError
+    def initialize(type, id, tag_key)
+      @type, @id, @tag_key = type, id, tag_key
+    end
+
+    attr_reader :type, :id, :tag_key
+
+    def render_opts
+      { :text => "Element #{@type}/#{@id} has duplicate tags with key #{@tag_key}.",
+        :status => :bad_request }
+    end
+  end
+  
+  # Raised when a way has more than the configured number of way nodes.
+  # This prevents ways from being to long and difficult to work with
+  class APITooManyWayNodesError < APIError
+    def initialize(provided, max)
+      @provided, @max = provided, max
+    end
+    
+    attr_reader :provided, :max
+    
+    def render_opts
+      { :text => "You tried to add #{provided} nodes to the way, however only #{max} are allowed",
+      :status => :bad_request }
+    end
   end
 
   # Helper methods for going to/from mercator and lat/lng.
@@ -190,7 +303,7 @@ module OSM
       doc.encoding = 'UTF-8' 
       root = XML::Node.new 'osm'
       root['version'] = API_VERSION
-      root['generator'] = 'OpenStreetMap server'
+      root['generator'] = GENERATOR
       doc.root = root
       return doc
     end
diff --git a/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 c80ed2242c8147db90177925af3db63eacee971f..fcf34336f7dbb23d55aa1eb151e8dea967967252 100644 (file)
@@ -13,6 +13,8 @@ var nonamekeys = {
 };
 
 OpenLayers._getScriptLocation = function () {
+  // Should really have this file as an erb, so that this can return 
+  // the real rails root
    return "/openlayers/";
 }
 
diff --git a/test/fixtures/changesets.yml b/test/fixtures/changesets.yml
new file mode 100644 (file)
index 0000000..2047af8
--- /dev/null
@@ -0,0 +1,41 @@
+# FIXME! all of these changesets need their bounding boxes set correctly!
+# 
+<% SCALE = 10000000 unless defined?(SCALE) %>
+
+# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
+normal_user_first_change:
+  id: 1
+  user_id: 1
+  created_at: "2007-01-01 00:00:00"
+  open: true
+  min_lon: <%= 1 * SCALE %>
+  min_lat: <%= 1 * SCALE %>
+  max_lon: <%= 5 * SCALE %>
+  max_lat: <%= 5 * SCALE %>
+  
+second_user_first_change:
+  id: 2
+  user_id: 2
+  created_at: "2008-05-01 01:23:45"
+  open: true
+
+normal_user_closed_change:
+  id: 3
+  user_id: 1
+  created_at: "2007-01-01 00:00:00"
+  open: false
+
+normal_user_version_change:
+  id: 4
+  user_id: 1
+  created_at: "2008-01-01 00:00:00"
+  open: true
+
+# changeset to contain all the invalid stuff that is in the
+# fixtures (nodes outside the world, etc...)
+invalid_changeset:
+  id: 5
+  user_id: 0
+  created_at: "2008-01-01 00:00:00"
+  open: false
+  
\ No newline at end of file
diff --git a/test/fixtures/current_node_tags.yml b/test/fixtures/current_node_tags.yml
new file mode 100644 (file)
index 0000000..8a699e9
--- /dev/null
@@ -0,0 +1,29 @@
+t1:
+  id: 1
+  k: testvisible
+  v: yes
+
+t2:
+  id: 2
+  k: testused
+  v: yes
+
+t3:
+  id: 3
+  k: test
+  v: yes
+
+t4:
+  id: 4
+  k: test
+  v: yes
+
+nv_t1:
+  id: 15
+  k: testing
+  v: added in node version 3
+
+nv_t2:
+  id: 15
+  k: testing two
+  v: modified in node version 4
index dd3bd248772a314f52e0bf8c92a68b0b076fca0c..6f21fd47f30a1267004e5e2116652cd1fe4f9ae2 100644 (file)
 # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
+<% SCALE = 10000000 unless defined?(SCALE) %>
+
 visible_node:
   id: 1
-  latitude: 1
-  longitude: 1
-  user_id: 1
-  visible: 1
-  tags: test=yes
+  latitude: <%= 1*SCALE %>
+  longitude: <%= 1*SCALE %>
+  changeset_id: 1
+  visible: true
+  version: 1
+  tile: <%= QuadTile.tile_for_point(1,1) %>
   timestamp: 2007-01-01 00:00:00
 
 invisible_node:
   id: 2
-  latitude: 2
-  longitude: 2
-  user_id: 1
-  visible: 0
-  tags: test=yes
+  latitude: <%= 2*SCALE %>
+  longitude: <%= 2*SCALE %>
+  changeset_id: 1
+  visible: false
+  version: 1
+  tile: <%= QuadTile.tile_for_point(2,2) %>
   timestamp: 2007-01-01 00:00:00
 
 used_node_1:
   id: 3
-  latitude: 3
-  longitude: 3
-  user_id: 1
-  visible: 1
-  tags: test=yes
+  latitude: <%= 3*SCALE %>
+  longitude: <%= 3*SCALE %>
+  changeset_id: 1
+  visible: true
+  version: 1
+  tile: <%= QuadTile.tile_for_point(3,3) %>
   timestamp: 2007-01-01 00:00:00
 
 used_node_2:
   id: 4
-  latitude: 4
-  longitude: 4
-  user_id: 1
-  visible: 1
-  tags: test=yes
+  latitude: <%= 4*SCALE %>
+  longitude: <%= 4*SCALE %>
+  changeset_id: 1
+  visible: true
+  version: 1
+  tile: <%= QuadTile.tile_for_point(4,4) %>
   timestamp: 2007-01-01 00:00:00
 
 node_used_by_relationship:
   id: 5
-  latitude: 5
-  longitude: 5
-  user_id: 1
-  visible: 1
-  tags: test=yes
+  latitude: <%= 5*SCALE %>
+  longitude: <%= 5*SCALE %>
+  changeset_id: 1
+  visible: true
+  version: 1
+  tile: <%= QuadTile.tile_for_point(5,5) %>
+  timestamp: 2007-01-01 00:00:00
+  
+node_too_far_north:
+  id: 6
+  latitude: <%= 90.01*SCALE %>
+  longitude: <%= 6*SCALE %>
+  changeset_id: 5
+  visible: true
+  version: 1
+  tile: <%= QuadTile.tile_for_point(90.01,6) %>
+  timestamp: 2007-01-01 00:00:00
+  
+node_north_limit:
+  id: 11
+  latitude: <%= 90*SCALE %>
+  longitude: <%= 11*SCALE %>
+  changeset_id: 5
+  visible: true
+  version: 1
+  tile: <%= QuadTile.tile_for_point(90,11) %>
+  timestamp: 2008-07-08 14:50:00
+  
+node_too_far_south:
+  id: 7
+  latitude: <%= -90.01*SCALE %>
+  longitude: <%= 7*SCALE %>
+  changeset_id: 5
+  visible: true
+  version: 1
+  tile: <%= QuadTile.tile_for_point(-90.01,7) %>
+  timestamp: 2007-01-01 00:00:00
+  
+node_south_limit:
+  id: 12
+  latitude: <%= -90*SCALE %>
+  longitude: <%= 12*SCALE %>
+  changeset_id: 5
+  visible: true
+  version: 1
+  tile: <%= QuadTile.tile_for_point(-90,12) %>
+  timestamp: 2008-07-08 15:02:18
+  
+node_too_far_west:
+  id: 8
+  latitude: <%= 8*SCALE %>
+  longitude: <%= -180.01*SCALE %>
+  changeset_id: 5
+  visible: true
+  version: 1
+  tile: <%= QuadTile.tile_for_point(8,-180.01) %>
+  timestamp: 2007-01-01 00:00:00
+  
+node_west_limit:
+  id: 13
+  latitude: <%= 13*SCALE %>
+  longitude: <%= -180*SCALE %>
+  changeset_id: 5
+  visible: true
+  version: 1
+  tile: <%= QuadTile.tile_for_point(13,-180) %>
+  timestamp: 2008-07-08 15:17:37
+  
+node_too_far_east:
+  id: 9
+  latitude: <%= 9*SCALE %>
+  longitude: <%= 180.01*SCALE %>
+  changeset_id: 5
+  visible: true
+  version: 1
+  tile: <%= QuadTile.tile_for_point(9,180.01) %>
+  timestamp: 2007-01-01 00:00:00
+  
+node_east_limit:
+  id: 14
+  latitude: <%= 14*SCALE %>
+  longitude: <%= 180*SCALE %>
+  changeset_id: 5
+  visible: true
+  version: 1
+  tile: <%= QuadTile.tile_for_point(14,180) %>
+  timestamp: 2008-07-08 15:46:16
+  
+node_totally_wrong:
+  id: 10
+  latitude: <%= 200*SCALE %>
+  longitude: <%= 200*SCALE %>
+  changeset_id: 5
+  visible: true
+  version: 1
+  tile: <%= QuadTile.tile_for_point(200,200) %>
   timestamp: 2007-01-01 00:00:00
+  
+node_with_versions:
+  id: 15
+  latitude: <%= 1*SCALE %>
+  longitude: <%= 1*SCALE %>
+  changeset_id: 4
+  visible: true
+  version: 4
+  tile: <%= QuadTile.tile_for_point(1,1) %>
+  timestamp: 2008-01-01 00:04:00
index bddc8a0dd0cf0a77dbd0825194240c9e2eeff40d..5696a365f23ac6f3a32abaaeeb90956ead0812b1 100644 (file)
@@ -21,3 +21,9 @@ t4:
   member_role: "some"
   member_type: "node"
   member_id: 5
+
+t5:
+  id: 2
+  member_role: "some"
+  member_type: "node"
+  member_id: 5
index 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 b129d7f45eb7d1a026f361bd0d3b0c5fafb7d22d..44a54caacbc2e429299069b9fa23975dcdf49cd1 100644 (file)
@@ -1,18 +1,27 @@
 visible_way:
   id: 1
-  user_id: 1
+  changeset_id: 1
   timestamp: 2007-01-01 00:00:00
-  visible: 1
+  visible: true
+  version: 1
 
 invisible_way:
   id: 2
-  user_id: 1
+  changeset_id: 1
   timestamp: 2007-01-01 00:00:00
-  visible: 0
+  visible: false
+  version: 1
 
 used_way:
   id: 3
-  user_id: 1
+  changeset_id: 1
   timestamp: 2007-01-01 00:00:00
-  visible: 1
+  visible: true
+  version: 1
 
+way_with_versions:
+  id: 4
+  changeset_id: 4
+  timestamp: 2008-01-01 00:01:00
+  visible: true
+  version: 4
diff --git a/test/fixtures/diary_comments.yml b/test/fixtures/diary_comments.yml
new file mode 100644 (file)
index 0000000..8bb9f49
--- /dev/null
@@ -0,0 +1,7 @@
+comment_for_geo_post:
+  id: 1
+  diary_entry_id: 2
+  user_id: 2
+  body: Some comment text
+  created_at: "2008-11-08 09:45:34"
+  updated_at: "2008-11-08 10:34:34"
diff --git a/test/fixtures/diary_entries.yml b/test/fixtures/diary_entries.yml
new file mode 100644 (file)
index 0000000..5d07e5f
--- /dev/null
@@ -0,0 +1,21 @@
+normal_user_entry_1:
+  id: 1
+  user_id: 1
+  title: Diary Entry 1
+  body: This is the body of diary entry 1.
+  created_at: "2008-11-07 17:43:34"
+  updated_at: "2008-11-07 17:43:34"
+  latitude: 
+  longitude: 
+  language: 
+  
+normal_user_geo_entry:
+  id: 2
+  user_id: 1
+  title: Geo Entry 1
+  body: This is the body of a geo diary entry in London.
+  created_at: "2008-11-07 17:47:34"
+  updated_at: "2008-11-07 17:47:34"
+  latitude: 51.50763
+  longitude: -0.10781
+  language: 
diff --git a/test/fixtures/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
diff --git a/test/fixtures/gpx_points.yml b/test/fixtures/gpx_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
+  
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..c6a3118
--- /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 cf1d1ff5600718b5db43998900a1d51a8823d62d..165f1a21e032a7623a9735cfc9d50d8e2d9eef97 100644 (file)
@@ -1,20 +1,20 @@
 visible_relation:
   id: 1
-  user_id: 1
+  changeset_id: 1
   timestamp: 2007-01-01 00:00:00
-  visible: 1
+  visible: true
   version: 1
 
 invisible_relation:
   id: 2
-  user_id: 1
+  changeset_id: 1
   timestamp: 2007-01-01 00:00:00
-  visible: 0
+  visible: false
   version: 1
 
 used_relation:
   id: 3
-  user_id: 1
+  changeset_id: 1
   timestamp: 2007-01-01 00:00:00
-  visible: 1
+  visible: true
   version: 1
index 5bf02933a3a79bfacb88dad9d4f4799f3cf20600..59ebd0542bd9cdcf672414c30a1ec8d91c43497d 100644 (file)
@@ -1,7 +1,11 @@
 # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
 
-# one:
-#   column: value
-#
-# two:
-#   column: value
+a:
+  user_id: 1
+  k: "key"
+  v: "value"
+
+two:
+  user_id: 1
+  k: "some_key"
+  v: "some_value"
index bcce2f7db5b9732ad1ff59109917c83060427711..709139d68e72aca707f94d21aa04eb74debf88db 100644 (file)
@@ -1,13 +1,39 @@
 # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
 normal_user:
-  email: test@openstreetmap.org
   id: 1
-  active: 1
+  email: test@openstreetmap.org
+  active: true
   pass_crypt: <%= Digest::MD5.hexdigest('test') %>
   creation_time: "2007-01-01 00:00:00"
   display_name: test
-  data_public: 0
+  data_public: false
   description: test
-  home_lat: 1
-  home_lon: 1
+  home_lat: 12.1
+  home_lon: 12.1
   home_zoom: 3
+  
+second_user:
+  id: 2
+  email: test@example.com
+  active: true
+  pass_crypt: <%= Digest::MD5.hexdigest('test') %>
+  creation_time: "2008-05-01 01:23:45"
+  display_name: test2
+  data_public: true
+  description: some test description
+  home_lat: 12
+  home_lon: 12
+  home_zoom: 12
+  
+inactive_user:
+  id: 3
+  email: inactive@openstreetmap.org
+  active: false
+  pass_crypt: <%= Digest::MD5::hexdigest('test2') %>
+  creation_time: "2008-07-01 02:23:45"
+  display_name: Inactive User
+  data_public: true
+  description: description
+  home_lat: 123.4
+  home_lon: 12.34
+  home_zoom: 15
index caeac16b1d7b16a1bbc1f580432b6047785d718e..0b43f6a9c1f586ce70c7c64f4276cb3a2e5a452b 100644 (file)
@@ -1,9 +1,9 @@
-t1:
+t1a:
   id: 1
   node_id: 3
   sequence_id: 1
   version: 1
-
+  
 t2:
   id: 2
   node_id: 3
@@ -15,3 +15,51 @@ t3:
   node_id: 3
   sequence_id: 1
   version: 1
+
+w4_v1_n1:
+  id: 4
+  node_id: 3
+  sequence_id: 1
+  version: 1
+
+w4_v1_n2:
+  id: 4
+  node_id: 4
+  sequence_id: 2
+  version: 1
+
+w4_v2_n1:
+  id: 4
+  node_id: 15
+  sequence_id: 1
+  version: 2
+
+w4_v2_n2:
+  id: 4
+  node_id: 3
+  sequence_id: 2
+  version: 2
+
+w4_v2_n3:
+  id: 4
+  node_id: 4
+  sequence_id: 3
+  version: 2
+
+w4_v3_n1:
+  id: 4
+  node_id: 15
+  sequence_id: 1
+  version: 3
+
+w4_v3_n2:
+  id: 4
+  node_id: 3
+  sequence_id: 2
+  version: 3
+
+w4_v4_n1:
+  id: 4
+  node_id: 15
+  sequence_id: 1
+  version: 4
index c8cf6dcf4d2f6568cffacd5ea0b39336d3b9d151..80b1da6426a8248f5b959be0fc3571eae0f8a963 100644 (file)
@@ -1,21 +1,49 @@
 visible_way:
   id: 1
-  user_id: 1
+  changeset_id: 1
   timestamp: 2007-01-01 00:00:00
-  visible: 1
+  visible: true
   version: 1
 
 invisible_way:
   id: 2
-  user_id: 1
+  changeset_id: 1
   timestamp: 2007-01-01 00:00:00
-  visible: 0
+  visible: false
   version: 1
 
 used_way:
   id: 3
-  user_id: 1
+  changeset_id: 1
   timestamp: 2007-01-01 00:00:00
-  visible: 0
+  visible: true
   version: 1
 
+way_with_versions_v1:
+  id: 4
+  changeset_id: 4
+  timestamp: 2008-01-01 00:01:00
+  visible: true
+  version: 1
+
+way_with_versions_v2:
+  id: 4
+  changeset_id: 4
+  timestamp: 2008-01-01 00:02:00
+  visible: true
+  version: 2
+
+way_with_versions:
+  id: 4
+  changeset_id: 4
+  timestamp: 2008-01-01 00:03:00
+  visible: true
+  version: 3
+
+way_with_versions_v4:
+  id: 4
+  changeset_id: 4
+  timestamp: 2008-01-01 00:04:00
+  visible: true
+  version: 4
+
diff --git a/test/functional/amf_controller_test.rb b/test/functional/amf_controller_test.rb
new file mode 100644 (file)
index 0000000..1d17a5b
--- /dev/null
@@ -0,0 +1,8 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class AmfControllerTest < ActionController::TestCase
+  # Replace this with your real tests.
+  def test_truth
+    assert true
+  end
+end
index 05cbe2af0ac5d8ab4a33816dfb30485e3dde1c72..6a0c2e2ace32db5b599b8bbce63483b81eaf7f29 100644 (file)
@@ -1,16 +1,21 @@
 require File.dirname(__FILE__) + '/../test_helper'
 require 'api_controller'
 
-# Re-raise errors caught by the controller.
-class ApiController; def rescue_action(e) raise e end; end
-
-class ApiControllerTest < Test::Unit::TestCase
+class ApiControllerTest < ActionController::TestCase
   api_fixtures
-
+  
   def setup
-    @controller = ApiController.new
-    @request    = ActionController::TestRequest.new
-    @response   = ActionController::TestResponse.new
+    super
+    @badbigbbox = %w{ -0.1,-0.1,1.1,1.1  10,10,11,11 }
+    @badmalformedbbox = %w{ -0.1  hello 
+    10N2W10.1N2.1W }
+    @badlatmixedbbox = %w{ 0,0.1,0.1,0  -0.1,80,0.1,70  0.24,54.34,0.25,54.33 }
+    @badlonmixedbbox = %w{ 80,-0.1,70,0.1  54.34,0.24,54.33,0.25 }  
+    #@badlatlonoutboundsbbox = %w{ 191,-0.1,193,0.1  -190.1,89.9,-190,90 }
+    @goodbbox = %w{ -0.1,-0.1,0.1,0.1  51.1,-0.1,51.2,0 
+    -0.1,%20-0.1,%200.1,%200.1  -0.1edcd,-0.1d,0.1,0.1  -0.1E,-0.1E,0.1S,0.1N S0.1,W0.1,N0.1,E0.1}
+    # That last item in the goodbbox really shouldn't be there, as the API should
+    # reall reject it, however this is to test to see if the api changes.
   end
 
   def basic_authorization(user, pass)
@@ -23,12 +28,201 @@ class ApiControllerTest < Test::Unit::TestCase
 
   def test_map
     node = current_nodes(:used_node_1)
-    bbox = "#{node.latitude-0.1},#{node.longitude-0.1},#{node.latitude+0.1},#{node.longitude+0.1}"
+    # Need to split the min/max lat/lon out into their own variables here
+    # so that we can test they are returned later.
+    minlon = node.lon-0.1
+    minlat = node.lat-0.1
+    maxlon = node.lon+0.1
+    maxlat = node.lat+0.1
+    bbox = "#{minlon},#{minlat},#{maxlon},#{maxlat}"
     get :map, :bbox => bbox
     if $VERBOSE
-        print @response.body
+      print @request.to_yaml
+      print @response.body
+    end
+    assert_response :success, "Expected scucess with the map call"
+    assert_select "osm[version='#{API_VERSION}'][generator='#{GENERATOR}']:root", :count => 1 do
+      assert_select "bounds[minlon=#{minlon}][minlat=#{minlat}][maxlon=#{maxlon}][maxlat=#{maxlat}]", :count => 1
+      assert_select "node[id=#{node.id}][lat=#{node.lat}][lon=#{node.lon}][version=#{node.version}][changeset=#{node.changeset_id}][visible=#{node.visible}][timestamp=#{node.timestamp.xmlschema}]", :count => 1 do
+        # This should really be more generic
+        assert_select "tag[k=test][v=1]"
+      end
+      # Should also test for the ways and relation
     end
+  end
+  
+  # This differs from the above test in that we are making the bbox exactly
+  # the same as the node we are looking at
+  def test_map_inclusive
+    node = current_nodes(:used_node_1)
+    bbox = "#{node.lon},#{node.lat},#{node.lon},#{node.lat}"
+    get :map, :bbox => bbox
+    #print @response.body
+    assert_response :success, "The map call should have succeeded"
+    assert_select "osm[version='#{API_VERSION}'][generator='#{GENERATOR}']:root:empty", :count => 1
+  end
+  
+  def test_tracepoints
+    point = gpx_files(:first_trace_file)
+    minlon = point.longitude-0.1
+    minlat = point.latitude-0.1
+    maxlon = point.longitude+0.1
+    maxlat = point.latitude+0.1
+    bbox = "#{minlon},#{minlat},#{maxlon},#{maxlat}"
+    get :trackpoints, :bbox => bbox
+    #print @response.body
     assert_response :success
+    assert_select "gpx[version=1.0][creator=OpenStreetMap.org][xmlns=http://www.topografix.com/GPX/1/0/]:root", :count => 1 do
+      assert_select "trk" do
+        assert_select "trkseg"
+      end
+    end
+  end
+  
+  def test_map_without_bbox
+    ["trackpoints", "map"].each do |tq|
+      get tq
+      assert_response :bad_request
+      assert_equal "The parameter bbox is required, and must be of the form min_lon,min_lat,max_lon,max_lat", @response.body, "A bbox param was expected"
+    end
+  end
+  
+  def test_traces_page_less_than_0
+    -10.upto(-1) do |i|
+      get :trackpoints, :page => i, :bbox => "-0.1,-0.1,0.1,0.1"
+      assert_response :bad_request
+      assert_equal "Page number must be greater than or equal to 0", @response.body, "The page number was #{i}"
+    end
+    0.upto(10) do |i|
+      get :trackpoints, :page => i, :bbox => "-0.1,-0.1,0.1,0.1"
+      assert_response :success, "The page number was #{i} and should have been accepted"
+    end
+  end
+  
+  def test_bbox_too_big
+    @badbigbbox.each do |bbox|
+      [ "trackpoints", "map" ].each do |tq|
+        get tq, :bbox => bbox
+        assert_response :bad_request, "The bbox:#{bbox} was expected to be too big"
+        assert_equal "The maximum bbox size is #{APP_CONFIG['max_request_area']}, and your request was too large. Either request a smaller area, or use planet.osm", @response.body, "bbox: #{bbox}"
+      end
+    end
+  end
+  
+  def test_bbox_malformed
+    @badmalformedbbox.each do |bbox|
+      [ "trackpoints", "map" ].each do |tq|
+        get tq, :bbox => bbox
+        assert_response :bad_request, "The bbox:#{bbox} was expected to be malformed"
+        assert_equal "The parameter bbox is required, and must be of the form min_lon,min_lat,max_lon,max_lat", @response.body, "bbox: #{bbox}"
+      end
+    end
+  end
+  
+  def test_bbox_lon_mixedup
+    @badlonmixedbbox.each do |bbox|
+      [ "trackpoints", "map" ].each do |tq|
+        get tq, :bbox => bbox
+        assert_response :bad_request, "The bbox:#{bbox} was expected to have the longitude mixed up"
+        assert_equal "The minimum longitude must be less than the maximum longitude, but it wasn't", @response.body, "bbox: #{bbox}"
+      end
+    end
+  end
+  
+  def test_bbox_lat_mixedup
+    @badlatmixedbbox.each do |bbox|
+      ["trackpoints", "map"].each do |tq|
+        get tq, :bbox => bbox
+        assert_response :bad_request, "The bbox:#{bbox} was expected to have the latitude mixed up"
+        assert_equal "The minimum latitude must be less than the maximum latitude, but it wasn't", @response.body, "bbox: #{bbox}"
+      end
+    end
+  end
+  
+  # We can't actually get an out of bounds error, as the bbox is sanitised.
+  #def test_latlon_outofbounds
+  #  @badlatlonoutboundsbbox.each do |bbox|
+  #    [ "trackpoints", "map" ].each do |tq|
+  #      get tq, :bbox => bbox
+  #      #print @request.to_yaml
+  #      assert_response :bad_request, "The bbox #{bbox} was expected to be out of range"
+  #      assert_equal "The latitudes must be between -90 an 90, and longitudes between -180 and 180", @response.body, "bbox: #{bbox}"
+  #    end
+  #  end
+  #end
+  
+  # MySQL requires that the C based functions are installed for this test to 
+  # work. More information is available from:
+  # http://wiki.openstreetmap.org/index.php/Rails#Installing_the_quadtile_functions
+  def test_changes_simple
+    get :changes
+    assert_response :success
+    #print @response.body
+    # As we have loaded the fixtures, we can assume that there are no 
+    # changes recently
+    now = Time.now
+    hourago = now - 1.hour
+    # Note that this may fail on a very slow machine, so isn't a great test
+    assert_select "osm[version='#{API_VERSION}'][generator='#{GENERATOR}']:root", :count => 1 do
+      assert_select "changes[starttime='#{hourago.xmlschema}'][endtime='#{now.xmlschema}']", :count => 1
+    end
+  end
+  
+  def test_changes_zoom_invalid
+    zoom_to_test = %w{ p -1 0 17 one two }
+    zoom_to_test.each do |zoom|
+      get :changes, :zoom => zoom
+      assert_response :bad_request
+      assert_equal @response.body, "Requested zoom is invalid, or the supplied start is after the end time, or the start duration is more than 24 hours"
+    end
+  end
+  
+  def test_changes_zoom_valid
+    1.upto(16) do |zoom|
+      get :changes, :zoom => zoom
+      assert_response :success
+      now = Time.now
+      hourago = now - 1.hour
+      # Note that this may fail on a very slow machine, so isn't a great test
+      assert_select "osm[version='#{API_VERSION}'][generator='#{GENERATOR}']:root", :count => 1 do
+        assert_select "changes[starttime='#{hourago.xmlschema}'][endtime='#{now.xmlschema}']", :count => 1
+      end
+    end
+  end
+  
+  def test_start_end_time_invalid
+    
+  end
+  
+  def test_start_end_time_invalid
+    
+  end
+  
+  def test_hours_invalid
+    invalid = %w{ -21 335 -1 0 25 26 100 one two three ping pong : }
+    invalid.each do |hour|
+      get :changes, :hours => hour
+      assert_response :bad_request, "Problem with the hour: #{hour}"
+      assert_equal @response.body, "Requested zoom is invalid, or the supplied start is after the end time, or the start duration is more than 24 hours", "Problem with the hour: #{hour}."
+    end
+  end
+  
+  def test_hours_valid
+    1.upto(24) do |hour|
+      get :changes, :hours => hour
+      assert_response :success
+    end
+  end
+  
+  def test_capabilities
+    get :capabilities
+    assert_response :success
+    assert_select "osm:root[version='#{API_VERSION}'][generator='#{GENERATOR}']", :count => 1 do
+      assert_select "api", :count => 1 do
+        assert_select "version[minimum=#{API_VERSION}][maximum=#{API_VERSION}]", :count => 1
+        assert_select "area[maximum=#{APP_CONFIG['max_request_area']}]", :count => 1
+        assert_select "tracepoints[per_page=#{APP_CONFIG['tracepoints_per_page']}]", :count => 1
+      end
+    end
   end
-
 end
diff --git a/test/functional/browse_controller_test.rb b/test/functional/browse_controller_test.rb
new file mode 100644 (file)
index 0000000..65e8510
--- /dev/null
@@ -0,0 +1,66 @@
+require File.dirname(__FILE__) + '/../test_helper'
+require 'browse_controller'
+
+class BrowseControllerTest < ActionController::TestCase
+  api_fixtures
+
+  def basic_authorization(user, pass)
+    @request.env["HTTP_AUTHORIZATION"] = "Basic %s" % Base64.encode64("#{user}:#{pass}")
+  end
+
+  def content(c)
+    @request.env["RAW_POST_DATA"] = c.to_s
+  end
+
+  # We need to load the home page, then activate the start rjs method
+  # and finally check that the new panel has loaded.
+  def test_start
+  
+  end
+  
+  # This should display the last 20 nodes that were edited.
+  def test_index
+    @nodes = Node.find(:all, :order => "timestamp DESC", :limit => 20)
+    assert @nodes.size <= 20
+    get :index
+    assert_response :success
+    assert_template "index"
+    # Now check that all 20 (or however many were returned) nodes are in the html
+    assert_select "h2", :text => "#{@nodes.size} Recently Changed Nodes", :count => 1
+    assert_select "ul[id='recently_changed'] li a", :count => @nodes.size
+    @nodes.each do |node|
+      name = node.tags_as_hash['name'].to_s
+      name = "(No name)" if name.length == 0
+      assert_select "ul[id='recently_changed'] li a[href=/browse/node/#{node.id}]", :text => "#{name} - #{node.id} (#{node.version})"
+    end
+  end
+  
+  # Test reading a relation
+  def test_read_relation
+    
+  end
+  
+  def test_read_relation_history
+    
+  end
+  
+  def test_read_way
+    
+  end
+  
+  def test_read_way_history
+    
+  end
+  
+  def test_read_node
+    
+  end
+  
+  def test_read_node_history
+    
+  end
+  
+  def test_read_changeset
+    
+  end
+end
diff --git a/test/functional/changeset_controller_test.rb b/test/functional/changeset_controller_test.rb
new file mode 100644 (file)
index 0000000..25ceca1
--- /dev/null
@@ -0,0 +1,624 @@
+require File.dirname(__FILE__) + '/../test_helper'
+require 'changeset_controller'
+
+class ChangesetControllerTest < ActionController::TestCase
+  api_fixtures
+
+  def basic_authorization(user, pass)
+    @request.env["HTTP_AUTHORIZATION"] = "Basic %s" % Base64.encode64("#{user}:#{pass}")
+  end
+
+  def content(c)
+    @request.env["RAW_POST_DATA"] = c.to_s
+  end
+  
+  # -----------------------
+  # Test simple changeset creation
+  # -----------------------
+  
+  def test_create
+    basic_authorization "test@openstreetmap.org", "test"
+    
+    # Create the first user's changeset
+    content "<osm><changeset>" +
+      "<tag k='created_by' v='osm test suite checking changesets'/>" + 
+      "</changeset></osm>"
+    put :create
+    
+    assert_response :success, "Creation of changeset did not return sucess status"
+    newid = @response.body
+  end
+  
+  def test_create_invalid
+    basic_authorization "test@openstreetmap.org", "test"
+    content "<osm><changeset></osm>"
+    put :create
+    assert_response :bad_request, "creating a invalid changeset should fail"
+  end
+
+  ##
+  # check that the changeset can be read and returns the correct
+  # document structure.
+  def test_read
+    changeset_id = changesets(:normal_user_first_change).id
+    get :read, :id => changeset_id
+    assert_response :success, "cannot get first changeset"
+    
+    assert_select "osm[version=#{API_VERSION}][generator=\"OpenStreetMap server\"]", 1
+    assert_select "osm>changeset[id=#{changeset_id}]", 1
+  end
+  
+  def test_close
+    # FIXME FIXME FIXME!
+  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 "osm[version=#{API_VERSION}][generator=\"OpenStreetMap server\"]", 1
+    assert_select "osm>node", 1
+    assert_select "osm>way", 1
+    assert_select "osm>relation", 1
+
+    # inspect the response to find out what the new element IDs are
+    doc = XML::Parser.string(@response.body).parse
+    new_node_id = doc.find("//osm/node").first["new_id"].to_i
+    new_way_id = doc.find("//osm/way").first["new_id"].to_i
+    new_rel_id = doc.find("//osm/relation").first["new_id"].to_i
+
+    # check the old IDs are all present and negative one
+    assert_equal -1, doc.find("//osm/node").first["old_id"].to_i
+    assert_equal -1, doc.find("//osm/way").first["old_id"].to_i
+    assert_equal -1, doc.find("//osm/relation").first["old_id"].to_i
+
+    # check the versions are present and equal one
+    assert_equal 1, doc.find("//osm/node").first["new_version"].to_i
+    assert_equal 1, doc.find("//osm/way").first["new_version"].to_i
+    assert_equal 1, doc.find("//osm/relation").first["new_version"].to_i
+
+    # check that the changes made it into the database
+    assert_equal 2, Node.find(new_node_id).tags.size, "new node should have two tags"
+    assert_equal 0, Way.find(new_way_id).tags.size, "new way should have no tags"
+    assert_equal 0, Relation.find(new_rel_id).tags.size, "new relation should have no tags"
+  end
+    
+  ##
+  # test a complex delete where we delete elements which rely on eachother
+  # in the same transaction.
+  def test_upload_delete
+    basic_authorization "test@openstreetmap.org", "test"
+
+    diff = XML::Document.new
+    diff.root = XML::Node.new "osmChange"
+    delete = XML::Node.new "delete"
+    diff.root << delete
+    delete << current_relations(:visible_relation).to_xml_node
+    delete << current_relations(:used_relation).to_xml_node
+    delete << current_ways(:used_way).to_xml_node
+    delete << current_nodes(:node_used_by_relationship).to_xml_node
+
+    # upload it
+    content diff
+    post :upload, :id => 1
+    assert_response :success, 
+      "can't upload a deletion diff to changeset: #{@response.body}"
+
+    # check that everything was deleted
+    assert_equal false, Node.find(current_nodes(:node_used_by_relationship).id).visible
+    assert_equal false, Way.find(current_ways(:used_way).id).visible
+    assert_equal false, Relation.find(current_relations(:visible_relation).id).visible
+    assert_equal false, Relation.find(current_relations(:used_relation).id).visible
+  end
+
+  ##
+  # test that deleting stuff in a transaction doesn't bypass the checks
+  # to ensure that used elements are not deleted.
+  def test_upload_delete_invalid
+    basic_authorization "test@openstreetmap.org", "test"
+
+    diff = XML::Document.new
+    diff.root = XML::Node.new "osmChange"
+    delete = XML::Node.new "delete"
+    diff.root << delete
+    delete << current_relations(:visible_relation).to_xml_node
+    delete << current_ways(:used_way).to_xml_node
+    delete << current_nodes(:node_used_by_relationship).to_xml_node
+
+    # upload it
+    content diff
+    post :upload, :id => 1
+    assert_response :precondition_failed, 
+      "shouldn't be able to upload a invalid deletion diff: #{@response.body}"
+
+    # check that nothing was, in fact, deleted
+    assert_equal true, Node.find(current_nodes(:node_used_by_relationship).id).visible
+    assert_equal true, Way.find(current_ways(:used_way).id).visible
+    assert_equal true, Relation.find(current_relations(:visible_relation).id).visible
+  end
+
+  ##
+  # upload something which creates new objects and inserts them into
+  # existing containers using placeholders.
+  def test_upload_complex
+    basic_authorization "test@openstreetmap.org", "test"
+
+    # simple diff to create a node way and relation using placeholders
+    diff = <<EOF
+<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 "osm[version=#{API_VERSION}][generator=\"#{GENERATOR}\"]", 1
+    assert_select "osm>node", 1
+    assert_select "osm>way", 1
+    assert_select "osm>relation", 1
+
+    # inspect the response to find out what the new element IDs are
+    doc = XML::Parser.string(@response.body).parse
+    new_node_id = doc.find("//osm/node").first["new_id"].to_i
+
+    # check that the changes made it into the database
+    assert_equal 2, Node.find(new_node_id).tags.size, "new node should have two tags"
+    assert_equal [new_node_id, 3], Way.find(1).nds, "way nodes should match"
+    Relation.find(1).members.each do |type,id,role|
+      if type == 'node'
+        assert_equal new_node_id, id, "relation should contain new node"
+      end
+    end
+  end
+    
+  ##
+  # create a diff which references several changesets, which should cause
+  # a rollback and none of the diff gets committed
+  def test_upload_invalid_changesets
+    basic_authorization "test@openstreetmap.org", "test"
+
+    # simple diff to create a node way and relation using placeholders
+    diff = <<EOF
+<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' changeset='4'>
+   <tag k='foo' v='bar'/>
+   <tag k='baz' v='bat'/>
+  </node>
+ </create>
+</osmChange>
+EOF
+    # cache the objects before uploading them
+    node = current_nodes(:visible_node)
+    way = current_ways(:visible_way)
+    rel = current_relations(:visible_relation)
+
+    # upload it
+    content diff
+    post :upload, :id => 1
+    assert_response :conflict, 
+      "uploading a diff with multiple changsets should have failed"
+
+    # check that objects are unmodified
+    assert_nodes_are_equal(node, Node.find(1))
+    assert_ways_are_equal(way, Way.find(1))
+  end
+    
+  ##
+  # upload multiple versions of the same element in the same diff.
+  def test_upload_multiple_valid
+    basic_authorization "test@openstreetmap.org", "test"
+
+    # change the location of a node multiple times, each time referencing
+    # the last version. doesn't this depend on version numbers being
+    # sequential?
+    diff = <<EOF
+<osmChange>
+ <modify>
+  <node id='1' lon='0' lat='0' changeset='1' version='1'/>
+  <node id='1' lon='1' lat='0' changeset='1' version='2'/>
+  <node id='1' lon='1' lat='1' changeset='1' version='3'/>
+  <node id='1' lon='1' lat='2' changeset='1' version='4'/>
+  <node id='1' lon='2' lat='2' changeset='1' version='5'/>
+  <node id='1' lon='3' lat='2' changeset='1' version='6'/>
+  <node id='1' lon='3' lat='3' changeset='1' version='7'/>
+  <node id='1' lon='9' lat='9' changeset='1' version='8'/>
+ </modify>
+</osmChange>
+EOF
+
+    # upload it
+    content diff
+    post :upload, :id => 1
+    assert_response :success, 
+      "can't upload multiple versions of an element in a diff: #{@response.body}"
+  end
+
+  ##
+  # upload multiple versions of the same element in the same diff, but
+  # keep the version numbers the same.
+  def test_upload_multiple_duplicate
+    basic_authorization "test@openstreetmap.org", "test"
+
+    diff = <<EOF
+<osmChange>
+ <modify>
+  <node id='1' lon='0' lat='0' changeset='1' version='1'/>
+  <node id='1' lon='1' lat='1' changeset='1' version='1'/>
+ </modify>
+</osmChange>
+EOF
+
+    # upload it
+    content diff
+    post :upload, :id => 1
+    assert_response :conflict, 
+      "shouldn't be able to upload the same element twice in a diff: #{@response.body}"
+  end
+
+  ##
+  # try to upload some elements without specifying the version
+  def test_upload_missing_version
+    basic_authorization "test@openstreetmap.org", "test"
+
+    diff = <<EOF
+<osmChange>
+ <modify>
+  <node id='1' lon='1' lat='1' changeset='1'/>
+ </modify>
+</osmChange>
+EOF
+
+    # upload it
+    content diff
+    post :upload, :id => 1
+    assert_response :bad_request, 
+      "shouldn't be able to upload an element without version: #{@response.body}"
+  end
+  
+  ##
+  # try to upload with commands other than create, modify, or delete
+  def test_action_upload_invalid
+    basic_authorization "test@openstreetmap.org", "test"
+    
+    diff = <<EOF
+<osmChange>
+  <ping>
+    <node id='1' lon='1' lat='1' changeset='1' />
+  </ping>
+</osmChange>
+EOF
+  content diff
+  post :upload, :id => 1
+  assert_response :bad_request, "Shouldn't be able to upload a diff with the action ping"
+  assert_equal @response.body, "Unknown action ping, choices are create, modify, delete."
+  end
+
+  ##
+  # when we make some simple changes we get the same changes back from the 
+  # diff download.
+  def test_diff_download_simple
+    basic_authorization(users(:normal_user).email, "test")
+
+    # create a temporary changeset
+    content "<osm><changeset>" +
+      "<tag k='created_by' v='osm test suite checking changesets'/>" + 
+      "</changeset></osm>"
+    put :create
+    assert_response :success
+    changeset_id = @response.body.to_i
+
+    # add a diff to it
+    diff = <<EOF
+<osmChange>
+ <modify>
+  <node id='1' lon='0' lat='0' changeset='#{changeset_id}' version='1'/>
+  <node id='1' lon='1' lat='0' changeset='#{changeset_id}' version='2'/>
+  <node id='1' lon='1' lat='1' changeset='#{changeset_id}' version='3'/>
+  <node id='1' lon='1' lat='2' changeset='#{changeset_id}' version='4'/>
+  <node id='1' lon='2' lat='2' changeset='#{changeset_id}' version='5'/>
+  <node id='1' lon='3' lat='2' changeset='#{changeset_id}' version='6'/>
+  <node id='1' lon='3' lat='3' changeset='#{changeset_id}' version='7'/>
+  <node id='1' lon='9' lat='9' changeset='#{changeset_id}' version='8'/>
+ </modify>
+</osmChange>
+EOF
+
+    # upload it
+    content diff
+    post :upload, :id => changeset_id
+    assert_response :success, 
+      "can't upload multiple versions of an element in a diff: #{@response.body}"
+    
+    get :download, :id => changeset_id
+    assert_response :success
+
+    assert_select "osmChange", 1
+    assert_select "osmChange>modify", 8
+    assert_select "osmChange>modify>node", 8
+  end
+  
+  ##
+  # 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]", 1
+    assert_select "osm>changeset[max_lon=1]", 1
+    assert_select "osm>changeset[min_lat=2]", 1
+    assert_select "osm>changeset[max_lat=2]", 1
+
+    # add another node to it
+    with_controller(NodeController.new) do
+      content "<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]", 1
+    assert_select "osm>changeset[max_lon=2]", 1
+    assert_select "osm>changeset[min_lat=1]", 1
+    assert_select "osm>changeset[max_lat=2]", 1
+
+    # add (delete) a way to it
+    with_controller(WayController.new) do
+      content update_changeset(current_ways(:visible_way).to_xml,
+                               changeset_id)
+      put :delete, :id => current_ways(:visible_way).id
+      assert_response :success, "Couldn't delete a way."
+    end
+
+    # get the bounding box back from the changeset
+    get :read, :id => changeset_id
+    assert_response :success, "Couldn't read back changeset for the third time."
+    assert_select "osm>changeset[min_lon=1]", 1
+    assert_select "osm>changeset[max_lon=3]", 1
+    assert_select "osm>changeset[min_lat=1]", 1
+    assert_select "osm>changeset[max_lat=3]", 1    
+  end
+
+  ##
+  # 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
+
+  ##
+  # check searching for changesets by bbox
+  def test_changeset_by_bbox
+    get :query, :bbox => "-10,-10, 10, 10"
+    assert_response :success, "can't get changesets in bbox"
+    # FIXME: write the actual test bit after fixing the fixtures!
+  end
+
+  #------------------------------------------------------------
+  # utility functions
+  #------------------------------------------------------------
+
+  ##
+  # 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 :include, :id => changeset_id
+    assert_response :success, "Setting include of changeset failed: #{@response.body}"
+
+    # check exactly one changeset
+    assert_select "osm>changeset", 1
+    assert_select "osm>changeset[id=#{changeset_id}]", 1
+
+    # check the bbox
+    doc = XML::Parser.string(@response.body).parse
+    changeset = doc.find("//osm/changeset").first
+    assert_equal bbox[0], changeset['min_lon'].to_f, "min lon"
+    assert_equal bbox[1], changeset['min_lat'].to_f, "min lat"
+    assert_equal bbox[2], changeset['max_lon'].to_f, "max lon"
+    assert_equal bbox[3], changeset['max_lat'].to_f, "max lat"
+  end
+
+  ##
+  # update the changeset_id of a way element
+  def update_changeset(xml, changeset_id)
+    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..7eebfa5
--- /dev/null
@@ -0,0 +1,35 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class DiaryEntryControllerTest < ActionController::TestCase
+  def basic_authorization(user, pass)
+    @request.env["HTTP_AUTHORIZATION"] = "Basic %s" % Base64.encode64("#{user}:#{pass}")
+  end
+
+  def content(c)
+    @request.env["RAW_POST_DATA"] = c.to_s
+  end
+  
+  def test_showing_create_diary_entry
+    
+  end
+  
+  def test_editing_diary_entry
+    
+  end
+  
+  def test_editing_creating_diary_comment
+    
+  end
+  
+  def test_listing_diary_entries
+    
+  end
+  
+  def test_rss
+    
+  end
+  
+  def test_viewing_diary_entry
+    
+  end
+end
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..9e5621f8b324adad189a9731c89af9b9a3141a04 100644 (file)
@@ -1,28 +1,24 @@
 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,7 +26,7 @@ class NodeControllerTest < Test::Unit::TestCase
     # compare values
     assert_in_delta lat * 10000000, checknode.latitude, 1, "saved node does not match requested latitude"
     assert_in_delta lon * 10000000, checknode.longitude, 1, "saved node does not match requested longitude"
-    assert_equal users(:normal_user).id, checknode.user_id, "saved node does not belong to user that created it"
+    assert_equal changesets(:normal_user_first_change).id, checknode.changeset_id, "saved node does not belong to changeset that it was created in"
     assert_equal true, checknode.visible, "saved node is not visible"
   end
 
@@ -51,19 +47,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 +84,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'"
+  end
+
+  # test whether string injection is possible
+  def test_string_injection
+    basic_authorization(users(:normal_user).email, "test")
+    changeset_id = changesets(:normal_user_first_change).id
+
+    # try and put something into a string that the API might 
+    # use unquoted and therefore allow code injection...
+    content "<osm><node lat='0' lon='0' changeset='#{changeset_id}'>" +
+      '<tag k="#{@user.inspect}" v="0"/>' +
+      '</node></osm>'
+    put :create
+    assert_response :success
+    nodeid = @response.body
+
+    # find the node in the database
+    checknode = Node.find(nodeid)
+    assert_not_nil checknode, "node not found in data base after upload"
+    
+    # and grab it using the api
+    get :read, :id => nodeid
+    assert_response :success
+    apinode = Node.from_xml(@response.body)
+    assert_not_nil apinode, "downloaded node is nil, but shouldn't be"
+    
+    # check the tags are not corrupted
+    assert_equal checknode.tags, apinode.tags
+    assert apinode.tags.include?('#{@user.inspect}')
+  end
 
   def basic_authorization(user, pass)
     @request.env["HTTP_AUTHORIZATION"] = "Basic %s" % Base64.encode64("#{user}:#{pass}")
   end
 
   def content(c)
-    @request.env["RAW_POST_DATA"] = c
+    @request.env["RAW_POST_DATA"] = c.to_s
+  end
+
+  ##
+  # update the changeset_id of a node element
+  def update_changeset(xml, changeset_id)
+    xml_attr_rewrite(xml, 'changeset', changeset_id)
+  end
+
+  ##
+  # update an attribute in the node element
+  def xml_attr_rewrite(xml, name, value)
+    xml.find("//osm/node").first[name] = value.to_s
+    return xml
+  end
+
+  ##
+  # parse some xml
+  def xml_parse(xml)
+    parser = XML::Parser.new
+    parser.string = xml
+    parser.parse
   end
 end
diff --git a/test/functional/old_node_controller_test.rb b/test/functional/old_node_controller_test.rb
new file mode 100644 (file)
index 0000000..f1328e6
--- /dev/null
@@ -0,0 +1,133 @@
+require File.dirname(__FILE__) + '/../test_helper'
+require 'old_node_controller'
+
+class OldNodeControllerTest < ActionController::TestCase
+  api_fixtures
+
+  #
+  # TODO: test history
+  #
+
+  ##
+  # test the version call by submitting several revisions of a new node
+  # to the API and ensuring that later calls to version return the 
+  # matching versions of the object.
+  def test_version
+    basic_authorization(users(:normal_user).email, "test")
+    changeset_id = changesets(:normal_user_first_change).id
+
+    # setup a simple XML node
+    xml_doc = current_nodes(:visible_node).to_xml
+    xml_node = xml_doc.find("//osm/node").first
+    nodeid = current_nodes(:visible_node).id
+
+    # keep a hash of the versions => string, as we'll need something
+    # to test against later
+    versions = Hash.new
+
+    # save a version for later checking
+    versions[xml_node['version']] = xml_doc.to_s
+
+    # randomly move the node about
+    20.times do 
+      # move the node somewhere else
+      xml_node['lat'] = precision(rand * 180 -  90).to_s
+      xml_node['lon'] = precision(rand * 360 - 180).to_s
+      with_controller(NodeController.new) do
+        content xml_doc
+        put :update, :id => nodeid
+        assert_response :success
+        xml_node['version'] = @response.body.to_s
+      end
+      # save a version for later checking
+      versions[xml_node['version']] = xml_doc.to_s
+    end
+
+    # add a bunch of random tags
+    30.times do 
+      xml_tag = XML::Node.new("tag")
+      xml_tag['k'] = random_string
+      xml_tag['v'] = random_string
+      xml_node << xml_tag
+      with_controller(NodeController.new) do
+        content xml_doc
+        put :update, :id => nodeid
+        assert_response :success,
+        "couldn't update node #{nodeid} (#{@response.body})"
+        xml_node['version'] = @response.body.to_s
+      end
+      # save a version for later checking
+      versions[xml_node['version']] = xml_doc.to_s
+    end
+
+    # check all the versions
+    versions.keys.each do |key|
+      get :version, :id => nodeid, :version => key.to_i
+
+      assert_response :success,
+         "couldn't get version #{key.to_i} of node #{nodeid}"
+
+      check_node = Node.from_xml(versions[key])
+      api_node = Node.from_xml(@response.body.to_s)
+
+      assert_nodes_are_equal check_node, api_node
+    end
+  end
+
+  ##
+  # Test that getting the current version is identical to picking
+  # that version with the version URI call.
+  def test_current_version
+    check_current_version(current_nodes(:visible_node))
+    check_current_version(current_nodes(:used_node_1))
+    check_current_version(current_nodes(:used_node_2))
+    check_current_version(current_nodes(:node_used_by_relationship))
+    check_current_version(current_nodes(:node_with_versions))
+  end
+  
+  def check_current_version(node_id)
+    # get the current version of the node
+    current_node = with_controller(NodeController.new) do
+      get :read, :id => node_id
+      assert_response :success, "cant get current node #{node_id}" 
+      Node.from_xml(@response.body)
+    end
+    assert_not_nil current_node, "getting node #{node_id} returned nil"
+
+    # get the "old" version of the node from the old_node interface
+    get :version, :id => node_id, :version => current_node.version
+    assert_response :success, "cant get old node #{node_id}, v#{current_node.version}" 
+    old_node = Node.from_xml(@response.body)
+
+    # check the nodes are the same
+    assert_nodes_are_equal current_node, old_node
+  end
+
+  ##
+  # returns a 16 character long string with some nasty characters in it.
+  # this ought to stress-test the tag handling as well as the versioning.
+  def random_string
+    letters = [['!','"','$','&',';','@'],
+               ('a'..'z').to_a,
+               ('A'..'Z').to_a,
+               ('0'..'9').to_a].flatten
+    (1..16).map { |i| letters[ rand(letters.length) ] }.join
+  end
+
+  ##
+  # truncate a floating point number to the scale that it is stored in
+  # the database. otherwise rounding errors can produce failing unit
+  # tests when they shouldn't.
+  def precision(f)
+    return (f * GeoRecord::SCALE).round.to_f / GeoRecord::SCALE
+  end
+
+  def basic_authorization(user, pass)
+    @request.env["HTTP_AUTHORIZATION"] = "Basic %s" % Base64.encode64("#{user}:#{pass}")
+  end
+
+  def content(c)
+    @request.env["RAW_POST_DATA"] = c.to_s
+  end
+
+end
index b8bf464b6030fd180dced5c4f75dc88efdfed56e..a52211e2e15485ee28c7acb1eacfacf538601c18 100644 (file)
@@ -1,22 +1,12 @@
 require File.dirname(__FILE__) + '/../test_helper'
 require 'old_relation_controller'
 
-# Re-raise errors caught by the controller.
-#class OldRelationController; def rescue_action(e) raise e end; end
-
-class OldRelationControllerTest < Test::Unit::TestCase
+class OldRelationControllerTest < ActionController::TestCase
   api_fixtures
 
-  def setup
-    @controller = OldRelationController.new
-    @request    = ActionController::TestRequest.new
-    @response   = ActionController::TestResponse.new
-  end
-
   # -------------------------------------
   # Test reading old relations.
   # -------------------------------------
-
   def test_history
     # check that a visible relations is returned properly
     get :history, :id => relations(:visible_relation).id
index 374ea7dc2de42bbba429d9c2946d24b0d601ccf5..31da1d2c784bb18d6598cbc52a486b77760343d7 100644 (file)
@@ -1,31 +1,89 @@
 require File.dirname(__FILE__) + '/../test_helper'
 require 'old_way_controller'
 
-# Re-raise errors caught by the controller.
-class OldWayController; def rescue_action(e) raise e end; end
-
-class OldWayControllerTest < Test::Unit::TestCase
+class OldWayControllerTest < ActionController::TestCase
   api_fixtures
 
-  def setup
-    @controller = OldWayController.new
-    @request    = ActionController::TestRequest.new
-    @response   = ActionController::TestResponse.new
-  end
-
   # -------------------------------------
   # Test reading old ways.
   # -------------------------------------
 
-  def test_history
+  def test_history_visible
     # check that a visible way is returned properly
     get :history, :id => ways(:visible_way).id
     assert_response :success
-
+  end
+  
+  def test_history_invisible
+    # check that an invisible way's history is returned properly
+    get :history, :id => ways(:invisible_way).id
+    assert_response :success
+  end
+  
+  def test_history_invalid
     # check chat a non-existent way is not returned
     get :history, :id => 0
     assert_response :not_found
+  end
+  
+  ##
+  # check that we can retrieve versions of a way
+  def test_version
+    check_current_version(current_ways(:visible_way).id)
+    check_current_version(current_ways(:used_way).id)
+    check_current_version(current_ways(:way_with_versions).id)
+  end
+
+  ##
+  # check that returned history is the same as getting all 
+  # versions of a way from the api.
+  def test_history_equals_versions
+    check_history_equals_versions(current_ways(:visible_way).id)
+    check_history_equals_versions(current_ways(:used_way).id)
+    check_history_equals_versions(current_ways(:way_with_versions).id)
+  end
+
+  ##
+  # check that the current version of a way is equivalent to the
+  # version which we're getting from the versions call.
+  def check_current_version(way_id)
+    # get the current version
+    current_way = with_controller(WayController.new) do
+      get :read, :id => way_id
+      assert_response :success, "can't get current way #{way_id}"
+      Way.from_xml(@response.body)
+    end
+    assert_not_nil current_way, "getting way #{way_id} returned nil"
+
+    # get the "old" version of the way from the version method
+    get :version, :id => way_id, :version => current_way.version
+    assert_response :success, "can't get old way #{way_id}, v#{current_way.version}"
+    old_way = Way.from_xml(@response.body)
+
+    # check that the ways are identical
+    assert_ways_are_equal current_way, old_way
+  end
+
+  ##
+  # look at all the versions of the way in the history and get each version from
+  # the versions call. check that they're the same.
+  def check_history_equals_versions(way_id)
+    get :history, :id => way_id
+    assert_response :success, "can't get way #{way_id} from API"
+    history_doc = XML::Parser.string(@response.body).parse
+    assert_not_nil history_doc, "parsing way #{way_id} history failed"
+
+    history_doc.find("//osm/way").each do |way_doc|
+      history_way = Way.from_xml_node(way_doc)
+      assert_not_nil history_way, "parsing way #{way_id} version failed"
 
+      get :version, :id => way_id, :version => history_way.version
+      assert_response :success, "couldn't get way #{way_id}, v#{history_way.version}"
+      version_way = Way.from_xml(@response.body)
+      assert_not_nil version_way, "failed to parse #{way_id}, v#{history_way.version}"
+      
+      assert_ways_are_equal history_way, version_way
+    end
   end
 
 end
index 202a015a87f737459b13ec30a4055e9dc5c67a37..5f23702db4c3891de137252bf70409a1977c07d6 100644 (file)
@@ -1,27 +1,15 @@
 require File.dirname(__FILE__) + '/../test_helper'
 require 'relation_controller'
 
-# Re-raise errors caught by the controller.
-class RelationController; def rescue_action(e) raise e end; end
-
-class RelationControllerTest < Test::Unit::TestCase
+class RelationControllerTest < ActionController::TestCase
   api_fixtures
-  fixtures :relations, :current_relations, :relation_members, :current_relation_members, :relation_tags, :current_relation_tags
-  set_fixture_class :current_relations => :Relation
-  set_fixture_class :relations => :OldRelation
-
-  def setup
-    @controller = RelationController.new
-    @request    = ActionController::TestRequest.new
-    @response   = ActionController::TestResponse.new
-  end
 
   def basic_authorization(user, pass)
     @request.env["HTTP_AUTHORIZATION"] = "Basic %s" % Base64.encode64("#{user}:#{pass}")
   end
 
   def content(c)
-    @request.env["RAW_POST_DATA"] = c
+    @request.env["RAW_POST_DATA"] = c.to_s
   end
 
   # -------------------------------------
@@ -40,31 +28,49 @@ class RelationControllerTest < Test::Unit::TestCase
     # check chat a non-existent relation is not returned
     get :read, :id => 0
     assert_response :not_found
+  end
 
-    # check the "relations for node" mode
-    get :relations_for_node, :id => current_nodes(:node_used_by_relationship).id
-    assert_response :success
-    # FIXME check whether this contains the stuff we want!
-    if $VERBOSE
-        print @response.body
-    end
+  ##
+  # check that all relations containing a particular node, and no extra
+  # relations, are returned from the relations_for_node call.
+  def test_relations_for_node
+    check_relations_for_element(:relations_for_node, "node", 
+                                current_nodes(:node_used_by_relationship).id,
+                                [ :visible_relation, :used_relation ])
+  end
 
-    # check the "relations for way" mode
-    get :relations_for_way, :id => current_ways(:used_way).id
-    assert_response :success
-    # FIXME check whether this contains the stuff we want!
-    if $VERBOSE
-        print @response.body
-    end
+  def test_relations_for_way
+    check_relations_for_element(:relations_for_way, "way",
+                                current_ways(:used_way).id,
+                                [ :visible_relation ])
+  end
 
+  def test_relations_for_relation
+    check_relations_for_element(:relations_for_relation, "relation",
+                                current_relations(:used_relation).id,
+                                [ :visible_relation ])
+  end
+
+  def check_relations_for_element(method, type, id, expected_relations)
     # check the "relations for relation" mode
-    get :relations_for_relation, :id => current_relations(:used_relation).id
+    get method, :id => id
     assert_response :success
-    # FIXME check whether this contains the stuff we want!
-    if $VERBOSE
-        print @response.body
+
+    # count one osm element
+    assert_select "osm[version=#{API_VERSION}][generator=\"OpenStreetMap server\"]", 1
+
+    # we should have only the expected number of relations
+    assert_select "osm>relation", expected_relations.size
+
+    # and each of them should contain the node we originally searched for
+    expected_relations.each do |r|
+      relation_id = current_relations(r).id
+      assert_select "osm>relation#?", relation_id
+      assert_select "osm>relation#?>member[type=\"#{type}\"][ref=#{id}]", relation_id
     end
+  end
 
+  def test_full
     # check the "full" mode
     get :full, :id => current_relations(:visible_relation).id
     assert_response :success
@@ -80,9 +86,12 @@ class RelationControllerTest < Test::Unit::TestCase
 
   def test_create
     basic_authorization "test@openstreetmap.org", "test"
+    
+    # put the relation in a dummy fixture changset
+    changeset_id = changesets(:normal_user_first_change).id
 
     # create an relation without members
-    content "<osm><relation><tag k='test' v='yes' /></relation></osm>"
+    content "<osm><relation changeset='#{changeset_id}'><tag k='test' v='yes' /></relation></osm>"
     put :create
     # hope for success
     assert_response :success, 
@@ -97,7 +106,9 @@ class RelationControllerTest < Test::Unit::TestCase
         "saved relation contains members but should not"
     assert_equal checkrelation.tags.length, 1, 
         "saved relation does not contain exactly one tag"
-    assert_equal users(:normal_user).id, checkrelation.user_id, 
+    assert_equal changeset_id, checkrelation.changeset.id,
+        "saved relation does not belong in the changeset it was assigned to"
+    assert_equal users(:normal_user).id, checkrelation.changeset.user_id, 
         "saved relation does not belong to user that created it"
     assert_equal true, checkrelation.visible, 
         "saved relation is not visible"
@@ -108,8 +119,9 @@ class RelationControllerTest < Test::Unit::TestCase
 
     # create an relation with a node as member
     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 type='node' ref='#{nid}' role='some'/>" +
+      "<tag k='test' v='yes' /></relation></osm>"
     put :create
     # hope for success
     assert_response :success, 
@@ -124,7 +136,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"
@@ -136,9 +150,10 @@ class RelationControllerTest < Test::Unit::TestCase
     # create an relation with a way and a node as members
     nid = current_nodes(:used_node_1).id
     wid = current_ways(:used_way).id
-    content "<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 +168,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,8 +187,13 @@ class RelationControllerTest < Test::Unit::TestCase
   def test_create_invalid
     basic_authorization "test@openstreetmap.org", "test"
 
+    # put the relation in a dummy fixture changset
+    changeset_id = changesets(:normal_user_first_change).id
+
     # create a relation with non-existing node as member
-    content "<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, 
@@ -183,8 +205,6 @@ class RelationControllerTest < Test::Unit::TestCase
   # -------------------------------------
   
   def test_delete
-  return true
-
     # first try to delete relation without auth
     delete :delete, :id => current_relations(:visible_relation).id
     assert_response :unauthorized
@@ -192,17 +212,178 @@ 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 :conflict
+
+    # try to delete with an invalid (closed) changeset
+    content update_changeset(current_relations(:visible_relation).to_xml,
+                             changesets(:normal_user_closed_change).id)
+    delete :delete, :id => current_relations(:visible_relation).id
+    assert_response :conflict
+
+    # try to delete with an invalid (non-existent) changeset
+    content update_changeset(current_relations(:visible_relation).to_xml,0)
+    delete :delete, :id => current_relations(:visible_relation).id
+    assert_response :conflict
+
+    # this won't work because the relation is in-use by another relation
+    content(relations(:used_relation).to_xml)
+    delete :delete, :id => current_relations(:used_relation).id
+    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
+  
+  ##
+  # 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]}]", 1
+      assert_select "osm>changeset[min_lat=#{bbox[1]}]", 1
+      assert_select "osm>changeset[max_lon=#{bbox[2]}]", 1
+      assert_select "osm>changeset[max_lat=#{bbox[3]}]", 1
+    end
+  end
+
+  ##
+  # update the changeset_id of a node element
+  def update_changeset(xml, changeset_id)
+    xml_attr_rewrite(xml, 'changeset', changeset_id)
+  end
+
+  ##
+  # update an attribute in the node element
+  def xml_attr_rewrite(xml, name, value)
+    xml.find("//osm/relation").first[name] = value.to_s
+    return xml
+  end
+
+  ##
+  # parse some xml
+  def xml_parse(xml)
+    parser = XML::Parser.new
+    parser.string = xml
+    parser.parse
+  end
 end
diff --git a/test/functional/search_controller_test.rb b/test/functional/search_controller_test.rb
new file mode 100644 (file)
index 0000000..a213253
--- /dev/null
@@ -0,0 +1,8 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class SearchControllerTest < ActionController::TestCase
+  # Replace this with your real tests.
+  def test_truth
+    assert true
+  end
+end
diff --git a/test/functional/site_controller_test.rb b/test/functional/site_controller_test.rb
new file mode 100644 (file)
index 0000000..39a6464
--- /dev/null
@@ -0,0 +1,8 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class SiteControllerTest < ActionController::TestCase
+  # Replace this with your real tests.
+  def test_truth
+    assert true
+  end
+end
diff --git a/test/functional/swf_controller_test.rb b/test/functional/swf_controller_test.rb
new file mode 100644 (file)
index 0000000..862d3a8
--- /dev/null
@@ -0,0 +1,8 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class SwfControllerTest < ActionController::TestCase
+  # Replace this with your real tests.
+  def test_truth
+    assert true
+  end
+end
diff --git a/test/functional/trace_controller_test.rb b/test/functional/trace_controller_test.rb
new file mode 100644 (file)
index 0000000..6b46dbc
--- /dev/null
@@ -0,0 +1,8 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class TraceControllerTest < ActionController::TestCase
+  # Replace this with your real tests.
+  def test_truth
+    assert true
+  end
+end
diff --git a/test/functional/user_controller_test.rb b/test/functional/user_controller_test.rb
new file mode 100644 (file)
index 0000000..2278aed
--- /dev/null
@@ -0,0 +1,8 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class UserControllerTest < ActionController::TestCase
+  # Replace this with your real tests.
+  def test_truth
+    assert true
+  end
+end
index 933dfb542edc9be778923735378a9052439b0de0..be4c41a394fb9318b091a5b7756eceaac1690143 100644 (file)
@@ -1,24 +1,15 @@
 require File.dirname(__FILE__) + '/../test_helper'
 require 'way_controller'
 
-# Re-raise errors caught by the controller.
-class WayController; def rescue_action(e) raise e end; end
-
-class WayControllerTest < Test::Unit::TestCase
+class WayControllerTest < ActionController::TestCase
   api_fixtures
 
-  def setup
-    @controller = WayController.new
-    @request    = ActionController::TestRequest.new
-    @response   = ActionController::TestResponse.new
-  end
-
   def basic_authorization(user, pass)
     @request.env["HTTP_AUTHORIZATION"] = "Basic %s" % Base64.encode64("#{user}:#{pass}")
   end
 
   def content(c)
-    @request.env["RAW_POST_DATA"] = c
+    @request.env["RAW_POST_DATA"] = c.to_s
   end
 
   # -------------------------------------
@@ -37,18 +28,35 @@ class WayControllerTest < Test::Unit::TestCase
     # check chat a non-existent way is not returned
     get :read, :id => 0
     assert_response :not_found
+  end
 
-    # check the "ways for node" mode
-    get :ways_for_node, :id => current_nodes(:used_node_1).id
-    assert_response :success
-    # FIXME check whether this contains the stuff we want!
-    print @response.body
+  ##
+  # check the "full" mode
+  def test_full
+    Way.find(:all).each do |way|
+      get :full, :id => way.id
 
-    # check the "full" mode
-    get :full, :id => current_ways(:visible_way).id
-    assert_response :success
-    # FIXME check whether this contains the stuff we want!
-    print @response.body
+      # full call should say "gone" for non-visible ways...
+      unless way.visible
+        assert_response :gone
+        next
+      end
+
+      # otherwise it should say success
+      assert_response :success
+      
+      # Check the way is correctly returned
+      assert_select "osm way[id=#{way.id}][version=#{way.version}][visible=#{way.visible}]", 1
+      
+      # check that each node in the way appears once in the output as a 
+      # reference and as the node element. note the slightly dodgy assumption
+      # that nodes appear only once. this is currently the case with the
+      # fixtures, but it doesn't have to be.
+      way.nodes.each do |n|
+        assert_select "osm way nd[ref=#{n.id}]", 1
+        assert_select "osm node[id=#{n.id}][version=#{n.version}][lat=#{n.lat}][lon=#{n.lon}]", 1
+      end
+    end
   end
 
   # -------------------------------------
@@ -60,8 +68,13 @@ class WayControllerTest < Test::Unit::TestCase
     nid2 = current_nodes(:used_node_2).id
     basic_authorization "test@openstreetmap.org", "test"
 
+    # use the first user's open changeset
+    changeset_id = changesets(:normal_user_first_change).id
+    
     # create a way with pre-existing nodes
-    content "<osm><way><nd ref='#{nid1}'/><nd ref='#{nid2}'/><tag k='test' v='yes' /></way></osm>"
+    content "<osm><way changeset='#{changeset_id}'>" +
+      "<nd ref='#{nid1}'/><nd ref='#{nid2}'/>" + 
+      "<tag k='test' v='yes' /></way></osm>"
     put :create
     # hope for success
     assert_response :success, 
@@ -78,7 +91,9 @@ class WayControllerTest < Test::Unit::TestCase
         "saved way does not contain the right node on pos 0"
     assert_equal checkway.nds[1], nid2, 
         "saved way does not contain the right node on pos 1"
-    assert_equal users(:normal_user).id, checkway.user_id, 
+    assert_equal checkway.changeset_id, changeset_id,
+        "saved way does not belong to the correct changeset"
+    assert_equal users(:normal_user).id, checkway.changeset.user_id, 
         "saved way does not belong to user that created it"
     assert_equal true, checkway.visible, 
         "saved way is not visible"
@@ -91,19 +106,34 @@ class WayControllerTest < Test::Unit::TestCase
   def test_create_invalid
     basic_authorization "test@openstreetmap.org", "test"
 
+    # use the first user's open changeset
+    open_changeset_id = changesets(:normal_user_first_change).id
+    closed_changeset_id = changesets(:normal_user_closed_change).id
+    nid1 = current_nodes(:used_node_1).id
+
     # create a way with non-existing node
-    content "<osm><way><nd ref='0'/><tag k='test' v='yes' /></way></osm>"
+    content "<osm><way changeset='#{open_changeset_id}'>" + 
+      "<nd ref='0'/><tag k='test' v='yes' /></way></osm>"
     put :create
     # expect failure
     assert_response :precondition_failed, 
         "way upload with invalid node did not return 'precondition failed'"
 
     # create a way with no nodes
-    content "<osm><way><tag k='test' v='yes' /></way></osm>"
+    content "<osm><way changeset='#{open_changeset_id}'>" +
+      "<tag k='test' v='yes' /></way></osm>"
     put :create
     # expect failure
     assert_response :precondition_failed, 
         "way upload with no node did not return 'precondition failed'"
+
+    # create a way inside a closed changeset
+    content "<osm><way changeset='#{closed_changeset_id}'>" +
+      "<nd ref='#{nid1}'/></way></osm>"
+    put :create
+    # expect failure
+    assert_response :conflict, 
+        "way upload to closed changeset did not return 'conflict'"    
   end
 
   # -------------------------------------
@@ -111,7 +141,6 @@ class WayControllerTest < Test::Unit::TestCase
   # -------------------------------------
   
   def test_delete
-
     # first try to delete way without auth
     delete :delete, :id => current_ways(:visible_way).id
     assert_response :unauthorized
@@ -119,17 +148,177 @@ class WayControllerTest < Test::Unit::TestCase
     # now set auth
     basic_authorization("test@openstreetmap.org", "test");  
 
-    # this should work
+    # this shouldn't work as with the 0.6 api we need pay load to delete
+    delete :delete, :id => current_ways(:visible_way).id
+    assert_response :bad_request
+    
+    # Now try without having a changeset
+    content "<osm><way id='#{current_ways(:visible_way).id}'></osm>"
+    delete :delete, :id => current_ways(:visible_way).id
+    assert_response :bad_request
+    
+    # try to delete with an invalid (closed) changeset
+    content update_changeset(current_ways(:visible_way).to_xml,
+                             changesets(:normal_user_closed_change).id)
+    delete :delete, :id => current_ways(:visible_way).id
+    assert_response :conflict
+
+    # try to delete with an invalid (non-existent) changeset
+    content update_changeset(current_ways(:visible_way).to_xml,0)
+    delete :delete, :id => current_ways(:visible_way).id
+    assert_response :conflict
+
+    # Now try with a valid changeset
+    content current_ways(:visible_way).to_xml
     delete :delete, :id => current_ways(:visible_way).id
     assert_response :success
 
+    # check the returned value - should be the new version number
+    # valid delete should return the new version number, which should
+    # be greater than the old version number
+    assert @response.body.to_i > current_ways(:visible_way).version,
+       "delete request should return a new version number for way"
+
     # this won't work since the way is already deleted
+    content current_ways(:invisible_way).to_xml
     delete :delete, :id => current_ways(:invisible_way).id
     assert_response :gone
 
+    # this shouldn't work as the way is used in a relation
+    content current_ways(:used_way).to_xml
+    delete :delete, :id => current_ways(:used_way).id
+    assert_response :precondition_failed, 
+       "shouldn't be able to delete a way used in a relation (#{@response.body})"
+
     # this won't work since the way never existed
     delete :delete, :id => 0
     assert_response :not_found
   end
 
+  # ------------------------------------------------------------
+  # test tags handling
+  # ------------------------------------------------------------
+
+  ##
+  # Try adding a duplicate of an existing tag to a way
+  def test_add_duplicate_tags
+    # setup auth
+    basic_authorization(users(:normal_user).email, "test")
+
+    # add an identical tag to the way
+    tag_xml = XML::Node.new("tag")
+    tag_xml['k'] = current_way_tags(:t1).k
+    tag_xml['v'] = current_way_tags(:t1).v
+
+    # add the tag into the existing xml
+    way_xml = current_ways(:visible_way).to_xml
+    way_xml.find("//osm/way").first << tag_xml
+
+    # try and upload it
+    content way_xml
+    put :update, :id => current_ways(:visible_way).id
+    assert_response :bad_request, 
+       "adding a duplicate tag to a way should fail with 'bad request'"
+  end
+
+  ##
+  # Try adding a new duplicate tags to a way
+  def test_new_duplicate_tags
+    # setup auth
+    basic_authorization(users(:normal_user).email, "test")
+
+    # create duplicate tag
+    tag_xml = XML::Node.new("tag")
+    tag_xml['k'] = "i_am_a_duplicate"
+    tag_xml['v'] = "foobar"
+
+    # add the tag into the existing xml
+    way_xml = current_ways(:visible_way).to_xml
+
+    # add two copies of the tag
+    way_xml.find("//osm/way").first << tag_xml.copy(true) << tag_xml
+
+    # try and upload it
+    content way_xml
+    put :update, :id => current_ways(:visible_way).id
+    assert_response :bad_request, 
+       "adding new duplicate tags to a way should fail with 'bad request'"
+  end
+
+  ##
+  # Try adding a new duplicate tags to a way.
+  # But be a bit subtle - use unicode decoding ambiguities to use different
+  # binary strings which have the same decoding.
+  #
+  # NOTE: I'm not sure this test is working correctly, as a lot of the tag
+  # keys seem to come out as "addr��housenumber". It might be something to
+  # do with Ruby's unicode handling...?
+  def test_invalid_duplicate_tags
+    # setup auth
+    basic_authorization(users(:normal_user).email, "test")
+
+    # add the tag into the existing xml
+    way_str = "<osm><way changeset='1'>"
+    way_str << "<tag k='addr:housenumber' v='1'/>"
+
+    # all of these keys have the same unicode decoding, but are binary
+    # not equal. libxml should make these identical as it decodes the
+    # XML document...
+    [ "addr\xc0\xbahousenumber",
+      "addr\xe0\x80\xbahousenumber",
+      "addr\xf0\x80\x80\xbahousenumber" ].each do |key|
+      # copy the XML doc to add the tags
+      way_str_copy = way_str.clone
+
+      # add all new tags to the way
+      way_str_copy << "<tag k='" << key << "' v='1'/>"
+      way_str_copy << "</way></osm>";
+
+      # try and upload it
+      content way_str_copy
+      put :create
+      assert_response :bad_request, 
+         "adding new duplicate tags to a way should fail with 'bad request'"
+    end
+  end
+
+  ##
+  # 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
index b1d7a8fcc280dec9ac39ddbbad7c90de4adf0c2c..f355bf7853cdb3f51d6a4f8d20d955d7198be8fe 100644 (file)
@@ -1,6 +1,7 @@
 ENV["RAILS_ENV"] = "test"
 require File.expand_path(File.dirname(__FILE__) + "/../config/environment")
 require 'test_help'
+load 'composite_primary_keys/fixtures.rb'
 
 class Test::Unit::TestCase
   # Transactional fixtures accelerate your tests by wrapping each test method
@@ -26,31 +27,83 @@ class Test::Unit::TestCase
 
   # Load standard fixtures needed to test API methods
   def self.api_fixtures
-    fixtures :users
+    #print "setting up the api_fixtures"
+    fixtures :users, :changesets
 
     fixtures :current_nodes, :nodes
-    set_fixture_class :current_nodes => :Node
-    set_fixture_class :nodes => :OldNode
+    set_fixture_class :current_nodes => Node
+    set_fixture_class :nodes => OldNode
+
+    fixtures  :current_node_tags,:node_tags
+    set_fixture_class :current_node_tags => NodeTag
+    set_fixture_class :node_tags => OldNodeTag
 
     fixtures :current_ways, :current_way_nodes, :current_way_tags
-    set_fixture_class :current_ways => :Way
-    set_fixture_class :current_way_nodes => :WayNode
-    set_fixture_class :current_way_tags => :WayTag
+    set_fixture_class :current_ways => Way
+    set_fixture_class :current_way_nodes => WayNode
+    set_fixture_class :current_way_tags => WayTag
 
     fixtures :ways, :way_nodes, :way_tags
-    set_fixture_class :ways => :OldWay
-    set_fixture_class :way_nodes => :OldWayNode
-    set_fixture_class :way_tags => :OldWayTag
+    set_fixture_class :ways => OldWay
+    set_fixture_class :way_nodes => OldWayNode
+    set_fixture_class :way_tags => OldWayTag
 
     fixtures :current_relations, :current_relation_members, :current_relation_tags
-    set_fixture_class :current_relations => :Relation
-    set_fixture_class :current_relation_members => :RelationMember
-    set_fixture_class :current_relation_tags => :RelationTag
+    set_fixture_class :current_relations => Relation
+    set_fixture_class :current_relation_members => RelationMember
+    set_fixture_class :current_relation_tags => RelationTag
 
     fixtures :relations, :relation_members, :relation_tags
-    set_fixture_class :relations => :OldRelation
-    set_fixture_class :relation_members => :OldRelationMember
-    set_fixture_class :relation_tags => :OldRelationTag
+    set_fixture_class :relations => OldRelation
+    set_fixture_class :relation_members => OldRelationMember
+    set_fixture_class :relation_tags => OldRelationTag
+    
+    fixtures :gpx_files, :gpx_points, :gpx_file_tags
+    set_fixture_class :gpx_files => Trace
+    set_fixture_class :gpx_points => Tracepoint
+    set_fixture_class :gpx_file_tags => Tracetag
+  end
+
+  ##
+  # 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
 
   # Add more helper methods to be used by all tests here...
diff --git a/test/unit/current_node_tag_test.rb b/test/unit/current_node_tag_test.rb
new file mode 100644 (file)
index 0000000..143fa24
--- /dev/null
@@ -0,0 +1,22 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class CurrentNodeTagTest < Test::Unit::TestCase
+  fixtures :current_node_tags, :current_nodes
+  set_fixture_class :current_nodes => Node
+  set_fixture_class :current_node_tags => NodeTag
+  
+  def test_tag_count
+    assert_equal 6, NodeTag.count
+    node_tag_count(:visible_node, 1)
+    node_tag_count(:invisible_node, 1)
+    node_tag_count(:used_node_1, 1)
+    node_tag_count(:used_node_2, 1)
+    node_tag_count(:node_with_versions, 2)
+  end
+  
+  def node_tag_count (node, count)
+    nod = current_nodes(node)
+    assert_equal count, nod.node_tags.count
+  end
+  
+end
index 8804fe003b6c54f794acfa8f56aaec792d44b08b..3b83bf95aec0bca8d81661be765922954cd7101f 100644 (file)
@@ -1,10 +1,96 @@
 require File.dirname(__FILE__) + '/../test_helper'
 
 class MessageTest < Test::Unit::TestCase
-  fixtures :messages
+  fixtures :messages, :users
 
-  # Replace this with your real tests.
-  def test_truth
-    assert true
+  EURO = "\xe2\x82\xac" #euro symbol
+
+  # This needs to be updated when new fixtures are added
+  # or removed.
+  def test_check_message_count
+    assert_equal 2, Message.count
+  end
+
+  def test_check_empty_message_fails
+    message = Message.new
+    assert !message.valid?
+    assert message.errors.invalid?(:title)
+    assert message.errors.invalid?(:body)
+    assert message.errors.invalid?(:sent_on)
+    assert true, message.message_read
+  end
+  
+  def test_validating_msgs
+    message = messages(:one)
+    assert message.valid?
+    massage = messages(:two)
+    assert message.valid?
+  end
+  
+  def test_invalid_send_recipient
+    message = messages(:one)
+    message.sender = nil
+    message.recipient = nil
+    assert !message.valid?
+
+    assert_raise(ActiveRecord::RecordNotFound) { User.find(0) }
+    message.from_user_id = 0
+    message.to_user_id = 0
+    assert_raise(ActiveRecord::RecordInvalid) {message.save!}
+  end
+
+  def test_utf8_roundtrip
+    (1..255).each do |i|
+      assert_message_ok('c', i)
+      assert_message_ok(EURO, i)
+    end
+  end
+
+  def test_length_oversize
+    assert_raise(ActiveRecord::RecordInvalid) { make_message('c', 256).save! }
+    assert_raise(ActiveRecord::RecordInvalid) { make_message(EURO, 256).save! }
   end
+
+  def test_invalid_utf8
+    # See e.g http://en.wikipedia.org/wiki/UTF-8 for byte sequences
+    # FIXME - Invalid Unicode characters can still be encoded into "valid" utf-8 byte sequences - maybe check this too?
+    invalid_sequences = ["\xC0",         # always invalid utf8
+                         "\xC2\x4a",     # 2-byte multibyte identifier, followed by plain ASCII
+                         "\xC2\xC2",     # 2-byte multibyte identifier, followed by another one
+                         "\x4a\x82",     # plain ASCII, followed by multibyte continuation
+                         "\x82\x82",     # multibyte continuations without multibyte identifier
+                         "\xe1\x82\x4a", # three-byte identifier, contination and (incorrectly) plain ASCII
+                        ]
+    invalid_sequences.each do |char|
+      begin
+        # create a message and save to the database
+        msg = make_message(char, 1)
+        # if the save throws, thats fine and the test should pass, as we're
+        # only testing invalid sequences anyway.
+        msg.save! 
+
+        # get the saved message back and check that it is identical - i.e: 
+        # its OK to accept invalid UTF-8 as long as we return it unmodified.
+        db_msg = msg.class.find(msg.id)
+        assert_equal char, db_msg.title, "Database silently truncated message title"
+
+      rescue ActiveRecord::RecordInvalid
+        # because we only test invalid sequences it is OK to barf on them
+      end
+    end
+  end  
+
+  def make_message(char, count)
+    message = messages(:one)
+    message.title = char * count
+    return message
+  end
+
+  def assert_message_ok(char, count)
+    message = make_message(char, count)
+    assert message.save!
+    response = message.class.find(message.id) # stand by for some über-generalisation...
+    assert_equal char * count, response.title, "message with #{count} #{char} chars (i.e. #{char.length*count} bytes) fails"
+  end
+
 end
index 95321b5cf0cb6d2e8803c484de0069163faa7c41..2c6515cb7db019344d8d0a525e22f1b11f30f689 100644 (file)
@@ -1,25 +1,95 @@
 require File.dirname(__FILE__) + '/../test_helper'
 
 class NodeTest < Test::Unit::TestCase
-  fixtures :current_nodes, :nodes, :users
-  set_fixture_class :current_nodes => :Node
-  set_fixture_class :nodes => :OldNode
-
+  fixtures :changesets, :current_nodes, :users, :current_node_tags, :nodes, :node_tags
+  set_fixture_class :current_nodes => Node
+  set_fixture_class :nodes => OldNode
+  set_fixture_class :node_tags => OldNodeTag
+  set_fixture_class :current_node_tags => NodeTag
+    
+  def test_node_too_far_north
+         invalid_node_test(:node_too_far_north)
+  end
+  
+  def test_node_north_limit
+    valid_node_test(:node_north_limit)
+  end
+  
+  def test_node_too_far_south
+    invalid_node_test(:node_too_far_south)
+  end
+  
+  def test_node_south_limit
+    valid_node_test(:node_south_limit)
+  end
+  
+  def test_node_too_far_west
+    invalid_node_test(:node_too_far_west)
+  end
+  
+  def test_node_west_limit
+    valid_node_test(:node_west_limit)
+  end
+  
+  def test_node_too_far_east
+    invalid_node_test(:node_too_far_east)
+  end
+  
+  def test_node_east_limit
+    valid_node_test(:node_east_limit)
+  end
+  
+  def test_totally_wrong
+    invalid_node_test(:node_totally_wrong)
+  end
+  
+  # This helper method will check to make sure that a node is within the world, and
+  # has the the same lat, lon and timestamp than what was put into the db by 
+  # the fixture
+  def valid_node_test(nod)
+    node = current_nodes(nod)
+    dbnode = Node.find(node.id)
+    assert_equal dbnode.lat, node.latitude.to_f/SCALE
+    assert_equal dbnode.lon, node.longitude.to_f/SCALE
+    assert_equal dbnode.changeset_id, node.changeset_id
+    assert_equal dbnode.timestamp, node.timestamp
+    assert_equal dbnode.version, node.version
+    assert_equal dbnode.visible, node.visible
+    #assert_equal node.tile, QuadTile.tile_for_point(node.lat, node.lon)
+    assert_valid node
+  end
+  
+  # This helper method will check to make sure that a node is outwith the world, 
+  # and has the same lat, lon and timesamp than what was put into the db by the
+  # fixture
+  def invalid_node_test(nod)
+    node = current_nodes(nod)
+    dbnode = Node.find(node.id)
+    assert_equal dbnode.lat, node.latitude.to_f/SCALE
+    assert_equal dbnode.lon, node.longitude.to_f/SCALE
+    assert_equal dbnode.changeset_id, node.changeset_id
+    assert_equal dbnode.timestamp, node.timestamp
+    assert_equal dbnode.version, node.version
+    assert_equal dbnode.visible, node.visible
+    #assert_equal node.tile, QuadTile.tile_for_point(node.lat, node.lon)
+    assert_equal false, dbnode.valid?
+  end
+  
+  # Check that you can create a node and store it
   def test_create
     node_template = Node.new(:latitude => 12.3456,
                              :longitude => 65.4321,
-                             :user_id => users(:normal_user).id,
-                             :visible => 1,
-                             :tags => "")
+                             :changeset_id => changesets(:normal_user_first_change),
+                             :visible => 1, 
+                             :version => 1)
     assert node_template.save_with_history!
 
     node = Node.find(node_template.id)
     assert_not_nil node
     assert_equal node_template.latitude, node.latitude
     assert_equal node_template.longitude, node.longitude
-    assert_equal node_template.user_id, node.user_id
+    assert_equal node_template.changeset_id, node.changeset_id
     assert_equal node_template.visible, node.visible
-    assert_equal node_template.tags, node.tags
     assert_equal node_template.timestamp.to_i, node.timestamp.to_i
 
     assert_equal OldNode.find(:all, :conditions => [ "id = ?", node_template.id ]).length, 1
@@ -27,14 +97,14 @@ class NodeTest < Test::Unit::TestCase
     assert_not_nil old_node
     assert_equal node_template.latitude, old_node.latitude
     assert_equal node_template.longitude, old_node.longitude
-    assert_equal node_template.user_id, old_node.user_id
+    assert_equal node_template.changeset_id, old_node.changeset_id
     assert_equal node_template.visible, old_node.visible
     assert_equal node_template.tags, old_node.tags
     assert_equal node_template.timestamp.to_i, old_node.timestamp.to_i
   end
 
   def test_update
-    node_template = Node.find(1)
+    node_template = Node.find(current_nodes(:visible_node).id)
     assert_not_nil node_template
 
     assert_equal OldNode.find(:all, :conditions => [ "id = ?", node_template.id ]).length, 1
@@ -43,16 +113,16 @@ class NodeTest < Test::Unit::TestCase
 
     node_template.latitude = 12.3456
     node_template.longitude = 65.4321
-    node_template.tags = "updated=yes"
+    #node_template.tags = "updated=yes"
     assert node_template.save_with_history!
 
     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,14 +131,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.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
@@ -82,9 +152,9 @@ class NodeTest < Test::Unit::TestCase
     assert_not_nil node
     assert_equal node_template.latitude, node.latitude
     assert_equal node_template.longitude, node.longitude
-    assert_equal node_template.user_id, node.user_id
+    assert_equal node_template.changeset_id, node.changeset_id
     assert_equal node_template.visible, node.visible
-    assert_equal node_template.tags, node.tags
+    #assert_equal node_template.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 +163,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_test.rb b/test/unit/old_node_test.rb
new file mode 100644 (file)
index 0000000..bdd6853
--- /dev/null
@@ -0,0 +1,79 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class OldNodeTest < Test::Unit::TestCase
+  set_fixture_class :current_nodes => Node
+  set_fixture_class :nodes => OldNode
+  set_fixture_class :node_tags => OldNodeTag
+  set_fixture_class :current_node_tags => NodeTag
+  fixtures :current_nodes, :users, :current_node_tags, :nodes, :node_tags
+    
+  def test_node_too_far_north
+         invalid_node_test(:node_too_far_north)
+  end
+  
+  def test_node_north_limit
+    valid_node_test(:node_north_limit)
+  end
+  
+  def test_node_too_far_south
+    invalid_node_test(:node_too_far_south)
+  end
+  
+  def test_node_south_limit
+    valid_node_test(:node_south_limit)
+  end
+  
+  def test_node_too_far_west
+    invalid_node_test(:node_too_far_west)
+  end
+  
+  def test_node_west_limit
+    valid_node_test(:node_west_limit)
+  end
+  
+  def test_node_too_far_east
+    invalid_node_test(:node_too_far_east)
+  end
+  
+  def test_node_east_limit
+    valid_node_test(:node_east_limit)
+  end
+  
+  def test_totally_wrong
+    invalid_node_test(:node_totally_wrong)
+  end
+  
+  # This helper method will check to make sure that a node is within the world, and
+  # has the the same lat, lon and timestamp than what was put into the db by 
+  # the fixture
+  def valid_node_test(nod)
+    node = nodes(nod)
+    dbnode = Node.find(node.id)
+    assert_equal dbnode.lat, node.latitude.to_f/SCALE
+    assert_equal dbnode.lon, node.longitude.to_f/SCALE
+    assert_equal dbnode.changeset_id, node.changeset_id
+    assert_equal dbnode.version, node.version
+    assert_equal dbnode.visible, node.visible
+    assert_equal dbnode.timestamp, node.timestamp
+    #assert_equal node.tile, QuadTile.tile_for_point(nodes(nod).lat, nodes(nod).lon)
+    assert_valid node
+  end
+  
+  # This helpermethod will check to make sure that a node is outwith the world, 
+  # and has the same lat, lon and timesamp than what was put into the db by the
+  # fixture
+  def invalid_node_test(nod)
+    node = nodes(nod)
+    dbnode = Node.find(node.id)
+    assert_equal dbnode.lat, node.latitude.to_f/SCALE
+    assert_equal dbnode.lon, node.longitude.to_f/SCALE
+    assert_equal dbnode.changeset_id, node.changeset_id
+    assert_equal dbnode.version, node.version
+    assert_equal dbnode.visible, node.visible
+    assert_equal dbnode.timestamp, node.timestamp
+    #assert_equal node.tile, QuadTile.tile_for_point(nodes(nod).lat, nodes(nod).lon)
+    assert_equal false, node.valid?
+  end
+  
+
+end
index bd4e800150c89d8200c20855ee78919cfeff4b98..d591db69d5d0ce35fac9dfeafd829dcab5225b8d 100644 (file)
@@ -1,8 +1,26 @@
 require File.dirname(__FILE__) + '/../test_helper'
 
 class UserPreferenceTest < ActiveSupport::TestCase
-  # Replace this with your real tests.
-  def test_truth
-    assert true
+  fixtures :users, :user_preferences
+
+  # This checks to make sure that there are two user preferences
+  # stored in the test database.
+  # This test needs to be updated for every addition/deletion from
+  # the fixture file
+  def test_check_count
+    assert_equal 2, UserPreference.count
+  end
+
+  # Checks that you cannot add a new preference, that is a duplicate
+  def test_add_duplicate_preference
+    up = user_preferences(:a)
+    newUP = UserPreference.new
+    newUP.user = users(:normal_user)
+    newUP.k = up.k
+    newUP.v = "some other value"
+    assert_not_equal newUP.v, up.v
+    assert_raise (ActiveRecord::StatementInvalid) {newUP.save}
   end
+  
+
 end
index 5468f7a2d90fc88f295f8beb1cfc595699bfce10..486344fee3c4bcec4f273b3cd9d484e5f679258d 100644 (file)
@@ -2,9 +2,138 @@ require File.dirname(__FILE__) + '/../test_helper'
 
 class UserTest < Test::Unit::TestCase
   fixtures :users
-
-  # Replace this with your real tests.
-  def test_truth
-    assert true
+  
+  def test_invalid_with_empty_attributes
+    user = User.new
+    assert !user.valid?
+    assert user.errors.invalid?(:email)
+    assert user.errors.invalid?(:pass_crypt)
+    assert user.errors.invalid?(:display_name)
+    assert user.errors.invalid?(:email)
+    assert !user.errors.invalid?(:home_lat)
+    assert !user.errors.invalid?(:home_lon)
+    assert !user.errors.invalid?(:home_zoom)
+  end
+  
+  def test_unique_email
+    new_user = User.new(:email => users(:normal_user).email,
+      :active => 1, 
+      :pass_crypt => Digest::MD5.hexdigest('test'),
+      :display_name => "new user",
+      :data_public => 1,
+      :description => "desc")
+    assert !new_user.save
+    assert_equal ActiveRecord::Errors.default_error_messages[:taken], new_user.errors.on(:email)
+  end
+  
+  def test_unique_display_name
+    new_user = User.new(:email => "tester@openstreetmap.org",
+      :active => 0,
+      :pass_crypt => Digest::MD5.hexdigest('test'),
+      :display_name => users(:normal_user).display_name, 
+      :data_public => 1,
+      :description => "desc")
+    assert !new_user.save
+    assert_equal ActiveRecord::Errors.default_error_messages[:taken], new_user.errors.on(:display_name)
+  end
+  
+  def test_email_valid
+    ok = %w{ a@s.com test@shaunmcdonald.me.uk hello_local@ping-d.ng 
+    test_local@openstreetmap.org test-local@example.com
+    輕觸搖晃的遊戲@ah.com も対応します@s.name }
+    bad = %w{ hi ht@ n@ @.com help@.me.uk help"hi.me.uk も対@応します }
+    
+    ok.each do |name|
+      user = users(:normal_user)
+      user.email = name
+      assert user.valid?, user.errors.full_messages
+    end
+    
+    bad.each do |name|
+      user = users(:normal_user)
+      user.email = name
+      assert !user.valid?, "#{name} is valid when it shouldn't be" 
+    end
+  end
+  
+  def test_display_name_length
+    user = users(:normal_user)
+    user.display_name = "123"
+    assert user.valid?, " should allow nil display name"
+    user.display_name = "12"
+    assert !user.valid?, "should not allow 2 char name"
+    user.display_name = ""
+    assert !user.valid?
+    user.display_name = nil
+    # Don't understand why it isn't allowing a nil value, 
+    # when the validates statements specifically allow it
+    # It appears the database does not allow null values
+    assert !user.valid?
+  end
+  
+  def test_display_name_valid
+    # Due to sanitisation in the view some of these that you might not 
+    # expact are allowed
+    # However, would they affect the xml planet dumps?
+    ok = [ "Name", "'me", "he\"", "#ping", "<hr>", "*ho", "\"help\"@", 
+           "vergrößern", "ルシステムにも対応します", "輕觸搖晃的遊戲" ]
+    # These need to be 3 chars in length, otherwise the length test above
+    # should be used.
+    bad = [ "<hr/>", "test@example.com", "s/f", "aa/", "aa;", "aa.",
+            "aa,", "aa?", "/;.,?", "も対応します/" ]
+    ok.each do |display_name|
+      user = users(:normal_user)
+      user.display_name = display_name
+      assert user.valid?, "#{display_name} is invalid, when it should be"
+    end
+    
+    bad.each do |display_name|
+      user = users(:normal_user)
+      user.display_name = display_name
+      assert !user.valid?, "#{display_name} is valid when it shouldn't be"
+      assert_equal "is invalid", user.errors.on(:display_name)
+    end
+  end
+  
+  def test_friend_with
+    assert_equal false, users(:normal_user).is_friends_with?(users(:second_user))
+    assert_equal false, users(:normal_user).is_friends_with?(users(:inactive_user))
+    assert_equal false, users(:second_user).is_friends_with?(users(:normal_user))
+    assert_equal false, users(:second_user).is_friends_with?(users(:inactive_user))
+    assert_equal false, users(:inactive_user).is_friends_with?(users(:normal_user))
+    assert_equal false, users(:inactive_user).is_friends_with?(users(:second_user))
+  end
+  
+  def test_users_nearby
+    # second user has their data public and is close by normal user
+    assert_equal [users(:second_user)], users(:normal_user).nearby
+    # second_user has normal user nearby, but normal user has their data private
+    assert_equal [], users(:second_user).nearby
+    # inactive_user has no user nearby
+    assert_equal [], users(:inactive_user).nearby
+  end
+  
+  def test_friends_with
+    # make normal user a friend of second user
+    # it should be a one way friend accossitation
+    assert_equal 0, Friend.count
+    norm = users(:normal_user)
+    sec = users(:second_user)
+    friend = Friend.new
+    friend.befriender = norm
+    friend.befriendee = sec
+    friend.save
+    assert_equal [sec], norm.nearby
+    assert_equal 1, norm.nearby.size
+    assert_equal 1, Friend.count
+    assert_equal true, norm.is_friends_with?(sec)
+    assert_equal false, sec.is_friends_with?(norm)
+    assert_equal false, users(:normal_user).is_friends_with?(users(:inactive_user))
+    assert_equal false, users(:second_user).is_friends_with?(users(:normal_user))
+    assert_equal false, users(:second_user).is_friends_with?(users(:inactive_user))
+    assert_equal false, users(:inactive_user).is_friends_with?(users(:normal_user))
+    assert_equal false, users(:inactive_user).is_friends_with?(users(:second_user))
+    Friend.delete(friend)
+    assert_equal 0, Friend.count
   end
 end
diff --git a/test/unit/way_test.rb b/test/unit/way_test.rb
new file mode 100644 (file)
index 0000000..cd565fd
--- /dev/null
@@ -0,0 +1,16 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class WayTest < Test::Unit::TestCase
+  api_fixtures
+
+  def test_bbox
+    node = current_nodes(:used_node_1)
+    [ :visible_way,
+      :invisible_way,
+      :used_way ].each do |way_symbol|
+      way = current_ways(way_symbol)
+      assert_equal node.bbox, way.bbox
+    end
+  end
+  
+end
diff --git a/vendor/plugins/deadlock_retry/README b/vendor/plugins/deadlock_retry/README
new file mode 100644 (file)
index 0000000..b5937ce
--- /dev/null
@@ -0,0 +1,10 @@
+Deadlock Retry
+==============
+
+Deadlock retry allows the database adapter (currently only tested with the
+MySQLAdapter) to retry transactions that fall into deadlock. It will retry
+such transactions three times before finally failing.
+
+This capability is automatically added to ActiveRecord. No code changes or otherwise are required.
+
+Copyright (c) 2005 Jamis Buck, released under the MIT license
\ No newline at end of file
diff --git a/vendor/plugins/deadlock_retry/Rakefile b/vendor/plugins/deadlock_retry/Rakefile
new file mode 100644 (file)
index 0000000..8063a6e
--- /dev/null
@@ -0,0 +1,10 @@
+require 'rake'
+require 'rake/testtask'
+
+desc "Default task"
+task :default => [ :test ]
+
+Rake::TestTask.new do |t|
+  t.test_files = Dir["test/**/*_test.rb"]
+  t.verbose = true
+end
diff --git a/vendor/plugins/deadlock_retry/init.rb b/vendor/plugins/deadlock_retry/init.rb
new file mode 100644 (file)
index 0000000..e090f68
--- /dev/null
@@ -0,0 +1,2 @@
+require 'deadlock_retry'
+ActiveRecord::Base.send :include, DeadlockRetry
diff --git a/vendor/plugins/deadlock_retry/lib/deadlock_retry.rb b/vendor/plugins/deadlock_retry/lib/deadlock_retry.rb
new file mode 100644 (file)
index 0000000..413cb82
--- /dev/null
@@ -0,0 +1,58 @@
+# Copyright (c) 2005 Jamis Buck
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+module DeadlockRetry
+  def self.append_features(base)
+    super
+    base.extend(ClassMethods)
+    base.class_eval do
+      class <<self
+        alias_method :transaction_without_deadlock_handling, :transaction
+        alias_method :transaction, :transaction_with_deadlock_handling
+      end
+    end
+  end
+
+  module ClassMethods
+    DEADLOCK_ERROR_MESSAGES = [
+      "Deadlock found when trying to get lock",
+      "Lock wait timeout exceeded"
+    ]
+
+    MAXIMUM_RETRIES_ON_DEADLOCK = 3
+
+    def transaction_with_deadlock_handling(*objects, &block)
+      retry_count = 0
+
+      begin
+        transaction_without_deadlock_handling(*objects, &block)
+      rescue ActiveRecord::StatementInvalid => error
+        if DEADLOCK_ERROR_MESSAGES.any? { |msg| error.message =~ /#{Regexp.escape(msg)}/ }
+          raise if retry_count >= MAXIMUM_RETRIES_ON_DEADLOCK
+          retry_count += 1
+          logger.info "Deadlock detected on retry #{retry_count}, restarting transaction"
+          retry
+        else
+          raise
+        end
+      end
+    end
+  end
+end
diff --git a/vendor/plugins/deadlock_retry/test/deadlock_retry_test.rb b/vendor/plugins/deadlock_retry/test/deadlock_retry_test.rb
new file mode 100644 (file)
index 0000000..db0f619
--- /dev/null
@@ -0,0 +1,65 @@
+begin
+  require 'active_record'
+rescue LoadError
+  if ENV['ACTIVERECORD_PATH'].nil?
+    abort <<MSG
+Please set the ACTIVERECORD_PATH environment variable to the directory
+containing the active_record.rb file.
+MSG
+  else
+    $LOAD_PATH.unshift << ENV['ACTIVERECORD_PATH']
+    begin
+      require 'active_record'
+    rescue LoadError
+      abort "ActiveRecord could not be found."
+    end
+  end
+end
+
+require 'test/unit'
+require "#{File.dirname(__FILE__)}/../lib/deadlock_retry"
+
+class MockModel
+  def self.transaction(*objects, &block)
+    block.call
+  end
+
+  def self.logger
+    @logger ||= Logger.new(nil)
+  end
+
+  include DeadlockRetry
+end
+
+class DeadlockRetryTest < Test::Unit::TestCase
+  DEADLOCK_ERROR = "MySQL::Error: Deadlock found when trying to get lock"
+  TIMEOUT_ERROR = "MySQL::Error: Lock wait timeout exceeded"
+
+  def test_no_errors
+    assert_equal :success, MockModel.transaction { :success }
+  end
+
+  def test_no_errors_with_deadlock
+    errors = [ DEADLOCK_ERROR ] * 3
+    assert_equal :success, MockModel.transaction { raise ActiveRecord::StatementInvalid, errors.shift unless errors.empty?; :success }
+    assert errors.empty?
+  end
+
+  def test_no_errors_with_lock_timeout
+    errors = [ TIMEOUT_ERROR ] * 3
+    assert_equal :success, MockModel.transaction { raise ActiveRecord::StatementInvalid, errors.shift unless errors.empty?; :success }
+    assert errors.empty?
+  end
+
+  def test_error_if_limit_exceeded
+    assert_raise(ActiveRecord::StatementInvalid) do
+      MockModel.transaction { raise ActiveRecord::StatementInvalid, DEADLOCK_ERROR }
+    end
+  end
+
+  def test_error_if_unrecognized_error
+    assert_raise(ActiveRecord::StatementInvalid) do
+      MockModel.transaction { raise ActiveRecord::StatementInvalid, "Something else" }
+    end
+  end
+end
diff --git a/vendor/plugins/file_column/CHANGELOG b/vendor/plugins/file_column/CHANGELOG
new file mode 100644 (file)
index 0000000..bb4e5c6
--- /dev/null
@@ -0,0 +1,69 @@
+*svn*
+    * allow for directories in file_column dirs as well
+    * use subdirs for versions instead of fiddling with filename
+    * url_for_image_column_helper for dynamic resizing of images from views
+    * new "crop" feature [Sean Treadway]
+    * url_for_file_column helper: do not require model objects to be stored in
+      instance variables
+    * allow more fined-grained control over :store_dir via callback
+      methods [Gerret Apelt]
+    * allow assignment of regular file objects
+    * validation of file format and file size [Kyle Maxwell]
+    * validation of image dimensions [Lee O'Mara]
+    * file permissions can be set via :permissions option
+    * fixed bug that prevents deleting of file via assigning nil if
+      column is declared as NON NULL on some databases
+    * don't expand absolute paths. This is necessary for file_column to work
+      when your rails app is deployed into a sub-directory via a symbolic link
+    * url_for_*_column will no longer return absolute URLs! Instead, although the
+      generated URL starts with a slash, it will be relative to your application's
+      root URL. This is so, because rails' image_tag helper will automatically
+      convert it to an absolute URL. If you need an absolute URL (e.g., to pass
+      it to link_to) use url_for_file_column's :absolute => true option.
+    * added support for file_column enabled unit tests [Manuel Holtgrewe]
+    * support for custom transformation of images [Frederik Fix]
+    * allow setting of image attributes (e.g., quality) [Frederik Fix]
+    * :magick columns can optionally ignore non-images (i.e., do not try to
+       resize them)
+
+0.3.1
+    * make object with file_columns serializable
+    * use normal require for RMagick, so that it works with gem
+      and custom install as well
+
+0.3
+    * fixed bug where empty file uploads were not recognized with some browsers
+    * fixed bug on windows when "file" utility is not present
+    * added option to disable automatic file extension correction
+    * Only allow one attribute per call to file_column, so that options only
+      apply to one argument
+    * try to detect when people forget to set the form encoding to
+      'multipart/form-data'
+    * converted to rails plugin
+    * easy integration with RMagick
+
+0.2
+    * complete rewrite using state pattern
+    * fixed sanitize filename [Michael Raidel]
+    * fixed bug when no file was uploaded [Michael Raidel]
+    * try to fix filename extensions [Michael Raidel]
+    * Feed absolute paths through File.expand_path to make them as simple as possible
+    * Make file_column_field helper work with auto-ids (e.g., "event[]")
+
+0.1.3
+    * test cases with more than 1 file_column
+    * fixed bug when file_column was called with several arguments
+    * treat empty ("") file_columns as nil
+    * support for binary files on windows
+
+0.1.2
+    * better rails integration, so that you do not have to include the modules yourself. You
+      just have to "require 'rails_file_column'" in your "config/environment.rb"
+    * Rakefile for testing and packaging
+
+0.1.1 (2005-08-11)
+    * fixed nasty bug in url_for_file_column that made it unusable on Apache
+    * prepared for public release
+    
+0.1 (2005-08-10)
+    * initial release
diff --git a/vendor/plugins/file_column/README b/vendor/plugins/file_column/README
new file mode 100644 (file)
index 0000000..07a6e96
--- /dev/null
@@ -0,0 +1,54 @@
+FEATURES
+========
+
+Let's assume an model class named Entry, where we want to define the "image" column
+as a "file_upload" column.
+
+class Entry < ActiveRecord::Base
+  file_column :image
+end
+
+* every entry can have one uploaded file, the filename will be stored in the "image" column
+
+* files will be stored in "public/entry/image/<entry.id>/filename.ext"
+
+* Newly uploaded files will be stored in "public/entry/tmp/<random>/filename.ext" so that
+  they can be reused in form redisplays (due to validation etc.)
+
+* in a view, "<%= file_column_field 'entry', 'image' %> will create a file upload field as well
+  as a hidden field to recover files uploaded before in a case of a form redisplay
+
+* in a view, "<%= url_for_file_column 'entry', 'image' %> will create an URL to access the
+  uploaded file. Note that you need an Entry object in the instance variable @entry for this
+  to work.
+
+* easy integration with RMagick to resize images and/or create thumb-nails.
+
+USAGE
+=====
+
+Just drop the whole directory into your application's "vendor/plugins" directory. Starting
+with version 1.0rc of rails, it will be automatically picked for you by rails plugin
+mechanism.
+
+DOCUMENTATION
+=============
+
+Please look at the rdoc-generated documentation in the "doc" directory.
+
+RUNNING UNITTESTS
+=================
+
+There are extensive unittests in the "test" directory. Currently, only MySQL is supported, but
+you should be able to easily fix this by looking at "connection.rb". You have to create a
+database for the tests and put the connection information into "connection.rb". The schema
+for MySQL can be found in "test/fixtures/mysql.sql".
+
+You can run the tests by starting the "*_test.rb" in the directory "test"
+
+BUGS & FEEDBACK
+===============
+
+Bug reports (as well as patches) and feedback are very welcome. Please send it to
+sebastian.kanthak@muehlheim.de
+
diff --git a/vendor/plugins/file_column/Rakefile b/vendor/plugins/file_column/Rakefile
new file mode 100644 (file)
index 0000000..0a24682
--- /dev/null
@@ -0,0 +1,36 @@
+task :default => [:test]
+
+PKG_NAME = "file-column"
+PKG_VERSION = "0.3.1"
+
+PKG_DIR = "release/#{PKG_NAME}-#{PKG_VERSION}"
+
+task :clean do
+  rm_rf "release"
+end
+
+task :setup_directories do
+  mkpath "release"
+end
+
+
+task :checkout_release => :setup_directories do
+  rm_rf PKG_DIR
+  revision = ENV["REVISION"] || "HEAD"
+  sh "svn export -r #{revision} . #{PKG_DIR}"
+end
+
+task :release_docs => :checkout_release do
+  sh "cd #{PKG_DIR}; rdoc lib"
+end
+
+task :package => [:checkout_release, :release_docs] do
+  sh "cd release; tar czf #{PKG_NAME}-#{PKG_VERSION}.tar.gz #{PKG_NAME}-#{PKG_VERSION}"
+end
+
+task :test do
+  sh "cd test; ruby file_column_test.rb"
+  sh "cd test; ruby file_column_helper_test.rb"
+  sh "cd test; ruby magick_test.rb"
+  sh "cd test; ruby magick_view_only_test.rb"
+end
diff --git a/vendor/plugins/file_column/TODO b/vendor/plugins/file_column/TODO
new file mode 100644 (file)
index 0000000..d46e9fa
--- /dev/null
@@ -0,0 +1,6 @@
+* document configuration options better
+* support setting of permissions
+* validation methods for file format/size
+* delete stale files from tmp directories
+
+* ensure valid URLs are created even when deployed at sub-path (compute_public_url?)
diff --git a/vendor/plugins/file_column/init.rb b/vendor/plugins/file_column/init.rb
new file mode 100644 (file)
index 0000000..d31ef1b
--- /dev/null
@@ -0,0 +1,13 @@
+# plugin init file for rails
+# this file will be picked up by rails automatically and
+# add the file_column extensions to rails
+
+require 'file_column'
+require 'file_compat'
+require 'file_column_helper'
+require 'validations'
+require 'test_case'
+
+ActiveRecord::Base.send(:include, FileColumn)
+ActionView::Base.send(:include, FileColumnHelper)
+ActiveRecord::Base.send(:include, FileColumn::Validations)
\ No newline at end of file
diff --git a/vendor/plugins/file_column/lib/file_column.rb b/vendor/plugins/file_column/lib/file_column.rb
new file mode 100644 (file)
index 0000000..791a5be
--- /dev/null
@@ -0,0 +1,720 @@
+require 'fileutils'
+require 'tempfile'
+require 'magick_file_column'
+
+module FileColumn # :nodoc:
+  def self.append_features(base)
+    super
+    base.extend(ClassMethods)
+  end
+
+  def self.create_state(instance,attr)
+    filename = instance[attr]
+    if filename.nil? or filename.empty?
+      NoUploadedFile.new(instance,attr)
+    else
+      PermanentUploadedFile.new(instance,attr)
+    end
+  end
+
+  def self.init_options(defaults, model, attr)
+    options = defaults.dup
+    options[:store_dir] ||= File.join(options[:root_path], model, attr)
+    unless options[:store_dir].is_a?(Symbol)
+      options[:tmp_base_dir] ||= File.join(options[:store_dir], "tmp")
+    end
+    options[:base_url] ||= options[:web_root] + File.join(model, attr)
+
+    [:store_dir, :tmp_base_dir].each do |dir_sym|
+      if options[dir_sym].is_a?(String) and !File.exists?(options[dir_sym])
+        FileUtils.mkpath(options[dir_sym])
+      end
+    end
+
+    options
+  end
+
+  class BaseUploadedFile # :nodoc:
+
+    def initialize(instance,attr)
+      @instance, @attr = instance, attr
+      @options_method = "#{attr}_options".to_sym
+    end
+
+
+    def assign(file)
+      if file.is_a? File
+        # this did not come in via a CGI request. However,
+        # assigning files directly may be useful, so we
+        # make just this file object similar enough to an uploaded
+        # file that we can handle it. 
+        file.extend FileColumn::FileCompat
+      end
+
+      if file.nil?
+        delete
+      else
+        if file.size == 0
+          # user did not submit a file, so we
+          # can simply ignore this
+          self
+        else
+          if file.is_a?(String)
+            # if file is a non-empty string it is most probably
+            # the filename and the user forgot to set the encoding
+            # to multipart/form-data. Since we would raise an exception
+            # because of the missing "original_filename" method anyways,
+            # we raise a more meaningful exception rightaway.
+            raise TypeError.new("Do not know how to handle a string with value '#{file}' that was passed to a file_column. Check if the form's encoding has been set to 'multipart/form-data'.")
+          end
+          upload(file)
+        end
+      end
+    end
+
+    def just_uploaded?
+      @just_uploaded
+    end
+
+    def on_save(&blk)
+      @on_save ||= []
+      @on_save << Proc.new
+    end
+    
+    # the following methods are overriden by sub-classes if needed
+
+    def temp_path
+      nil
+    end
+
+    def absolute_dir
+      if absolute_path then File.dirname(absolute_path) else nil end
+    end
+
+    def relative_dir
+      if relative_path then File.dirname(relative_path) else nil end
+    end
+
+    def after_save
+      @on_save.each { |blk| blk.call } if @on_save
+      self
+    end
+
+    def after_destroy
+    end
+
+    def options
+      @instance.send(@options_method)
+    end
+
+    private
+    
+    def store_dir
+      if options[:store_dir].is_a? Symbol
+        raise ArgumentError.new("'#{options[:store_dir]}' is not an instance method of class #{@instance.class.name}") unless @instance.respond_to?(options[:store_dir])
+
+        dir = File.join(options[:root_path], @instance.send(options[:store_dir]))
+        FileUtils.mkpath(dir) unless File.exists?(dir)
+        dir
+      else 
+        options[:store_dir]
+      end
+    end
+
+    def tmp_base_dir
+      if options[:tmp_base_dir]
+        options[:tmp_base_dir] 
+      else
+        dir = File.join(store_dir, "tmp")
+        FileUtils.mkpath(dir) unless File.exists?(dir)
+        dir
+      end
+    end
+
+    def clone_as(klass)
+      klass.new(@instance, @attr)
+    end
+
+  end
+    
+
+  class NoUploadedFile < BaseUploadedFile # :nodoc:
+    def delete
+      # we do not have a file so deleting is easy
+      self
+    end
+
+    def upload(file)
+      # replace ourselves with a TempUploadedFile
+      temp = clone_as TempUploadedFile
+      temp.store_upload(file)
+      temp
+    end
+
+    def absolute_path(subdir=nil)
+      nil
+    end
+
+
+    def relative_path(subdir=nil)
+      nil
+    end
+
+    def assign_temp(temp_path)
+      return self if temp_path.nil? or temp_path.empty?
+      temp = clone_as TempUploadedFile
+      temp.parse_temp_path temp_path
+      temp
+    end
+  end
+
+  class RealUploadedFile < BaseUploadedFile # :nodoc:
+    def absolute_path(subdir=nil)
+      if subdir
+        File.join(@dir, subdir, @filename)
+      else
+        File.join(@dir, @filename)
+      end
+    end
+
+    def relative_path(subdir=nil)
+      if subdir
+        File.join(relative_path_prefix, subdir, @filename)
+      else
+        File.join(relative_path_prefix, @filename)
+      end
+    end
+
+    private
+
+    # regular expressions to try for identifying extensions
+    EXT_REGEXPS = [ 
+      /^(.+)\.([^.]+\.[^.]+)$/, # matches "something.tar.gz"
+      /^(.+)\.([^.]+)$/ # matches "something.jpg"
+    ]
+
+    def split_extension(filename,fallback=nil)
+      EXT_REGEXPS.each do |regexp|
+        if filename =~ regexp
+          base,ext = $1, $2
+          return [base, ext] if options[:extensions].include?(ext.downcase)
+        end
+      end
+      if fallback and filename =~ EXT_REGEXPS.last
+        return [$1, $2]
+      end
+      [filename, ""]
+    end
+    
+  end
+
+  class TempUploadedFile < RealUploadedFile # :nodoc:
+
+    def store_upload(file)
+      @tmp_dir = FileColumn.generate_temp_name
+      @dir = File.join(tmp_base_dir, @tmp_dir)      
+      FileUtils.mkdir(@dir)
+      
+      @filename = FileColumn::sanitize_filename(file.original_filename)
+      local_file_path = File.join(tmp_base_dir,@tmp_dir,@filename)
+      
+      # stored uploaded file into local_file_path
+      # If it was a Tempfile object, the temporary file will be
+      # cleaned up automatically, so we do not have to care for this
+      if file.respond_to?(:local_path) and file.local_path and File.exists?(file.local_path)
+        FileUtils.copy_file(file.local_path, local_file_path)
+      elsif file.respond_to?(:read)
+        File.open(local_file_path, "wb") { |f| f.write(file.read) }
+      else
+        raise ArgumentError.new("Do not know how to handle #{file.inspect}")
+      end
+      File.chmod(options[:permissions], local_file_path)
+      
+      if options[:fix_file_extensions]
+        # try to determine correct file extension and fix
+        # if necessary
+        content_type = get_content_type((file.content_type.chomp if file.content_type))
+        if content_type and options[:mime_extensions][content_type]
+          @filename = correct_extension(@filename,options[:mime_extensions][content_type])
+        end
+
+        new_local_file_path = File.join(tmp_base_dir,@tmp_dir,@filename)
+        File.rename(local_file_path, new_local_file_path) unless new_local_file_path == local_file_path
+        local_file_path = new_local_file_path
+      end
+      
+      @instance[@attr] = @filename
+      @just_uploaded = true
+    end
+
+
+    # tries to identify and strip the extension of filename
+    # if an regular expresion from EXT_REGEXPS matches and the
+    # downcased extension is a known extension (in options[:extensions])
+    # we'll strip this extension
+    def strip_extension(filename)
+      split_extension(filename).first
+    end
+
+    def correct_extension(filename, ext)
+      strip_extension(filename) << ".#{ext}"
+    end
+    
+    def parse_temp_path(temp_path, instance_options=nil)
+      raise ArgumentError.new("invalid format of '#{temp_path}'") unless temp_path =~ %r{^((\d+\.)+\d+)/([^/].+)$}
+      @tmp_dir, @filename = $1, FileColumn.sanitize_filename($3)
+      @dir = File.join(tmp_base_dir, @tmp_dir)
+
+      @instance[@attr] = @filename unless instance_options == :ignore_instance
+    end
+    
+    def upload(file)
+      # store new file
+      temp = clone_as TempUploadedFile
+      temp.store_upload(file)
+      
+      # delete old copy
+      delete_files
+
+      # and return new TempUploadedFile object
+      temp
+    end
+
+    def delete
+      delete_files
+      @instance[@attr] = ""
+      clone_as NoUploadedFile
+    end
+
+    def assign_temp(temp_path)
+      return self if temp_path.nil? or temp_path.empty?
+      # we can ignore this since we've already received a newly uploaded file
+
+      # however, we delete the old temporary files
+      temp = clone_as TempUploadedFile
+      temp.parse_temp_path(temp_path, :ignore_instance)
+      temp.delete_files
+
+      self
+    end
+
+    def temp_path
+      File.join(@tmp_dir, @filename)
+    end
+
+    def after_save
+      super
+
+      # we have a newly uploaded image, move it to the correct location
+      file = clone_as PermanentUploadedFile
+      file.move_from(File.join(tmp_base_dir, @tmp_dir), @just_uploaded)
+
+      # delete temporary files
+      delete_files
+
+      # replace with the new PermanentUploadedFile object
+      file
+    end
+
+    def delete_files
+      FileUtils.rm_rf(File.join(tmp_base_dir, @tmp_dir))
+    end
+
+    def get_content_type(fallback=nil)
+      if options[:file_exec]
+        begin
+          content_type = `#{options[:file_exec]} -bi "#{File.join(@dir,@filename)}"`.chomp
+          content_type = fallback unless $?.success?
+          content_type.gsub!(/;.+$/,"") if content_type
+          content_type
+        rescue
+          fallback
+        end
+      else
+        fallback
+      end
+    end
+
+    private
+
+    def relative_path_prefix
+      File.join("tmp", @tmp_dir)
+    end
+  end
+
+  
+  class PermanentUploadedFile < RealUploadedFile # :nodoc:
+    def initialize(*args)
+      super *args
+      @dir = File.join(store_dir, relative_path_prefix)
+      @filename = @instance[@attr]
+      @filename = nil if @filename.empty?
+    end
+
+    def move_from(local_dir, just_uploaded)
+      # remove old permament dir first
+      # this creates a short moment, where neither the old nor
+      # the new files exist but we can't do much about this as
+      # filesystems aren't transactional.
+      FileUtils.rm_rf @dir
+
+      FileUtils.mv local_dir, @dir
+
+      @just_uploaded = just_uploaded
+    end
+
+    def upload(file)
+      temp = clone_as TempUploadedFile
+      temp.store_upload(file)
+      temp
+    end
+
+    def delete
+      file = clone_as NoUploadedFile
+      @instance[@attr] = ""
+      file.on_save { delete_files }
+      file
+    end
+
+    def assign_temp(temp_path)
+      return nil if temp_path.nil? or temp_path.empty?
+
+      temp = clone_as TempUploadedFile
+      temp.parse_temp_path(temp_path)
+      temp
+    end
+
+    def after_destroy
+      delete_files
+    end
+
+    def delete_files
+      FileUtils.rm_rf @dir
+    end
+
+    private
+    
+    def relative_path_prefix
+      raise RuntimeError.new("Trying to access file_column, but primary key got lost.") if @instance.id.to_s.empty?
+      @instance.id.to_s
+    end
+  end
+    
+  # The FileColumn module allows you to easily handle file uploads. You can designate
+  # one or more columns of your model's table as "file columns" like this:
+  #
+  #   class Entry < ActiveRecord::Base
+  #
+  #     file_column :image
+  #   end
+  #
+  # Now, by default, an uploaded file "test.png" for an entry object with primary key 42 will
+  # be stored in in "public/entry/image/42/test.png". The filename "test.png" will be stored
+  # in the record's "image" column. The "entries" table should have a +VARCHAR+ column
+  # named "image".
+  #
+  # The methods of this module are automatically included into <tt>ActiveRecord::Base</tt>
+  # as class methods, so that you can use them in your models.
+  #
+  # == Generated Methods
+  #
+  # After calling "<tt>file_column :image</tt>" as in the example above, a number of instance methods
+  # will automatically be generated, all prefixed by "image":
+  #
+  # * <tt>Entry#image=(uploaded_file)</tt>: this will handle a newly uploaded file
+  #   (see below). Note that
+  #   you can simply call your upload field "entry[image]" in your view (or use the
+  #   helper).
+  # * <tt>Entry#image(subdir=nil)</tt>: This will return an absolute path (as a
+  #   string) to the currently uploaded file
+  #   or nil if no file has been uploaded
+  # * <tt>Entry#image_relative_path(subdir=nil)</tt>: This will return a path relative to
+  #   this file column's base directory
+  #   as a string or nil if no file has been uploaded. This would be "42/test.png" in the example.
+  # * <tt>Entry#image_just_uploaded?</tt>: Returns true if a new file has been uploaded to this instance.
+  #   You can use this in your code to perform certain actions (e. g., validation,
+  #   custom post-processing) only on newly uploaded files.
+  #
+  # You can access the raw value of the "image" column (which will contain the filename) via the
+  # <tt>ActiveRecord::Base#attributes</tt> or <tt>ActiveRecord::Base#[]</tt> methods like this:
+  #
+  #   entry['image']    # e.g."test.png"
+  #
+  # == Storage of uploaded files
+  #
+  # For a model class +Entry+ and a column +image+, all files will be stored under
+  # "public/entry/image". A sub-directory named after the primary key of the object will
+  # be created, so that files can be stored using their real filename. For example, a file
+  # "test.png" stored in an Entry object with id 42 will be stored in
+  #
+  #   public/entry/image/42/test.png
+  #
+  # Files will be moved to this location in an +after_save+ callback. They will be stored in
+  # a temporary location previously as explained in the next section.
+  #
+  # By default, files will be created with unix permissions of <tt>0644</tt> (i. e., owner has
+  # read/write access, group and others only have read access). You can customize
+  # this by passing the desired mode as a <tt>:permissions</tt> options. The value
+  # you give here is passed directly to <tt>File::chmod</tt>, so on Unix you should
+  # give some octal value like 0644, for example.
+  #
+  # == Handling of form redisplay
+  #
+  # Suppose you have a form for creating a new object where the user can upload an image. The form may
+  # have to be re-displayed because of validation errors. The uploaded file has to be stored somewhere so
+  # that the user does not have to upload it again. FileColumn will store these in a temporary directory
+  # (called "tmp" and located under the column's base directory by default) so that it can be moved to
+  # the final location if the object is successfully created. If the form is never completed, though, you
+  # can easily remove all the images in this "tmp" directory once per day or so.
+  #
+  # So in the example above, the image "test.png" would first be stored in 
+  # "public/entry/image/tmp/<some_random_key>/test.png" and be moved to
+  # "public/entry/image/<primary_key>/test.png".
+  #
+  # This temporary location of newly uploaded files has another advantage when updating objects. If the
+  # update fails for some reasons (e.g. due to validations), the existing image will not be overwritten, so
+  # it has a kind of "transactional behaviour".
+  #
+  # == Additional Files and Directories
+  #
+  # FileColumn allows you to keep more than one file in a directory and will move/delete
+  # all the files and directories it finds in a model object's directory when necessary.
+  #
+  # As a convenience you can access files stored in sub-directories via the +subdir+
+  # parameter if they have the same filename.
+  #
+  # Suppose your uploaded file is named "vancouver.jpg" and you want to create a
+  # thumb-nail and store it in the "thumb" directory. If you call
+  # <tt>image("thumb")</tt>, you
+  # will receive an absolute path for the file "thumb/vancouver.jpg" in the same
+  # directory "vancouver.jpg" is stored. Look at the documentation of FileColumn::Magick
+  # for more examples and how to create these thumb-nails automatically.
+  #
+  # == File Extensions
+  #
+  # FileColumn will try to fix the file extension of uploaded files, so that
+  # the files are served with the correct mime-type by your web-server. Most
+  # web-servers are setting the mime-type based on the file's extension. You
+  # can disable this behaviour by passing the <tt>:fix_file_extensions</tt> option
+  # with a value of +nil+ to +file_column+.
+  #
+  # In order to set the correct extension, FileColumn tries to determine
+  # the files mime-type first. It then uses the +MIME_EXTENSIONS+ hash to
+  # choose the corresponding file extension. You can override this hash
+  # by passing in a <tt>:mime_extensions</tt> option to +file_column+.
+  #
+  # The mime-type of the uploaded file is determined with the following steps:
+  #
+  # 1. Run the external "file" utility. You can specify the full path to
+  #    the executable in the <tt>:file_exec</tt> option or set this option
+  #    to +nil+ to disable this step
+  #
+  # 2. If the file utility couldn't determine the mime-type or the utility was not
+  #    present, the content-type provided by the user's browser is used
+  #    as a fallback.
+  #
+  # == Custom Storage Directories
+  #
+  # FileColumn's storage location is determined in the following way. All
+  # files are saved below the so-called "root_path" directory, which defaults to
+  # "RAILS_ROOT/public". For every file_column, you can set a separte "store_dir"
+  # option. It defaults to "model_name/attribute_name".
+  # 
+  # Files will always be stored in sub-directories of the store_dir path. The
+  # subdirectory is named after the instance's +id+ attribute for a saved model,
+  # or "tmp/<randomkey>" for unsaved models.
+  #
+  # You can specify a custom root_path by setting the <tt>:root_path</tt> option.
+  # 
+  # You can specify a custom storage_dir by setting the <tt>:storage_dir</tt> option.
+  #
+  # For setting a static storage_dir that doesn't change with respect to a particular
+  # instance, you assign <tt>:storage_dir</tt> a String representing a directory
+  # as an absolute path.
+  #
+  # If you need more fine-grained control over the storage directory, you
+  # can use the name of a callback-method as a symbol for the
+  # <tt>:store_dir</tt> option. This method has to be defined as an
+  # instance method in your model. It will be called without any arguments
+  # whenever the storage directory for an uploaded file is needed. It should return
+  # a String representing a directory relativeo to root_path.
+  #
+  # Uploaded files for unsaved models objects will be stored in a temporary
+  # directory. By default this directory will be a "tmp" directory in
+  # your <tt>:store_dir</tt>. You can override this via the
+  # <tt>:tmp_base_dir</tt> option.
+  module ClassMethods
+
+    # default mapping of mime-types to file extensions. FileColumn will try to
+    # rename a file to the correct extension if it detects a known mime-type
+    MIME_EXTENSIONS = {
+      "image/gif" => "gif",
+      "image/jpeg" => "jpg",
+      "image/pjpeg" => "jpg",
+      "image/x-png" => "png",
+      "image/jpg" => "jpg",
+      "image/png" => "png",
+      "application/x-shockwave-flash" => "swf",
+      "application/pdf" => "pdf",
+      "application/pgp-signature" => "sig",
+      "application/futuresplash" => "spl",
+      "application/msword" => "doc",
+      "application/postscript" => "ps",
+      "application/x-bittorrent" => "torrent",
+      "application/x-dvi" => "dvi",
+      "application/x-gzip" => "gz",
+      "application/x-ns-proxy-autoconfig" => "pac",
+      "application/x-shockwave-flash" => "swf",
+      "application/x-tgz" => "tar.gz",
+      "application/x-tar" => "tar",
+      "application/zip" => "zip",
+      "audio/mpeg" => "mp3",
+      "audio/x-mpegurl" => "m3u",
+      "audio/x-ms-wma" => "wma",
+      "audio/x-ms-wax" => "wax",
+      "audio/x-wav" => "wav",
+      "image/x-xbitmap" => "xbm",             
+      "image/x-xpixmap" => "xpm",             
+      "image/x-xwindowdump" => "xwd",             
+      "text/css" => "css",             
+      "text/html" => "html",                          
+      "text/javascript" => "js",
+      "text/plain" => "txt",
+      "text/xml" => "xml",
+      "video/mpeg" => "mpeg",
+      "video/quicktime" => "mov",
+      "video/x-msvideo" => "avi",
+      "video/x-ms-asf" => "asf",
+      "video/x-ms-wmv" => "wmv"
+    }
+
+    EXTENSIONS = Set.new MIME_EXTENSIONS.values
+    EXTENSIONS.merge %w(jpeg)
+
+    # default options. You can override these with +file_column+'s +options+ parameter
+    DEFAULT_OPTIONS = {
+      :root_path => File.join(RAILS_ROOT, "public"),
+      :web_root => "",
+      :mime_extensions => MIME_EXTENSIONS,
+      :extensions => EXTENSIONS,
+      :fix_file_extensions => true,
+      :permissions => 0644,
+
+      # path to the unix "file" executbale for
+      # guessing the content-type of files
+      :file_exec => "file" 
+    }
+    
+    # handle the +attr+ attribute as a "file-upload" column, generating additional methods as explained
+    # above. You should pass the attribute's name as a symbol, like this:
+    #
+    #   file_column :image
+    #
+    # You can pass in an options hash that overrides the options
+    # in +DEFAULT_OPTIONS+.
+    def file_column(attr, options={})
+      options = DEFAULT_OPTIONS.merge(options) if options
+      
+      my_options = FileColumn::init_options(options, 
+        ActiveSupport::Inflector.underscore(self.name).to_s,
+                                            attr.to_s)
+      
+      state_attr = "@#{attr}_state".to_sym
+      state_method = "#{attr}_state".to_sym
+      
+      define_method state_method do
+        result = instance_variable_get state_attr
+        if result.nil?
+          result = FileColumn::create_state(self, attr.to_s)
+          instance_variable_set state_attr, result
+        end
+        result
+      end
+      
+      private state_method
+      
+      define_method attr do |*args|
+        send(state_method).absolute_path *args
+      end
+      
+      define_method "#{attr}_relative_path" do |*args|
+        send(state_method).relative_path *args
+      end
+
+      define_method "#{attr}_dir" do
+        send(state_method).absolute_dir
+      end
+
+      define_method "#{attr}_relative_dir" do
+        send(state_method).relative_dir
+      end
+
+      define_method "#{attr}=" do |file|
+        state = send(state_method).assign(file)
+        instance_variable_set state_attr, state
+        if state.options[:after_upload] and state.just_uploaded?
+          state.options[:after_upload].each do |sym|
+            self.send sym
+          end
+        end
+      end
+      
+      define_method "#{attr}_temp" do
+        send(state_method).temp_path
+      end
+      
+      define_method "#{attr}_temp=" do |temp_path|
+        instance_variable_set state_attr, send(state_method).assign_temp(temp_path)
+      end
+      
+      after_save_method = "#{attr}_after_save".to_sym
+      
+      define_method after_save_method do
+        instance_variable_set state_attr, send(state_method).after_save
+      end
+      
+      after_save after_save_method
+      
+      after_destroy_method = "#{attr}_after_destroy".to_sym
+      
+      define_method after_destroy_method do
+        send(state_method).after_destroy
+      end
+      after_destroy after_destroy_method
+      
+      define_method "#{attr}_just_uploaded?" do
+        send(state_method).just_uploaded?
+      end
+
+      # this creates a closure keeping a reference to my_options
+      # right now that's the only way we store the options. We
+      # might use a class attribute as well
+      define_method "#{attr}_options" do
+        my_options
+      end
+
+      private after_save_method, after_destroy_method
+
+      FileColumn::MagickExtension::file_column(self, attr, my_options) if options[:magick]
+    end
+    
+  end
+  
+  private
+  
+  def self.generate_temp_name
+    now = Time.now
+    "#{now.to_i}.#{now.usec}.#{Process.pid}"
+  end
+  
+  def self.sanitize_filename(filename)
+    filename = File.basename(filename.gsub("\\", "/")) # work-around for IE
+    filename.gsub!(/[^a-zA-Z0-9\.\-\+_]/,"_")
+    filename = "_#{filename}" if filename =~ /^\.+$/
+    filename = "unnamed" if filename.size == 0
+    filename
+  end
+  
+end
+
+
diff --git a/vendor/plugins/file_column/lib/file_column_helper.rb b/vendor/plugins/file_column/lib/file_column_helper.rb
new file mode 100644 (file)
index 0000000..f4ebe38
--- /dev/null
@@ -0,0 +1,150 @@
+# This module contains helper methods for displaying and uploading files
+# for attributes created by +FileColumn+'s +file_column+ method. It will be
+# automatically included into ActionView::Base, thereby making this module's
+# methods available in all your views.
+module FileColumnHelper
+  
+  # Use this helper to create an upload field for a file_column attribute. This will generate
+  # an additional hidden field to keep uploaded files during form-redisplays. For example,
+  # when called with
+  #
+  #   <%= file_column_field("entry", "image") %>
+  #
+  # the following HTML will be generated (assuming the form is redisplayed and something has
+  # already been uploaded):
+  #
+  #   <input type="hidden" name="entry[image_temp]" value="..." />
+  #   <input type="file" name="entry[image]" />
+  #
+  # You can use the +option+ argument to pass additional options to the file-field tag.
+  #
+  # Be sure to set the enclosing form's encoding to 'multipart/form-data', by
+  # using something like this:
+  #
+  #    <%= form_tag {:action => "create", ...}, :multipart => true %>
+  def file_column_field(object, method, options={})
+    result = ActionView::Helpers::InstanceTag.new(object.dup, method.to_s+"_temp", self).to_input_field_tag("hidden", {})
+    result << ActionView::Helpers::InstanceTag.new(object.dup, method, self).to_input_field_tag("file", options)
+  end
+  
+  # Creates an URL where an uploaded file can be accessed. When called for an Entry object with
+  # id 42 (stored in <tt>@entry</tt>) like this
+  #
+  #   <%= url_for_file_column(@entry, "image")
+  #
+  # the following URL will be produced, assuming the file "test.png" has been stored in
+  # the "image"-column of an Entry object stored in <tt>@entry</tt>:
+  #
+  #  /entry/image/42/test.png
+  #
+  # This will produce a valid URL even for temporary uploaded files, e.g. files where the object
+  # they are belonging to has not been saved in the database yet.
+  #
+  # The URL produces, although starting with a slash, will be relative
+  # to your app's root. If you pass it to one rails' +image_tag+
+  # helper, rails will properly convert it to an absolute
+  # URL. However, this will not be the case, if you create a link with
+  # the +link_to+ helper. In this case, you can pass <tt>:absolute =>
+  # true</tt> to +options+, which will make sure, the generated URL is
+  # absolute on your server.  Examples:
+  #
+  #    <%= image_tag url_for_file_column(@entry, "image") %>
+  #    <%= link_to "Download", url_for_file_column(@entry, "image", :absolute => true) %>
+  #
+  # If there is currently no uploaded file stored in the object's column this method will
+  # return +nil+.
+  def url_for_file_column(object, method, options=nil)
+    case object
+    when String, Symbol
+      object = instance_variable_get("@#{object.to_s}")
+    end
+
+    # parse options
+    subdir = nil
+    absolute = false
+    if options
+      case options
+      when Hash
+        subdir = options[:subdir]
+        absolute = options[:absolute]
+      when String, Symbol
+        subdir = options
+      end
+    end
+    
+    relative_path = object.send("#{method}_relative_path", subdir)
+    return nil unless relative_path
+
+    url = ""
+    url << request.relative_url_root.to_s if absolute
+    url << "/"
+    url << object.send("#{method}_options")[:base_url] << "/"
+    url << relative_path
+  end
+
+  # Same as +url_for_file_colum+ but allows you to access different versions
+  # of the image that have been processed by RMagick.
+  #
+  # If your +options+ parameter is non-nil this will
+  # access a different version of an image that will be produced by
+  # RMagick. You can use the following types for +options+:
+  #
+  # * a <tt>:symbol</tt> will select a version defined in the model
+  #   via FileColumn::Magick's <tt>:versions</tt> feature.
+  # * a <tt>geometry_string</tt> will dynamically create an
+  #   image resized as specified by <tt>geometry_string</tt>. The image will
+  #   be stored so that it does not have to be recomputed the next time the
+  #   same version string is used.
+  # * <tt>some_hash</tt> will dynamically create an image
+  #   that is created according to the options in <tt>some_hash</tt>. This
+  #   accepts exactly the same options as Magick's version feature.
+  #
+  # The version produced by RMagick will be stored in a special sub-directory.
+  # The directory's name will be derived from the options you specified
+  # (via a hash function) but if you want
+  # to set it yourself, you can use the <tt>:name => name</tt> option.
+  #
+  # Examples:
+  #
+  #    <%= url_for_image_column @entry, "image", "640x480" %>
+  #
+  # will produce an URL like this
+  #
+  #    /entry/image/42/bdn19n/filename.jpg
+  #    # "640x480".hash.abs.to_s(36) == "bdn19n"
+  #
+  # and
+  #
+  #    <%= url_for_image_column @entry, "image", 
+  #       :size => "50x50", :crop => "1:1", :name => "thumb" %>
+  #
+  # will produce something like this:
+  #
+  #    /entry/image/42/thumb/filename.jpg
+  #
+  # Hint: If you are using the same geometry string / options hash multiple times, you should
+  # define it in a helper to stay with DRY. Another option is to define it in the model via
+  # FileColumn::Magick's <tt>:versions</tt> feature and then refer to it via a symbol.
+  #
+  # The URL produced by this method is relative to your application's root URL,
+  # although it will start with a slash.
+  # If you pass this URL to rails' +image_tag+ helper, it will be converted to an
+  # absolute URL automatically.
+  # If there is currently no image uploaded, or there is a problem while loading
+  # the image this method will return +nil+.
+  def url_for_image_column(object, method, options=nil)
+    case object
+    when String, Symbol
+      object = instance_variable_get("@#{object.to_s}")
+    end
+    subdir = nil
+    if options
+      subdir = object.send("#{method}_state").create_magick_version_if_needed(options)
+    end
+    if subdir.nil?
+      nil
+    else
+      url_for_file_column(object, method, subdir)
+    end
+  end
+end
diff --git a/vendor/plugins/file_column/lib/file_compat.rb b/vendor/plugins/file_column/lib/file_compat.rb
new file mode 100644 (file)
index 0000000..f284410
--- /dev/null
@@ -0,0 +1,28 @@
+module FileColumn
+
+  # This bit of code allows you to pass regular old files to
+  # file_column.  file_column depends on a few extra methods that the
+  # CGI uploaded file class adds.  We will add the equivalent methods
+  # to file objects if necessary by extending them with this module. This
+  # avoids opening up the standard File class which might result in
+  # naming conflicts.
+
+  module FileCompat # :nodoc:
+    def original_filename
+      File.basename(path)
+    end
+    
+    def size
+      File.size(path)
+    end
+    
+    def local_path
+      path
+    end
+    
+    def content_type
+      nil
+    end
+  end
+end
+
diff --git a/vendor/plugins/file_column/lib/magick_file_column.rb b/vendor/plugins/file_column/lib/magick_file_column.rb
new file mode 100644 (file)
index 0000000..c4dc06f
--- /dev/null
@@ -0,0 +1,260 @@
+module FileColumn # :nodoc:
+  
+  class BaseUploadedFile # :nodoc:
+    def transform_with_magick
+      if needs_transform?
+        begin
+          img = ::Magick::Image::read(absolute_path).first
+        rescue ::Magick::ImageMagickError
+          if options[:magick][:image_required]
+            @magick_errors ||= []
+            @magick_errors << "invalid image"
+          end
+          return
+        end
+        
+        if options[:magick][:versions]
+          options[:magick][:versions].each_pair do |version, version_options|
+            next if version_options[:lazy]
+            dirname = version_options[:name]
+            FileUtils.mkdir File.join(@dir, dirname)
+            transform_image(img, version_options, absolute_path(dirname))
+          end
+        end
+        if options[:magick][:size] or options[:magick][:crop] or options[:magick][:transformation] or options[:magick][:attributes]
+          transform_image(img, options[:magick], absolute_path)
+        end
+
+        GC.start
+      end
+    end
+
+    def create_magick_version_if_needed(version)
+      # RMagick might not have been loaded so far.
+      # We do not want to require it on every call of this method
+      # as this might be fairly expensive, so we just try if ::Magick
+      # exists and require it if not.
+      begin 
+        ::Magick 
+      rescue NameError
+        require 'RMagick'
+      end
+
+      if version.is_a?(Symbol)
+        version_options = options[:magick][:versions][version]
+      else
+        version_options = MagickExtension::process_options(version)
+      end
+
+      unless File.exists?(absolute_path(version_options[:name]))
+        begin
+          img = ::Magick::Image::read(absolute_path).first
+        rescue ::Magick::ImageMagickError
+          # we might be called directly from the view here
+          # so we just return nil if we cannot load the image
+          return nil
+        end
+        dirname = version_options[:name]
+        FileUtils.mkdir File.join(@dir, dirname)
+        transform_image(img, version_options, absolute_path(dirname))
+      end
+
+      version_options[:name]
+    end
+
+    attr_reader :magick_errors
+    
+    def has_magick_errors?
+      @magick_errors and !@magick_errors.empty?
+    end
+
+    private
+    
+    def needs_transform?
+      options[:magick] and just_uploaded? and 
+        (options[:magick][:size] or options[:magick][:versions] or options[:magick][:transformation] or options[:magick][:attributes])
+    end
+
+    def transform_image(img, img_options, dest_path)
+      begin
+        if img_options[:transformation]
+          if img_options[:transformation].is_a?(Symbol)
+            img = @instance.send(img_options[:transformation], img)
+          else
+            img = img_options[:transformation].call(img)
+          end
+        end
+        if img_options[:crop]
+          dx, dy = img_options[:crop].split(':').map { |x| x.to_f }
+          w, h = (img.rows * dx / dy), (img.columns * dy / dx)
+          img = img.crop(::Magick::CenterGravity, [img.columns, w].min, 
+                         [img.rows, h].min, true)
+        end
+
+        if img_options[:size]
+          img = img.change_geometry(img_options[:size]) do |c, r, i|
+            i.resize(c, r)
+          end
+        end
+      ensure
+        img.write(dest_path) do
+          if img_options[:attributes]
+            img_options[:attributes].each_pair do |property, value| 
+              self.send "#{property}=", value
+            end
+          end
+        end
+        File.chmod options[:permissions], dest_path
+      end
+    end
+  end
+
+  # If you are using file_column to upload images, you can
+  # directly process the images with RMagick,
+  # a ruby extension
+  # for accessing the popular imagemagick libraries. You can find
+  # more information about RMagick at http://rmagick.rubyforge.org.
+  #
+  # You can control what to do by adding a <tt>:magick</tt> option
+  # to your options hash. All operations are performed immediately
+  # after a new file is assigned to the file_column attribute (i.e.,
+  # when a new file has been uploaded).
+  #
+  # == Resizing images
+  #
+  # To resize the uploaded image according to an imagemagick geometry
+  # string, just use the <tt>:size</tt> option:
+  #
+  #    file_column :image, :magick => {:size => "800x600>"}
+  #
+  # If the uploaded file cannot be loaded by RMagick, file_column will
+  # signal a validation error for the corresponding attribute. If you
+  # want to allow non-image files to be uploaded in a column that uses
+  # the <tt>:magick</tt> option, you can set the <tt>:image_required</tt>
+  # attribute to +false+:
+  #
+  #    file_column :image, :magick => {:size => "800x600>",
+  #                                    :image_required => false }
+  #
+  # == Multiple versions
+  #
+  # You can also create additional versions of your image, for example
+  # thumb-nails, like this:
+  #    file_column :image, :magick => {:versions => {
+  #         :thumb => {:size => "50x50"},
+  #         :medium => {:size => "640x480>"}
+  #       }
+  #
+  # These versions will be stored in separate sub-directories, named like the
+  # symbol you used to identify the version. So in the previous example, the
+  # image versions will be stored in "thumb", "screen" and "widescreen"
+  # directories, resp. 
+  # A name different from the symbol can be set via the <tt>:name</tt> option.
+  #
+  # These versions can be accessed via FileColumnHelper's +url_for_image_column+
+  # method like this:
+  #
+  #    <%= url_for_image_column "entry", "image", :thumb %>
+  #
+  # == Cropping images
+  #
+  # If you wish to crop your images with a size ratio before scaling
+  # them according to your version geometry, you can use the :crop directive.
+  #    file_column :image, :magick => {:versions => {
+  #         :square => {:crop => "1:1", :size => "50x50", :name => "thumb"},
+  #         :screen => {:crop => "4:3", :size => "640x480>"},
+  #         :widescreen => {:crop => "16:9", :size => "640x360!"},
+  #       }
+  #    }
+  #
+  # == Custom attributes
+  #
+  # To change some of the image properties like compression level before they
+  # are saved you can set the <tt>:attributes</tt> option.
+  # For a list of available attributes go to http://www.simplesystems.org/RMagick/doc/info.html
+  # 
+  #     file_column :image, :magick => { :attributes => { :quality => 30 } }
+  # 
+  # == Custom transformations
+  #
+  # To perform custom transformations on uploaded images, you can pass a
+  # callback to file_column:
+  #    file_column :image, :magick => 
+  #       Proc.new { |image| image.quantize(256, Magick::GRAYColorspace) }
+  #
+  # The callback you give, receives one argument, which is an instance
+  # of Magick::Image, the RMagick image class. It should return a transformed
+  # image. Instead of passing a <tt>Proc</tt> object, you can also give a
+  # <tt>Symbol</tt>, the name of an instance method of your model.
+  #
+  # Custom transformations can be combined via the standard :size and :crop
+  # features, by using the :transformation option:
+  #   file_column :image, :magick => {
+  #      :transformation => Proc.new { |image| ... },
+  #      :size => "640x480"
+  #    }
+  #
+  # In this case, the standard resizing operations will be performed after the
+  # custom transformation.
+  #
+  # Of course, custom transformations can be used in versions, as well.
+  #
+  # <b>Note:</b> You'll need the
+  # RMagick extension being installed  in order to use file_column's
+  # imagemagick integration.
+  module MagickExtension
+
+    def self.file_column(klass, attr, options) # :nodoc:
+      require 'RMagick'
+      options[:magick] = process_options(options[:magick],false) if options[:magick]
+      if options[:magick][:versions]
+        options[:magick][:versions].each_pair do |name, value|
+          options[:magick][:versions][name] = process_options(value, name.to_s)
+        end
+      end
+      state_method = "#{attr}_state".to_sym
+      after_assign_method = "#{attr}_magick_after_assign".to_sym
+      
+      klass.send(:define_method, after_assign_method) do
+        self.send(state_method).transform_with_magick
+      end
+      
+      options[:after_upload] ||= []
+      options[:after_upload] << after_assign_method
+      
+      klass.validate do |record|
+        state = record.send(state_method)
+        if state.has_magick_errors?
+          state.magick_errors.each do |error|
+            record.errors.add attr, error
+          end
+        end
+      end
+    end
+
+    
+    def self.process_options(options,create_name=true)
+      case options
+      when String then options = {:size => options}
+      when Proc, Symbol then options = {:transformation => options }
+      end
+      if options[:geometry]
+        options[:size] = options.delete(:geometry)
+      end
+      options[:image_required] = true unless options.key?(:image_required)
+      if options[:name].nil? and create_name
+        if create_name == true
+          hash = 0
+          for key in [:size, :crop]
+            hash = hash ^ options[key].hash if options[key]
+          end
+          options[:name] = hash.abs.to_s(36)
+        else
+          options[:name] = create_name
+        end
+      end
+      options
+    end
+
+  end
+end
diff --git a/vendor/plugins/file_column/lib/rails_file_column.rb b/vendor/plugins/file_column/lib/rails_file_column.rb
new file mode 100644 (file)
index 0000000..af8c95a
--- /dev/null
@@ -0,0 +1,19 @@
+# require this file from your "config/environment.rb" (after rails has been loaded)
+# to integrate the file_column extension into rails.
+
+require 'file_column'
+require 'file_column_helper'
+
+
+module ActiveRecord # :nodoc:
+  class Base # :nodoc:
+    # make file_column method available in all active record decendants
+    include FileColumn
+  end
+end
+
+module ActionView # :nodoc:
+  class Base # :nodoc:
+    include FileColumnHelper
+  end
+end
diff --git a/vendor/plugins/file_column/lib/test_case.rb b/vendor/plugins/file_column/lib/test_case.rb
new file mode 100644 (file)
index 0000000..1416a1e
--- /dev/null
@@ -0,0 +1,124 @@
+require 'test/unit'
+
+# Add the methods +upload+, the <tt>setup_file_fixtures</tt> and
+# <tt>teardown_file_fixtures</tt> to the class Test::Unit::TestCase.
+class Test::Unit::TestCase
+  # Returns a +Tempfile+ object as it would have been generated on file upload.
+  # Use this method to create the parameters when emulating form posts with 
+  # file fields.
+  #
+  # === Example:
+  #
+  #    def test_file_column_post
+  #      entry = { :title => 'foo', :file => upload('/tmp/foo.txt')}
+  #      post :upload, :entry => entry
+  #  
+  #      # ...
+  #    end
+  #
+  # === Parameters
+  #
+  # * <tt>path</tt> The path to the file to upload.
+  # * <tt>content_type</tt> The MIME type of the file. If it is <tt>:guess</tt>,
+  #   the method will try to guess it.
+  def upload(path, content_type=:guess, type=:tempfile)
+    if content_type == :guess
+      case path
+      when /\.jpg$/ then content_type = "image/jpeg"
+      when /\.png$/ then content_type = "image/png"
+      else content_type = nil
+      end
+    end
+    uploaded_file(path, content_type, File.basename(path), type)
+  end
+  
+  # Copies the fixture files from "RAILS_ROOT/test/fixtures/file_column" into
+  # the temporary storage directory used for testing
+  # ("RAILS_ROOT/test/tmp/file_column"). Call this method in your
+  # <tt>setup</tt> methods to get the file fixtures (images, for example) into
+  # the directory used by file_column in testing.
+  #
+  # Note that the files and directories in the "fixtures/file_column" directory 
+  # must have the same structure as you would expect in your "/public" directory
+  # after uploading with FileColumn.
+  #
+  # For example, the directory structure could look like this:
+  #
+  #   test/fixtures/file_column/
+  #   `-- container
+  #       |-- first_image
+  #       |   |-- 1
+  #       |   |   `-- image1.jpg
+  #       |   `-- tmp
+  #       `-- second_image
+  #           |-- 1
+  #           |   `-- image2.jpg
+  #           `-- tmp
+  #
+  # Your fixture file for this one "container" class fixture could look like this:
+  #
+  #   first:
+  #     id:           1
+  #     first_image:  image1.jpg
+  #     second_image: image1.jpg
+  #
+  # A usage example:
+  #
+  #  def setup
+  #    setup_fixture_files
+  #
+  #    # ...
+  #  end
+  def setup_fixture_files
+    tmp_path = File.join(RAILS_ROOT, "test", "tmp", "file_column")
+    file_fixtures = Dir.glob File.join(RAILS_ROOT, "test", "fixtures", "file_column", "*")
+    
+    FileUtils.mkdir_p tmp_path unless File.exists?(tmp_path)
+    FileUtils.cp_r file_fixtures, tmp_path
+  end
+  
+  # Removes the directory "RAILS_ROOT/test/tmp/file_column/" so the files
+  # copied on test startup are removed. Call this in your unit test's +teardown+
+  # method.
+  #
+  # A usage example:
+  #
+  #  def teardown
+  #    teardown_fixture_files
+  #
+  #    # ...
+  #  end
+  def teardown_fixture_files
+    FileUtils.rm_rf File.join(RAILS_ROOT, "test", "tmp", "file_column")
+  end
+  
+  private
+  
+  def uploaded_file(path, content_type, filename, type=:tempfile) # :nodoc:
+    if type == :tempfile
+      t = Tempfile.new(File.basename(filename))
+      FileUtils.copy_file(path, t.path)
+    else
+      if path
+        t = StringIO.new(IO.read(path))
+      else
+        t = StringIO.new
+      end
+    end
+    (class << t; self; end).class_eval do
+      alias local_path path if type == :tempfile
+      define_method(:local_path) { "" } if type == :stringio
+      define_method(:original_filename) {filename}
+      define_method(:content_type) {content_type}
+    end
+    return t
+  end
+end
+
+# If we are running in the "test" environment, we overwrite the default 
+# settings for FileColumn so that files are not uploaded into "/public/"
+# in tests but rather into the directory "/test/tmp/file_column".
+if RAILS_ENV == "test"
+  FileColumn::ClassMethods::DEFAULT_OPTIONS[:root_path] =
+    File.join(RAILS_ROOT, "test", "tmp", "file_column")
+end
diff --git a/vendor/plugins/file_column/lib/validations.rb b/vendor/plugins/file_column/lib/validations.rb
new file mode 100644 (file)
index 0000000..5b961eb
--- /dev/null
@@ -0,0 +1,112 @@
+module FileColumn
+  module Validations #:nodoc:
+    
+    def self.append_features(base)
+      super
+      base.extend(ClassMethods)
+    end
+
+    # This module contains methods to create validations of uploaded files. All methods
+    # in this module will be included as class methods into <tt>ActiveRecord::Base</tt>
+    # so that you can use them in your models like this:
+    #
+    #    class Entry < ActiveRecord::Base
+    #      file_column :image
+    #      validates_filesize_of :image, :in => 0..1.megabyte
+    #    end
+    module ClassMethods
+      EXT_REGEXP = /\.([A-z0-9]+)$/
+    
+      # This validates the file type of one or more file_columns.  A list of file columns
+      # should be given followed by an options hash.
+      #
+      # Required options:
+      # * <tt>:in</tt> => list of extensions or mime types. If mime types are used they
+      #   will be mapped into an extension via FileColumn::ClassMethods::MIME_EXTENSIONS.
+      #
+      # Examples:
+      #     validates_file_format_of :field, :in => ["gif", "png", "jpg"]
+      #     validates_file_format_of :field, :in => ["image/jpeg"]
+      def validates_file_format_of(*attrs)
+      
+        options = attrs.pop if attrs.last.is_a?Hash
+        raise ArgumentError, "Please include the :in option." if !options || !options[:in]
+        options[:in] = [options[:in]] if options[:in].is_a?String
+        raise ArgumentError, "Invalid value for option :in" unless options[:in].is_a?Array
+      
+        validates_each(attrs, options) do |record, attr, value|
+          unless value.blank?
+            mime_extensions = record.send("#{attr}_options")[:mime_extensions]
+            extensions = options[:in].map{|o| mime_extensions[o] || o }
+            record.errors.add attr, "is not a valid format." unless extensions.include?(value.scan(EXT_REGEXP).flatten.first)
+          end
+        end
+      
+      end
+    
+      # This validates the file size of one or more file_columns.  A list of file columns
+      # should be given followed by an options hash.
+      #
+      # Required options:
+      # * <tt>:in</tt> => A size range.  Note that you can use ActiveSupport's
+      #   numeric extensions for kilobytes, etc.
+      #
+      # Examples:
+      #    validates_filesize_of :field, :in => 0..100.megabytes
+      #    validates_filesize_of :field, :in => 15.kilobytes..1.megabyte
+      def validates_filesize_of(*attrs)  
+      
+        options = attrs.pop if attrs.last.is_a?Hash
+        raise ArgumentError, "Please include the :in option." if !options || !options[:in]
+        raise ArgumentError, "Invalid value for option :in" unless options[:in].is_a?Range
+      
+        validates_each(attrs, options) do |record, attr, value|
+          unless value.blank?
+            size = File.size(value)
+            record.errors.add attr, "is smaller than the allowed size range." if size < options[:in].first
+            record.errors.add attr, "is larger than the allowed size range." if size > options[:in].last
+          end
+        end
+      
+      end 
+
+      IMAGE_SIZE_REGEXP = /^(\d+)x(\d+)$/
+
+      # Validates the image size of one or more file_columns.  A list of file columns
+      # should be given followed by an options hash. The validation will pass
+      # if both image dimensions (rows and columns) are at least as big as
+      # given in the <tt>:min</tt> option.
+      #
+      # Required options:
+      # * <tt>:min</tt> => minimum image dimension string, in the format NNxNN
+      #   (columns x rows).
+      #
+      # Example:
+      #    validates_image_size :field, :min => "1200x1800"
+      #
+      # This validation requires RMagick to be installed on your system
+      # to check the image's size.
+      def validates_image_size(*attrs)      
+        options = attrs.pop if attrs.last.is_a?Hash
+        raise ArgumentError, "Please include a :min option." if !options || !options[:min]
+        minimums = options[:min].scan(IMAGE_SIZE_REGEXP).first.collect{|n| n.to_i} rescue []
+        raise ArgumentError, "Invalid value for option :min (should be 'XXxYY')" unless minimums.size == 2
+
+        require 'RMagick'
+
+        validates_each(attrs, options) do |record, attr, value|
+          unless value.blank?
+            begin
+              img = ::Magick::Image::read(value).first
+              record.errors.add('image', "is too small, must be at least #{minimums[0]}x#{minimums[1]}") if ( img.rows < minimums[1] || img.columns < minimums[0] )
+            rescue ::Magick::ImageMagickError
+              record.errors.add('image', "invalid image")
+            end
+            img = nil
+            GC.start
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/vendor/plugins/file_column/test/abstract_unit.rb b/vendor/plugins/file_column/test/abstract_unit.rb
new file mode 100644 (file)
index 0000000..22bc53b
--- /dev/null
@@ -0,0 +1,63 @@
+require 'test/unit'
+require 'rubygems'
+require 'active_support'
+require 'active_record'
+require 'action_view'
+require File.dirname(__FILE__) + '/connection'
+require 'stringio'
+
+RAILS_ROOT = File.dirname(__FILE__)
+RAILS_ENV = ""
+
+$: << "../lib"
+
+require 'file_column'
+require 'file_compat'
+require 'validations'
+require 'test_case'
+
+# do not use the file executable normally in our tests as
+# it may not be present on the machine we are running on
+FileColumn::ClassMethods::DEFAULT_OPTIONS = 
+  FileColumn::ClassMethods::DEFAULT_OPTIONS.merge({:file_exec => nil})
+
+class ActiveRecord::Base
+    include FileColumn
+    include FileColumn::Validations
+end
+
+
+class RequestMock
+  attr_accessor :relative_url_root
+
+  def initialize
+    @relative_url_root = ""
+  end
+end
+
+class Test::Unit::TestCase
+
+  def assert_equal_paths(expected_path, path)
+    assert_equal normalize_path(expected_path), normalize_path(path)
+  end
+
+
+  private
+  
+  def normalize_path(path)
+    Pathname.new(path).realpath
+  end
+
+  def clear_validations
+    [:validate, :validate_on_create, :validate_on_update].each do |attr|
+        Entry.write_inheritable_attribute attr, []
+        Movie.write_inheritable_attribute attr, []
+      end
+  end
+
+  def file_path(filename)
+    File.expand_path("#{File.dirname(__FILE__)}/fixtures/#{filename}")
+  end
+
+  alias_method :f, :file_path
+end
diff --git a/vendor/plugins/file_column/test/connection.rb b/vendor/plugins/file_column/test/connection.rb
new file mode 100644 (file)
index 0000000..a2f28ba
--- /dev/null
@@ -0,0 +1,17 @@
+print "Using native MySQL\n"
+require 'logger'
+
+ActiveRecord::Base.logger = Logger.new("debug.log")
+
+db = 'file_column_test'
+
+ActiveRecord::Base.establish_connection(
+  :adapter  => "mysql",
+  :host     => "localhost",
+  :username => "rails",
+  :password => "",
+  :database => db,
+  :socket => "/var/run/mysqld/mysqld.sock"
+)
+
+load File.dirname(__FILE__) + "/fixtures/schema.rb"
diff --git a/vendor/plugins/file_column/test/file_column_helper_test.rb b/vendor/plugins/file_column/test/file_column_helper_test.rb
new file mode 100644 (file)
index 0000000..ffb2c43
--- /dev/null
@@ -0,0 +1,97 @@
+require File.dirname(__FILE__) + '/abstract_unit'
+require File.dirname(__FILE__) + '/fixtures/entry'
+
+class UrlForFileColumnTest < Test::Unit::TestCase
+  include FileColumnHelper
+
+  def setup
+    Entry.file_column :image
+    @request = RequestMock.new
+  end
+
+  def test_url_for_file_column_with_temp_entry
+    @e = Entry.new(:image => upload(f("skanthak.png")))
+    url = url_for_file_column("e", "image")
+    assert_match %r{^/entry/image/tmp/\d+(\.\d+)+/skanthak.png$}, url
+  end
+
+  def test_url_for_file_column_with_saved_entry
+    @e = Entry.new(:image => upload(f("skanthak.png")))
+    assert @e.save
+
+    url = url_for_file_column("e", "image")
+    assert_equal "/entry/image/#{@e.id}/skanthak.png", url
+  end
+
+  def test_url_for_file_column_works_with_symbol
+    @e = Entry.new(:image => upload(f("skanthak.png")))
+    assert @e.save
+
+    url = url_for_file_column(:e, :image)
+    assert_equal "/entry/image/#{@e.id}/skanthak.png", url
+  end
+  
+  def test_url_for_file_column_works_with_object
+    e = Entry.new(:image => upload(f("skanthak.png")))
+    assert e.save
+
+    url = url_for_file_column(e, "image")
+    assert_equal "/entry/image/#{e.id}/skanthak.png", url
+  end
+
+  def test_url_for_file_column_should_return_nil_on_no_uploaded_file
+    e = Entry.new
+    assert_nil url_for_file_column(e, "image")
+  end
+
+  def test_url_for_file_column_without_extension
+    e = Entry.new
+    e.image = uploaded_file(file_path("kerb.jpg"), "something/unknown", "local_filename")
+    assert e.save
+    assert_equal "/entry/image/#{e.id}/local_filename", url_for_file_column(e, "image")
+  end
+end
+
+class UrlForFileColumnTest < Test::Unit::TestCase
+  include FileColumnHelper
+  include ActionView::Helpers::AssetTagHelper
+  include ActionView::Helpers::TagHelper
+  include ActionView::Helpers::UrlHelper
+
+  def setup
+    Entry.file_column :image
+
+    # mock up some request data structures for AssetTagHelper
+    @request = RequestMock.new
+    @request.relative_url_root = "/foo/bar"
+    @controller = self
+  end
+
+  def request
+    @request
+  end
+
+  IMAGE_URL = %r{^/foo/bar/entry/image/.+/skanthak.png$}
+  def test_with_image_tag
+    e = Entry.new(:image => upload(f("skanthak.png")))
+    html = image_tag url_for_file_column(e, "image")
+    url = html.scan(/src=\"(.+)\"/).first.first
+
+    assert_match IMAGE_URL, url
+  end
+
+  def test_with_link_to_tag
+    e = Entry.new(:image => upload(f("skanthak.png")))
+    html = link_to "Download", url_for_file_column(e, "image", :absolute => true)
+    url = html.scan(/href=\"(.+)\"/).first.first
+    
+    assert_match IMAGE_URL, url
+  end
+
+  def test_relative_url_root_not_modified
+    e = Entry.new(:image => upload(f("skanthak.png")))
+    url_for_file_column(e, "image", :absolute => true)
+    
+    assert_equal "/foo/bar", @request.relative_url_root
+  end
+end
diff --git a/vendor/plugins/file_column/test/file_column_test.rb b/vendor/plugins/file_column/test/file_column_test.rb
new file mode 100755 (executable)
index 0000000..452b781
--- /dev/null
@@ -0,0 +1,650 @@
+require File.dirname(__FILE__) + '/abstract_unit'
+
+require File.dirname(__FILE__) + '/fixtures/entry'
+
+class Movie < ActiveRecord::Base
+end
+
+
+class FileColumnTest < Test::Unit::TestCase
+  
+  def setup
+    # we define the file_columns here so that we can change
+    # settings easily in a single test
+
+    Entry.file_column :image
+    Entry.file_column :file
+    Movie.file_column :movie
+
+    clear_validations
+  end
+  
+  def teardown
+    FileUtils.rm_rf File.dirname(__FILE__)+"/public/entry/"
+    FileUtils.rm_rf File.dirname(__FILE__)+"/public/movie/"
+    FileUtils.rm_rf File.dirname(__FILE__)+"/public/my_store_dir/"
+  end
+  
+  def test_column_write_method
+    assert Entry.new.respond_to?("image=")
+  end
+  
+  def test_column_read_method
+    assert Entry.new.respond_to?("image")
+  end
+  
+  def test_sanitize_filename
+    assert_equal "test.jpg", FileColumn::sanitize_filename("test.jpg")
+    assert FileColumn::sanitize_filename("../../very_tricky/foo.bar") !~ /[\\\/]/, "slashes not removed"
+    assert_equal "__foo", FileColumn::sanitize_filename('`*foo')
+    assert_equal "foo.txt", FileColumn::sanitize_filename('c:\temp\foo.txt')
+    assert_equal "_.", FileColumn::sanitize_filename(".")
+  end
+  
+  def test_default_options
+    e = Entry.new
+    assert_match %r{/public/entry/image}, e.image_options[:store_dir]
+    assert_match %r{/public/entry/image/tmp}, e.image_options[:tmp_base_dir]
+  end
+  
+  def test_assign_without_save_with_tempfile
+    do_test_assign_without_save(:tempfile)
+  end
+  
+  def test_assign_without_save_with_stringio
+    do_test_assign_without_save(:stringio)
+  end
+  
+  def do_test_assign_without_save(upload_type)
+    e = Entry.new
+    e.image = uploaded_file(file_path("skanthak.png"), "image/png", "skanthak.png", upload_type)
+    assert e.image.is_a?(String), "#{e.image.inspect} is not a String"
+    assert File.exists?(e.image)
+    assert FileUtils.identical?(e.image, file_path("skanthak.png"))
+  end
+  
+  def test_filename_preserved
+    e = Entry.new
+    e.image = uploaded_file(file_path("kerb.jpg"), "image/jpeg", "local_filename.jpg")
+    assert_equal "local_filename.jpg", File.basename(e.image)
+  end
+  
+  def test_filename_stored_in_attribute
+    e = Entry.new("image" => uploaded_file(file_path("kerb.jpg"), "image/jpeg", "kerb.jpg"))
+    assert_equal "kerb.jpg", e["image"]
+  end
+  
+  def test_extension_added
+    e = Entry.new
+    e.image = uploaded_file(file_path("kerb.jpg"), "image/jpeg", "local_filename")
+    assert_equal "local_filename.jpg", File.basename(e.image)
+    assert_equal "local_filename.jpg", e["image"]
+  end
+
+  def test_no_extension_without_content_type
+    e = Entry.new
+    e.image = uploaded_file(file_path("kerb.jpg"), "something/unknown", "local_filename")
+    assert_equal "local_filename", File.basename(e.image)
+    assert_equal "local_filename", e["image"]
+  end
+
+  def test_extension_unknown_type
+    e = Entry.new
+    e.image = uploaded_file(file_path("kerb.jpg"), "not/known", "local_filename")
+    assert_equal "local_filename", File.basename(e.image)
+    assert_equal "local_filename", e["image"]
+  end
+
+  def test_extension_unknown_type_with_extension
+    e = Entry.new
+    e.image = uploaded_file(file_path("kerb.jpg"), "not/known", "local_filename.abc")
+    assert_equal "local_filename.abc", File.basename(e.image)
+    assert_equal "local_filename.abc", e["image"]
+  end
+
+  def test_extension_corrected
+    e = Entry.new
+    e.image = uploaded_file(file_path("kerb.jpg"), "image/jpeg", "local_filename.jpeg")
+    assert_equal "local_filename.jpg", File.basename(e.image)
+    assert_equal "local_filename.jpg", e["image"]
+  end
+
+  def test_double_extension
+    e = Entry.new
+    e.image = uploaded_file(file_path("kerb.jpg"), "application/x-tgz", "local_filename.tar.gz")
+    assert_equal "local_filename.tar.gz", File.basename(e.image)
+    assert_equal "local_filename.tar.gz", e["image"]
+  end
+
+  FILE_UTILITY = "/usr/bin/file"
+
+  def test_get_content_type_with_file
+    Entry.file_column :image, :file_exec => FILE_UTILITY
+
+    # run this test only if the machine we are running on
+    # has the file utility installed
+    if File.executable?(FILE_UTILITY)
+      e = Entry.new
+      file = FileColumn::TempUploadedFile.new(e, "image")
+      file.instance_variable_set :@dir, File.dirname(file_path("kerb.jpg"))
+      file.instance_variable_set :@filename, File.basename(file_path("kerb.jpg"))
+      
+      assert_equal "image/jpeg", file.get_content_type
+    else
+      puts "Warning: Skipping test_get_content_type_with_file test as '#{options[:file_exec]}' does not exist"
+    end
+  end
+
+  def test_fix_extension_with_file
+    Entry.file_column :image, :file_exec => FILE_UTILITY
+
+    # run this test only if the machine we are running on
+    # has the file utility installed
+    if File.executable?(FILE_UTILITY)
+      e = Entry.new(:image => uploaded_file(file_path("skanthak.png"), "", "skanthak.jpg"))
+      
+      assert_equal "skanthak.png", File.basename(e.image)
+    else
+      puts "Warning: Skipping test_fix_extension_with_file test as '#{options[:file_exec]}' does not exist"
+    end
+  end
+
+  def test_do_not_fix_file_extensions
+    Entry.file_column :image, :fix_file_extensions => false
+
+    e = Entry.new(:image => uploaded_file(file_path("kerb.jpg"), "image/jpeg", "kerb"))
+
+    assert_equal "kerb", File.basename(e.image)
+  end
+
+  def test_correct_extension
+    e = Entry.new
+    file = FileColumn::TempUploadedFile.new(e, "image")
+    
+    assert_equal "filename.jpg", file.correct_extension("filename.jpeg","jpg")
+    assert_equal "filename.tar.gz", file.correct_extension("filename.jpg","tar.gz")
+    assert_equal "filename.jpg", file.correct_extension("filename.tar.gz","jpg")
+    assert_equal "Protokoll_01.09.2005.doc", file.correct_extension("Protokoll_01.09.2005","doc")
+    assert_equal "strange.filenames.exist.jpg", file.correct_extension("strange.filenames.exist","jpg")
+    assert_equal "another.strange.one.jpg", file.correct_extension("another.strange.one.png","jpg")
+  end
+
+  def test_assign_with_save
+    e = Entry.new
+    e.image = uploaded_file(file_path("kerb.jpg"), "image/jpeg", "kerb.jpg")
+    tmp_file_path = e.image
+    assert e.save
+    assert File.exists?(e.image)
+    assert FileUtils.identical?(e.image, file_path("kerb.jpg"))
+    assert_equal "#{e.id}/kerb.jpg", e.image_relative_path
+    assert !File.exists?(tmp_file_path), "temporary file '#{tmp_file_path}' not removed"
+    assert !File.exists?(File.dirname(tmp_file_path)), "temporary directory '#{File.dirname(tmp_file_path)}' not removed"
+    
+    local_path = e.image
+    e = Entry.find(e.id)
+    assert_equal local_path, e.image
+  end
+
+  def test_dir_methods
+    e = Entry.new
+    e.image = uploaded_file(file_path("kerb.jpg"), "image/jpeg", "kerb.jpg")
+    e.save
+    
+    assert_equal_paths File.join(RAILS_ROOT, "public", "entry", "image", e.id.to_s), e.image_dir
+    assert_equal File.join(e.id.to_s), e.image_relative_dir
+  end
+
+  def test_store_dir_callback
+    Entry.file_column :image, {:store_dir => :my_store_dir}
+    e = Entry.new
+
+    e.image = uploaded_file(file_path("kerb.jpg"), "image/jpeg", "kerb.jpg")    
+    assert e.save
+    
+    assert_equal_paths File.join(RAILS_ROOT, "public", "my_store_dir", e.id), e.image_dir   
+  end
+
+  def test_tmp_dir_with_store_dir_callback
+    Entry.file_column :image, {:store_dir => :my_store_dir}
+    e = Entry.new
+    e.image = upload(f("kerb.jpg"))
+    
+    assert_equal File.expand_path(File.join(RAILS_ROOT, "public", "my_store_dir", "tmp")), File.expand_path(File.join(e.image_dir,".."))
+  end
+
+  def test_invalid_store_dir_callback
+    Entry.file_column :image, {:store_dir => :my_store_dir_doesnt_exit}    
+    e = Entry.new
+    assert_raise(ArgumentError) {
+      e.image = uploaded_file(file_path("kerb.jpg"), "image/jpeg", "kerb.jpg")
+      e.save
+    }
+  end
+
+  def test_subdir_parameter
+    e = Entry.new
+    assert_nil e.image("thumb")
+    assert_nil e.image_relative_path("thumb")
+    assert_nil e.image(nil)
+
+    e.image = uploaded_file(file_path("kerb.jpg"), "image/jpeg", "kerb.jpg")
+    
+    assert_equal "kerb.jpg", File.basename(e.image("thumb"))
+    assert_equal "kerb.jpg", File.basename(e.image_relative_path("thumb"))
+
+    assert_equal File.join(e.image_dir,"thumb","kerb.jpg"), e.image("thumb")
+    assert_match %r{/thumb/kerb\.jpg$}, e.image_relative_path("thumb") 
+
+    assert_equal e.image, e.image(nil)
+    assert_equal e.image_relative_path, e.image_relative_path(nil)
+  end
+
+  def test_cleanup_after_destroy
+    e = Entry.new("image" => uploaded_file(file_path("kerb.jpg"), "image/jpeg", "kerb.jpg"))
+    assert e.save
+    local_path = e.image
+    assert File.exists?(local_path)
+    assert e.destroy
+    assert !File.exists?(local_path), "'#{local_path}' still exists although entry was destroyed"
+    assert !File.exists?(File.dirname(local_path))
+  end
+  
+  def test_keep_tmp_image
+    e = Entry.new("image" => uploaded_file(file_path("kerb.jpg"), "image/jpeg", "kerb.jpg"))
+    e.validation_should_fail = true
+    assert !e.save, "e should not save due to validation errors"
+    assert File.exists?(local_path = e.image)
+    image_temp = e.image_temp
+    e = Entry.new("image_temp" => image_temp)
+    assert_equal local_path, e.image
+    assert e.save
+    assert FileUtils.identical?(e.image, file_path("kerb.jpg"))
+  end
+  
+  def test_keep_tmp_image_with_existing_image
+    e = Entry.new("image" =>uploaded_file(file_path("kerb.jpg"), "image/jpeg", "kerb.jpg"))
+    assert e.save
+    assert File.exists?(local_path = e.image)
+    e = Entry.find(e.id)
+    e.image = uploaded_file(file_path("skanthak.png"), "image/png", "skanthak.png")
+    e.validation_should_fail = true
+    assert !e.save
+    temp_path = e.image_temp
+    e = Entry.find(e.id)
+    e.image_temp = temp_path
+    assert e.save
+    
+    assert FileUtils.identical?(e.image, file_path("skanthak.png"))
+    assert !File.exists?(local_path), "old image has not been deleted"
+  end
+  
+  def test_replace_tmp_image_temp_first
+    do_test_replace_tmp_image([:image_temp, :image])
+  end
+  
+  def test_replace_tmp_image_temp_last
+    do_test_replace_tmp_image([:image, :image_temp])
+  end
+  
+  def do_test_replace_tmp_image(order)
+    e = Entry.new("image" => uploaded_file(file_path("kerb.jpg"), "image/jpeg", "kerb.jpg"))
+    e.validation_should_fail = true
+    assert !e.save
+    image_temp = e.image_temp
+    temp_path = e.image
+    new_img = uploaded_file(file_path("skanthak.png"), "image/png", "skanthak.png")
+    e = Entry.new
+    for method in order
+      case method
+      when :image_temp then e.image_temp = image_temp
+      when :image then e.image = new_img
+      end
+    end
+    assert e.save
+    assert FileUtils.identical?(e.image, file_path("skanthak.png")), "'#{e.image}' is not the expected 'skanthak.png'"
+    assert !File.exists?(temp_path), "temporary file '#{temp_path}' is not cleaned up"
+    assert !File.exists?(File.dirname(temp_path)), "temporary directory not cleaned up"
+    assert e.image_just_uploaded?
+  end
+  
+  def test_replace_image_on_saved_object
+    e = Entry.new("image" => uploaded_file(file_path("kerb.jpg"), "image/jpeg", "kerb.jpg"))
+    assert e.save
+    old_file = e.image
+    e = Entry.find(e.id)
+    e.image = uploaded_file(file_path("skanthak.png"), "image/png", "skanthak.png")
+    assert e.save
+    assert FileUtils.identical?(file_path("skanthak.png"), e.image)
+    assert old_file != e.image
+    assert !File.exists?(old_file), "'#{old_file}' has not been cleaned up"
+  end
+  
+  def test_edit_without_touching_image
+    e = Entry.new("image" => uploaded_file(file_path("kerb.jpg"), "image/jpeg", "kerb.jpg"))
+    assert e.save
+    e = Entry.find(e.id)
+    assert e.save
+    assert FileUtils.identical?(file_path("kerb.jpg"), e.image)
+  end
+  
+  def test_save_without_image
+    e = Entry.new
+    assert e.save
+    e.reload
+    assert_nil e.image
+  end
+  
+  def test_delete_saved_image
+    e = Entry.new("image" => uploaded_file(file_path("kerb.jpg"), "image/jpeg", "kerb.jpg"))
+    assert e.save
+    local_path = e.image
+    e.image = nil
+    assert_nil e.image
+    assert File.exists?(local_path), "file '#{local_path}' should not be deleted until transaction is saved"
+    assert e.save
+    assert_nil e.image
+    assert !File.exists?(local_path)
+    e.reload
+    assert e["image"].blank?
+    e = Entry.find(e.id)
+    assert_nil e.image
+  end
+  
+  def test_delete_tmp_image
+    e = Entry.new("image" => uploaded_file(file_path("kerb.jpg"), "image/jpeg", "kerb.jpg"))
+    local_path = e.image
+    e.image = nil
+    assert_nil e.image
+    assert e["image"].blank?
+    assert !File.exists?(local_path)
+  end
+  
+  def test_delete_nonexistant_image
+    e = Entry.new
+    e.image = nil
+    assert e.save
+    assert_nil e.image
+  end
+
+  def test_delete_image_on_non_null_column
+    e = Entry.new("file" => upload(f("skanthak.png")))
+    assert e.save
+
+    local_path = e.file
+    assert File.exists?(local_path)
+    e.file = nil
+    assert e.save
+    assert !File.exists?(local_path)
+  end
+
+  def test_ie_filename
+    e = Entry.new("image" => uploaded_file(file_path("kerb.jpg"), "image/jpeg", 'c:\images\kerb.jpg'))
+    assert e.image_relative_path =~ /^tmp\/[\d\.]+\/kerb\.jpg$/, "relative path '#{e.image_relative_path}' was not as expected"
+    assert File.exists?(e.image)
+  end
+  
+  def test_just_uploaded?
+    e = Entry.new("image" => uploaded_file(file_path("kerb.jpg"), "image/jpeg", 'c:\images\kerb.jpg'))
+    assert e.image_just_uploaded?
+    assert e.save
+    assert e.image_just_uploaded?
+    
+    e = Entry.new("image" => uploaded_file(file_path("kerb.jpg"), "image/jpeg", 'kerb.jpg'))
+    temp_path = e.image_temp
+    e = Entry.new("image_temp" => temp_path)
+    assert !e.image_just_uploaded?
+    assert e.save
+    assert !e.image_just_uploaded?
+  end
+  
+  def test_empty_tmp
+    e = Entry.new
+    e.image_temp = ""
+    assert_nil e.image
+  end
+  
+  def test_empty_tmp_with_image
+    e = Entry.new
+    e.image_temp = ""
+    e.image = uploaded_file(file_path("kerb.jpg"), "image/jpeg", 'c:\images\kerb.jpg')
+    local_path = e.image
+    assert File.exists?(local_path)
+    e.image_temp = ""
+    assert local_path, e.image
+  end
+  
+  def test_empty_filename
+    e = Entry.new
+    assert_equal "", e["file"]
+    assert_nil e.file
+    assert_nil e["image"]
+    assert_nil e.image
+  end
+  
+  def test_with_two_file_columns
+    e = Entry.new
+    e.image = uploaded_file(file_path("kerb.jpg"), "image/jpeg", "kerb.jpg")
+    e.file = uploaded_file(file_path("skanthak.png"), "image/png", "skanthak.png")
+    assert e.save
+    assert_match %{/entry/image/}, e.image
+    assert_match %{/entry/file/}, e.file
+    assert FileUtils.identical?(e.image, file_path("kerb.jpg"))
+    assert FileUtils.identical?(e.file, file_path("skanthak.png"))
+  end
+  
+  def test_with_two_models
+    e = Entry.new(:image => uploaded_file(file_path("kerb.jpg"), "image/jpeg", "kerb.jpg"))
+    m = Movie.new(:movie => uploaded_file(file_path("skanthak.png"), "image/png", "skanthak.png"))
+    assert e.save
+    assert m.save
+    assert_match %{/entry/image/}, e.image
+    assert_match %{/movie/movie/}, m.movie
+    assert FileUtils.identical?(e.image, file_path("kerb.jpg"))
+    assert FileUtils.identical?(m.movie, file_path("skanthak.png"))
+  end
+
+  def test_no_file_uploaded
+    e = Entry.new
+    assert_nothing_raised { e.image =
+        uploaded_file(nil, "application/octet-stream", "", :stringio) }
+    assert_equal nil, e.image
+  end
+
+  # when safari submits a form where no file has been
+  # selected, it does not transmit a content-type and
+  # the result is an empty string ""
+  def test_no_file_uploaded_with_safari
+    e = Entry.new
+    assert_nothing_raised { e.image = "" }
+    assert_equal nil, e.image
+  end
+
+  def test_detect_wrong_encoding
+    e = Entry.new
+    assert_raise(TypeError) { e.image ="img42.jpg" }
+  end
+
+  def test_serializable_before_save
+    e = Entry.new
+    e.image = uploaded_file(file_path("skanthak.png"), "image/png", "skanthak.png")
+    assert_nothing_raised { 
+      flash = Marshal.dump(e) 
+      e = Marshal.load(flash)
+    }
+    assert File.exists?(e.image)
+  end
+
+  def test_should_call_after_upload_on_new_upload
+    Entry.file_column :image, :after_upload => [:after_assign]
+    e = Entry.new
+    e.image = upload(f("skanthak.png"))
+    assert e.after_assign_called?
+  end
+
+  def test_should_call_user_after_save_on_save
+    e = Entry.new(:image => upload(f("skanthak.png")))
+    assert e.save
+    
+    assert_kind_of FileColumn::PermanentUploadedFile, e.send(:image_state)
+    assert e.after_save_called?
+  end
+
+
+  def test_assign_standard_files
+    e = Entry.new
+    e.image = File.new(file_path('skanthak.png'))
+    
+    assert_equal 'skanthak.png', File.basename(e.image)
+    assert FileUtils.identical?(file_path('skanthak.png'), e.image)
+    
+    assert e.save
+  end
+
+
+  def test_validates_filesize
+    Entry.validates_filesize_of :image, :in => 50.kilobytes..100.kilobytes
+
+    e = Entry.new(:image => upload(f("kerb.jpg")))
+    assert e.save
+
+    e.image = upload(f("skanthak.png"))
+    assert !e.save
+    assert e.errors.invalid?("image")
+  end
+
+  def test_validates_file_format_simple
+    e = Entry.new(:image => upload(f("skanthak.png")))
+    assert e.save
+    
+    Entry.validates_file_format_of :image, :in => ["jpg"]
+
+    e.image = upload(f("kerb.jpg"))
+    assert e.save
+
+    e.image = upload(f("mysql.sql"))
+    assert !e.save
+    assert e.errors.invalid?("image")
+    
+  end
+
+  def test_validates_image_size
+    Entry.validates_image_size :image, :min => "640x480"
+    
+    e = Entry.new(:image => upload(f("kerb.jpg")))
+    assert e.save
+
+    e = Entry.new(:image => upload(f("skanthak.png")))
+    assert !e.save
+    assert e.errors.invalid?("image")
+  end
+
+  def do_permission_test(uploaded_file, permissions=0641)
+    Entry.file_column :image, :permissions => permissions
+    
+    e = Entry.new(:image => uploaded_file)
+    assert e.save
+
+    assert_equal permissions, (File.stat(e.image).mode & 0777)
+  end
+
+  def test_permissions_with_small_file
+    do_permission_test upload(f("skanthak.png"), :guess, :stringio)
+  end
+
+  def test_permission_with_big_file
+    do_permission_test upload(f("kerb.jpg"))
+  end
+
+  def test_permission_that_overrides_umask
+    do_permission_test upload(f("skanthak.png"), :guess, :stringio), 0666
+    do_permission_test upload(f("kerb.jpg")), 0666
+  end
+
+  def test_access_with_empty_id
+    # an empty id might happen after a clone or through some other
+    # strange event. Since we would create a path that contains nothing
+    # where the id would have been, we should fail fast with an exception
+    # in this case
+    
+    e = Entry.new(:image => upload(f("skanthak.png")))
+    assert e.save
+    id = e.id
+
+    e = Entry.find(id)
+    
+    e["id"] = ""
+    assert_raise(RuntimeError) { e.image }
+    
+    e = Entry.find(id)
+    e["id"] = nil
+    assert_raise(RuntimeError) { e.image }
+  end
+end
+
+# Tests for moving temp dir to permanent dir
+class FileColumnMoveTest < Test::Unit::TestCase
+  
+  def setup
+    # we define the file_columns here so that we can change
+    # settings easily in a single test
+
+    Entry.file_column :image
+    
+  end
+  
+  def teardown
+    FileUtils.rm_rf File.dirname(__FILE__)+"/public/entry/"
+  end
+
+  def test_should_move_additional_files_from_tmp
+    e = Entry.new
+    e.image = uploaded_file(file_path("skanthak.png"), "image/png", "skanthak.png")
+    FileUtils.cp file_path("kerb.jpg"), File.dirname(e.image)
+    assert e.save
+    dir = File.dirname(e.image)
+    assert File.exists?(File.join(dir, "skanthak.png"))
+    assert File.exists?(File.join(dir, "kerb.jpg"))
+  end
+
+  def test_should_move_direcotries_on_save
+    e = Entry.new(:image => upload(f("skanthak.png")))
+    
+    FileUtils.mkdir( e.image_dir+"/foo" )
+    FileUtils.cp file_path("kerb.jpg"), e.image_dir+"/foo/kerb.jpg"
+    
+    assert e.save
+
+    assert File.exists?(e.image)
+    assert File.exists?(File.dirname(e.image)+"/foo/kerb.jpg")
+  end
+
+  def test_should_overwrite_dirs_with_files_on_reupload
+    e = Entry.new(:image => upload(f("skanthak.png")))
+
+    FileUtils.mkdir( e.image_dir+"/kerb.jpg")
+    FileUtils.cp file_path("kerb.jpg"), e.image_dir+"/kerb.jpg/"
+    assert e.save
+
+    e.image = upload(f("kerb.jpg"))
+    assert e.save
+
+    assert File.file?(e.image_dir+"/kerb.jpg")
+  end
+
+  def test_should_overwrite_files_with_dirs_on_reupload
+    e = Entry.new(:image => upload(f("skanthak.png")))
+
+    assert e.save
+    assert File.file?(e.image_dir+"/skanthak.png")
+
+    e.image = upload(f("kerb.jpg"))
+    FileUtils.mkdir(e.image_dir+"/skanthak.png")
+    
+    assert e.save
+    assert File.file?(e.image_dir+"/kerb.jpg")
+    assert !File.file?(e.image_dir+"/skanthak.png")
+    assert File.directory?(e.image_dir+"/skanthak.png")
+  end
+
+end
+
diff --git a/vendor/plugins/file_column/test/fixtures/entry.rb b/vendor/plugins/file_column/test/fixtures/entry.rb
new file mode 100644 (file)
index 0000000..b9f7c95
--- /dev/null
@@ -0,0 +1,32 @@
+class Entry < ActiveRecord::Base
+  attr_accessor :validation_should_fail
+
+  def validate
+    errors.add("image","some stupid error") if @validation_should_fail
+  end
+  
+  def after_assign
+    @after_assign_called = true
+  end
+  
+  def after_assign_called?
+    @after_assign_called
+  end
+  
+  def after_save
+    @after_save_called = true
+  end
+
+  def after_save_called?
+    @after_save_called
+  end
+
+  def my_store_dir
+    # not really dynamic but at least it could be...
+    "my_store_dir"
+  end
+
+  def load_image_with_rmagick(path)
+    Magick::Image::read(path).first
+  end
+end
diff --git a/vendor/plugins/file_column/test/fixtures/invalid-image.jpg b/vendor/plugins/file_column/test/fixtures/invalid-image.jpg
new file mode 100644 (file)
index 0000000..bd4933b
--- /dev/null
@@ -0,0 +1 @@
+this is certainly not a JPEG image
diff --git a/vendor/plugins/file_column/test/fixtures/kerb.jpg b/vendor/plugins/file_column/test/fixtures/kerb.jpg
new file mode 100644 (file)
index 0000000..083138e
Binary files /dev/null and b/vendor/plugins/file_column/test/fixtures/kerb.jpg differ
diff --git a/vendor/plugins/file_column/test/fixtures/mysql.sql b/vendor/plugins/file_column/test/fixtures/mysql.sql
new file mode 100644 (file)
index 0000000..55143f2
--- /dev/null
@@ -0,0 +1,25 @@
+-- MySQL dump 9.11
+--
+-- Host: localhost    Database: file_column_test
+-- ------------------------------------------------------
+-- Server version      4.0.24
+
+--
+-- Table structure for table `entries`
+--
+
+DROP TABLE IF EXISTS entries;
+CREATE TABLE entries (
+  id int(11) NOT NULL auto_increment,
+  image varchar(200) default NULL,
+  file varchar(200) NOT NULL,
+  PRIMARY KEY  (id)
+) TYPE=MyISAM;
+
+DROP TABLE IF EXISTS movies;
+CREATE TABLE movies (
+  id int(11) NOT NULL auto_increment,
+  movie varchar(200) default NULL,
+  PRIMARY KEY  (id)
+) TYPE=MyISAM;
+
diff --git a/vendor/plugins/file_column/test/fixtures/schema.rb b/vendor/plugins/file_column/test/fixtures/schema.rb
new file mode 100644 (file)
index 0000000..49b5ddb
--- /dev/null
@@ -0,0 +1,10 @@
+ActiveRecord::Schema.define do
+  create_table :entries, :force => true do |t|
+    t.column :image, :string, :null => true
+    t.column :file, :string, :null => false
+  end
+  
+  create_table :movies, :force => true do |t|
+    t.column :movie, :string
+  end
+end
diff --git a/vendor/plugins/file_column/test/fixtures/skanthak.png b/vendor/plugins/file_column/test/fixtures/skanthak.png
new file mode 100644 (file)
index 0000000..7415eb6
Binary files /dev/null and b/vendor/plugins/file_column/test/fixtures/skanthak.png differ
diff --git a/vendor/plugins/file_column/test/magick_test.rb b/vendor/plugins/file_column/test/magick_test.rb
new file mode 100644 (file)
index 0000000..0362719
--- /dev/null
@@ -0,0 +1,380 @@
+require File.dirname(__FILE__) + '/abstract_unit'
+require 'RMagick'
+require File.dirname(__FILE__) + '/fixtures/entry'
+
+
+class AbstractRMagickTest < Test::Unit::TestCase
+  def teardown
+    FileUtils.rm_rf File.dirname(__FILE__)+"/public/entry/"
+  end
+
+  def test_truth
+    assert true
+  end
+
+  private
+
+  def read_image(path)
+    Magick::Image::read(path).first
+  end
+
+  def assert_max_image_size(img, s)
+    assert img.columns <= s, "img has #{img.columns} columns, expected: #{s}"
+    assert img.rows <= s, "img has #{img.rows} rows, expected: #{s}"
+    assert_equal s, [img.columns, img.rows].max
+  end
+end
+
+class RMagickSimpleTest < AbstractRMagickTest
+  def setup
+    Entry.file_column :image, :magick => { :geometry => "100x100" }
+  end
+
+  def test_simple_resize_without_save
+    e = Entry.new
+    e.image = upload(f("kerb.jpg"))
+    
+    img = read_image(e.image)
+    assert_max_image_size img, 100
+  end
+
+  def test_simple_resize_with_save
+    e = Entry.new
+    e.image = upload(f("kerb.jpg"))
+    assert e.save
+    e.reload
+    
+    img = read_image(e.image)
+    assert_max_image_size img, 100
+  end
+
+  def test_resize_on_saved_image
+    Entry.file_column :image, :magick => { :geometry => "100x100" }
+    
+    e = Entry.new
+    e.image = upload(f("skanthak.png"))
+    assert e.save
+    e.reload
+    old_path = e.image
+    
+    e.image = upload(f("kerb.jpg"))
+    assert e.save
+    assert "kerb.jpg", File.basename(e.image)
+    assert !File.exists?(old_path), "old image '#{old_path}' still exists"
+
+    img = read_image(e.image)
+    assert_max_image_size img, 100
+  end
+
+  def test_invalid_image
+    e = Entry.new
+    assert_nothing_raised { e.image = upload(f("invalid-image.jpg")) }
+    assert !e.valid?
+  end
+
+  def test_serializable
+    e = Entry.new
+    e.image = upload(f("skanthak.png"))
+    assert_nothing_raised {
+      flash = Marshal.dump(e)
+      e = Marshal.load(flash)
+    }
+    assert File.exists?(e.image)
+  end
+
+  def test_imagemagick_still_usable
+    e = Entry.new
+    assert_nothing_raised {
+      img = e.load_image_with_rmagick(file_path("skanthak.png"))
+      assert img.kind_of?(Magick::Image)
+    }
+  end
+end
+
+class RMagickRequiresImageTest < AbstractRMagickTest
+  def setup
+    Entry.file_column :image, :magick => { 
+      :size => "100x100>",
+      :image_required => false,
+      :versions => {
+        :thumb => "80x80>",
+        :large => {:size => "200x200>", :lazy => true}
+      }
+    }
+  end
+
+  def test_image_required_with_image
+    e = Entry.new(:image => upload(f("skanthak.png")))
+    assert_max_image_size read_image(e.image), 100
+    assert e.valid?
+  end
+
+  def test_image_required_with_invalid_image
+    e = Entry.new(:image => upload(f("invalid-image.jpg")))
+    assert e.valid?, "did not ignore invalid image"
+    assert FileUtils.identical?(e.image, f("invalid-image.jpg")), "uploaded file has not been left alone"
+  end
+
+  def test_versions_with_invalid_image
+    e = Entry.new(:image => upload(f("invalid-image.jpg")))
+    assert e.valid?
+
+    image_state = e.send(:image_state)
+    assert_nil image_state.create_magick_version_if_needed(:thumb)
+    assert_nil image_state.create_magick_version_if_needed(:large)
+    assert_nil image_state.create_magick_version_if_needed("300x300>")
+  end
+end
+
+class RMagickCustomAttributesTest < AbstractRMagickTest
+  def assert_image_property(img, property, value, text = nil)
+    assert File.exists?(img), "the image does not exist"
+    assert_equal value, read_image(img).send(property), text
+  end
+
+  def test_simple_attributes
+    Entry.file_column :image, :magick => { :attributes => { :quality => 20 } }
+    e = Entry.new("image" => upload(f("kerb.jpg")))
+    assert_image_property e.image, :quality, 20, "the quality was not set"
+  end
+
+  def test_version_attributes
+    Entry.file_column :image, :magick => {
+      :versions => {
+        :thumb => { :attributes => { :quality => 20 } }
+      }
+    }
+    e = Entry.new("image" => upload(f("kerb.jpg")))
+    assert_image_property e.image("thumb"), :quality, 20, "the quality was not set"
+  end
+  
+  def test_lazy_attributes
+    Entry.file_column :image, :magick => {
+      :versions => {
+        :thumb => { :attributes => { :quality => 20 }, :lazy => true }
+      }
+    }
+    e = Entry.new("image" => upload(f("kerb.jpg")))
+    e.send(:image_state).create_magick_version_if_needed(:thumb)
+    assert_image_property e.image("thumb"), :quality, 20, "the quality was not set"
+  end
+end
+
+class RMagickVersionsTest < AbstractRMagickTest
+  def setup
+    Entry.file_column :image, :magick => {:geometry => "200x200",
+      :versions => {
+        :thumb => "50x50",
+        :medium => {:geometry => "100x100", :name => "100_100"},
+        :large => {:geometry => "150x150", :lazy => true}
+      }
+    }
+  end
+
+
+  def test_should_create_thumb
+    e = Entry.new("image" => upload(f("skanthak.png")))
+    
+    assert File.exists?(e.image("thumb")), "thumb-nail not created"
+    
+    assert_max_image_size read_image(e.image("thumb")), 50
+  end
+
+  def test_version_name_can_be_different_from_key
+    e = Entry.new("image" => upload(f("skanthak.png")))
+    
+    assert File.exists?(e.image("100_100"))
+    assert !File.exists?(e.image("medium"))
+  end
+
+  def test_should_not_create_lazy_versions
+    e = Entry.new("image" => upload(f("skanthak.png")))
+    assert !File.exists?(e.image("large")), "lazy versions should not be created unless needed"
+  end
+
+  def test_should_create_lazy_version_on_demand
+    e = Entry.new("image" => upload(f("skanthak.png")))
+    
+    e.send(:image_state).create_magick_version_if_needed(:large)
+    
+    assert File.exists?(e.image("large")), "lazy version should be created on demand"
+    
+    assert_max_image_size read_image(e.image("large")), 150
+  end
+
+  def test_generated_name_should_not_change
+    e = Entry.new("image" => upload(f("skanthak.png")))
+    
+    name1 = e.send(:image_state).create_magick_version_if_needed("50x50")
+    name2 = e.send(:image_state).create_magick_version_if_needed("50x50")
+    name3 = e.send(:image_state).create_magick_version_if_needed(:geometry => "50x50")
+    assert_equal name1, name2, "hash value has changed"
+    assert_equal name1, name3, "hash value has changed"
+  end
+
+  def test_should_create_version_with_string
+    e = Entry.new("image" => upload(f("skanthak.png")))
+    
+    name = e.send(:image_state).create_magick_version_if_needed("32x32")
+    
+    assert File.exists?(e.image(name))
+
+    assert_max_image_size read_image(e.image(name)), 32
+  end
+
+  def test_should_create_safe_auto_id
+    e = Entry.new("image" => upload(f("skanthak.png")))
+
+    name = e.send(:image_state).create_magick_version_if_needed("32x32")
+
+    assert_match /^[a-zA-Z0-9]+$/, name
+  end
+end
+
+class RMagickCroppingTest < AbstractRMagickTest
+  def setup
+    Entry.file_column :image, :magick => {:geometry => "200x200",
+      :versions => {
+        :thumb => {:crop => "1:1", :geometry => "50x50"}
+      }
+    }
+  end
+  
+  def test_should_crop_image_on_upload
+    e = Entry.new("image" => upload(f("skanthak.png")))
+    
+    img = read_image(e.image("thumb"))
+    
+    assert_equal 50, img.rows 
+    assert_equal 50, img.columns
+  end
+    
+end
+
+class UrlForImageColumnTest < AbstractRMagickTest
+  include FileColumnHelper
+
+  def setup
+    Entry.file_column :image, :magick => {
+      :versions => {:thumb => "50x50"} 
+    }
+    @request = RequestMock.new
+  end
+    
+  def test_should_use_version_on_symbol_option
+    e = Entry.new(:image => upload(f("skanthak.png")))
+    
+    url = url_for_image_column(e, "image", :thumb)
+    assert_match %r{^/entry/image/tmp/.+/thumb/skanthak.png$}, url
+  end
+
+  def test_should_use_string_as_size
+    e = Entry.new(:image => upload(f("skanthak.png")))
+
+    url = url_for_image_column(e, "image", "50x50")
+    
+    assert_match %r{^/entry/image/tmp/.+/.+/skanthak.png$}, url
+    
+    url =~ /\/([^\/]+)\/skanthak.png$/
+    dirname = $1
+    
+    assert_max_image_size read_image(e.image(dirname)), 50
+  end
+
+  def test_should_accept_version_hash
+    e = Entry.new(:image => upload(f("skanthak.png")))
+
+    url = url_for_image_column(e, "image", :size => "50x50", :crop => "1:1", :name => "small")
+
+    assert_match %r{^/entry/image/tmp/.+/small/skanthak.png$}, url
+
+    img = read_image(e.image("small"))
+    assert_equal 50, img.rows
+    assert_equal 50, img.columns
+  end
+end
+
+class RMagickPermissionsTest < AbstractRMagickTest
+  def setup
+    Entry.file_column :image, :magick => {:geometry => "200x200",
+      :versions => {
+        :thumb => {:crop => "1:1", :geometry => "50x50"}
+      }
+    }, :permissions => 0616
+  end
+  
+  def check_permissions(e)
+    assert_equal 0616, (File.stat(e.image).mode & 0777)
+    assert_equal 0616, (File.stat(e.image("thumb")).mode & 0777)
+  end
+
+  def test_permissions_with_rmagick
+    e = Entry.new(:image => upload(f("skanthak.png")))
+    
+    check_permissions e
+
+    assert e.save
+
+    check_permissions e
+  end
+end
+
+class Entry 
+  def transform_grey(img)
+    img.quantize(256, Magick::GRAYColorspace)
+  end
+end
+
+class RMagickTransformationTest < AbstractRMagickTest
+  def assert_transformed(image)
+    assert File.exists?(image), "the image does not exist"
+    assert 256 > read_image(image).number_colors, "the number of colors was not changed"
+  end
+  
+  def test_simple_transformation
+    Entry.file_column :image, :magick => { :transformation => Proc.new { |image| image.quantize(256, Magick::GRAYColorspace) } }
+    e = Entry.new("image" => upload(f("skanthak.png")))
+    assert_transformed(e.image)
+  end
+  
+  def test_simple_version_transformation
+    Entry.file_column :image, :magick => {
+      :versions => { :thumb => Proc.new { |image| image.quantize(256, Magick::GRAYColorspace) } }
+    }
+    e = Entry.new("image" => upload(f("skanthak.png")))
+    assert_transformed(e.image("thumb"))
+  end
+  
+  def test_complex_version_transformation
+    Entry.file_column :image, :magick => {
+      :versions => {
+        :thumb => { :transformation => Proc.new { |image| image.quantize(256, Magick::GRAYColorspace) } }
+      }
+    }
+    e = Entry.new("image" => upload(f("skanthak.png")))
+    assert_transformed(e.image("thumb"))
+  end
+  
+  def test_lazy_transformation
+    Entry.file_column :image, :magick => {
+      :versions => {
+        :thumb => { :transformation => Proc.new { |image| image.quantize(256, Magick::GRAYColorspace) }, :lazy => true }
+      }
+    }
+    e = Entry.new("image" => upload(f("skanthak.png")))
+    e.send(:image_state).create_magick_version_if_needed(:thumb)
+    assert_transformed(e.image("thumb"))
+  end
+
+  def test_simple_callback_transformation
+    Entry.file_column :image, :magick => :transform_grey
+    e = Entry.new(:image => upload(f("skanthak.png")))
+    assert_transformed(e.image)
+  end
+
+  def test_complex_callback_transformation
+    Entry.file_column :image, :magick => { :transformation => :transform_grey }
+    e = Entry.new(:image => upload(f("skanthak.png")))
+    assert_transformed(e.image)
+  end
+end
diff --git a/vendor/plugins/file_column/test/magick_view_only_test.rb b/vendor/plugins/file_column/test/magick_view_only_test.rb
new file mode 100644 (file)
index 0000000..a7daa61
--- /dev/null
@@ -0,0 +1,21 @@
+require File.dirname(__FILE__) + '/abstract_unit'
+require File.dirname(__FILE__) + '/fixtures/entry'
+
+class RMagickViewOnlyTest < Test::Unit::TestCase
+  include FileColumnHelper
+
+  def setup
+    Entry.file_column :image
+    @request = RequestMock.new
+  end
+
+  def teardown
+    FileUtils.rm_rf File.dirname(__FILE__)+"/public/entry/"
+  end
+
+  def test_url_for_image_column_without_model_versions
+    e = Entry.new(:image => upload(f("skanthak.png")))
+    
+    assert_nothing_raised { url_for_image_column e, "image", "50x50" }
+  end
+end
diff --git a/vendor/plugins/sql_session_store/LICENSE b/vendor/plugins/sql_session_store/LICENSE
new file mode 100644 (file)
index 0000000..5cb5c7b
--- /dev/null
@@ -0,0 +1,20 @@
+Copyright (c) 2006-2008 Dr.-Ing. Stefan Kaes
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/vendor/plugins/sql_session_store/README b/vendor/plugins/sql_session_store/README
new file mode 100755 (executable)
index 0000000..07b0833
--- /dev/null
@@ -0,0 +1,60 @@
+== SqlSessionStore
+
+See http://railsexpress.de/blog/articles/2005/12/19/roll-your-own-sql-session-store
+
+Only Mysql, Postgres and Oracle are currently supported (others work,
+but you won't see much performance improvement).
+
+== Step 1
+
+If you have generated your sessions table using rake db:sessions:create, go to Step 2
+
+If you're using an old version of sql_session_store, run
+    script/generate sql_session_store DB
+where DB is mysql, postgresql or oracle
+
+Then run
+    rake migrate
+or
+    rake db:migrate
+for edge rails.
+
+== Step 2
+
+Add the code below after the initializer config section:
+
+    ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS.
+      update(:database_manager => SqlSessionStore)
+
+Finally, depending on your database type, add
+
+    SqlSessionStore.session_class = MysqlSession
+or
+
+    SqlSessionStore.session_class = PostgresqlSession
+or
+    SqlSessionStore.session_class = OracleSession
+
+after the initializer section in environment.rb
+
+== Step 3 (optional)
+
+If you want to use a database separate from your default one to store
+your sessions, specify a configuration in your database.yml file (say
+sessions), and establish the connection on SqlSession in
+environment.rb:
+
+   SqlSession.establish_connection :sessions
+
+
+== IMPORTANT NOTES
+
+1. The class name SQLSessionStore has changed to SqlSessionStore to
+   let Rails work its autoload magic.
+
+2. You will need the binary drivers for Mysql or Postgresql.
+   These have been verified to work:
+
+   * ruby-postgres (0.7.1.2005.12.21) with postgreql 8.1
+   * ruby-mysql 2.7.1 with Mysql 4.1
+   * ruby-mysql 2.7.2 with Mysql 5.0
diff --git a/vendor/plugins/sql_session_store/Rakefile b/vendor/plugins/sql_session_store/Rakefile
new file mode 100755 (executable)
index 0000000..0145def
--- /dev/null
@@ -0,0 +1,22 @@
+require 'rake'
+require 'rake/testtask'
+require 'rake/rdoctask'
+
+desc 'Default: run unit tests.'
+task :default => :test
+
+desc 'Test the sql_session_store plugin.'
+Rake::TestTask.new(:test) do |t|
+  t.libs << 'lib'
+  t.pattern = 'test/**/*_test.rb'
+  t.verbose = true
+end
+
+desc 'Generate documentation for the sql_session_store plugin.'
+Rake::RDocTask.new(:rdoc) do |rdoc|
+  rdoc.rdoc_dir = 'rdoc'
+  rdoc.title    = 'SqlSessionStore'
+  rdoc.options << '--line-numbers' << '--inline-source'
+  rdoc.rdoc_files.include('README')
+  rdoc.rdoc_files.include('lib/**/*.rb')
+end
diff --git a/vendor/plugins/sql_session_store/generators/sql_session_store/USAGE b/vendor/plugins/sql_session_store/generators/sql_session_store/USAGE
new file mode 100755 (executable)
index 0000000..1e3f58a
--- /dev/null
@@ -0,0 +1,17 @@
+Description:
+    The sql_session_store generator creates a migration for use with
+    the sql session store.  It takes one argument: the database
+    type. Only mysql and postgreql are currently supported.
+
+Example:
+    ./script/generate sql_session_store mysql
+
+    This will create the following migration:
+
+      db/migrate/XXX_add_sql_session.rb
+
+    Use
+
+    ./script/generate sql_session_store postgreql
+
+    to get a migration for postgres.
diff --git a/vendor/plugins/sql_session_store/generators/sql_session_store/sql_session_store_generator.rb b/vendor/plugins/sql_session_store/generators/sql_session_store/sql_session_store_generator.rb
new file mode 100755 (executable)
index 0000000..6af6bd0
--- /dev/null
@@ -0,0 +1,25 @@
+class SqlSessionStoreGenerator < Rails::Generator::NamedBase
+  def initialize(runtime_args, runtime_options = {})
+    runtime_args.insert(0, 'add_sql_session')
+    if runtime_args.include?('postgresql')
+      @_database = 'postgresql'
+    elsif runtime_args.include?('mysql')
+      @_database = 'mysql'
+    elsif runtime_args.include?('oracle')
+      @_database = 'oracle'
+    else
+      puts "error: database type not given.\nvalid arguments are: mysql or postgresql"
+      exit
+    end
+    super
+  end
+
+  def manifest
+    record do |m|
+      m.migration_template("migration.rb", 'db/migrate',
+                           :assigns => { :migration_name => "SqlSessionStoreSetup", :database => @_database },
+                           :migration_file_name => "sql_session_store_setup"
+                           )
+    end
+  end
+end
diff --git a/vendor/plugins/sql_session_store/generators/sql_session_store/templates/migration.rb b/vendor/plugins/sql_session_store/generators/sql_session_store/templates/migration.rb
new file mode 100755 (executable)
index 0000000..5126500
--- /dev/null
@@ -0,0 +1,38 @@
+class <%= migration_name %> < ActiveRecord::Migration
+
+  class Session < ActiveRecord::Base; end
+
+  def self.up
+    c = ActiveRecord::Base.connection
+    if c.tables.include?('sessions')
+      if (columns = Session.column_names).include?('sessid')
+        rename_column :sessions, :sessid, :session_id
+      else
+        add_column :sessions, :session_id, :string unless columns.include?('session_id')
+        add_column :sessions, :data, :text unless columns.include?('data')
+        if columns.include?('created_on')
+          rename_column :sessions, :created_on, :created_at
+        else
+          add_column :sessions, :created_at, :timestamp unless columns.include?('created_at')
+        end
+        if columns.include?('updated_on')
+          rename_column :sessions, :updated_on, :updated_at
+        else
+          add_column :sessions, :updated_at, :timestamp unless columns.include?('updated_at')
+        end
+      end
+    else
+      create_table :sessions, :options => '<%= database == "mysql" ? "ENGINE=MyISAM" : "" %>' do |t|
+        t.column :session_id, :string
+        t.column :data,       :text
+        t.column :created_at, :timestamp
+        t.column :updated_at, :timestamp
+      end
+      add_index :sessions, :session_id, :name => 'session_id_idx'
+    end
+  end
+
+  def self.down
+    raise IrreversibleMigration
+  end
+end
diff --git a/vendor/plugins/sql_session_store/init.rb b/vendor/plugins/sql_session_store/init.rb
new file mode 100755 (executable)
index 0000000..956151e
--- /dev/null
@@ -0,0 +1 @@
+require 'sql_session_store'\r
diff --git a/vendor/plugins/sql_session_store/install.rb b/vendor/plugins/sql_session_store/install.rb
new file mode 100755 (executable)
index 0000000..f40549d
--- /dev/null
@@ -0,0 +1,2 @@
+# Install hook code here
+puts IO.read(File.join(File.dirname(__FILE__), 'README'))
diff --git a/vendor/plugins/sql_session_store/lib/mysql_session.rb b/vendor/plugins/sql_session_store/lib/mysql_session.rb
new file mode 100755 (executable)
index 0000000..8c86384
--- /dev/null
@@ -0,0 +1,132 @@
+require 'mysql'
+
+# allow access to the real Mysql connection
+class ActiveRecord::ConnectionAdapters::MysqlAdapter
+  attr_reader :connection
+end
+
+# MysqlSession is a down to the bare metal session store
+# implementation to be used with +SQLSessionStore+. It is much faster
+# than the default ActiveRecord implementation.
+#
+# The implementation assumes that the table column names are 'id',
+# 'data', 'created_at' and 'updated_at'. If you want use other names,
+# you will need to change the SQL statments in the code.
+
+class MysqlSession
+
+  # if you need Rails components, and you have a pages which create
+  # new sessions, and embed components insides this pages that need
+  # session access, then you *must* set +eager_session_creation+ to
+  # true (as of Rails 1.0).
+  cattr_accessor :eager_session_creation
+  @@eager_session_creation = false
+
+  attr_accessor :id, :session_id, :data
+
+  def initialize(session_id, data)
+    @session_id = session_id
+    @data = data
+    @id = nil
+  end
+
+  class << self
+
+    # retrieve the session table connection and get the 'raw' Mysql connection from it
+    def session_connection
+      SqlSession.connection.connection
+    end
+
+    # try to find a session with a given +session_id+. returns nil if
+    # no such session exists. note that we don't retrieve
+    # +created_at+ and +updated_at+ as they are not accessed anywhyere
+    # outside this class
+    def find_session(session_id)
+      connection = session_connection
+      connection.query_with_result = true
+      session_id = Mysql::quote(session_id)
+      result = connection.query("SELECT id, data FROM sessions WHERE `session_id`='#{session_id}' LIMIT 1")
+      my_session = nil
+      # each is used below, as other methods barf on my 64bit linux machine
+      # I suspect this to be a bug in mysql-ruby
+      result.each do |row|
+        my_session = new(session_id, row[1])
+        my_session.id = row[0]
+      end
+      result.free
+      my_session
+    end
+
+    # create a new session with given +session_id+ and +data+
+    # and save it immediately to the database
+    def create_session(session_id, data)
+      session_id = Mysql::quote(session_id)
+      new_session = new(session_id, data)
+      if @@eager_session_creation
+        connection = session_connection
+        connection.query("INSERT INTO sessions (`created_at`, `updated_at`, `session_id`, `data`) VALUES (NOW(), NOW(), '#{session_id}', '#{Mysql::quote(data)}')")
+        new_session.id = connection.insert_id
+      end
+      new_session
+    end
+
+    # delete all sessions meeting a given +condition+. it is the
+    # caller's responsibility to pass a valid sql condition
+    def delete_all(condition=nil)
+      if condition
+        session_connection.query("DELETE FROM sessions WHERE #{condition}")
+      else
+        session_connection.query("DELETE FROM sessions")
+      end
+    end
+
+  end # class methods
+
+  # update session with given +data+.
+  # unlike the default implementation using ActiveRecord, updating of
+  # column `updated_at` will be done by the datbase itself
+  def update_session(data)
+    connection = self.class.session_connection
+    if @id
+      # if @id is not nil, this is a session already stored in the database
+      # update the relevant field using @id as key
+      connection.query("UPDATE sessions SET `updated_at`=NOW(), `data`='#{Mysql::quote(data)}' WHERE id=#{@id}")
+    else
+      # if @id is nil, we need to create a new session in the database
+      # and set @id to the primary key of the inserted record
+      connection.query("INSERT INTO sessions (`created_at`, `updated_at`, `session_id`, `data`) VALUES (NOW(), NOW(), '#{@session_id}', '#{Mysql::quote(data)}')")
+      @id = connection.insert_id
+    end
+  end
+
+  # destroy the current session
+  def destroy
+    self.class.delete_all("session_id='#{session_id}'")
+  end
+
+end
+
+__END__
+
+# This software is released under the MIT license
+#
+# Copyright (c) 2005-2008 Stefan Kaes
+
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/vendor/plugins/sql_session_store/lib/oracle_session.rb b/vendor/plugins/sql_session_store/lib/oracle_session.rb
new file mode 100755 (executable)
index 0000000..0b82f63
--- /dev/null
@@ -0,0 +1,143 @@
+require 'oci8'
+
+# allow access to the real Oracle connection
+class ActiveRecord::ConnectionAdapters::OracleAdapter
+  attr_reader :connection
+end
+
+# OracleSession is a down to the bare metal session store
+# implementation to be used with +SQLSessionStore+. It is much faster
+# than the default ActiveRecord implementation.
+#
+# The implementation assumes that the table column names are 'id',
+# 'session_id', 'data', 'created_at' and 'updated_at'. If you want use
+# other names, you will need to change the SQL statments in the code.
+#
+# This table layout is compatible with ActiveRecordStore.
+
+class OracleSession
+
+  # if you need Rails components, and you have a pages which create
+  # new sessions, and embed components insides these pages that need
+  # session access, then you *must* set +eager_session_creation+ to
+  # true (as of Rails 1.0). Not needed for Rails 1.1 and up.
+  cattr_accessor :eager_session_creation
+  @@eager_session_creation = false
+
+  attr_accessor :id, :session_id, :data
+
+  def initialize(session_id, data)
+    @session_id = session_id
+    @data = data
+    @id = nil
+  end
+
+  class << self
+
+    # retrieve the session table connection and get the 'raw' Oracle connection from it
+    def session_connection
+      SqlSession.connection.connection
+    end
+
+    # try to find a session with a given +session_id+. returns nil if
+    # no such session exists. note that we don't retrieve
+    # +created_at+ and +updated_at+ as they are not accessed anywhyere
+    # outside this class.
+    def find_session(session_id)
+      new_session = nil
+      connection = session_connection
+      result = connection.exec("SELECT id, data FROM sessions WHERE session_id = :a and rownum=1", session_id)
+
+      # Make sure to save the @id if we find an existing session
+      while row = result.fetch
+        new_session = new(session_id,row[1].read)
+        new_session.id = row[0]
+      end
+      result.close
+      new_session
+    end
+
+    # create a new session with given +session_id+ and +data+
+    # and save it immediately to the database
+    def create_session(session_id, data)
+      new_session = new(session_id, data)
+      if @@eager_session_creation
+        connection = session_connection
+        connection.exec("INSERT INTO sessions (id, created_at, updated_at, session_id, data)"+
+                        " VALUES (sessions_seq.nextval, SYSDATE, SYSDATE, :a, :b)",
+                         session_id, data)
+        result = connection.exec("SELECT sessions_seq.currval FROM dual")
+        row = result.fetch
+        new_session.id = row[0].to_i
+      end
+      new_session
+    end
+
+    # delete all sessions meeting a given +condition+. it is the
+    # caller's responsibility to pass a valid sql condition
+    def delete_all(condition=nil)
+      if condition
+        session_connection.exec("DELETE FROM sessions WHERE #{condition}")
+      else
+        session_connection.exec("DELETE FROM sessions")
+      end
+    end
+
+  end # class methods
+
+  # update session with given +data+.
+  # unlike the default implementation using ActiveRecord, updating of
+  # column `updated_at` will be done by the database itself
+  def update_session(data)
+    connection = self.class.session_connection
+    if @id
+      # if @id is not nil, this is a session already stored in the database
+      # update the relevant field using @id as key
+      connection.exec("UPDATE sessions SET updated_at = SYSDATE, data = :a WHERE id = :b",
+                       data, @id)
+    else
+      # if @id is nil, we need to create a new session in the database
+      # and set @id to the primary key of the inserted record
+      connection.exec("INSERT INTO sessions (id, created_at, updated_at, session_id, data)"+
+                      " VALUES (sessions_seq.nextval, SYSDATE, SYSDATE, :a, :b)",
+                       @session_id, data)
+      result = connection.exec("SELECT sessions_seq.currval FROM dual")
+      row = result.fetch
+      @id = row[0].to_i
+    end
+  end
+
+  # destroy the current session
+  def destroy
+    self.class.delete_all("session_id='#{session_id}'")
+  end
+
+end
+
+__END__
+
+# This software is released under the MIT license
+#
+# Copyright (c) 2006-2008 Stefan Kaes
+# Copyright (c) 2006-2008 Tiago Macedo
+# Copyright (c) 2007-2008 Nate Wiger
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
diff --git a/vendor/plugins/sql_session_store/lib/postgresql_session.rb b/vendor/plugins/sql_session_store/lib/postgresql_session.rb
new file mode 100755 (executable)
index 0000000..d922913
--- /dev/null
@@ -0,0 +1,136 @@
+require 'postgres'
+
+# allow access to the real Mysql connection
+class ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
+  attr_reader :connection
+end
+
+# PostgresqlSession is a down to the bare metal session store
+# implementation to be used with +SQLSessionStore+. It is much faster
+# than the default ActiveRecord implementation.
+#
+# The implementation assumes that the table column names are 'id',
+# 'session_id', 'data', 'created_at' and 'updated_at'. If you want use
+# other names, you will need to change the SQL statments in the code.
+#
+# This table layout is compatible with ActiveRecordStore.
+
+class PostgresqlSession
+
+  # if you need Rails components, and you have a pages which create
+  # new sessions, and embed components insides these pages that need
+  # session access, then you *must* set +eager_session_creation+ to
+  # true (as of Rails 1.0). Not needed for Rails 1.1 and up.
+  cattr_accessor :eager_session_creation
+  @@eager_session_creation = false
+
+  attr_accessor :id, :session_id, :data
+
+  def initialize(session_id, data)
+    @session_id = session_id
+    @data = data
+    @id = nil
+  end
+
+  class << self
+
+    # retrieve the session table connection and get the 'raw' Postgresql connection from it
+    def session_connection
+      SqlSession.connection.connection
+    end
+
+    # try to find a session with a given +session_id+. returns nil if
+    # no such session exists. note that we don't retrieve
+    # +created_at+ and +updated_at+ as they are not accessed anywhyere
+    # outside this class.
+    def find_session(session_id)
+      connection = session_connection
+      # postgres adds string delimiters when quoting, so strip them off
+      session_id = PGconn::quote(session_id)[1..-2]
+      result = connection.query("SELECT id, data FROM sessions WHERE session_id='#{session_id}' LIMIT 1")
+      my_session = nil
+      # each is used below, as other methods barf on my 64bit linux machine
+      # I suspect this to be a bug in mysql-ruby
+      result.each do |row|
+        my_session = new(session_id, row[1])
+        my_session.id = row[0]
+      end
+      result.clear
+      my_session
+    end
+
+    # create a new session with given +session_id+ and +data+
+    # and save it immediately to the database
+    def create_session(session_id, data)
+      # postgres adds string delimiters when quoting, so strip them off
+      session_id = PGconn::quote(session_id)[1..-2]
+      new_session = new(session_id, data)
+      if @@eager_session_creation
+        connection = session_connection
+        connection.query("INSERT INTO sessions (\"created_at\", \"updated_at\", \"session_id\", \"data\") VALUES (NOW(), NOW(), '#{session_id}', #{PGconn::quote(data)})")
+        new_session.id = connection.lastval
+      end
+      new_session
+    end
+
+    # delete all sessions meeting a given +condition+. it is the
+    # caller's responsibility to pass a valid sql condition
+    def delete_all(condition=nil)
+      if condition
+        session_connection.query("DELETE FROM sessions WHERE #{condition}")
+      else
+        session_connection.query("DELETE FROM sessions")
+      end
+    end
+
+  end # class methods
+
+  # update session with given +data+.
+  # unlike the default implementation using ActiveRecord, updating of
+  # column `updated_at` will be done by the database itself
+  def update_session(data)
+    connection = self.class.session_connection
+    if @id
+      # if @id is not nil, this is a session already stored in the database
+      # update the relevant field using @id as key
+      connection.query("UPDATE sessions SET \"updated_at\"=NOW(), \"data\"=#{PGconn::quote(data)} WHERE id=#{@id}")
+    else
+      # if @id is nil, we need to create a new session in the database
+      # and set @id to the primary key of the inserted record
+      connection.query("INSERT INTO sessions (\"created_at\",  \"updated_at\", \"session_id\", \"data\") VALUES (NOW(), NOW(), '#{@session_id}', #{PGconn::quote(data)})")
+      @id = connection.lastval rescue connection.query("select lastval()").first[0]
+    end
+  end
+
+  # destroy the current session
+  def destroy
+    self.class.delete_all("session_id=#{PGconn.quote(session_id)}")
+  end
+
+end
+
+__END__
+
+# This software is released under the MIT license
+#
+# Copyright (c) 2006-2008 Stefan Kaes
+
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
diff --git a/vendor/plugins/sql_session_store/lib/sql_session.rb b/vendor/plugins/sql_session_store/lib/sql_session.rb
new file mode 100644 (file)
index 0000000..19d2ad5
--- /dev/null
@@ -0,0 +1,27 @@
+# An ActiveRecord class which corresponds to the database table\r
+# +sessions+. Functions +find_session+, +create_session+,\r
+# +update_session+ and +destroy+ constitute the interface to class\r
+# +SqlSessionStore+.\r
+\r
+class SqlSession < ActiveRecord::Base\r
+  # this class should not be reloaded\r
+  def self.reloadable?\r
+    false\r
+  end\r
+\r
+  # retrieve session data for a given +session_id+ from the database,\r
+  # return nil if no such session exists\r
+  def self.find_session(session_id)\r
+    find :first, :conditions => "session_id='#{session_id}'"\r
+  end\r
+\r
+  # create a new session with given +session_id+ and +data+\r
+  def self.create_session(session_id, data)\r
+    new(:session_id => session_id, :data => data)\r
+  end\r
+\r
+  # update session data and store it in the database\r
+  def update_session(data)\r
+    update_attribute('data', data)\r
+  end\r
+end\r
diff --git a/vendor/plugins/sql_session_store/lib/sql_session_store.rb b/vendor/plugins/sql_session_store/lib/sql_session_store.rb
new file mode 100755 (executable)
index 0000000..8b0ff15
--- /dev/null
@@ -0,0 +1,116 @@
+require 'active_record'
+require 'cgi'
+require 'cgi/session'
+begin
+  require 'base64'
+rescue LoadError
+end
+
+# +SqlSessionStore+ is a stripped down, optimized for speed version of
+# class +ActiveRecordStore+.
+
+class SqlSessionStore
+
+  # The class to be used for creating, retrieving and updating sessions.
+  # Defaults to SqlSessionStore::Session, which is derived from +ActiveRecord::Base+.
+  #
+  # In order to achieve acceptable performance you should implement
+  # your own session class, similar to the one provided for Myqsl.
+  #
+  # Only functions +find_session+, +create_session+,
+  # +update_session+ and +destroy+ are required. See file +mysql_session.rb+.
+
+  cattr_accessor :session_class
+  @@session_class = SqlSession
+
+  # Create a new SqlSessionStore instance.
+  #
+  # +session+ is the session for which this instance is being created.
+  #
+  # +option+ is currently ignored as no options are recognized.
+
+  def initialize(session, option=nil)
+    if @session = @@session_class.find_session(session.session_id)
+      @data = unmarshalize(@session.data)
+    else
+      @session = @@session_class.create_session(session.session_id, marshalize({}))
+      @data = {}
+    end
+  end
+
+  # Update the database and disassociate the session object
+  def close
+    if @session
+      @session.update_session(marshalize(@data))
+      @session = nil
+    end
+  end
+
+  # Delete the current session, disassociate and destroy session object
+  def delete
+    if @session
+      @session.destroy
+      @session = nil
+    end
+  end
+
+  # Restore session data from the session object
+  def restore
+    if @session
+      @data = unmarshalize(@session.data)
+    end
+  end
+
+  # Save session data in the session object
+  def update
+    if @session
+      @session.update_session(marshalize(@data))
+    end
+  end
+
+  private
+  if defined?(Base64)
+    def unmarshalize(data)
+      Marshal.load(Base64.decode64(data))
+    end
+
+    def marshalize(data)
+      Base64.encode64(Marshal.dump(data))
+    end
+  else
+    def unmarshalize(data)
+      Marshal.load(data.unpack("m").first)
+    end
+
+    def marshalize(data)
+      [Marshal.dump(data)].pack("m")
+    end
+  end
+
+end
+
+__END__
+
+# This software is released under the MIT license
+#
+# Copyright (c) 2005-2008 Stefan Kaes
+
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
diff --git a/vendor/plugins/sql_session_store/lib/sqlite_session.rb b/vendor/plugins/sql_session_store/lib/sqlite_session.rb
new file mode 100755 (executable)
index 0000000..822b232
--- /dev/null
@@ -0,0 +1,133 @@
+require 'sqlite3'
+
+# allow access to the real Sqlite connection
+#class ActiveRecord::ConnectionAdapters::SQLiteAdapter
+#  attr_reader :connection
+#end
+
+# SqliteSession is a down to the bare metal session store
+# implementation to be used with +SQLSessionStore+. It is much faster
+# than the default ActiveRecord implementation.
+#
+# The implementation assumes that the table column names are 'id',
+# 'data', 'created_at' and 'updated_at'. If you want use other names,
+# you will need to change the SQL statments in the code.
+
+class SqliteSession
+
+  # if you need Rails components, and you have a pages which create
+  # new sessions, and embed components insides this pages that need
+  # session access, then you *must* set +eager_session_creation+ to
+  # true (as of Rails 1.0).
+  cattr_accessor :eager_session_creation
+  @@eager_session_creation = false
+
+  attr_accessor :id, :session_id, :data
+
+  def initialize(session_id, data)
+    @session_id = session_id
+    @data = data
+    @id = nil
+  end
+
+  class << self
+
+    # retrieve the session table connection and get the 'raw' Sqlite connection from it
+    def session_connection
+      SqlSession.connection.instance_variable_get(:@connection)
+    end
+
+    # try to find a session with a given +session_id+. returns nil if
+    # no such session exists. note that we don't retrieve
+    # +created_at+ and +updated_at+ as they are not accessed anywhyere
+    # outside this class
+    def find_session(session_id)
+      connection = session_connection
+      session_id = SQLite3::Database.quote(session_id)
+      result = connection.execute("SELECT id, data FROM sessions WHERE `session_id`='#{session_id}' LIMIT 1")
+      my_session = nil
+      # each is used below, as other methods barf on my 64bit linux machine
+      # I suspect this to be a bug in sqlite-ruby
+      result.each do |row|
+        my_session = new(session_id, row[1])
+        my_session.id = row[0]
+      end
+#      result.free
+      my_session
+    end
+
+    # create a new session with given +session_id+ and +data+
+    # and save it immediately to the database
+    def create_session(session_id, data)
+      session_id = SQLite3::Database.quote(session_id)
+      new_session = new(session_id, data)
+      if @@eager_session_creation
+        connection = session_connection
+        connection.execute("INSERT INTO sessions ('id', `created_at`, `updated_at`, `session_id`, `data`) VALUES (NULL, datetime('now'), datetime('now'), '#{session_id}', '#{SQLite3::Database.quote(data)}')")
+        new_session.id = connection.last_insert_row_id()
+      end
+      new_session
+    end
+
+    # delete all sessions meeting a given +condition+. it is the
+    # caller's responsibility to pass a valid sql condition
+    def delete_all(condition=nil)
+      if condition
+        session_connection.execute("DELETE FROM sessions WHERE #{condition}")
+      else
+        session_connection.execute("DELETE FROM sessions")
+      end
+    end
+
+  end # class methods
+
+  # update session with given +data+.
+  # unlike the default implementation using ActiveRecord, updating of
+  # column `updated_at` will be done by the database itself
+  def update_session(data)
+    connection = SqlSession.connection.instance_variable_get(:@connection) #self.class.session_connection
+    if @id
+      # if @id is not nil, this is a session already stored in the database
+      # update the relevant field using @id as key
+      connection.execute("UPDATE sessions SET `updated_at`=datetime('now'), `data`='#{SQLite3::Database.quote(data)}' WHERE id=#{@id}")
+    else
+      # if @id is nil, we need to create a new session in the database
+      # and set @id to the primary key of the inserted record
+      connection.execute("INSERT INTO sessions ('id', `created_at`, `updated_at`, `session_id`, `data`) VALUES (NULL, datetime('now'), datetime('now'), '#{@session_id}', '#{SQLite3::Database.quote(data)}')")
+      @id = connection.last_insert_row_id()
+    end
+  end
+
+  # destroy the current session
+  def destroy
+    connection = SqlSession.connection.instance_variable_get(:@connection)
+    connection.execute("delete from sessions where session_id='#{session_id}'")
+  end
+
+end
+
+__END__
+
+# This software is released under the MIT license
+#
+# Copyright (c) 2005-2008 Stefan Kaes
+# Copyright (c) 2006-2008 Ted X Toth
+
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.