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