can :create, Note unless user
can [:read, :download], Changeset
+ can :read, ChangesetComment
can :read, Tracepoint
can :read, User
can :read, [Node, Way, Relation, OldNode, OldWay, OldRelation]
module Api
class ChangesetCommentsController < ApiController
- before_action :check_api_writable
- before_action :authorize
+ include QueryMethods
+
+ before_action :check_api_writable, :except => [:index]
+ before_action :authorize, :except => [:index]
authorize_resource
before_action :set_request_formats
+ ##
+ # show all comments or search for a subset
+ def index
+ @comments = ChangesetComment.includes(:author).where(:visible => true).order("created_at DESC")
+ @comments = query_conditions_time(@comments)
+ @comments = query_conditions_user(@comments, :author)
+ @comments = query_limit(@comments)
+ end
+
##
# Add a comment to a changeset
def create
module Api
class ChangesetsController < ApiController
+ include QueryMethods
+
before_action :check_api_writable, :only => [:create, :update, :upload, :subscribe, :unsubscribe]
before_action :setup_user_auth, :only => [:show]
before_action :authorize, :only => [:create, :update, :upload, :close, :subscribe, :unsubscribe]
changesets = conditions_bbox(changesets, bbox)
changesets = conditions_user(changesets, params["user"], params["display_name"])
changesets = conditions_time(changesets, params["time"])
- changesets = conditions_from_to(changesets, params["from"], params["to"])
+ changesets = query_conditions_time(changesets)
changesets = conditions_open(changesets, params["open"])
changesets = conditions_closed(changesets, params["closed"])
changesets = conditions_ids(changesets, params["changesets"])
end
# limit the result
- changesets = changesets.limit(result_limit)
+ changesets = query_limit(changesets)
# preload users, tags and comments, and render result
@changesets = changesets.preload(:user, :changeset_tags, :comments)
raise OSM::APIBadUserInput, e.message.to_s
end
- ##
- # restrict changesets to those opened during a particular time period
- # works similar to from..to of notes controller, including the requirement of 'from' when specifying 'to'
- def conditions_from_to(changesets, from, to)
- if from
- begin
- from = Time.parse(from).utc
- rescue ArgumentError
- raise OSM::APIBadUserInput, "Date #{from} is in a wrong format"
- end
-
- begin
- to = if to
- Time.parse(to).utc
- else
- Time.now.utc
- end
- rescue ArgumentError
- raise OSM::APIBadUserInput, "Date #{to} is in a wrong format"
- end
-
- changesets.where(:created_at => from..to)
- else
- changesets
- end
- end
-
##
# return changesets which are open (haven't been closed yet)
# we do this by seeing if the 'closed at' time is in the future. Also if we've
changesets.where(:id => ids)
end
end
-
- ##
- # Get the maximum number of results to return
- def result_limit
- if params[:limit]
- if params[:limit].to_i.positive? && params[:limit].to_i <= Settings.max_changeset_query_limit
- params[:limit].to_i
- else
- raise OSM::APIBadUserInput, "Changeset limit must be between 1 and #{Settings.max_changeset_query_limit}"
- end
- else
- Settings.default_changeset_query_limit
- end
- end
end
end
module Api
class NotesController < ApiController
+ include QueryMethods
+
before_action :check_api_writable, :only => [:create, :comment, :close, :reopen, :destroy]
before_action :setup_user_auth, :only => [:create, :show]
before_action :authorize, :only => [:close, :reopen, :destroy, :comment]
@max_lat = bbox.max_lat
# Find the notes we want to return
- @notes = notes.bbox(bbox).order("updated_at DESC").limit(result_limit).preload(:comments)
+ notes = notes.bbox(bbox).order("updated_at DESC")
+ notes = query_limit(notes)
+ @notes = notes.preload(:comments)
# Render the result
respond_to do |format|
# Find the comments we want to return
@comments = NoteComment.where(:note => notes)
- .order(:created_at => :desc).limit(result_limit)
- .preload(:author, :note => { :comments => :author })
+ .order(:created_at => :desc)
+ @comments = query_limit(@comments)
+ @comments = @comments.preload(:author, :note => { :comments => :author })
# Render the result
respond_to do |format|
@notes = bbox_condition(@notes)
# Add any user filter
- if params[:display_name] || params[:user]
- if params[:display_name]
- @user = User.find_by(:display_name => params[:display_name])
-
- raise OSM::APIBadUserInput, "User #{params[:display_name]} not known" unless @user
- else
- @user = User.find_by(:id => params[:user])
-
- raise OSM::APIBadUserInput, "User #{params[:user]} not known" unless @user
- end
-
- @notes = @notes.joins(:comments).where(:note_comments => { :author_id => @user })
- end
+ user = query_conditions_user_value
+ @notes = @notes.joins(:comments).where(:note_comments => { :author_id => user }) if user
# Add any text filter
if params[:q]
end
# Add any date filter
- if params[:from]
- begin
- from = Time.parse(params[:from]).utc
- rescue ArgumentError
- raise OSM::APIBadUserInput, "Date #{params[:from]} is in a wrong format"
- end
-
- begin
- to = if params[:to]
- Time.parse(params[:to]).utc
- else
- Time.now.utc
- end
- rescue ArgumentError
- raise OSM::APIBadUserInput, "Date #{params[:to]} is in a wrong format"
- end
-
- @notes = if params[:sort] == "updated_at"
- @notes.where(:updated_at => from..to)
- else
- @notes.where(:created_at => from..to)
- end
- end
+ time_filter_property = if params[:sort] == "updated_at"
+ :updated_at
+ else
+ :created_at
+ end
+ @notes = query_conditions_time(@notes, time_filter_property)
# Choose the sort order
@notes = if params[:sort] == "created_at"
end
# Find the notes we want to return
- @notes = @notes.distinct.limit(result_limit).preload(:comments)
+ @notes = query_limit(@notes.distinct)
+ @notes = @notes.preload(:comments)
# Render the result
respond_to do |format|
# utility functions below.
#------------------------------------------------------------
- ##
- # Get the maximum number of results to return
- def result_limit
- if params[:limit]
- if params[:limit].to_i.positive? && params[:limit].to_i <= Settings.max_note_query_limit
- params[:limit].to_i
- else
- raise OSM::APIBadUserInput, "Note limit must be between 1 and #{Settings.max_note_query_limit}"
- end
- else
- Settings.default_note_query_limit
- end
- end
-
##
# Generate a condition to choose which notes we want based
# on their status and the user's request parameters
module ChangesetComments
class FeedsController < ApplicationController
+ include QueryMethods
+
before_action :authorize_web
before_action :set_locale
changeset = Changeset.find(changeset_id)
# Return comments for this changeset only
- @comments = changeset.comments.includes(:author, :changeset).reverse_order.limit(comments_limit)
+ @comments = changeset.comments.includes(:author, :changeset).reverse_order
+ @comments = query_limit(@comments)
else
# Return comments
- @comments = ChangesetComment.includes(:author, :changeset).where(:visible => true).order("created_at DESC").limit(comments_limit).preload(:changeset)
+ @comments = ChangesetComment.includes(:author, :changeset).where(:visible => true).order("created_at DESC")
+ @comments = query_limit(@comments)
+ @comments = @comments.preload(:changeset)
end
# Render the result
rescue OSM::APIBadUserInput
head :bad_request
end
-
- private
-
- ##
- # Get the maximum number of comments to return
- def comments_limit
- if params[:limit]
- if params[:limit].to_i.positive? && params[:limit].to_i <= 10000
- params[:limit].to_i
- else
- raise OSM::APIBadUserInput, "Comments limit must be between 1 and 10000"
- end
- else
- 100
- end
- end
end
end
--- /dev/null
+module QueryMethods
+ extend ActiveSupport::Concern
+
+ private
+
+ ##
+ # Filter the resulting items by user
+ def query_conditions_user(items, filter_property)
+ user = query_conditions_user_value
+ items = items.where(filter_property => user) if user
+ items
+ end
+
+ ##
+ # Get user value for query filtering by user
+ # Raises OSM::APIBadUserInput if user not found like notes api does, changesets api raises OSM::APINotFoundError instead
+ def query_conditions_user_value
+ if params[:display_name] || params[:user]
+ if params[:display_name]
+ user = User.find_by(:display_name => params[:display_name])
+
+ raise OSM::APIBadUserInput, "User #{params[:display_name]} not known" unless user
+ else
+ user = User.find_by(:id => params[:user])
+
+ raise OSM::APIBadUserInput, "User #{params[:user]} not known" unless user
+ end
+
+ user
+ end
+ end
+
+ ##
+ # Restrict the resulting items to those created during a particular time period
+ # Using 'to' requires specifying 'from' as well for historical reasons
+ def query_conditions_time(items, filter_property = :created_at)
+ interval = query_conditions_time_value
+
+ if interval
+ items.where(filter_property => interval)
+ else
+ items
+ end
+ end
+
+ ##
+ # Get query time interval from request parameters or nil
+ def query_conditions_time_value
+ if params[:from]
+ begin
+ from = Time.parse(params[:from]).utc
+ rescue ArgumentError
+ raise OSM::APIBadUserInput, "Date #{params[:from]} is in a wrong format"
+ end
+
+ begin
+ to = if params[:to]
+ Time.parse(params[:to]).utc
+ else
+ Time.now.utc
+ end
+ rescue ArgumentError
+ raise OSM::APIBadUserInput, "Date #{params[:to]} is in a wrong format"
+ end
+
+ from..to
+ end
+ end
+
+ ##
+ # Limit the result according to request parameters and settings
+ def query_limit(items)
+ items.limit(query_limit_value)
+ end
+
+ ##
+ # Get query limit value from request parameters and settings
+ def query_limit_value
+ name = controller_path.sub(%r{^api/}, "").tr("/", "_").singularize
+ max_limit = Settings["max_#{name}_query_limit"]
+ default_limit = Settings["default_#{name}_query_limit"]
+ if params[:limit]
+ if params[:limit].to_i.positive? && params[:limit].to_i <= max_limit
+ params[:limit].to_i
+ else
+ raise OSM::APIBadUserInput, "#{controller_name.classify} limit must be between 1 and #{max_limit}"
+ end
+ else
+ default_limit
+ end
+ end
+end
--- /dev/null
+json.id changeset_comment.id
+json.visible changeset_comment.visible
+json.date changeset_comment.created_at.xmlschema
+if changeset_comment.author.data_public?
+ json.uid changeset_comment.author.id
+ json.user changeset_comment.author.display_name
+end
+json.text changeset_comment.body
--- /dev/null
+cattrs = {
+ "id" => changeset_comment.id,
+ "date" => changeset_comment.created_at.xmlschema,
+ "visible" => changeset_comment.visible
+}
+if changeset_comment.author.data_public?
+ cattrs["uid"] = changeset_comment.author.id
+ cattrs["user"] = changeset_comment.author.display_name
+end
+xml.comment(cattrs) do |comment_xml_node|
+ comment_xml_node.text(changeset_comment.body)
+end
--- /dev/null
+json.partial! "api/root_attributes"
+
+json.comments(@comments) do |comment|
+ json.partial! comment
+end
--- /dev/null
+xml.instruct! :xml, :version => "1.0"
+
+xml.osm(OSM::API.new.xml_root_attributes) do |osm|
+ @comments.includes(:author).each do |comment|
+ osm << render(comment)
+ end
+end
if @comments
json.comments(@comments) do |comment|
- json.id comment.id
- json.visible comment.visible
- json.date comment.created_at.xmlschema
- if comment.author.data_public?
- json.uid comment.author.id
- json.user comment.author.display_name
- end
- json.text comment.body
+ json.partial! comment
end
end
if @comments
changeset_xml_node.discussion do |discussion_xml_node|
@comments.each do |comment|
- cattrs = {
- "id" => comment.id,
- "date" => comment.created_at.xmlschema,
- "visible" => comment.visible
- }
- if comment.author.data_public?
- cattrs["uid"] = comment.author.id
- cattrs["user"] = comment.author.display_name
- end
- discussion_xml_node.comment(cattrs) do |comment_xml_node|
- comment_xml_node.text(comment.body)
- end
+ discussion_xml_node << render(comment)
end
end
end
end
namespace :api, :path => "api/0.6" do
+ resources :changeset_comments, :only => :index
+
resources :nodes, :only => [:index, :create]
resources :nodes, :path => "node", :id => /\d+/, :only => [:show, :update, :destroy] do
scope :module => :nodes do
default_changeset_query_limit: 100
# Maximum limit on the number of changesets returned by the changeset query api method
max_changeset_query_limit: 100
+# Default limit on the number of changeset comments returned by the api
+default_changeset_comment_query_limit: 100
+# Maximum limit on the number of changesets comments returned by the api
+max_changeset_comment_query_limit: 10000
+# Default limit on the number of changeset comments in feeds
+default_changeset_comments_feed_query_limit: 100
+# Maximum limit on the number of changesets comments in feeds
+max_changeset_comments_feed_query_limit: 10000
# Maximum number of nodes that will be returned by the api in a map request
max_number_of_nodes: 50000
# Maximum number of nodes that can be in a way (checked on save)
##
# test all routes which lead to this controller
def test_routes
+ assert_routing(
+ { :path => "/api/0.6/changeset_comments", :method => :get },
+ { :controller => "api/changeset_comments", :action => "index" }
+ )
+ assert_routing(
+ { :path => "/api/0.6/changeset_comments.json", :method => :get },
+ { :controller => "api/changeset_comments", :action => "index", :format => "json" }
+ )
assert_routing(
{ :path => "/api/0.6/changeset/1/comment", :method => :post },
{ :controller => "api/changeset_comments", :action => "create", :id => "1" }
)
end
+ def test_index
+ user1 = create(:user)
+ user2 = create(:user)
+ changeset1 = create(:changeset, :closed, :user => user2)
+ comment11 = create(:changeset_comment, :changeset => changeset1, :author => user1, :created_at => "2023-01-01", :body => "changeset 1 question")
+ comment12 = create(:changeset_comment, :changeset => changeset1, :author => user2, :created_at => "2023-02-01", :body => "changeset 1 answer")
+ changeset2 = create(:changeset, :closed, :user => user1)
+ comment21 = create(:changeset_comment, :changeset => changeset2, :author => user1, :created_at => "2023-03-01", :body => "changeset 2 note")
+ comment22 = create(:changeset_comment, :changeset => changeset2, :author => user1, :created_at => "2023-04-01", :body => "changeset 2 extra note")
+ comment23 = create(:changeset_comment, :changeset => changeset2, :author => user2, :created_at => "2023-05-01", :body => "changeset 2 review")
+
+ get api_changeset_comments_path
+ assert_response :success
+ assert_comments_in_order [comment23, comment22, comment21, comment12, comment11]
+
+ get api_changeset_comments_path(:limit => 3)
+ assert_response :success
+ assert_comments_in_order [comment23, comment22, comment21]
+
+ get api_changeset_comments_path(:from => "2023-03-15T00:00:00Z")
+ assert_response :success
+ assert_comments_in_order [comment23, comment22]
+
+ get api_changeset_comments_path(:from => "2023-01-15T00:00:00Z", :to => "2023-04-15T00:00:00Z")
+ assert_response :success
+ assert_comments_in_order [comment22, comment21, comment12]
+
+ get api_changeset_comments_path(:user => user1.id)
+ assert_response :success
+ assert_comments_in_order [comment22, comment21, comment11]
+
+ get api_changeset_comments_path(:from => "2023-03-15T00:00:00Z", :format => "json")
+ assert_response :success
+ js = ActiveSupport::JSON.decode(@response.body)
+ assert_not_nil js
+ assert_equal 2, js["comments"].count
+ assert_equal comment23.id, js["comments"][0]["id"]
+ assert_equal comment22.id, js["comments"][1]["id"]
+ end
+
def test_create_by_unauthorized
assert_no_difference "ChangesetComment.count" do
post changeset_comment_path(create(:changeset, :closed), :text => "This is a comment")
assert_response :success
assert comment.reload.visible
end
+
+ private
+
+ ##
+ # check that certain comments exist in the output in the specified order
+ def assert_comments_in_order(comments)
+ assert_dom "osm > comment", comments.size do |dom_comments|
+ comments.zip(dom_comments).each do |comment, dom_comment|
+ assert_dom dom_comment, "> @id", comment.id.to_s
+ assert_dom dom_comment, "> @uid", comment.author.id.to_s
+ assert_dom dom_comment, "> @user", comment.author.display_name
+ assert_dom dom_comment, "> text", comment.body
+ end
+ end
+ end
end
end
assert_select "gpx", :count => 1 do
assert_select "wpt", :count => 1
end
+
+ user2 = create(:user)
+ get search_api_notes_path(:user => user2.id, :format => "xml")
+ assert_response :success
+ assert_equal "application/xml", @response.media_type
+ assert_select "osm", :count => 1 do
+ assert_select "note", :count => 0
+ end
+ end
+
+ def test_search_by_time_success
+ note1 = create(:note, :created_at => "2020-02-01T00:00:00Z", :updated_at => "2020-04-01T00:00:00Z")
+ note2 = create(:note, :created_at => "2020-03-01T00:00:00Z", :updated_at => "2020-05-01T00:00:00Z")
+
+ get search_api_notes_path(:from => "2020-02-15T00:00:00Z", :to => "2020-04-15T00:00:00Z", :format => "xml")
+ assert_response :success
+ assert_equal "application/xml", @response.media_type
+ assert_select "osm", :count => 1 do
+ assert_select "note", :count => 1 do
+ assert_select "id", note2.id.to_s
+ end
+ end
+
+ get search_api_notes_path(:from => "2020-02-15T00:00:00Z", :to => "2020-04-15T00:00:00Z", :sort => "updated_at", :format => "xml")
+ assert_response :success
+ assert_equal "application/xml", @response.media_type
+ assert_select "osm", :count => 1 do
+ assert_select "note", :count => 1 do
+ assert_select "id", note1.id.to_s
+ end
+ end
end
def test_search_by_bbox_success