source: subversion/sites/rails_port_branches/api06/app/controllers/changeset_controller.rb @ 12508

Revision 12508, 13.6 KB checked in by richard, 5 years ago (diff)

view changesets by user

Line 
1# The ChangesetController is the RESTful interface to Changeset objects
2
3class ChangesetController < ApplicationController
4  layout 'site'
5  require 'xml/libxml'
6
7# session :off
8# FIXME is this required?
9  before_filter :authorize_web, :only => [:list]
10  before_filter :authorize, :only => [:create, :update, :delete, :upload, :include, :close]
11  before_filter :check_write_availability, :only => [:create, :update, :delete, :upload, :include]
12  before_filter :check_read_availability, :except => [:create, :update, :delete, :upload, :download, :query]
13  after_filter :compress_output
14
15  # Help methods for checking boundary sanity and area size
16  include MapBoundary
17
18  # Helper methods for checking consistency
19  include ConsistencyValidations
20
21  # Create a changeset from XML.
22  def create
23    if request.put?
24      cs = Changeset.from_xml(request.raw_post, true)
25
26      if cs
27        cs.user_id = @user.id
28        cs.save_with_tags!
29        render :text => cs.id.to_s, :content_type => "text/plain"
30      else
31        render :nothing => true, :status => :bad_request
32      end
33    else
34      render :nothing => true, :status => :method_not_allowed
35    end
36  end
37
38  ##
39  # Return XML giving the basic info about the changeset. Does not
40  # return anything about the nodes, ways and relations in the changeset.
41  def read
42    begin
43      changeset = Changeset.find(params[:id])
44      render :text => changeset.to_xml.to_s, :content_type => "text/xml"
45    rescue ActiveRecord::RecordNotFound
46      render :nothing => true, :status => :not_found
47    end
48  end
49 
50  ##
51  # marks a changeset as closed. this may be called multiple times
52  # on the same changeset, so is idempotent.
53  def close 
54    unless request.put?
55      render :nothing => true, :status => :method_not_allowed
56      return
57    end
58   
59    changeset = Changeset.find(params[:id])   
60    check_changeset_consistency(changeset, @user)
61
62    # to close the changeset, we'll just set its closed_at time to
63    # now. this might not be enough if there are concurrency issues,
64    # but we'll have to wait and see.
65    changeset.set_closed_time_now
66
67    changeset.save!
68    render :nothing => true
69  rescue ActiveRecord::RecordNotFound
70    render :nothing => true, :status => :not_found
71  rescue OSM::APIError => ex
72    render ex.render_opts
73  end
74
75  ##
76  # insert a (set of) points into a changeset bounding box. this can only
77  # increase the size of the bounding box. this is a hint that clients can
78  # set either before uploading a large number of changes, or changes that
79  # the client (but not the server) knows will affect areas further away.
80  def expand_bbox
81    # only allow POST requests, because although this method is
82    # idempotent, there is no "document" to PUT really...
83    if request.post?
84      cs = Changeset.find(params[:id])
85      check_changeset_consistency(cs, @user)
86
87      # keep an array of lons and lats
88      lon = Array.new
89      lat = Array.new
90
91      # the request is in pseudo-osm format... this is kind-of an
92      # abuse, maybe should change to some other format?
93      doc = XML::Parser.string(request.raw_post).parse
94      doc.find("//osm/node").each do |n|
95        lon << n['lon'].to_f * GeoRecord::SCALE
96        lat << n['lat'].to_f * GeoRecord::SCALE
97      end
98
99      # add the existing bounding box to the lon-lat array
100      lon << cs.min_lon unless cs.min_lon.nil?
101      lat << cs.min_lat unless cs.min_lat.nil?
102      lon << cs.max_lon unless cs.max_lon.nil?
103      lat << cs.max_lat unless cs.max_lat.nil?
104
105      # collapse the arrays to minimum and maximum
106      cs.min_lon, cs.min_lat, cs.max_lon, cs.max_lat = 
107        lon.min, lat.min, lon.max, lat.max
108
109      # save the larger bounding box and return the changeset, which
110      # will include the bigger bounding box.
111      cs.save!
112      render :text => cs.to_xml.to_s, :content_type => "text/xml"
113
114    else
115      render :nothing => true, :status => :method_not_allowed
116    end
117
118  rescue ActiveRecord::RecordNotFound
119    render :nothing => true, :status => :not_found
120  rescue OSM::APIError => ex
121    render ex.render_opts
122  end
123
124  ##
125  # Upload a diff in a single transaction.
126  #
127  # This means that each change within the diff must succeed, i.e: that
128  # each version number mentioned is still current. Otherwise the entire
129  # transaction *must* be rolled back.
130  #
131  # Furthermore, each element in the diff can only reference the current
132  # changeset.
133  #
134  # Returns: a diffResult document, as described in
135  # http://wiki.openstreetmap.org/index.php/OSM_Protocol_Version_0.6
136  def upload
137    # only allow POST requests, as the upload method is most definitely
138    # not idempotent, as several uploads with placeholder IDs will have
139    # different side-effects.
140    # see http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.1.2
141    unless request.post?
142      render :nothing => true, :status => :method_not_allowed
143      return
144    end
145
146    changeset = Changeset.find(params[:id])
147    check_changeset_consistency(changeset, @user)
148   
149    diff_reader = DiffReader.new(request.raw_post, changeset)
150    Changeset.transaction do
151      result = diff_reader.commit
152      render :text => result.to_s, :content_type => "text/xml"
153    end
154   
155  rescue ActiveRecord::RecordNotFound
156    render :nothing => true, :status => :not_found
157  rescue OSM::APIError => ex
158    render ex.render_opts
159  end
160
161  ##
162  # download the changeset as an osmChange document.
163  #
164  # to make it easier to revert diffs it would be better if the osmChange
165  # format were reversible, i.e: contained both old and new versions of
166  # modified elements. but it doesn't at the moment...
167  #
168  # this method cannot order the database changes fully (i.e: timestamp and
169  # version number may be too coarse) so the resulting diff may not apply
170  # to a different database. however since changesets are not atomic this
171  # behaviour cannot be guaranteed anyway and is the result of a design
172  # choice.
173  def download
174    changeset = Changeset.find(params[:id])
175   
176    # get all the elements in the changeset and stick them in a big array.
177    elements = [changeset.old_nodes, 
178                changeset.old_ways, 
179                changeset.old_relations].flatten
180   
181    # sort the elements by timestamp and version number, as this is the
182    # almost sensible ordering available. this would be much nicer if
183    # global (SVN-style) versioning were used - then that would be
184    # unambiguous.
185    elements.sort! do |a, b| 
186      if (a.timestamp == b.timestamp)
187        a.version <=> b.version
188      else
189        a.timestamp <=> b.timestamp
190      end
191    end
192   
193    # create an osmChange document for the output
194    result = OSM::API.new.get_xml_doc
195    result.root.name = "osmChange"
196
197    # generate an output element for each operation. note: we avoid looking
198    # at the history because it is simpler - but it would be more correct to
199    # check these assertions.
200    elements.each do |elt|
201      result.root <<
202        if (elt.version == 1) 
203          # first version, so it must be newly-created.
204          created = XML::Node.new "create"
205          created << elt.to_xml_node
206        else
207          # get the previous version from the element history
208          prev_elt = elt.class.find(:first, :conditions => 
209                                    ['id = ? and version = ?',
210                                     elt.id, elt.version])
211          unless elt.visible
212            # if the element isn't visible then it must have been deleted, so
213            # output the *previous* XML
214            deleted = XML::Node.new "delete"
215            deleted << prev_elt.to_xml_node
216          else
217            # must be a modify, for which we don't need the previous version
218            # yet...
219            modified = XML::Node.new "modify"
220            modified << elt.to_xml_node
221          end
222        end
223    end
224
225    render :text => result.to_s, :content_type => "text/xml"
226           
227  rescue ActiveRecord::RecordNotFound
228    render :nothing => true, :status => :not_found
229  rescue OSM::APIError => ex
230    render ex.render_opts
231  end
232
233  ##
234  # query changesets by bounding box, time, user or open/closed status.
235  def query
236    # create the conditions that the user asked for. some or all of
237    # these may be nil.
238    conditions = conditions_bbox(params['bbox'])
239    conditions = cond_merge conditions, conditions_user(params['user'])
240    conditions = cond_merge conditions, conditions_time(params['time'])
241    conditions = cond_merge conditions, conditions_open(params['open'])
242
243    # create the results document
244    results = OSM::API.new.get_xml_doc
245
246    # add all matching changesets to the XML results document
247    Changeset.find(:all, 
248                   :conditions => conditions, 
249                   :limit => 100,
250                   :order => 'created_at desc').each do |cs|
251      results.root << cs.to_xml_node
252    end
253
254    render :text => results.to_s, :content_type => "text/xml"
255
256  rescue ActiveRecord::RecordNotFound
257    render :nothing => true, :status => :not_found
258  rescue OSM::APIError => ex
259    render ex.render_opts
260  end
261 
262  ##
263  # updates a changeset's tags. none of the changeset's attributes are
264  # user-modifiable, so they will be ignored.
265  #
266  # changesets are not (yet?) versioned, so we don't have to deal with
267  # history tables here. changesets are locked to a single user, however.
268  #
269  # after succesful update, returns the XML of the changeset.
270  def update
271    # request *must* be a PUT.
272    unless request.put?
273      render :nothing => true, :status => :method_not_allowed
274      return
275    end
276   
277    changeset = Changeset.find(params[:id])
278    new_changeset = Changeset.from_xml(request.raw_post)
279
280    unless new_changeset.nil?
281      check_changeset_consistency(changeset, @user)
282      changeset.update_from(new_changeset, @user)
283      render :text => changeset.to_xml, :mime_type => "text/xml"
284    else
285     
286      render :nothing => true, :status => :bad_request
287    end
288     
289  rescue ActiveRecord::RecordNotFound
290    render :nothing => true, :status => :not_found
291  rescue OSM::APIError => ex
292    render ex.render_opts
293  end
294
295  ##
296  # list edits belonging to a user
297  def list
298    user = User.find(:first, :conditions => [ "visible = ? and display_name = ?", true, params[:display_name]])
299    @edit_pages, @edits = paginate(:changesets,
300                                   :include => [:user, :changeset_tags],
301                                   :conditions => ["changesets.user_id = ? AND min_lat IS NOT NULL", user.id],
302                                   :order => "changesets.created_at DESC",
303                                   :per_page => 20)
304   
305    @action = 'list'
306    @display_name = user.display_name
307    # FIXME needs rescues in here
308  end
309
310private
311  #------------------------------------------------------------
312  # utility functions below.
313  #------------------------------------------------------------ 
314
315  ##
316  # merge two conditions
317  def cond_merge(a, b)
318    if a and b
319      a_str = a.shift
320      b_str = b.shift
321      return [ a_str + " and " + b_str ] + a + b
322    elsif a
323      return a
324    else b
325      return b
326    end
327  end
328
329  ##
330  # if a bounding box was specified then parse it and do some sanity
331  # checks. this is mostly the same as the map call, but without the
332  # area restriction.
333  def conditions_bbox(bbox)
334    unless bbox.nil?
335      raise OSM::APIBadUserInput.new("Bounding box should be min_lon,min_lat,max_lon,max_lat") unless bbox.count(',') == 3
336      bbox = sanitise_boundaries(bbox.split(/,/))
337      raise OSM::APIBadUserInput.new("Minimum longitude should be less than maximum.") unless bbox[0] <= bbox[2]
338      raise OSM::APIBadUserInput.new("Minimum latitude should be less than maximum.") unless bbox[1] <= bbox[3]
339      return ['min_lon < ? and max_lon > ? and min_lat < ? and max_lat > ?',
340              bbox[2] * GeoRecord::SCALE, bbox[0] * GeoRecord::SCALE, bbox[3]* GeoRecord::SCALE, bbox[1] * GeoRecord::SCALE]
341    else
342      return nil
343    end
344  end
345
346  ##
347  # restrict changesets to those by a particular user
348  def conditions_user(user)
349    unless user.nil?
350      # user input checking, we don't have any UIDs < 1
351      raise OSM::APIBadUserInput.new("invalid user ID") if user.to_i < 1
352
353      u = User.find(user.to_i)
354      # should be able to get changesets of public users only, or
355      # our own changesets regardless of public-ness.
356      unless u.data_public?
357        # get optional user auth stuff so that users can see their own
358        # changesets if they're non-public
359        setup_user_auth
360       
361        raise OSM::APINotFoundError if @user.nil? or @user.id != u.id
362      end
363      return ['user_id = ?', u.id]
364    else
365      return nil
366    end
367  end
368
369  ##
370  # restrict changes to those during a particular time period
371  def conditions_time(time) 
372    unless time.nil?
373      # if there is a range, i.e: comma separated, then the first is
374      # low, second is high - same as with bounding boxes.
375      if time.count(',') == 1
376        # check that we actually have 2 elements in the array
377        times = time.split(/,/)
378        raise OSM::APIBadUserInput.new("bad time range") if times.size != 2 
379
380        from, to = times.collect { |t| DateTime.parse(t) }
381        return ['closed_at >= ? and created_at <= ?', from, to]
382      else
383        # if there is no comma, assume its a lower limit on time
384        return ['closed_at >= ?', DateTime.parse(time)]
385      end
386    else
387      return nil
388    end
389    # stupid DateTime seems to throw both of these for bad parsing, so
390    # we have to catch both and ensure the correct code path is taken.
391  rescue ArgumentError => ex
392    raise OSM::APIBadUserInput.new(ex.message.to_s)
393  rescue RuntimeError => ex
394    raise OSM::APIBadUserInput.new(ex.message.to_s)
395  end
396
397  ##
398  # restrict changes to those which are open
399  def conditions_open(open)
400    return open.nil? ? nil : ['closed_at >= ?', DateTime.now]
401  end
402
403end
Note: See TracBrowser for help on using the repository browser.