Rework notes interface using OpenLayers and rails
[rails.git] / app / controllers / note_controller.rb
1 class NoteController < ApplicationController
2
3   layout 'site', :only => [:mine]
4
5   before_filter :check_api_readable
6   before_filter :authorize_web, :only => [:create, :close, :update, :delete, :mine]
7   before_filter :check_api_writable, :only => [:create, :close, :update, :delete]
8   before_filter :set_locale, :only => [:mine]
9   after_filter :compress_output
10   around_filter :api_call_handle_error, :api_call_timeout
11
12   # Help methods for checking boundary sanity and area size
13   include MapBoundary
14
15   ##
16   # Return a list of notes in a given area
17   def list
18     # Figure out the bbox - we prefer a bbox argument but also
19     # support the old, deprecated, method with four arguments
20     if params[:bbox]
21       raise OSM::APIBadUserInput.new("Invalid bbox") unless params[:bbox].count(",") == 3
22
23       bbox = params[:bbox].split(",")
24     else
25       raise OSM::APIBadUserInput.new("No l was given") unless params[:l]
26       raise OSM::APIBadUserInput.new("No r was given") unless params[:r]
27       raise OSM::APIBadUserInput.new("No b was given") unless params[:b]
28       raise OSM::APIBadUserInput.new("No t was given") unless params[:t]
29
30       bbox = [ params[:l], params[:b], params[:r], params[:t] ]
31     end
32
33     # Get the sanitised boundaries
34     @min_lon, @min_lat, @max_lon, @max_lat = sanitise_boundaries(bbox)
35
36     # Get any conditions that need to be applied
37     conditions = closed_condition
38
39     # Check that the boundaries are valid
40     check_boundaries(@min_lon, @min_lat, @max_lon, @max_lat, MAX_NOTE_REQUEST_AREA)
41
42     # Find the notes we want to return
43     @notes = Note.find_by_area(@min_lat, @min_lon, @max_lat, @max_lon,
44                                :include => :comments, 
45                                :conditions => conditions,
46                                :order => "updated_at DESC", 
47                                :limit => result_limit)
48
49     # Render the result
50     respond_to do |format|
51       format.rss
52       format.xml
53       format.json
54       format.gpx
55     end
56   end
57
58   ##
59   # Create a new note
60   def create
61     # Check the arguments are sane
62     raise OSM::APIBadUserInput.new("No lat was given") unless params[:lat]
63     raise OSM::APIBadUserInput.new("No lon was given") unless params[:lon]
64     raise OSM::APIBadUserInput.new("No text was given") unless params[:text]
65
66     # Extract the arguments
67     lon = params[:lon].to_f
68     lat = params[:lat].to_f
69     comment = params[:text]
70     name = params[:name]
71
72     # Include in a transaction to ensure that there is always a note_comment for every note
73     Note.transaction do
74       # Create the note
75       @note = Note.create(:lat => lat, :lon => lon)
76       raise OSM::APIBadUserInput.new("The note is outside this world") unless @note.in_world?
77
78       #TODO: move this into a helper function
79       begin
80         url = "http://nominatim.openstreetmap.org/reverse?lat=" + lat.to_s + "&lon=" + lon.to_s + "&zoom=16" 
81         response = REXML::Document.new(Net::HTTP.get(URI.parse(url))) 
82                 
83         if result = response.get_text("reversegeocode/result") 
84           @note.nearby_place = result.to_s 
85         else 
86           @note.nearby_place = "unknown"
87         end
88       rescue Exception => err
89         @note.nearby_place = "unknown"
90       end
91
92       # Save the note
93       @note.save
94
95       # Add a comment to the note
96       add_comment(@note, comment, name, "opened")
97     end
98
99     # Send an OK response
100     render_ok
101   end
102
103   ##
104   # Add a comment to an existing note
105   def update
106     # Check the arguments are sane
107     raise OSM::APIBadUserInput.new("No id was given") unless params[:id]
108     raise OSM::APIBadUserInput.new("No text was given") unless params[:text]
109
110     # Extract the arguments
111     id = params[:id].to_i
112     comment = params[:text]
113     name = params[:name] or "NoName"
114
115     # Find the note and check it is valid
116     note = Note.find(id)
117     raise OSM::APINotFoundError unless note
118     raise OSM::APIAlreadyDeletedError unless note.visible?
119
120     # Add a comment to the note
121     Note.transaction do
122       add_comment(note, comment, name, "commented")
123     end
124
125     # Send an OK response
126     render_ok
127   end
128
129   ##
130   # Close a note
131   def close
132     # Check the arguments are sane
133     raise OSM::APIBadUserInput.new("No id was given") unless params[:id]
134
135     # Extract the arguments
136     id = params[:id].to_i
137     name = params[:name]
138
139     # Find the note and check it is valid
140     note = Note.find_by_id(id)
141     raise OSM::APINotFoundError unless note
142     raise OSM::APIAlreadyDeletedError unless note.visible?
143
144     # Close the note and add a comment
145     Note.transaction do
146       note.close
147
148       add_comment(note, nil, name, "closed")
149     end
150
151     # Send an OK response
152     render_ok
153   end 
154
155   ##
156   # Get a feed of recent notes and comments
157   def rss
158     # Get any conditions that need to be applied
159     conditions = closed_condition
160
161     # Process any bbox
162     if params[:bbox]
163       raise OSM::APIBadUserInput.new("Invalid bbox") unless params[:bbox].count(",") == 3
164
165       @min_lon, @min_lat, @max_lon, @max_lat = sanitise_boundaries(params[:bbox].split(','))
166
167       check_boundaries(@min_lon, @min_lat, @max_lon, @max_lat, MAX_NOTE_REQUEST_AREA)
168
169       conditions = cond_merge conditions, [OSM.sql_for_area(@min_lat, @min_lon, @max_lat, @max_lon, "notes.")]
170     end
171
172     # Find the comments we want to return
173     @comments = NoteComment.find(:all, 
174                                  :conditions => conditions,
175                                  :order => "created_at DESC",
176                                  :limit => result_limit,
177                                  :joins => :note, 
178                                  :include => :note)
179
180     # Render the result
181     respond_to do |format|
182       format.rss
183     end
184   end
185
186   ##
187   # Read a note
188   def read
189     # Check the arguments are sane
190     raise OSM::APIBadUserInput.new("No id was given") unless params[:id]
191
192     # Find the note and check it is valid
193     @note = Note.find(params[:id])
194     raise OSM::APINotFoundError unless @note
195     raise OSM::APIAlreadyDeletedError unless @note.visible?
196     
197     # Render the result
198     respond_to do |format|
199       format.xml
200       format.rss
201       format.json
202       format.gpx
203     end
204   end
205
206   ##
207   # Delete (hide) a note
208   def delete
209     # Check the arguments are sane
210     raise OSM::APIBadUserInput.new("No id was given") unless params[:id]
211
212     # Extract the arguments
213     id = params[:id].to_i
214     name = params[:name]
215
216     # Find the note and check it is valid
217     note = Note.find(id)
218     raise OSM::APINotFoundError unless note
219     raise OSM::APIAlreadyDeletedError unless note.visible?
220
221     # Mark the note as hidden
222     Note.transaction do
223       note.status = "hidden"
224       note.save
225
226       add_comment(note, nil, name, "hidden")
227     end
228
229     # Render the result
230     render :text => "ok\n", :content_type => "text/html" 
231   end
232
233   ##
234   # Return a list of notes matching a given string
235   def search
236     # Check the arguments are sane
237     raise OSM::APIBadUserInput.new("No query string was given") unless params[:q]
238
239     # Get any conditions that need to be applied
240     conditions = closed_condition
241     conditions = cond_merge conditions, ['note_comments.body ~ ?', params[:q]]
242         
243     # Find the notes we want to return
244     @notes = Note.find(:all, 
245                        :conditions => conditions,
246                        :order => "updated_at DESC",
247                        :limit => result_limit,
248                        :joins => :comments,
249                        :include => :comments)
250
251     # Render the result
252     respond_to do |format|
253       format.html { render :action => :list, :format => :rjs, :content_type => "text/javascript"}
254       format.rss { render :action => :list }
255       format.js
256       format.xml { render :action => :list }
257       format.json { render :action => :list }
258       format.gpx { render :action => :list }
259     end
260   end
261
262   def mine
263     if params[:display_name] 
264       @user2 = User.find_by_display_name(params[:display_name], :conditions => { :status => ["active", "confirmed"] }) 
265  
266       if @user2  
267         if @user2.data_public? or @user2 == @user 
268           conditions = ['note_comments.author_id = ?', @user2.id] 
269         else 
270           conditions = ['false'] 
271         end 
272       else #if request.format == :html 
273         @title = t 'user.no_such_user.title' 
274         @not_found_user = params[:display_name] 
275         render :template => 'user/no_such_user', :status => :not_found 
276         return
277       end 
278     end
279
280     if @user2 
281       user_link = render_to_string :partial => "user", :object => @user2 
282     end 
283     
284     @title =  t 'note.mine.title', :user => @user2.display_name 
285     @heading =  t 'note.mine.heading', :user => @user2.display_name 
286     @description = t 'note.mine.description', :user => user_link
287     
288     @page = (params[:page] || 1).to_i 
289     @page_size = 10
290
291     @notes = Note.find(:all, 
292                        :include => [:comments, {:comments => :author}],
293                        :joins => :comments,
294                        :order => "updated_at DESC",
295                        :conditions => conditions,
296                        :offset => (@page - 1) * @page_size, 
297                        :limit => @page_size).uniq
298   end
299
300 private 
301   #------------------------------------------------------------ 
302   # utility functions below. 
303   #------------------------------------------------------------   
304  
305   ## 
306   # merge two conditions 
307   # TODO: this is a copy from changeset_controler.rb and should be factored out to share
308   def cond_merge(a, b) 
309     if a and b 
310       a_str = a.shift 
311       b_str = b.shift 
312       return [ a_str + " AND " + b_str ] + a + b 
313     elsif a  
314       return a 
315     else b 
316       return b 
317     end 
318   end 
319
320   ##
321   # Render an OK response
322   def render_ok
323     if params[:format] == "js"
324       render :text => "osbResponse();", :content_type => "text/javascript" 
325     else
326       render :text => "ok " + @note.id.to_s + "\n", :content_type => "text/plain" if @note
327       render :text => "ok\n", :content_type => "text/plain" unless @note
328     end
329   end
330
331   ##
332   # Get the maximum number of results to return
333   def result_limit
334     if params[:limit] and params[:limit].to_i > 0 and params[:limit].to_i < 10000
335       params[:limit].to_i
336     else
337       100
338     end
339   end
340
341   ##
342   # Generate a condition to choose which bugs we want based
343   # on their status and the user's request parameters
344   def closed_condition
345     if params[:closed]
346       closed_since = params[:closed].to_i
347     else
348       closed_since = 7
349     end
350         
351     if closed_since < 0
352       conditions = ["status != 'hidden'"]
353     elsif closed_since > 0
354       conditions = ["(status = 'open' OR (status = 'closed' AND closed_at > '#{Time.now - closed_since.days}'))"]
355     else
356       conditions = ["status = 'open'"]
357     end
358
359     return conditions
360   end
361
362   ##
363   # Add a comment to a note
364   def add_comment(note, text, name, event)
365     name = "NoName" if name.nil?
366
367     attributes = { :visible => true, :event => event, :body => text }
368
369     if @user  
370       attributes[:author_id] = @user.id
371       attributes[:author_name] = @user.display_name
372     else  
373       attributes[:author_ip] = request.remote_ip
374       attributes[:author_name] = name + " (a)"
375     end
376
377     note.comments.create(attributes)
378
379     note.comments.map { |c| c.author }.uniq.each do |user|
380       if user and user != @user
381         Notifier.deliver_note_comment_notification(comment, user)
382       end
383     end
384   end
385 end