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