]> git.openstreetmap.org Git - rails.git/blob - app/controllers/api/notes_controller.rb
Updates notes filtering to search description too
[rails.git] / app / controllers / api / notes_controller.rb
1 module Api
2   class NotesController < ApiController
3     before_action :check_api_writable, :only => [:create, :comment, :close, :reopen, :destroy]
4     before_action :setup_user_auth, :only => [:create, :show]
5     before_action :authorize, :only => [:close, :reopen, :destroy, :comment]
6
7     authorize_resource
8
9     before_action :set_locale
10     before_action :set_request_formats, :except => [:feed]
11
12     ##
13     # Return a list of notes in a given area
14     def index
15       # Figure out the bbox - we prefer a bbox argument but also
16       # support the old, deprecated, method with four arguments
17       if params[:bbox]
18         bbox = BoundingBox.from_bbox_params(params)
19       elsif params[:l] && params[:r] && params[:b] && params[:t]
20         bbox = BoundingBox.from_lrbt_params(params)
21       else
22         raise OSM::APIBadUserInput, "The parameter bbox is required"
23       end
24
25       # Get any conditions that need to be applied
26       notes = closed_condition(Note.all)
27
28       # Check that the boundaries are valid
29       bbox.check_boundaries
30
31       # Check the the bounding box is not too big
32       bbox.check_size(Settings.max_note_request_area)
33       @min_lon = bbox.min_lon
34       @min_lat = bbox.min_lat
35       @max_lon = bbox.max_lon
36       @max_lat = bbox.max_lat
37
38       # Find the notes we want to return
39       @notes = notes.bbox(bbox).order("updated_at DESC").limit(result_limit).preload(:comments)
40
41       # Render the result
42       respond_to do |format|
43         format.rss
44         format.xml
45         format.json
46         format.gpx
47       end
48     end
49
50     ##
51     # Read a note
52     def show
53       # Check the arguments are sane
54       raise OSM::APIBadUserInput, "No id was given" unless params[:id]
55
56       # Find the note and check it is valid
57       @note = Note.find(params[:id])
58       raise OSM::APINotFoundError unless @note
59       raise OSM::APIAlreadyDeletedError.new("note", @note.id) unless @note.visible? || current_user&.moderator?
60
61       # Render the result
62       respond_to do |format|
63         format.xml
64         format.rss
65         format.json
66         format.gpx
67       end
68     end
69
70     ##
71     # Create a new note
72     def create
73       # Check the ACLs
74       raise OSM::APIAccessDenied if current_user.nil? && Acl.no_note_comment(request.remote_ip)
75
76       # Check the arguments are sane
77       raise OSM::APIBadUserInput, "No lat was given" unless params[:lat]
78       raise OSM::APIBadUserInput, "No lon was given" unless params[:lon]
79       raise OSM::APIBadUserInput, "No text was given" if params[:text].blank?
80
81       # Extract the arguments
82       lon = OSM.parse_float(params[:lon], OSM::APIBadUserInput, "lon was not a number")
83       lat = OSM.parse_float(params[:lat], OSM::APIBadUserInput, "lat was not a number")
84       comment = params[:text]
85
86       # Include in a transaction to ensure that there is always a note_comment for every note
87       Note.transaction do
88         # Create the note
89         @note = Note.create(:lat => lat, :lon => lon)
90         raise OSM::APIBadUserInput, "The note is outside this world" unless @note.in_world?
91
92         # Save the note
93         @note.save!
94
95         # Add a comment to the note
96         add_comment(@note, comment, "opened")
97       end
98
99       # Return a copy of the new note
100       respond_to do |format|
101         format.xml { render :action => :show }
102         format.json { render :action => :show }
103       end
104     end
105
106     ##
107     # Delete (hide) a note
108     def destroy
109       # Check the arguments are sane
110       raise OSM::APIBadUserInput, "No id was given" unless params[:id]
111
112       # Extract the arguments
113       id = params[:id].to_i
114       comment = params[:text]
115
116       # Find the note and check it is valid
117       Note.transaction do
118         @note = Note.lock.find(id)
119         raise OSM::APINotFoundError unless @note
120         raise OSM::APIAlreadyDeletedError.new("note", @note.id) unless @note.visible?
121
122         # Mark the note as hidden
123         @note.status = "hidden"
124         @note.save
125
126         add_comment(@note, comment, "hidden", :notify => false)
127       end
128
129       # Return a copy of the updated note
130       respond_to do |format|
131         format.xml { render :action => :show }
132         format.json { render :action => :show }
133       end
134     end
135
136     ##
137     # Add a comment to an existing note
138     def comment
139       # Check the arguments are sane
140       raise OSM::APIBadUserInput, "No id was given" unless params[:id]
141       raise OSM::APIBadUserInput, "No text was given" if params[:text].blank?
142
143       # Extract the arguments
144       id = params[:id].to_i
145       comment = params[:text]
146
147       # Find the note and check it is valid
148       Note.transaction do
149         @note = Note.lock.find(id)
150         raise OSM::APINotFoundError unless @note
151         raise OSM::APIAlreadyDeletedError.new("note", @note.id) unless @note.visible?
152         raise OSM::APINoteAlreadyClosedError, @note if @note.closed?
153
154         # Add a comment to the note
155         add_comment(@note, comment, "commented")
156       end
157
158       # Return a copy of the updated note
159       respond_to do |format|
160         format.xml { render :action => :show }
161         format.json { render :action => :show }
162       end
163     end
164
165     ##
166     # Close a note
167     def close
168       # Check the arguments are sane
169       raise OSM::APIBadUserInput, "No id was given" unless params[:id]
170
171       # Extract the arguments
172       id = params[:id].to_i
173       comment = params[:text]
174
175       # Find the note and check it is valid
176       Note.transaction do
177         @note = Note.lock.find_by(:id => id)
178         raise OSM::APINotFoundError unless @note
179         raise OSM::APIAlreadyDeletedError.new("note", @note.id) unless @note.visible?
180         raise OSM::APINoteAlreadyClosedError, @note if @note.closed?
181
182         # Close the note and add a comment
183         @note.close
184
185         add_comment(@note, comment, "closed")
186       end
187
188       # Return a copy of the updated note
189       respond_to do |format|
190         format.xml { render :action => :show }
191         format.json { render :action => :show }
192       end
193     end
194
195     ##
196     # Reopen a note
197     def reopen
198       # Check the arguments are sane
199       raise OSM::APIBadUserInput, "No id was given" unless params[:id]
200
201       # Extract the arguments
202       id = params[:id].to_i
203       comment = params[:text]
204
205       # Find the note and check it is valid
206       Note.transaction do
207         @note = Note.lock.find_by(:id => id)
208         raise OSM::APINotFoundError unless @note
209         raise OSM::APIAlreadyDeletedError.new("note", @note.id) unless @note.visible? || current_user.moderator?
210         raise OSM::APINoteAlreadyOpenError, @note unless @note.closed? || !@note.visible?
211
212         # Reopen the note and add a comment
213         @note.reopen
214
215         add_comment(@note, comment, "reopened")
216       end
217
218       # Return a copy of the updated note
219       respond_to do |format|
220         format.xml { render :action => :show }
221         format.json { render :action => :show }
222       end
223     end
224
225     ##
226     # Get a feed of recent notes and comments
227     def feed
228       # Get any conditions that need to be applied
229       notes = closed_condition(Note.all)
230       notes = bbox_condition(notes)
231
232       # Find the comments we want to return
233       @comments = NoteComment.where(:note => notes)
234                              .order(:created_at => :desc).limit(result_limit)
235                              .preload(:author, :note => { :comments => :author })
236
237       # Render the result
238       respond_to do |format|
239         format.rss
240       end
241     end
242
243     ##
244     # Return a list of notes matching a given string
245     def search
246       # Get the initial set of notes
247       @notes = closed_condition(Note.all)
248       @notes = bbox_condition(@notes)
249
250       # Add any user filter
251       if params[:display_name] || params[:user]
252         if params[:display_name]
253           @user = User.find_by(:display_name => params[:display_name])
254
255           raise OSM::APIBadUserInput, "User #{params[:display_name]} not known" unless @user
256         else
257           @user = User.find_by(:id => params[:user])
258
259           raise OSM::APIBadUserInput, "User #{params[:user]} not known" unless @user
260         end
261
262         @notes = @notes.joins(:comments).where(:note_comments => { :author_id => @user })
263       end
264
265       # Add any text filter
266       if params[:q]
267         @notes = @notes.joins(:comments).where("to_tsvector('english', note_comments.body) @@ plainto_tsquery('english', ?) OR to_tsvector('english', notes.description) @@ plainto_tsquery('english', ?)", params[:q], params[:q])
268       end
269
270       # Add any date filter
271       if params[:from]
272         begin
273           from = Time.parse(params[:from]).utc
274         rescue ArgumentError
275           raise OSM::APIBadUserInput, "Date #{params[:from]} is in a wrong format"
276         end
277
278         begin
279           to = if params[:to]
280                  Time.parse(params[:to]).utc
281                else
282                  Time.now.utc
283                end
284         rescue ArgumentError
285           raise OSM::APIBadUserInput, "Date #{params[:to]} is in a wrong format"
286         end
287
288         @notes = if params[:sort] == "updated_at"
289                    @notes.where(:updated_at => from..to)
290                  else
291                    @notes.where(:created_at => from..to)
292                  end
293       end
294
295       # Choose the sort order
296       @notes = if params[:sort] == "created_at"
297                  if params[:order] == "oldest"
298                    @notes.order("created_at ASC")
299                  else
300                    @notes.order("created_at DESC")
301                  end
302                else
303                  if params[:order] == "oldest"
304                    @notes.order("updated_at ASC")
305                  else
306                    @notes.order("updated_at DESC")
307                  end
308                end
309
310       # Find the notes we want to return
311       @notes = @notes.distinct.limit(result_limit).preload(:comments)
312
313       # Render the result
314       respond_to do |format|
315         format.rss { render :action => :index }
316         format.xml { render :action => :index }
317         format.json { render :action => :index }
318         format.gpx { render :action => :index }
319       end
320     end
321
322     private
323
324     #------------------------------------------------------------
325     # utility functions below.
326     #------------------------------------------------------------
327
328     ##
329     # Get the maximum number of results to return
330     def result_limit
331       if params[:limit]
332         if params[:limit].to_i.positive? && params[:limit].to_i <= Settings.max_note_query_limit
333           params[:limit].to_i
334         else
335           raise OSM::APIBadUserInput, "Note limit must be between 1 and #{Settings.max_note_query_limit}"
336         end
337       else
338         Settings.default_note_query_limit
339       end
340     end
341
342     ##
343     # Generate a condition to choose which notes we want based
344     # on their status and the user's request parameters
345     def closed_condition(notes)
346       closed_since = if params[:closed]
347                        params[:closed].to_i.days
348                      else
349                        Note::DEFAULT_FRESHLY_CLOSED_LIMIT
350                      end
351
352       if closed_since.negative?
353         notes.where.not(:status => "hidden")
354       elsif closed_since.positive?
355         notes.where(:status => "open")
356              .or(notes.where(:status => "closed")
357                       .where(notes.arel_table[:closed_at].gt(Time.now.utc - closed_since)))
358       else
359         notes.where(:status => "open")
360       end
361     end
362
363     ##
364     # Generate a condition to choose which notes we want based
365     # on the user's bounding box request parameters
366     def bbox_condition(notes)
367       if params[:bbox]
368         bbox = BoundingBox.from_bbox_params(params)
369
370         bbox.check_boundaries
371         bbox.check_size(Settings.max_note_request_area)
372
373         @min_lon = bbox.min_lon
374         @min_lat = bbox.min_lat
375         @max_lon = bbox.max_lon
376         @max_lat = bbox.max_lat
377
378         notes.bbox(bbox)
379       else
380         notes
381       end
382     end
383
384     ##
385     # Add a comment to a note
386     def add_comment(note, text, event, notify: true)
387       attributes = { :visible => true, :event => event, :body => text }
388
389       author = current_user if scope_enabled?(:write_notes)
390
391       if author
392         attributes[:author_id] = author.id
393       else
394         attributes[:author_ip] = request.remote_ip
395       end
396
397       comment = note.comments.create!(attributes)
398
399       if notify
400         note.subscribers.visible.each do |user|
401           UserMailer.note_comment_notification(comment, user).deliver_later if current_user != user
402         end
403       end
404
405       NoteSubscription.find_or_create_by(:note => note, :user => current_user) if current_user
406     end
407   end
408 end