Create api changeset download resource

This commit is contained in:
Anton Khorev 2025-02-20 03:41:04 +03:00
parent 1f1a20e539
commit 49fac49f9d
9 changed files with 166 additions and 143 deletions

View file

@ -10,7 +10,7 @@ class ApiAbility
can [:read, :feed, :search], Note
can :create, Note unless user
can [:read, :download], Changeset
can :read, Changeset
can :read, ChangesetComment
can :read, Tracepoint
can :read, User

View file

@ -0,0 +1,61 @@
module Api
module Changesets
class DownloadsController < ApiController
authorize_resource :changeset
before_action :set_request_formats
##
# download the changeset as an osmChange document.
#
# to make it easier to revert diffs it would be better if the osmChange
# format were reversible, i.e: contained both old and new versions of
# modified elements. but it doesn't at the moment...
#
# this method cannot order the database changes fully (i.e: timestamp and
# version number may be too coarse) so the resulting diff may not apply
# to a different database. however since changesets are not atomic this
# behaviour cannot be guaranteed anyway and is the result of a design
# choice.
def show
changeset = Changeset.find(params[:changeset_id])
# get all the elements in the changeset which haven't been redacted
# and stick them in a big array.
elements = [changeset.old_nodes.unredacted,
changeset.old_ways.unredacted,
changeset.old_relations.unredacted].flatten
# sort the elements by timestamp and version number, as this is the
# almost sensible ordering available. this would be much nicer if
# global (SVN-style) versioning were used - then that would be
# unambiguous.
elements.sort_by! { |e| [e.timestamp, e.version] }
# generate an output element for each operation. note: we avoid looking
# at the history because it is simpler - but it would be more correct to
# check these assertions.
@created = []
@modified = []
@deleted = []
elements.each do |elt|
if elt.version == 1
# first version, so it must be newly-created.
@created << elt
elsif elt.visible
# must be a modify
@modified << elt
else
# if the element isn't visible then it must have been deleted
@deleted << elt
end
end
respond_to do |format|
format.xml
end
end
end
end
end

View file

@ -130,58 +130,6 @@ module Api
end
end
##
# download the changeset as an osmChange document.
#
# to make it easier to revert diffs it would be better if the osmChange
# format were reversible, i.e: contained both old and new versions of
# modified elements. but it doesn't at the moment...
#
# this method cannot order the database changes fully (i.e: timestamp and
# version number may be too coarse) so the resulting diff may not apply
# to a different database. however since changesets are not atomic this
# behaviour cannot be guaranteed anyway and is the result of a design
# choice.
def download
changeset = Changeset.find(params[:id])
# get all the elements in the changeset which haven't been redacted
# and stick them in a big array.
elements = [changeset.old_nodes.unredacted,
changeset.old_ways.unredacted,
changeset.old_relations.unredacted].flatten
# sort the elements by timestamp and version number, as this is the
# almost sensible ordering available. this would be much nicer if
# global (SVN-style) versioning were used - then that would be
# unambiguous.
elements.sort_by! { |e| [e.timestamp, e.version] }
# generate an output element for each operation. note: we avoid looking
# at the history because it is simpler - but it would be more correct to
# check these assertions.
@created = []
@modified = []
@deleted = []
elements.each do |elt|
if elt.version == 1
# first version, so it must be newly-created.
@created << elt
elsif elt.visible
# must be a modify
@modified << elt
else
# if the element isn't visible then it must have been deleted
@deleted << elt
end
end
respond_to do |format|
format.xml
end
end
##
# updates a changeset's tags. none of the changeset's attributes are
# user-modifiable, so they will be ignored.

View file

@ -3,17 +3,17 @@ xml.instruct! :xml, :version => "1.0"
xml.osmChange(OSM::API.new.xml_root_attributes) do |osm|
@created.each do |elt|
osm.create do |create|
create << render(elt)
create << render(:partial => "api/#{elt.to_partial_path}", :object => elt)
end
end
@modified.each do |elt|
osm.modify do |modify|
modify << render(elt)
modify << render(:partial => "api/#{elt.to_partial_path}", :object => elt)
end
end
@deleted.each do |elt|
osm.delete do |delete|
delete << render(elt)
delete << render(:partial => "api/#{elt.to_partial_path}", :object => elt)
end
end
end

View file

@ -21,7 +21,7 @@ atom_feed(:language => I18n.locale, :schema_date => 2009,
:href => api_changeset_url(changeset, :only_path => false),
:type => "application/osm+xml"
entry.link :rel => "alternate",
:href => changeset_download_url(changeset, :only_path => false),
:href => api_changeset_download_url(changeset, :only_path => false),
:type => "application/osmChange+xml"
if !changeset.tags.empty? && changeset.tags.key?("comment")

View file

@ -107,7 +107,7 @@
<div class='secondary-actions'>
<%= link_to t(".changesetxml"), api_changeset_path(@changeset) %>
&middot;
<%= link_to(t(".osmchangexml"), :controller => "api/changesets", :action => "download") %>
<%= link_to t(".osmchangexml"), api_changeset_download_path(@changeset) %>
</div>
<% if @next_by_user || @prev_by_user %>

View file

@ -18,7 +18,6 @@ OpenStreetMap::Application.routes.draw do
get "permissions" => "permissions#show"
post "changeset/:id/upload" => "changesets#upload", :as => :changeset_upload, :id => /\d+/
get "changeset/:id/download" => "changesets#download", :as => :changeset_download, :id => /\d+/
post "changeset/:id/subscribe" => "changesets#subscribe", :as => :api_changeset_subscribe, :id => /\d+/
post "changeset/:id/unsubscribe" => "changesets#unsubscribe", :as => :api_changeset_unsubscribe, :id => /\d+/
put "changeset/:id/close" => "changesets#close", :as => :changeset_close, :id => /\d+/
@ -29,7 +28,9 @@ OpenStreetMap::Application.routes.draw do
namespace :api, :path => "api/0.6" do
resources :changesets, :only => [:index, :create]
resources :changesets, :path => "changeset", :id => /\d+/, :only => [:show, :update]
resources :changesets, :path => "changeset", :id => /\d+/, :only => [:show, :update] do
resource :download, :module => :changesets, :only => :show
end
put "changeset/create" => "changesets#create", :as => nil
resources :changeset_comments, :only => :index

View file

@ -0,0 +1,93 @@
require "test_helper"
module Api
module Changesets
class DownloadsControllerTest < ActionDispatch::IntegrationTest
##
# test all routes which lead to this controller
def test_routes
assert_routing(
{ :path => "/api/0.6/changeset/1/download", :method => :get },
{ :controller => "api/changesets/downloads", :action => "show", :changeset_id => "1" }
)
end
def test_show
changeset = create(:changeset)
node = create(:node, :with_history, :version => 1, :changeset => changeset)
tag = create(:old_node_tag, :old_node => node.old_nodes.find_by(:version => 1))
node2 = create(:node, :with_history, :version => 1, :changeset => changeset)
_node3 = create(:node, :with_history, :deleted, :version => 1, :changeset => changeset)
_relation = create(:relation, :with_history, :version => 1, :changeset => changeset)
_relation2 = create(:relation, :with_history, :deleted, :version => 1, :changeset => changeset)
get api_changeset_download_path(changeset)
assert_response :success
# FIXME: needs more assert_select tests
assert_select "osmChange[version='#{Settings.api_version}'][generator='#{Settings.generator}']" do
assert_select "create", :count => 5
assert_select "create>node[id='#{node.id}'][visible='#{node.visible?}'][version='#{node.version}']" do
assert_select "tag[k='#{tag.k}'][v='#{tag.v}']"
end
assert_select "create>node[id='#{node2.id}']"
end
end
def test_show_should_sort_by_timestamp
changeset = create(:changeset)
node1 = create(:old_node, :version => 2, :timestamp => "2020-02-01", :changeset => changeset)
node0 = create(:old_node, :version => 2, :timestamp => "2020-01-01", :changeset => changeset)
get api_changeset_download_path(changeset)
assert_response :success
assert_dom "modify", :count => 2 do |modify|
assert_dom modify[0], ">node", :count => 1 do |node|
assert_dom node, ">@id", node0.node_id.to_s
end
assert_dom modify[1], ">node", :count => 1 do |node|
assert_dom node, ">@id", node1.node_id.to_s
end
end
end
def test_show_should_sort_by_version
changeset = create(:changeset)
node1 = create(:old_node, :version => 3, :timestamp => "2020-01-01", :changeset => changeset)
node0 = create(:old_node, :version => 2, :timestamp => "2020-01-01", :changeset => changeset)
get api_changeset_download_path(changeset)
assert_response :success
assert_dom "modify", :count => 2 do |modify|
assert_dom modify[0], ">node", :count => 1 do |node|
assert_dom node, ">@id", node0.node_id.to_s
end
assert_dom modify[1], ">node", :count => 1 do |node|
assert_dom node, ">@id", node1.node_id.to_s
end
end
end
##
# check that the changeset download for a changeset with a redacted
# element in it doesn't contain that element.
def test_show_redacted
changeset = create(:changeset)
node = create(:node, :with_history, :version => 2, :changeset => changeset)
node_v1 = node.old_nodes.find_by(:version => 1)
node_v1.redact!(create(:redaction))
get api_changeset_download_path(changeset)
assert_response :success
assert_select "osmChange", 1
# this changeset contains the node in versions 1 & 2, but 1 should
# be hidden.
assert_select "osmChange node[id='#{node.id}']", 1
assert_select "osmChange node[id='#{node.id}'][version='1']", 0
end
end
end
end

View file

@ -33,10 +33,6 @@ module Api
{ :path => "/api/0.6/changeset/1/upload", :method => :post },
{ :controller => "api/changesets", :action => "upload", :id => "1" }
)
assert_routing(
{ :path => "/api/0.6/changeset/1/download", :method => :get },
{ :controller => "api/changesets", :action => "download", :id => "1" }
)
assert_routing(
{ :path => "/api/0.6/changeset/1/subscribe", :method => :post },
{ :controller => "api/changesets", :action => "subscribe", :id => "1" }
@ -2184,7 +2180,7 @@ module Api
assert_response :success,
"can't upload multiple versions of an element in a diff: #{@response.body}"
get changeset_download_path(changeset_id)
get api_changeset_download_path(changeset_id)
assert_response :success
assert_select "osmChange", 1
@ -2242,7 +2238,7 @@ module Api
assert_response :success,
"can't upload a diff from JOSM: #{@response.body}"
get changeset_download_path(changeset_id)
get api_changeset_download_path(changeset_id)
assert_response :success
assert_select "osmChange", 1
@ -2297,7 +2293,7 @@ module Api
assert_response :success,
"can't upload multiple versions of an element in a diff: #{@response.body}"
get changeset_download_path(changeset_id)
get api_changeset_download_path(changeset_id)
assert_response :success
assert_select "osmChange", 1
@ -2310,63 +2306,6 @@ module Api
assert_select "osmChange>modify>way", 1
end
def test_changeset_download
changeset = create(:changeset)
node = create(:node, :with_history, :version => 1, :changeset => changeset)
tag = create(:old_node_tag, :old_node => node.old_nodes.find_by(:version => 1))
node2 = create(:node, :with_history, :version => 1, :changeset => changeset)
_node3 = create(:node, :with_history, :deleted, :version => 1, :changeset => changeset)
_relation = create(:relation, :with_history, :version => 1, :changeset => changeset)
_relation2 = create(:relation, :with_history, :deleted, :version => 1, :changeset => changeset)
get changeset_download_path(changeset)
assert_response :success
# FIXME: needs more assert_select tests
assert_select "osmChange[version='#{Settings.api_version}'][generator='#{Settings.generator}']" do
assert_select "create", :count => 5
assert_select "create>node[id='#{node.id}'][visible='#{node.visible?}'][version='#{node.version}']" do
assert_select "tag[k='#{tag.k}'][v='#{tag.v}']"
end
assert_select "create>node[id='#{node2.id}']"
end
end
test "sorts downloaded elements by timestamp" do
changeset = create(:changeset)
node1 = create(:old_node, :version => 2, :timestamp => "2020-02-01", :changeset => changeset)
node0 = create(:old_node, :version => 2, :timestamp => "2020-01-01", :changeset => changeset)
get changeset_download_path(changeset)
assert_response :success
assert_dom "modify", :count => 2 do |modify|
assert_dom modify[0], ">node", :count => 1 do |node|
assert_dom node, ">@id", node0.node_id.to_s
end
assert_dom modify[1], ">node", :count => 1 do |node|
assert_dom node, ">@id", node1.node_id.to_s
end
end
end
test "sorts downloaded elements by version" do
changeset = create(:changeset)
node1 = create(:old_node, :version => 3, :timestamp => "2020-01-01", :changeset => changeset)
node0 = create(:old_node, :version => 2, :timestamp => "2020-01-01", :changeset => changeset)
get changeset_download_path(changeset)
assert_response :success
assert_dom "modify", :count => 2 do |modify|
assert_dom modify[0], ">node", :count => 1 do |node|
assert_dom node, ">@id", node0.node_id.to_s
end
assert_dom modify[1], ">node", :count => 1 do |node|
assert_dom node, ">@id", node1.node_id.to_s
end
end
end
##
# check that the bounding box of a changeset gets updated correctly
# FIXME: This should really be moded to a integration test due to the with_controller
@ -2564,25 +2503,6 @@ module Api
"element limit.")
end
##
# check that the changeset download for a changeset with a redacted
# element in it doesn't contain that element.
def test_diff_download_redacted
changeset = create(:changeset)
node = create(:node, :with_history, :version => 2, :changeset => changeset)
node_v1 = node.old_nodes.find_by(:version => 1)
node_v1.redact!(create(:redaction))
get changeset_download_path(changeset)
assert_response :success
assert_select "osmChange", 1
# this changeset contains the node in versions 1 & 2, but 1 should
# be hidden.
assert_select "osmChange node[id='#{node.id}']", 1
assert_select "osmChange node[id='#{node.id}'][version='1']", 0
end
##
# test subscribe success
def test_subscribe_success