Ticket #742: rails_port_notes.diff

File rails_port_notes.diff, 12.4 KB (added by openstreetmap@…, 12 years ago)

A first cut patch to implement the missing functionality.

Line 
1Index: app/models/old_note.rb
2===================================================================
3--- app/models/old_note.rb      (revision 0)
4+++ app/models/old_note.rb      (revision 0)
5@@ -0,0 +1,50 @@
6+class OldNote < GeoRecord
7+  set_table_name 'notes'
8
9+  validates_presence_of :timestamp
10+  validates_inclusion_of :visible, :in => [ true, false ]
11+  validates_numericality_of :latitude, :longitude
12+  validate :validate_position
13+
14+  def validate_position
15+    errors.add_to_base("Note is not in the world") unless in_world?
16+  end
17+
18+  def in_world?
19+    return false if self.lat < -90 or self.lat > 90
20+    return false if self.lon < -180 or self.lon > 180
21+    return true
22+  end
23+
24+  def self.from_note(note)
25+    old_note = OldNote.new
26+    old_note.latitude = note.latitude
27+    old_note.longitude = note.longitude
28+    old_note.visible = note.visible
29+    old_note.timestamp = note.timestamp
30+    old_note.date = note.date
31+    old_note.id = note.id
32+    old_note.summary = note.summary
33+    old_note.description = note.description
34+    old_note.status = note.status
35+    old_note.trace = note.trace
36+    old_note.node = note.node
37+    return old_note
38+  end
39+
40+  def to_xml_note
41+    el1 = XML::Node.new 'note'
42+    el1['id'] = self.id.to_s
43+    el1['lat'] = self.lat.to_s
44+    el1['lon'] = self.lon.to_s
45+    el1['summary'] = self.summary
46+    el1['description'] = self.description
47+    el1['status'] = self.status
48+    el1['trace'] = self.trace
49+    el1['node'] = self.node
50+    el1['visible'] = self.visible.to_s
51+    el1['date'] = self.date.xmlschema
52+    el1['timestamp'] = self.timestamp.xmlschema
53+    return el1
54+  end
55+end
56Index: app/models/note.rb
57===================================================================
58--- app/models/note.rb  (revision 0)
59+++ app/models/note.rb  (revision 0)
60@@ -0,0 +1,121 @@
61+# The note model represents a current existing note, that is, the latest version. Use OldNote for historical notes.
62+
63+class Note < GeoRecord
64+  require 'xml/libxml'
65+
66+  set_table_name 'current_notes'
67
68+  validates_presence_of :timestamp
69+  validates_inclusion_of :visible, :in => [ true, false ]
70+  validates_numericality_of :latitude, :longitude
71+  validate :validate_position
72+
73+  belongs_to :trace
74+  belongs_to :node
75+
76+  # Sanity check the latitude and longitude and add an error if it's broken
77+  def validate_position
78+    errors.add_to_base("Note is not in the world") unless in_world?
79+  end
80+
81+  #
82+  # Search for notes within bounding_box
83+  #
84+  def self.search(bounding_box)
85+    min_lon, min_lat, max_lon, max_lat = *bounding_box
86+    find_by_area(min_lat, min_lon, max_lat, max_lon, :conditions => 'visible = 1')
87+  end
88+
89+  # Read in xml as text and return it's Node object representation
90+  def self.from_xml(xml, create=false)
91+    begin
92+      p = XML::Parser.new
93+      p.string = xml
94+      doc = p.parse
95
96+      note = Note.new
97+
98+      doc.find('//osm/note').each do |pt|
99+        note.lat = pt['lat'].to_f
100+        note.lon = pt['lon'].to_f
101+
102+        return nil unless note.in_world?
103+
104+        unless create
105+          if pt['id'] != '0'
106+            note.id = pt['id'].to_i
107+          end
108+        end
109+
110+        note.visible = pt['visible'] and pt['visible'] == 'true'
111+
112+        note.summary = pt['summary']
113+       if pt['description']
114+         note.description = pt['description']
115+        end
116+       if pt['email']
117+         note.email = pt['email']
118+        end
119+       if pt['status']
120+         note.status = pt['status']
121+        end
122+       if pt['trace']
123+         note.trace = pt['trace']
124+        end
125+       if pt['node']
126+         note.node = pt['node']
127+        end
128+       
129+        if pt['date']
130+         note.date = Time.parse(pt['date'])
131+       end
132+        if pt['timestamp']
133+         note.timestamp = Time.parse(pt['timestamp'])
134+        else
135+         if create
136+            note.timestamp = Time.now
137+         end
138+       end
139+       
140+      end
141+    rescue
142+      note = nil
143+    end
144+
145+    return note
146+  end
147+
148+  # Save this node with the appropriate OldNode object to represent it's history.
149+  def save_with_history!
150+    Note.transaction do
151+      self.timestamp = Time.now
152+      self.save!
153+      old_note = OldNote.from_note(self)
154+      old_note.save!
155+    end
156+  end
157+
158+  # Turn this Node in to a complete OSM XML object with <osm> wrapper
159+  def to_xml
160+    doc = OSM::API.new.get_xml_doc
161+    doc.root << to_xml_note()
162+    return doc
163+  end
164+
165+  # Turn this Note in to an XML Note without the <osm> wrapper.
166+  def to_xml_note(user_display_name_cache = nil)
167+    el1 = XML::Node.new 'note'
168+    el1['id'] = self.id.to_s
169+    el1['lat'] = self.lat.to_s
170+    el1['lon'] = self.lon.to_s
171+    el1['summary'] = self.summary
172+    el1['description'] = self.description
173+    el1['status'] = self.status
174+    el1['trace'] = self.trace
175+    el1['node'] = self.node
176+    el1['visible'] = self.visible.to_s
177+    el1['date'] = self.date.xmlschema
178+    el1['timestamp'] = self.timestamp.xmlschema
179+    return el1
180+  end
181+end
182Index: app/controllers/note_controller.rb
183===================================================================
184--- app/controllers/note_controller.rb  (revision 0)
185+++ app/controllers/note_controller.rb  (revision 0)
186@@ -0,0 +1,84 @@
187+# The NoteController is the RESTful interface to Note objects
188+
189+class NoteController < ApplicationController
190+  require 'xml/libxml'
191+
192+  session :off
193+  before_filter :check_write_availability, :only => [:create, :update, :delete]
194+  before_filter :check_read_availability, :except => [:create, :update, :delete]
195+  after_filter :compress_output
196+
197+  # Create a note from XML.
198+  def create
199+    if request.put?
200+      note = Note.from_xml(request.raw_post, true)
201+     
202+      if note
203+        note.visible = true
204+        note.save_with_history!
205+       
206+        render :text => note.id.to_s, :content_type => "text/plain"
207+      else
208+        render :nothing => true, :status => :bad_request
209+      end
210+    else
211+      render :nothing => true, :status => :method_not_allowed
212+    end
213+  end
214
215+  # Dump the details on a note given in params[:id]
216+  def read
217+    begin
218+      note = Note.find(params[:id])
219+      if note.visible
220+        response.headers['Last-Modified'] = note.timestamp.rfc822
221+        render :text => note.to_xml.to_s, :content_type => "text/xml"
222+      else
223+        render :text => "", :status => :gone
224+      end
225+    rescue ActiveRecord::RecordNotFound
226+      render :nothing => true, :status => :not_found
227+    end
228+  end
229
230+  # Update a note from given XML
231+  def update
232+    begin
233+      note = Note.find(params[:id])
234+      new_note = Note.from_xml(request.raw_post)
235+     
236+      if new_note and new_note.id == note.id
237+        note = new_note
238+       note.visible = true
239+        note.save_with_history!
240+       
241+        render :nothing => true
242+      else
243+        render :nothing => true, :status => :bad_request
244+      end
245+    rescue ActiveRecord::RecordNotFound
246+      render :nothing => true, :status => :not_found
247+    end
248+  end
249
250+  # Delete a note. Doesn't actually delete it, but retains its history in a wiki-like way.
251+  def delete
252+    begin
253+      note = Note.find(params[:id])
254+     
255+      if note.visible
256+        note.user_id = @user.id
257+        note.visible = 0
258+        note.save_with_history!
259+       
260+        render :nothing => true
261+      else
262+        render :text => "", :status => :gone
263+      end
264+    rescue ActiveRecord::RecordNotFound
265+      render :nothing => true, :status => :not_found
266+    end
267+  end
268+
269+end
270+
271Index: app/controllers/api_controller.rb
272===================================================================
273--- app/controllers/api_controller.rb   (revision 7082)
274+++ app/controllers/api_controller.rb   (working copy)
275@@ -211,6 +209,183 @@
276     end
277   end
278 
279+  def notes
280+    GC.start
281+    @@count+=1
282+    # Figure out the bbox
283+    bbox = params['bbox']
284+
285+    unless bbox and bbox.count(',') == 3
286+      # alternatively: report_error(TEXT['boundary_parameter_required']
287+      report_error("The parameter bbox is required, and must be of the form min_lon,min_lat,max_lon,max_lat")
288+      return
289+    end
290+
291+    bbox = bbox.split(',')
292+
293+    min_lon, min_lat, max_lon, max_lat = sanitise_boundaries(bbox)
294+
295+    # check boundary is sane and area within defined
296+    # see /config/application.yml
297+    begin
298+      check_boundaries(min_lon, min_lat, max_lon, max_lat)
299+    rescue Exception => err
300+      report_error(err.message)
301+      return
302+    end
303+
304+    @notes = Note.find_by_area(min_lat, min_lon, max_lat, max_lon, :conditions => "visible = 1")
305+
306+    if @notes.length == 0
307+      render :text => "<osm version='0.5' length='0'></osm>", :content_type => "text/xml"
308+      return
309+    end
310+   
311+    doc = OSM::API.new.get_xml_doc
312+   
313+    @notes.each do |note|
314+      if note.visible?
315+        doc.root << note.to_xml_note()
316+      end
317+    end
318+   
319+    render :text => doc.to_s, :content_type => "text/xml"
320+   
321+    #exit when we have too many requests
322+    if @@count > MAX_COUNT
323+      @@count = COUNT
324+     
325+      exit!
326+    end
327+  end
328+
329   def changes
330     zoom = (params[:zoom] || '12').to_i
331 
332Index: db/migrate/016_create_notes.rb
333===================================================================
334--- db/migrate/016_create_notes.rb      (revision 0)
335+++ db/migrate/016_create_notes.rb      (revision 0)
336@@ -0,0 +1,50 @@
337+class CreateNotes < ActiveRecord::Migration
338+  def self.up
339+    create_table "current_notes", innodb_table do |t|
340+      t.column "id",          :bigint, :limit => 64, :null => false
341+      t.column "latitude",    :double
342+      t.column "longitude",   :double
343+      t.column "user_id",     :bigint, :limit => 20
344+      t.column "visible",     :boolean
345+      t.column "timestamp",   :datetime
346+      t.column "date",        :datetime
347+      t.column "tile",        :integer
348+      t.column "summary",     :string
349+      t.column "description", :string
350+      t.column "email",       :string
351+      t.column "status",      :string
352+      t.column "trace",       :bigint, :limit => 64
353+      t.column "node",        :bigint, :limit => 64
354+    end
355+
356+    add_primary_key "current_notes", ["id"]
357+    add_index "current_notes", ["latitude", "longitude"], :name => "current_notes_lat_lon_idx"
358+    add_index "current_notes", ["tile"], :name => "current_notes_tile_idx"
359+   
360+    change_column "current_notes", "id", :bigint, :limit => 64, :null => false, :options => "AUTO_INCREMENT"
361+   
362+    create_table "notes", innodb_table do |t|
363+      t.column "id",          :bigint, :limit => 64, :null => false, :options => "AUTO_INCREMENT"
364+      t.column "latitude",    :double
365+      t.column "longitude",   :double
366+      t.column "user_id",     :bigint, :limit => 20
367+      t.column "visible",     :boolean
368+      t.column "timestamp",   :datetime
369+      t.column "tile",        :integer
370+      t.column "summary",     :string
371+      t.column "description", :string
372+      t.column "email",       :string
373+      t.column "status",      :string
374+      t.column "trace",       :bigint, :limit => 64
375+      t.column "node",        :bigint, :limit => 64
376+    end
377+
378+    add_primary_key "notes", ["id"]
379+    add_index "notes", ["tile"], :name => "notes_tile_idx"
380+  end
381+
382+  def self.down
383+    drop_table :current_notes
384+    drop_table :notes
385+  end
386+end
387Index: config/routes.rb
388===================================================================
389--- config/routes.rb    (revision 7082)
390+++ config/routes.rb    (working copy)
391@@ -10,6 +10,13 @@
392   map.connect "api/#{API_VERSION}/node/:id", :controller => 'node', :action => 'delete', :id => /\d+/, :conditions => { :method => :delete }
393   map.connect "api/#{API_VERSION}/nodes", :controller => 'node', :action => 'nodes', :id => nil
394   
395+  map.connect "api/#{API_VERSION}/note/create", :controller => 'note', :action => 'create'
396+  map.connect "api/#{API_VERSION}/note/:id/history", :controller => 'old_note', :action => 'history', :id => /\d+/
397+  map.connect "api/#{API_VERSION}/note/:id", :controller => 'note', :action => 'read', :id => /\d+/, :conditions => { :method => :get }
398+  map.connect "api/#{API_VERSION}/note/:id", :controller => 'note', :action => 'update', :id => /\d+/, :conditions => { :method => :put }
399+  map.connect "api/#{API_VERSION}/note/:id", :controller => 'note', :action => 'delete', :id => /\d+/, :conditions => { :method => :delete }
400+  map.connect "api/#{API_VERSION}/notes", :controller => 'api', :action => 'notes'
401+
402   map.connect "api/#{API_VERSION}/way/create", :controller => 'way', :action => 'create'
403   map.connect "api/#{API_VERSION}/way/:id/history", :controller => 'old_way', :action => 'history', :id => /\d+/
404   map.connect "api/#{API_VERSION}/way/:id/full", :controller => 'way', :action => 'full', :id => /\d+/
405