diff --git a/app/controllers/admin/collections_controller.rb b/app/controllers/admin/collections_controller.rb new file mode 100644 index 00000000000..4701500f9f8 --- /dev/null +++ b/app/controllers/admin/collections_controller.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Admin + class CollectionsController < BaseController + before_action :set_account + before_action :set_collection, only: :show + + def show + authorize @collection, :show? + end + + private + + def set_account + @account = Account.find(params[:account_id]) + end + + def set_collection + @collection = @account.collections.includes(accepted_collection_items: :account).find(params[:id]) + end + end +end diff --git a/app/controllers/admin/reports_controller.rb b/app/controllers/admin/reports_controller.rb index aa877f1448c..44ee7206bf7 100644 --- a/app/controllers/admin/reports_controller.rb +++ b/app/controllers/admin/reports_controller.rb @@ -50,7 +50,7 @@ module Admin private def filtered_reports - ReportFilter.new(filter_params).results.order(id: :desc).includes(:account, :target_account) + ReportFilter.new(filter_params).results.order(id: :desc).includes(:account, :target_account, :collections) end def filter_params @@ -58,7 +58,7 @@ module Admin end def set_report - @report = Report.find(params[:id]) + @report = Report.includes(collections: :accepted_collection_items).find(params[:id]) end end end diff --git a/app/models/collection.rb b/app/models/collection.rb index e11cb731884..d8386e43b44 100644 --- a/app/models/collection.rb +++ b/app/models/collection.rb @@ -26,6 +26,7 @@ class Collection < ApplicationRecord belongs_to :tag, optional: true has_many :collection_items, dependent: :delete_all + has_many :accepted_collection_items, -> { accepted }, class_name: 'CollectionItem', inverse_of: :collection # rubocop:disable Rails/HasManyOrHasOneDependent has_many :collection_reports, dependent: :delete_all validates :name, presence: true diff --git a/app/views/admin/accounts/_account.html.haml b/app/views/admin/accounts/_account.html.haml index 6b5b5efbdc8..74f84945620 100644 --- a/app/views/admin/accounts/_account.html.haml +++ b/app/views/admin/accounts/_account.html.haml @@ -1,6 +1,7 @@ .batch-table__row{ class: [!account.unavailable? && account.user_pending? && 'batch-table__row--attention', (account.unavailable? || account.user_unconfirmed?) && 'batch-table__row--muted'] } %label.batch-table__row__select.batch-table__row__select--aligned.batch-checkbox - = f.check_box :account_ids, { multiple: true, include_hidden: false }, account.id + - if local_assigns[:f].present? + = f.check_box :account_ids, { multiple: true, include_hidden: false }, account.id .batch-table__row__content.batch-table__row__content--unpadded %table.accounts-table %tbody diff --git a/app/views/admin/collections/show.html.haml b/app/views/admin/collections/show.html.haml new file mode 100644 index 00000000000..8f29b26309d --- /dev/null +++ b/app/views/admin/collections/show.html.haml @@ -0,0 +1,21 @@ +- content_for :page_title do + = t('admin.collections.collection_title', name: @account.pretty_acct) + +- content_for :heading_actions do + = link_to t('admin.collections.open'), account_collection_path(@account, @collection), class: 'button', target: '_blank', rel: 'noopener' + +%h3= t('admin.collections.contents') + += render 'admin/shared/collection', collection: @collection + +%hr.spacer/ + +%h3= t('admin.collections.accounts') + +.batch-table + .batch-table__toolbar + .batch-table__body + - if @collection.accepted_collection_items.none? + = nothing_here 'nothing-here--under-tabs' + - else + = render partial: 'admin/accounts/account', collection: @collection.accepted_collection_items.map(&:account) diff --git a/app/views/admin/reports/index.html.haml b/app/views/admin/reports/index.html.haml index b910e1aab52..1049cf733e2 100644 --- a/app/views/admin/reports/index.html.haml +++ b/app/views/admin/reports/index.html.haml @@ -67,6 +67,11 @@ = material_symbol('photo_camera') = report.media_attachments_count + - if Mastodon::Feature.collections_enabled? + %span.report-card__summary__item__content__icon{ title: t('admin.accounts.collections') } + = material_symbol('groups-fill') + = report.collections.size + - if report.forwarded? · = t('admin.reports.forwarded_to', domain: target_account.domain) diff --git a/app/views/admin/reports/show.html.haml b/app/views/admin/reports/show.html.haml index af92b05768e..7ea690dc347 100644 --- a/app/views/admin/reports/show.html.haml +++ b/app/views/admin/reports/show.html.haml @@ -32,7 +32,7 @@ %hr.spacer/ %h3 - = t 'admin.reports.statuses' + = t 'admin.reports.reported_content' %small.section-skip-link = link_to '#actions' do = material_symbol 'keyboard_double_arrow_down' @@ -41,6 +41,9 @@ %p = t 'admin.reports.statuses_description_html' +%h4 + = t 'admin.reports.statuses' + = form_with model: @form, url: batch_admin_account_statuses_path(@report.target_account_id, report_id: @report.id) do |f| .batch-table .batch-table__toolbar @@ -58,6 +61,22 @@ - else = render partial: 'admin/shared/status_batch_row', collection: @statuses, as: :status, locals: { f: f } +- if Mastodon::Feature.collections_enabled? + %h4 + = t 'admin.reports.collections' + + %form + .batch-table + .batch-table__toolbar + %label.batch-table__toolbar__select.batch-checkbox-all + -# = check_box_tag :batch_checkbox_all, nil, false + .batch-table__toolbar__actions + .batch-table__body + - if @report.collections.empty? + = nothing_here 'nothing-here--under-tabs' + - else + = render partial: 'admin/shared/collection_batch_row', collection: @report.collections, as: :collection + - if @report.unresolved? %hr.spacer/ diff --git a/app/views/admin/shared/_collection.html.haml b/app/views/admin/shared/_collection.html.haml new file mode 100644 index 00000000000..e300a986ba6 --- /dev/null +++ b/app/views/admin/shared/_collection.html.haml @@ -0,0 +1,22 @@ +.status__card + - if collection.tag.present? + .status__prepend + = link_to collection.tag.formatted_name, admin_tag_path(collection.tag_id) + + .status__content + %h6= collection.name + + %p= collection.description + + .detailed-status__meta + = conditional_link_to can?(:show, collection), admin_account_collection_path(collection.account.id, collection), class: 'detailed-status__datetime' do + %time.formatted{ datetime: collection.created_at.iso8601, title: l(collection.created_at) }><= l(collection.created_at) + - if collection.sensitive? +  · + = material_symbol('visibility_off') + = t('stream_entries.sensitive_content') +  · + = t('admin.collections.number_of_accounts', count: collection.accepted_collection_items.size) +  · + = link_to account_collection_path(collection.account, collection), class: 'detailed-status__link', target: 'blank', rel: 'noopener' do + = t('admin.collections.view_publicly') diff --git a/app/views/admin/shared/_collection_batch_row.html.haml b/app/views/admin/shared/_collection_batch_row.html.haml new file mode 100644 index 00000000000..8bf7857e95a --- /dev/null +++ b/app/views/admin/shared/_collection_batch_row.html.haml @@ -0,0 +1,5 @@ +.batch-table__row + %label.batch-table__row__select.batch-checkbox + -# = f.check_box :collection_ids, { multiple: true, include_hidden: false }, collection.id + .batch-table__row__content + = render partial: 'admin/shared/collection', object: collection diff --git a/config/locales/en.yml b/config/locales/en.yml index 2c777e72d32..71c559d7381 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -56,6 +56,7 @@ en: label: Change role no_role: No role title: Change role for %{username} + collections: Collections confirm: Confirm confirmed: Confirmed confirming: Confirming @@ -340,6 +341,15 @@ en: unpublish: Unpublish unpublished_msg: Announcement successfully unpublished! updated_msg: Announcement successfully updated! + collections: + accounts: Accounts + collection_title: Collection by %{name} + contents: Contents + number_of_accounts: + one: 1 account + other: "%{count} accounts" + open: Open + view_publicly: View publicly critical_update_pending: Critical update pending custom_emojis: assign_category: Assign category @@ -679,6 +689,7 @@ en: cancel: Cancel category: Category category_description_html: The reason this account and/or content was reported will be cited in communication with the reported account + collections: Collections comment: none: None comment_description_html: 'To provide more information, %{name} wrote:' @@ -708,12 +719,13 @@ en: report: 'Report #%{id}' reported_account: Reported account reported_by: Reported by + reported_content: Reported content reported_with_application: Reported with application resolved: Resolved resolved_msg: Report successfully resolved! skip_to_actions: Skip to actions status: Status - statuses: Reported content + statuses: Posts statuses_description_html: Offending content will be cited in communication with the reported account summary: action_preambles: diff --git a/config/routes/admin.rb b/config/routes/admin.rb index 97f84da44e7..84beea4611c 100644 --- a/config/routes/admin.rb +++ b/config/routes/admin.rb @@ -153,6 +153,8 @@ namespace :admin do resource :reset, only: [:create] resource :action, only: [:new, :create], controller: 'account_actions' + resources :collections, only: [:show] + resources :statuses, only: [:index, :show] do collection do post :batch diff --git a/spec/requests/admin/collections_spec.rb b/spec/requests/admin/collections_spec.rb new file mode 100644 index 00000000000..0e87e277ae8 --- /dev/null +++ b/spec/requests/admin/collections_spec.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Admin Collections' do + describe 'GET /admin/accounts/:account_id/collections/:id' do + let(:collection) { Fabricate(:collection) } + + before do + sign_in Fabricate(:admin_user) + end + + it 'returns success' do + get admin_account_collection_path(collection.account_id, collection) + + expect(response) + .to have_http_status(200) + end + end +end diff --git a/spec/requests/admin/reports_spec.rb b/spec/requests/admin/reports_spec.rb new file mode 100644 index 00000000000..d44db637953 --- /dev/null +++ b/spec/requests/admin/reports_spec.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Admin Reports' do + describe 'GET /admin/reports' do + before do + sign_in Fabricate(:admin_user) + + Fabricate.times(2, :report) + end + + it 'returns success' do + get admin_reports_path + + expect(response) + .to have_http_status(200) + end + end + + describe 'GET /admin/reports/:id' do + let(:report) { Fabricate(:report) } + + before do + sign_in Fabricate(:admin_user) + end + + shared_examples 'successful return' do + it 'returns success' do + get admin_report_path(report) + + expect(response) + .to have_http_status(200) + end + end + + context 'with a simple report' do + it_behaves_like 'successful return' + end + + context 'with a reported status' do + before do + status = Fabricate(:status, account: report.target_account) + report.update(status_ids: [status.id]) + end + + it_behaves_like 'successful return' + end + + context 'with a reported collection', feature: :collections do + before do + report.collections << Fabricate(:collection, account: report.target_account) + end + + it_behaves_like 'successful return' + end + + context 'with both status and collection', feature: :collections do + before do + status = Fabricate(:status, account: report.target_account) + report.update(status_ids: [status.id]) + report.collections << Fabricate(:collection, account: report.target_account) + end + + it_behaves_like 'successful return' + end + end +end