From 2bc2bdd7f2d1d8b31309073db35c2d16a7744312 Mon Sep 17 00:00:00 2001 From: Rub21 Date: Thu, 3 Sep 2026 12:55:31 +0200 Subject: [PATCH] Add a page to change the visibility of legacy traces --- .rubocop_todo.yml | 1 + .../traces/legacy_visibilities_controller.rb | 77 ++++++++ app/helpers/trace_helper.rb | 16 +- .../traces/legacy_visibilities/edit.html.erb | 88 +++++++++ config/locales/en.yml | 28 +++ config/routes.rb | 1 + .../legacy_visibilities_controller_test.rb | 175 ++++++++++++++++++ 7 files changed, 385 insertions(+), 1 deletion(-) create mode 100644 app/controllers/traces/legacy_visibilities_controller.rb create mode 100644 app/views/traces/legacy_visibilities/edit.html.erb create mode 100644 test/controllers/traces/legacy_visibilities_controller_test.rb diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 3c4f8c675..319622009 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -14,6 +14,7 @@ FactoryBot/ExcessiveCreateList: - 'test/controllers/changesets_controller_test.rb' - 'test/controllers/diary_entries_controller_test.rb' - 'test/controllers/notes_controller_test.rb' + - 'test/controllers/traces/legacy_visibilities_controller_test.rb' - 'test/controllers/traces_controller_test.rb' - 'test/controllers/user_blocks_controller_test.rb' - 'test/controllers/users/issued_blocks_controller_test.rb' diff --git a/app/controllers/traces/legacy_visibilities_controller.rb b/app/controllers/traces/legacy_visibilities_controller.rb new file mode 100644 index 000000000..dc97f436e --- /dev/null +++ b/app/controllers/traces/legacy_visibilities_controller.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +module Traces + # Bulk change of traces that still use a legacy visibility + class LegacyVisibilitiesController < ApplicationController + include PaginationMethods + + layout :site_layout + + before_action :authorize_web + before_action :set_locale + before_action :check_database_readable + + authorize_resource :class => Trace + + before_action :check_database_writable, :only => :update + before_action :offline_warning, :only => :edit + before_action :offline_redirect, :only => :update + + def edit + @title = t ".title" + @params = params.permit(:visibility, :from, :to, :before, :after) + + traces = legacy_traces + @count = traces.count + @traces = get_page_items(traces, :includes => [:user, :tags]) + end + + def update + visibility = params[:new_visibility] + + if Trace::VISIBILITIES.include?(visibility) + # the traces are legacy and the new visibility is a current one, so no validation can fail + # rubocop:disable-next Rails/SkipsModelValidations + count = legacy_traces.update_all(:visibility => visibility) + flash[:notice] = t ".updated", :count => count + else + flash[:error] = t ".invalid_visibility" + end + + redirect_to edit_traces_legacy_visibility_path(:visibility => params[:visibility], + :from => params[:from], + :to => params[:to]) + end + + private + + ## + # the current user's legacy traces, with the optional filters applied + def legacy_traces + traces = current_user.traces.visible.where(:visibility => Trace::LEGACY_VISIBILITIES) + traces = traces.where(:visibility => params[:visibility]) if Trace::LEGACY_VISIBILITIES.include?(params[:visibility]) + + from = filter_date(params[:from]) + to = filter_date(params[:to]) + traces = traces.where(:timestamp => from..) if from + # timestamp is a datetime, so the upper bound is the start of the next day + traces = traces.where(:timestamp => ...(to + 1)) if to + + traces + end + + def filter_date(value) + Date.parse(value) if value.present? + rescue ArgumentError + nil + end + + def offline_warning + flash.now[:warning] = t "traces.offline_warning.message" if Settings.status == "gpx_offline" + end + + def offline_redirect + render :template => "traces/offline" if Settings.status == "gpx_offline" + end + end +end diff --git a/app/helpers/trace_helper.rb b/app/helpers/trace_helper.rb index 2b68b7034..3b580fe5d 100644 --- a/app/helpers/trace_helper.rb +++ b/app/helpers/trace_helper.rb @@ -20,7 +20,21 @@ module TraceHelper end def trace_visibility_options(trace) - trace.selectable_visibilities.map do |visibility| + trace_visibility_select_options(trace.selectable_visibilities) + end + + def trace_visibility_options_for_filter + trace_visibility_select_options(Trace::LEGACY_VISIBILITIES) + end + + def trace_visibility_options_for_update + trace_visibility_select_options(Trace::VISIBILITIES) + end + + private + + def trace_visibility_select_options(visibilities) + visibilities.map do |visibility| [t("traces.visibility.#{visibility}"), visibility] end end diff --git a/app/views/traces/legacy_visibilities/edit.html.erb b/app/views/traces/legacy_visibilities/edit.html.erb new file mode 100644 index 000000000..3cd3fd65f --- /dev/null +++ b/app/views/traces/legacy_visibilities/edit.html.erb @@ -0,0 +1,88 @@ +<% content_for :heading_class, "pb-0" %> +<% content_for :heading do %> +

<%= @title %>

+

<%= t ".description" %>

+ + +<% end %> + +<%= form_tag edit_traces_legacy_visibility_path, :method => :get, :class => "row g-3 align-items-end mb-3" do %> +
+ <%= label_tag :visibility, t(".filter.visibility"), :class => "form-label" %> + <%= select_tag :visibility, + options_for_select([[t(".filter.any"), ""]] + trace_visibility_options_for_filter, params[:visibility]), + :class => "form-select" %> +
+
+ <%= label_tag :from, t(".filter.from"), :class => "form-label" %> + <%= date_field_tag :from, params[:from], :autocomplete => "off", :class => "form-control" %> +
+
+ <%= label_tag :to, t(".filter.to"), :class => "form-label" %> + <%= date_field_tag :to, params[:to], :autocomplete => "off", :class => "form-control" %> +
+
+ <%= submit_tag t(".filter.apply"), :name => nil, :class => "btn btn-secondary w-100" %> +
+<% end %> + +<% if @traces.items.empty? %> +

<%= t ".empty" %>

+<% else %> + <%= form_tag traces_legacy_visibility_path, :method => :patch do %> + <%= hidden_field_tag :visibility, params[:visibility] %> + <%= hidden_field_tag :from, params[:from] %> + <%= hidden_field_tag :to, params[:to] %> + +
+
+ <%= label_tag :new_visibility, :class => "form-label" do %> + <%= t ".new_visibility" %> + <%= t ".no_undo" %> + <% end %> + <%= select_tag :new_visibility, + options_for_select(trace_visibility_options_for_update, params[:new_visibility]), + :class => "form-select" %> +
+
+ +
+
+ + + <% end %> + +
+ + <%= render "traces/page", :traces => @traces, :params => @params, :translation_scope => "application.pagination.traces" %> +<% end %> diff --git a/config/locales/en.yml b/config/locales/en.yml index a301d3a41..54608fa9f 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -3091,6 +3091,34 @@ en: remove_tag_filter: "Remove Tag Filter" destroy: scheduled_for_deletion: "Trace scheduled for deletion" + legacy_visibilities: + edit: + title: "My Legacy GPS Traces" + description: "These are your traces that still use the old private or public visibility. You can change them here to trackable or identifiable. Planned improvements to GPS traces will only cover trackable and identifiable." + filter: + visibility: "Current visibility" + any: "Private and public" + from: "Uploaded from" + to: "Uploaded to" + apply: "Filter" + empty: "No legacy traces match this filter." + new_visibility: "New visibility" + no_undo: "This change cannot be undone." + button: + one: "Change %{count} trace" + other: "Change %{count} traces" + confirmation: + header: "Confirm visibility change" + body: + one: "Change the visibility of %{count} trace? This cannot be undone." + other: "Change the visibility of %{count} traces? This cannot be undone." + cancel: "Cancel" + confirm: "Change visibility" + update: + updated: + one: "Changed the visibility of %{count} trace." + other: "Changed the visibility of %{count} traces." + invalid_visibility: "Please choose a visibility to change to." offline_warning: message: "The GPX file upload system is currently unavailable" offline: diff --git a/config/routes.rb b/config/routes.rb index 60cd94f0a..39511ac2a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -280,6 +280,7 @@ OpenStreetMap::Application.routes.draw do namespace :traces, :path => "" do resource :feed, :path => "(/user/:display_name)/traces(/tag/:tag)/rss", :only => :show, :defaults => { :format => :rss } + resource :legacy_visibility, :path => "/traces/mine/legacy_visibility", :only => [:edit, :update] end end diff --git a/test/controllers/traces/legacy_visibilities_controller_test.rb b/test/controllers/traces/legacy_visibilities_controller_test.rb new file mode 100644 index 000000000..237902431 --- /dev/null +++ b/test/controllers/traces/legacy_visibilities_controller_test.rb @@ -0,0 +1,175 @@ +# frozen_string_literal: true + +require "test_helper" + +module Traces + class LegacyVisibilitiesControllerTest < ActionDispatch::IntegrationTest + ## + # test all routes which lead to this controller + def test_routes + assert_routing( + { :path => "/traces/mine/legacy_visibility/edit", :method => :get }, + { :controller => "traces/legacy_visibilities", :action => "edit" } + ) + assert_routing( + { :path => "/traces/mine/legacy_visibility", :method => :patch }, + { :controller => "traces/legacy_visibilities", :action => "update" } + ) + end + + def test_edit_requires_login + get edit_traces_legacy_visibility_path + assert_redirected_to login_path(:referer => edit_traces_legacy_visibility_path) + end + + def test_edit_links_back_to_the_trace_lists + user = create(:user) + + session_for(user) + get edit_traces_legacy_visibility_path + assert_response :success + assert_select "a[href=?]", traces_path + assert_select "a[href=?]", traces_mine_path + end + + def test_edit_shows_only_own_legacy_traces + user = create(:user) + public_trace = create(:trace, :without_validations, :visibility => "public", :user => user) + private_trace = create(:trace, :without_validations, :visibility => "private", :user => user) + trackable_trace = create(:trace, :visibility => "trackable", :user => user) + identifiable_trace = create(:trace, :visibility => "identifiable", :user => user) + other_users_trace = create(:trace, :without_validations, :visibility => "public") + + session_for(user) + get edit_traces_legacy_visibility_path + assert_response :success + assert_select "table#trace_list tbody tr", :count => 2 + assert_select "table#trace_list tbody tr a", public_trace.name + assert_select "table#trace_list tbody tr a", private_trace.name + assert_select "table#trace_list tbody tr a", :text => trackable_trace.name, :count => 0 + assert_select "table#trace_list tbody tr a", :text => identifiable_trace.name, :count => 0 + assert_select "table#trace_list tbody tr a", :text => other_users_trace.name, :count => 0 + end + + def test_edit_filters_by_visibility_and_date + user = create(:user) + old_public = create(:trace, :without_validations, :visibility => "public", :user => user, :timestamp => Date.new(2015, 6, 1)) + create(:trace, :without_validations, :visibility => "private", :user => user, :timestamp => Date.new(2015, 6, 1)) + create(:trace, :without_validations, :visibility => "public", :user => user, :timestamp => Date.new(2022, 6, 1)) + + session_for(user) + get edit_traces_legacy_visibility_path(:visibility => "public", :to => "2020-01-01") + assert_response :success + assert_select "table#trace_list tbody tr", :count => 1 + assert_select "table#trace_list tbody tr a", old_public.name + end + + def test_edit_date_range_includes_both_ends + user = create(:user) + trace = create(:trace, :without_validations, :visibility => "public", :user => user, :timestamp => Time.utc(2020, 1, 1, 23, 30)) + + session_for(user) + get edit_traces_legacy_visibility_path(:from => "2020-01-01", :to => "2020-01-01") + assert_response :success + assert_select "table#trace_list tbody tr", :count => 1 + assert_select "table#trace_list tbody tr a", trace.name + end + + def test_edit_paged + user = create(:user) + # one trace more than fits on a single page + create_list(:trace, 20, :without_validations, :visibility => "public", :user => user) + newest = create(:trace, :without_validations, :visibility => "public", :user => user) + ids = user.traces.order(:id => :desc).pluck(:id) + + session_for(user) + get edit_traces_legacy_visibility_path + assert_response :success + assert_select "table#trace_list tbody tr", :count => 20 + assert_select "table#trace_list tbody tr a", newest.name + assert_select "a", :text => "Older Traces", :count => 2 + assert_select "a", :text => "Newer Traces", :count => 0 + + get edit_traces_legacy_visibility_path(:before => ids[19]) + assert_response :success + assert_select "table#trace_list tbody tr", :count => 1 + assert_select "a", :text => "Newer Traces", :count => 2 + assert_select "a", :text => "Older Traces", :count => 0 + end + + def test_edit_with_no_legacy_traces + user = create(:user) + create(:trace, :visibility => "trackable", :user => user) + + session_for(user) + get edit_traces_legacy_visibility_path + assert_response :success + assert_select "table#trace_list", :count => 0 + end + + def test_edit_disabled + with_settings(:traces_disabled => true) do + get edit_traces_legacy_visibility_path + assert_response :not_found + end + end + + def test_update_requires_login + patch traces_legacy_visibility_path, :params => { :new_visibility => "identifiable" } + assert_response :forbidden + end + + def test_update_changes_matching_traces_only + user = create(:user) + old_public = create(:trace, :without_validations, :visibility => "public", :user => user, :timestamp => Date.new(2015, 6, 1)) + old_private = create(:trace, :without_validations, :visibility => "private", :user => user, :timestamp => Date.new(2019, 6, 1)) + recent_public = create(:trace, :without_validations, :visibility => "public", :user => user, :timestamp => Date.new(2022, 6, 1)) + other_users_trace = create(:trace, :without_validations, :visibility => "public", :timestamp => Date.new(2015, 6, 1)) + + session_for(user) + patch traces_legacy_visibility_path, :params => { :visibility => "public", :to => "2020-01-01", :new_visibility => "identifiable" } + assert_redirected_to edit_traces_legacy_visibility_path(:visibility => "public", :from => nil, :to => "2020-01-01") + assert_equal "Changed the visibility of 1 trace.", flash[:notice] + + assert_equal "identifiable", old_public.reload.visibility + assert_equal "private", old_private.reload.visibility + assert_equal "public", recent_public.reload.visibility + assert_equal "public", other_users_trace.reload.visibility + end + + def test_update_counts_every_matching_trace + user = create(:user) + create_list(:trace, 3, :without_validations, :visibility => "public", :user => user) + + session_for(user) + patch traces_legacy_visibility_path, :params => { :new_visibility => "trackable" } + assert_equal "Changed the visibility of 3 traces.", flash[:notice] + assert_equal 3, user.traces.where(:visibility => "trackable").count + end + + def test_update_rejects_legacy_visibility + user = create(:user) + trace = create(:trace, :without_validations, :visibility => "public", :user => user) + + session_for(user) + patch traces_legacy_visibility_path, :params => { :new_visibility => "private" } + assert_redirected_to edit_traces_legacy_visibility_path(:visibility => nil, :from => nil, :to => nil) + assert_equal "public", trace.reload.visibility + + patch traces_legacy_visibility_path, :params => { :new_visibility => "" } + assert_redirected_to edit_traces_legacy_visibility_path(:visibility => nil, :from => nil, :to => nil) + assert_equal "public", trace.reload.visibility + + patch traces_legacy_visibility_path + assert_redirected_to edit_traces_legacy_visibility_path(:visibility => nil, :from => nil, :to => nil) + assert_equal "public", trace.reload.visibility + end + + def test_update_disabled + with_settings(:traces_disabled => true) do + patch traces_legacy_visibility_path, :params => { :new_visibility => "identifiable" } + assert_response :not_found + end + end + end +end -- 2.47.3