From 261e52268c05d2da4459a23e2898555dd5db5771 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 9 Sep 2019 12:50:09 +0200 Subject: Add batch approve/reject for pending hashtags in admin UI (#11791) --- app/models/form/tag_batch.rb | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 app/models/form/tag_batch.rb (limited to 'app/models/form') diff --git a/app/models/form/tag_batch.rb b/app/models/form/tag_batch.rb new file mode 100644 index 000000000..fd517a1a6 --- /dev/null +++ b/app/models/form/tag_batch.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +class Form::TagBatch + include ActiveModel::Model + include Authorization + + attr_accessor :tag_ids, :action, :current_account + + def save + case action + when 'approve' + approve! + when 'reject' + reject! + end + end + + private + + def tags + Tag.where(id: tag_ids) + end + + def approve! + tags.each { |tag| authorize(tag, :update?) } + tags.update_all(trendable: true, reviewed_at: Time.now.utc) + end + + def reject! + tags.each { |tag| authorize(tag, :update?) } + tags.update_all(trendable: false, reviewed_at: Time.now.utc) + end +end -- cgit From 1110ea1a9162d5488e1ed5dbccd0803618e713f8 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 9 Sep 2019 22:44:17 +0200 Subject: Add batch actions and categories to admin UI for custom emojis (#11793) --- app/controllers/admin/custom_emojis_controller.rb | 102 +++++++------------- app/javascript/styles/mastodon/tables.scss | 41 ++++++++ app/models/custom_emoji.rb | 6 ++ app/models/custom_emoji_category.rb | 2 + app/models/custom_emoji_filter.rb | 8 +- app/models/form/custom_emoji_batch.rb | 106 +++++++++++++++++++++ .../admin/custom_emojis/_custom_emoji.html.haml | 55 ++++++----- app/views/admin/custom_emojis/index.html.haml | 66 ++++++++++--- config/locales/en.yml | 3 + config/routes.rb | 8 +- .../admin/custom_emojis_controller_spec.rb | 60 ------------ 11 files changed, 281 insertions(+), 176 deletions(-) create mode 100644 app/models/form/custom_emoji_batch.rb (limited to 'app/models/form') diff --git a/app/controllers/admin/custom_emojis_controller.rb b/app/controllers/admin/custom_emojis_controller.rb index f77699166..2af90f051 100644 --- a/app/controllers/admin/custom_emojis_controller.rb +++ b/app/controllers/admin/custom_emojis_controller.rb @@ -2,19 +2,20 @@ module Admin class CustomEmojisController < BaseController - before_action :set_custom_emoji, except: [:index, :new, :create] - before_action :set_filter_params - include ObfuscateFilename + obfuscate_filename [:custom_emoji, :image] def index authorize :custom_emoji, :index? + @custom_emojis = filtered_custom_emojis.eager_load(:local_counterpart).page(params[:page]) + @form = Form::CustomEmojiBatch.new end def new authorize :custom_emoji, :create? + @custom_emoji = CustomEmoji.new end @@ -31,69 +32,17 @@ module Admin end end - def update - authorize @custom_emoji, :update? - - if @custom_emoji.update(resource_params) - log_action :update, @custom_emoji - flash[:notice] = I18n.t('admin.custom_emojis.updated_msg') - else - flash[:alert] = I18n.t('admin.custom_emojis.update_failed_msg') - end - redirect_to admin_custom_emojis_path(page: params[:page], **@filter_params) - end - - def destroy - authorize @custom_emoji, :destroy? - @custom_emoji.destroy! - log_action :destroy, @custom_emoji - flash[:notice] = I18n.t('admin.custom_emojis.destroyed_msg') - redirect_to admin_custom_emojis_path(page: params[:page], **@filter_params) - end - - def copy - authorize @custom_emoji, :copy? - - emoji = CustomEmoji.find_or_initialize_by(domain: nil, - shortcode: @custom_emoji.shortcode) - emoji.image = @custom_emoji.image - - if emoji.save - log_action :create, emoji - flash[:notice] = I18n.t('admin.custom_emojis.copied_msg') - else - flash[:alert] = I18n.t('admin.custom_emojis.copy_failed_msg') - end - - redirect_to admin_custom_emojis_path(page: params[:page], **@filter_params) - end - - def enable - authorize @custom_emoji, :enable? - @custom_emoji.update!(disabled: false) - log_action :enable, @custom_emoji - flash[:notice] = I18n.t('admin.custom_emojis.enabled_msg') - redirect_to admin_custom_emojis_path(page: params[:page], **@filter_params) - end - - def disable - authorize @custom_emoji, :disable? - @custom_emoji.update!(disabled: true) - log_action :disable, @custom_emoji - flash[:notice] = I18n.t('admin.custom_emojis.disabled_msg') - redirect_to admin_custom_emojis_path(page: params[:page], **@filter_params) + def batch + @form = Form::CustomEmojiBatch.new(form_custom_emoji_batch_params.merge(current_account: current_account, action: action_from_button)) + @form.save + rescue ActionController::ParameterMissing + flash[:alert] = I18n.t('admin.accounts.no_account_selected') + ensure + redirect_to admin_custom_emojis_path(filter_params) end private - def set_custom_emoji - @custom_emoji = CustomEmoji.find(params[:id]) - end - - def set_filter_params - @filter_params = filter_params.to_hash.symbolize_keys - end - def resource_params params.require(:custom_emoji).permit(:shortcode, :image, :visible_in_picker) end @@ -103,12 +52,29 @@ module Admin end def filter_params - params.permit( - :local, - :remote, - :by_domain, - :shortcode - ) + params.slice(:local, :remote, :by_domain, :shortcode, :page).permit(:local, :remote, :by_domain, :shortcode, :page) + end + + def action_from_button + if params[:update] + 'update' + elsif params[:list] + 'list' + elsif params[:unlist] + 'unlist' + elsif params[:enable] + 'enable' + elsif params[:disable] + 'disable' + elsif params[:copy] + 'copy' + elsif params[:delete] + 'delete' + end + end + + def form_custom_emoji_batch_params + params.require(:form_custom_emoji_batch).permit(:action, :category_id, :category_name, custom_emoji_ids: []) end end end diff --git a/app/javascript/styles/mastodon/tables.scss b/app/javascript/styles/mastodon/tables.scss index 2aef099e6..d6403986f 100644 --- a/app/javascript/styles/mastodon/tables.scss +++ b/app/javascript/styles/mastodon/tables.scss @@ -180,6 +180,18 @@ a.table-action-link { } } + &__form { + padding: 16px; + border: 1px solid darken($ui-base-color, 8%); + border-top: 0; + background: $ui-base-color; + + .fields-row { + padding-top: 0; + margin-bottom: 0; + } + } + &__row { border: 1px solid darken($ui-base-color, 8%); border-top: 0; @@ -210,6 +222,35 @@ a.table-action-link { &--unpadded { padding: 0; } + + &--with-image { + display: flex; + align-items: center; + } + + &__image { + flex: 0 0 auto; + display: flex; + justify-content: center; + align-items: center; + margin-right: 10px; + + .emojione { + width: 32px; + height: 32px; + } + } + + &__text { + flex: 1 1 auto; + } + + &__extra { + flex: 0 0 auto; + text-align: right; + color: $darker-text-color; + font-weight: 500; + } } .directory__tag { diff --git a/app/models/custom_emoji.rb b/app/models/custom_emoji.rb index b21ad9042..0a4201a14 100644 --- a/app/models/custom_emoji.rb +++ b/app/models/custom_emoji.rb @@ -59,6 +59,12 @@ class CustomEmoji < ApplicationRecord :emoji end + def copy! + copy = self.class.find_or_initialize_by(domain: nil, shortcode: shortcode) + copy.image = image + copy.save! + end + class << self def from_text(text, domain) return [] if text.blank? diff --git a/app/models/custom_emoji_category.rb b/app/models/custom_emoji_category.rb index 7d8c0ee2d..3c87f2b2e 100644 --- a/app/models/custom_emoji_category.rb +++ b/app/models/custom_emoji_category.rb @@ -12,4 +12,6 @@ class CustomEmojiCategory < ApplicationRecord has_many :emojis, class_name: 'CustomEmoji', foreign_key: 'category_id', inverse_of: :category + + validates :name, presence: true, uniqueness: true end diff --git a/app/models/custom_emoji_filter.rb b/app/models/custom_emoji_filter.rb index 7649055d2..15b8da1d1 100644 --- a/app/models/custom_emoji_filter.rb +++ b/app/models/custom_emoji_filter.rb @@ -11,6 +11,8 @@ class CustomEmojiFilter scope = CustomEmoji.alphabetic params.each do |key, value| + next if key.to_s == 'page' + scope.merge!(scope_for(key, value)) if value.present? end @@ -22,13 +24,13 @@ class CustomEmojiFilter def scope_for(key, value) case key.to_s when 'local' - CustomEmoji.local + CustomEmoji.local.left_joins(:category).reorder(Arel.sql('custom_emoji_categories.name ASC NULLS FIRST, custom_emojis.shortcode ASC')) when 'remote' CustomEmoji.remote when 'by_domain' - CustomEmoji.where(domain: value.downcase) + CustomEmoji.where(domain: value.strip.downcase) when 'shortcode' - CustomEmoji.search(value) + CustomEmoji.search(value.strip) else raise "Unknown filter: #{key}" end diff --git a/app/models/form/custom_emoji_batch.rb b/app/models/form/custom_emoji_batch.rb new file mode 100644 index 000000000..076e8c9e3 --- /dev/null +++ b/app/models/form/custom_emoji_batch.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true + +class Form::CustomEmojiBatch + include ActiveModel::Model + include Authorization + include AccountableConcern + + attr_accessor :custom_emoji_ids, :action, :current_account, + :category_id, :category_name, :visible_in_picker + + def save + case action + when 'update' + update! + when 'list' + list! + when 'unlist' + unlist! + when 'enable' + enable! + when 'disable' + disable! + when 'copy' + copy! + when 'delete' + delete! + end + end + + private + + def custom_emojis + CustomEmoji.where(id: custom_emoji_ids) + end + + def update! + custom_emojis.each { |custom_emoji| authorize(custom_emoji, :update?) } + + category = begin + if category_id.present? + CustomEmojiCategory.find(category_id) + elsif category_name.present? + CustomEmojiCategory.create!(name: category_name) + end + end + + custom_emojis.each do |custom_emoji| + custom_emoji.update(category_id: category&.id) + log_action :update, custom_emoji + end + end + + def list! + custom_emojis.each { |custom_emoji| authorize(custom_emoji, :update?) } + + custom_emojis.each do |custom_emoji| + custom_emoji.update(visible_in_picker: true) + log_action :update, custom_emoji + end + end + + def unlist! + custom_emojis.each { |custom_emoji| authorize(custom_emoji, :update?) } + + custom_emojis.each do |custom_emoji| + custom_emoji.update(visible_in_picker: false) + log_action :update, custom_emoji + end + end + + def enable! + custom_emojis.each { |custom_emoji| authorize(custom_emoji, :enable?) } + + custom_emojis.each do |custom_emoji| + custom_emoji.update(disabled: false) + log_action :enable, custom_emoji + end + end + + def disable! + custom_emojis.each { |custom_emoji| authorize(custom_emoji, :disable?) } + + custom_emojis.each do |custom_emoji| + custom_emoji.update(disabled: true) + log_action :disable, custom_emoji + end + end + + def copy! + custom_emojis.each { |custom_emoji| authorize(custom_emoji, :copy?) } + + custom_emojis.each do |custom_emoji| + copied_custom_emoji = custom_emoji.copy! + log_action :create, copied_custom_emoji + end + end + + def delete! + custom_emojis.each { |custom_emoji| authorize(custom_emoji, :destroy?) } + + custom_emojis.each do |custom_emoji| + custom_emoji.destroy + log_action :destroy, custom_emoji + end + end +end diff --git a/app/views/admin/custom_emojis/_custom_emoji.html.haml b/app/views/admin/custom_emojis/_custom_emoji.html.haml index fbaa9a174..9e06a3b42 100644 --- a/app/views/admin/custom_emojis/_custom_emoji.html.haml +++ b/app/views/admin/custom_emojis/_custom_emoji.html.haml @@ -1,28 +1,31 @@ -%tr - %td - = custom_emoji_tag(custom_emoji) - %td - %samp= ":#{custom_emoji.shortcode}:" - %td - - if custom_emoji.local? - = t('admin.accounts.location.local') - - else - = link_to custom_emoji.domain, admin_custom_emojis_path(by_domain: custom_emoji.domain) - %td - - if custom_emoji.local? - - if custom_emoji.visible_in_picker - = table_link_to 'eye', t('admin.custom_emojis.listed'), admin_custom_emoji_path(custom_emoji, custom_emoji: { visible_in_picker: false }, page: params[:page], **@filter_params), method: :patch +.batch-table__row + %label.batch-table__row__select.batch-table__row__select--aligned.batch-checkbox + = f.check_box :custom_emoji_ids, { multiple: true, include_hidden: false }, custom_emoji.id + .batch-table__row__content.batch-table__row__content--with-image + .batch-table__row__content__image + = custom_emoji_tag(custom_emoji) + + .batch-table__row__content__text + %samp= ":#{custom_emoji.shortcode}:" + + - if custom_emoji.local? + %span.account-role.bot= custom_emoji.category&.name || t('admin.custom_emojis.uncategorized') + + .batch-table__row__content__extra + - if custom_emoji.local? + = t('admin.accounts.location.local') - else - = table_link_to 'eye-slash', t('admin.custom_emojis.unlisted'), admin_custom_emoji_path(custom_emoji, custom_emoji: { visible_in_picker: true }, page: params[:page], **@filter_params), method: :patch - - else - - if custom_emoji.local_counterpart.present? - = link_to safe_join([custom_emoji_tag(custom_emoji.local_counterpart), t('admin.custom_emojis.overwrite')]), copy_admin_custom_emoji_path(custom_emoji, page: params[:page], **@filter_params), method: :post, class: 'table-action-link' + = custom_emoji.domain + + %br/ + + - if custom_emoji.disabled? + = t('admin.custom_emojis.disabled') - else - = table_link_to 'copy', t('admin.custom_emojis.copy'), copy_admin_custom_emoji_path(custom_emoji, page: params[:page], **@filter_params), method: :post - %td - - if custom_emoji.disabled? - = table_link_to 'power-off', t('admin.custom_emojis.enable'), enable_admin_custom_emoji_path(custom_emoji, page: params[:page], **@filter_params), method: :post, data: { confirm: t('admin.accounts.are_you_sure') } - - else - = table_link_to 'power-off', t('admin.custom_emojis.disable'), disable_admin_custom_emoji_path(custom_emoji, page: params[:page], **@filter_params), method: :post, data: { confirm: t('admin.accounts.are_you_sure') } - %td - = table_link_to 'times', t('admin.custom_emojis.delete'), admin_custom_emoji_path(custom_emoji, page: params[:page], **@filter_params), method: :delete, data: { confirm: t('admin.accounts.are_you_sure') } + = t('admin.custom_emojis.enabled') + - if custom_emoji.local? + • + - if custom_emoji.visible_in_picker? + = t('admin.custom_emojis.listed') + - else + = t('admin.custom_emojis.unlisted') diff --git a/app/views/admin/custom_emojis/index.html.haml b/app/views/admin/custom_emojis/index.html.haml index 3a119276c..7320ce1bb 100644 --- a/app/views/admin/custom_emojis/index.html.haml +++ b/app/views/admin/custom_emojis/index.html.haml @@ -1,6 +1,9 @@ - content_for :page_title do = t('admin.custom_emojis.title') +- content_for :header_tags do + = javascript_pack_tag 'admin', integrity: true, async: true, crossorigin: 'anonymous' + .filters .filter-subset %strong= t('admin.accounts.location.title') @@ -20,8 +23,7 @@ = form_tag admin_custom_emojis_url, method: 'GET', class: 'simple_form' do .fields-group - Admin::FilterHelper::CUSTOM_EMOJI_FILTERS.each do |key| - - if params[key].present? - = hidden_field_tag key, params[key] + = hidden_field_tag key, params[key] if params[key].present? - %i(shortcode by_domain).each do |key| .input.string.optional @@ -31,18 +33,54 @@ %button= t('admin.accounts.search') = link_to t('admin.accounts.reset'), admin_custom_emojis_path, class: 'button negative' -.table-wrapper - %table.table - %thead - %tr - %th= t('admin.custom_emojis.emoji') - %th= t('admin.custom_emojis.shortcode') - %th= t('admin.accounts.domain') - %th - %th - %th - %tbody - = render @custom_emojis += form_for(@form, url: batch_admin_custom_emojis_path) do |f| + = hidden_field_tag :page, params[:page] || 1 + + - Admin::FilterHelper::CUSTOM_EMOJI_FILTERS.each do |key| + = hidden_field_tag key, params[key] if params[key].present? + + .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 + - if params[:local] == '1' + = f.button safe_join([fa_icon('save'), t('generic.save_changes')]), name: :update, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } + + = f.button safe_join([fa_icon('eye'), t('admin.custom_emojis.list')]), name: :list, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } + + = f.button safe_join([fa_icon('eye-slash'), t('admin.custom_emojis.unlist')]), name: :unlist, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } + + = f.button safe_join([fa_icon('power-off'), t('admin.custom_emojis.enable')]), name: :enable, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } + + = f.button safe_join([fa_icon('power-off'), t('admin.custom_emojis.disable')]), name: :disable, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } + + = f.button safe_join([fa_icon('times'), t('admin.custom_emojis.delete')]), name: :delete, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } + + - unless params[:local] == '1' + = f.button safe_join([fa_icon('copy'), t('admin.custom_emojis.copy')]), name: :copy, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } + + - if params[:local] == '1' + .batch-table__form.simple_form + .fields-row + .fields-group.fields-row__column.fields-row__column-6 + .input.select.optional + .label_input + = f.select :category_id, options_from_collection_for_select(CustomEmojiCategory.all, 'id', 'name'), prompt: t('admin.custom_emojis.assign_category'), class: 'select optional', 'aria-label': t('admin.custom_emojis.assign_category') + + .fields-group.fields-row__column.fields-row__column-6 + .input.string.optional + .label_input + = f.text_field :category_name, class: 'string optional', placeholder: t('admin.custom_emojis.create_new_category'), 'aria-label': t('admin.custom_emojis.create_new_category') + + .batch-table__body + - if @custom_emojis.empty? + = nothing_here 'nothing-here--under-tabs' + - else + = render partial: 'custom_emoji', collection: @custom_emojis, locals: { f: f } = paginate @custom_emojis + +%hr.spacer/ + = link_to t('admin.custom_emojis.upload'), new_admin_custom_emoji_path, class: 'button' diff --git a/config/locales/en.yml b/config/locales/en.yml index 42d8e0eb8..52cb4a269 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -225,10 +225,12 @@ en: deleted_status: "(deleted status)" title: Audit log custom_emojis: + assign_category: Assign category by_domain: Domain copied_msg: Successfully created local copy of the emoji copy: Copy copy_failed_msg: Could not make a local copy of that emoji + create_new_category: Create new category created_msg: Emoji successfully created! delete: Delete destroyed_msg: Emojo successfully destroyed! @@ -245,6 +247,7 @@ en: shortcode: Shortcode shortcode_hint: At least 2 characters, only alphanumeric characters and underscores title: Custom emojis + uncategorized: Uncategorized unlisted: Unlisted update_failed_msg: Could not update that emoji updated_msg: Emoji successfully updated! diff --git a/config/routes.rb b/config/routes.rb index 534e68814..d22a9e56a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -242,11 +242,9 @@ Rails.application.routes.draw do resource :two_factor_authentication, only: [:destroy] end - resources :custom_emojis, only: [:index, :new, :create, :update, :destroy] do - member do - post :copy - post :enable - post :disable + resources :custom_emojis, only: [:index, :new, :create] do + collection do + post :batch end end diff --git a/spec/controllers/admin/custom_emojis_controller_spec.rb b/spec/controllers/admin/custom_emojis_controller_spec.rb index b7e2894e9..a8d96948c 100644 --- a/spec/controllers/admin/custom_emojis_controller_spec.rb +++ b/spec/controllers/admin/custom_emojis_controller_spec.rb @@ -52,64 +52,4 @@ describe Admin::CustomEmojisController do end end end - - describe 'PUT #update' do - let(:custom_emoji) { Fabricate(:custom_emoji, shortcode: 'test') } - let(:image) { fixture_file_upload(Rails.root.join('spec', 'fixtures', 'files', 'emojo.png'), 'image/png') } - - before do - put :update, params: { id: custom_emoji.id, custom_emoji: params } - end - - context 'when parameter is valid' do - let(:params) { { shortcode: 'updated', image: image } } - - it 'succeeds in updating custom emoji' do - expect(flash[:notice]).to eq I18n.t('admin.custom_emojis.updated_msg') - expect(custom_emoji.reload).to have_attributes(shortcode: 'updated') - end - end - - context 'when parameter is invalid' do - let(:params) { { shortcode: 'u', image: image } } - - it 'fails to update custom emoji' do - expect(flash[:alert]).to eq I18n.t('admin.custom_emojis.update_failed_msg') - expect(custom_emoji.reload).to have_attributes(shortcode: 'test') - end - end - end - - describe 'POST #copy' do - subject { post :copy, params: { id: custom_emoji.id } } - - let(:custom_emoji) { Fabricate(:custom_emoji, shortcode: 'test') } - - it 'copies custom emoji' do - expect { subject }.to change { CustomEmoji.where(shortcode: 'test').count }.by(1) - expect(flash[:notice]).to eq I18n.t('admin.custom_emojis.copied_msg') - end - end - - describe 'POST #enable' do - let(:custom_emoji) { Fabricate(:custom_emoji, shortcode: 'test', disabled: true) } - - before { post :enable, params: { id: custom_emoji.id } } - - it 'enables custom emoji' do - expect(response).to redirect_to admin_custom_emojis_path - expect(custom_emoji.reload).to have_attributes(disabled: false) - end - end - - describe 'POST #disable' do - let(:custom_emoji) { Fabricate(:custom_emoji, shortcode: 'test', disabled: false) } - - before { post :disable, params: { id: custom_emoji.id } } - - it 'enables custom emoji' do - expect(response).to redirect_to admin_custom_emojis_path - expect(custom_emoji.reload).to have_attributes(disabled: true) - end - end end -- cgit From 4fe127664b0ae22a528b4a4467ab2de92e3da3ef Mon Sep 17 00:00:00 2001 From: Tao Bror Bojlén Date: Wed, 11 Sep 2019 07:44:58 +0100 Subject: add admin setting for default search engine indexing (fix #11750) (#11804) --- app/lib/settings/scoped_settings.rb | 1 + app/models/form/admin_settings.rb | 2 ++ app/views/admin/settings/edit.html.haml | 3 +++ config/locales/en.yml | 3 +++ spec/controllers/application_controller_spec.rb | 1 + 5 files changed, 10 insertions(+) (limited to 'app/models/form') diff --git a/app/lib/settings/scoped_settings.rb b/app/lib/settings/scoped_settings.rb index 3653ab114..9ca39510a 100644 --- a/app/lib/settings/scoped_settings.rb +++ b/app/lib/settings/scoped_settings.rb @@ -4,6 +4,7 @@ module Settings class ScopedSettings DEFAULTING_TO_UNSCOPED = %w( theme + noindex ).freeze def initialize(object) diff --git a/app/models/form/admin_settings.rb b/app/models/form/admin_settings.rb index 6bc3ca9f5..24196e182 100644 --- a/app/models/form/admin_settings.rb +++ b/app/models/form/admin_settings.rb @@ -32,6 +32,7 @@ class Form::AdminSettings trends show_domain_blocks show_domain_blocks_rationale + noindex ).freeze BOOLEAN_KEYS = %i( @@ -45,6 +46,7 @@ class Form::AdminSettings profile_directory spam_check_enabled trends + noindex ).freeze UPLOAD_KEYS = %i( diff --git a/app/views/admin/settings/edit.html.haml b/app/views/admin/settings/edit.html.haml index 28880c087..752386b3c 100644 --- a/app/views/admin/settings/edit.html.haml +++ b/app/views/admin/settings/edit.html.haml @@ -71,6 +71,9 @@ .fields-group = f.input :trends, as: :boolean, wrapper: :with_label, label: t('admin.settings.trends.title'), hint: t('admin.settings.trends.desc_html') + .fields-group + = f.input :noindex, as: :boolean, wrapper: :with_label, label: t('admin.settings.default_noindex.title'), hint: t('admin.settings.default_noindex.desc_html') + .fields-group = f.input :spam_check_enabled, as: :boolean, wrapper: :with_label, label: t('admin.settings.spam_check_enabled.title'), hint: t('admin.settings.spam_check_enabled.desc_html') diff --git a/config/locales/en.yml b/config/locales/en.yml index 52cb4a269..0a5ca31c1 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -427,6 +427,9 @@ en: custom_css: desc_html: Modify the look with CSS loaded on every page title: Custom CSS + default_noindex: + desc_html: Affects all users who have not changed this setting themselves + title: Opt users out of search engine indexing by default domain_blocks: all: To everyone disabled: To no one diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index 1811500df..da4a794cd 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -110,6 +110,7 @@ describe ApplicationController, type: :controller do sign_in current_user allow(Setting).to receive(:[]).with('theme').and_return 'contrast' + allow(Setting).to receive(:[]).with('noindex').and_return false expect(controller.view_context.current_theme).to eq 'contrast' end -- cgit From c5d37f18cb3f4d6212fb8f3e1c4e1e027f677ec5 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 11 Sep 2019 16:32:44 +0200 Subject: Change deletes to preserve soft-deleted statuses in unresolved reports (#11805) Change all account actions except "none" to resolve all unresolved reports Refactor `SuspendAccountService` to be more readable --- app/controllers/admin/accounts_controller.rb | 2 +- app/controllers/admin/report_notes_controller.rb | 9 ++-- .../api/v1/admin/accounts_controller.rb | 2 +- app/lib/activitypub/activity/delete.rb | 3 +- app/models/account.rb | 1 + app/models/admin/account_action.rb | 24 +++++++-- app/models/form/account_batch.rb | 2 +- app/models/form/status_batch.rb | 2 +- app/models/report.rb | 1 + app/models/status.rb | 4 ++ app/models/user.rb | 4 ++ app/services/block_domain_service.rb | 2 +- app/services/remove_status_service.rb | 7 +-- app/services/suspend_account_service.rb | 62 ++++++++++++++++------ app/services/unallow_domain_service.rb | 2 +- app/workers/admin/suspension_worker.rb | 2 +- lib/mastodon/accounts_cli.rb | 4 +- lib/mastodon/domains_cli.rb | 2 +- .../admin/reported_statuses_controller_spec.rb | 2 +- spec/controllers/admin/statuses_controller_spec.rb | 2 +- spec/models/form/status_batch_spec.rb | 4 +- 21 files changed, 98 insertions(+), 45 deletions(-) (limited to 'app/models/form') diff --git a/app/controllers/admin/accounts_controller.rb b/app/controllers/admin/accounts_controller.rb index 2fa1dfe5f..68b6352f8 100644 --- a/app/controllers/admin/accounts_controller.rb +++ b/app/controllers/admin/accounts_controller.rb @@ -41,7 +41,7 @@ module Admin def reject authorize @account.user, :reject? - SuspendAccountService.new.call(@account, including_user: true, destroy: true, skip_distribution: true) + SuspendAccountService.new.call(@account, reserve_email: false, reserve_username: false) redirect_to admin_pending_accounts_path end diff --git a/app/controllers/admin/report_notes_controller.rb b/app/controllers/admin/report_notes_controller.rb index bcb3f2026..b816c5b5d 100644 --- a/app/controllers/admin/report_notes_controller.rb +++ b/app/controllers/admin/report_notes_controller.rb @@ -5,10 +5,10 @@ module Admin before_action :set_report_note, only: [:destroy] def create - authorize ReportNote, :create? + authorize :report_note, :create? @report_note = current_account.report_notes.new(resource_params) - @report = @report_note.report + @report = @report_note.report if @report_note.save if params[:create_and_resolve] @@ -26,9 +26,8 @@ module Admin redirect_to admin_report_path(@report), notice: I18n.t('admin.report_notes.created_msg') else - @report_notes = @report.notes.latest - @report_history = @report.history - @form = Form::StatusBatch.new + @report_notes = (@report.notes.latest + @report.history + @report.target_account.targeted_account_warnings.latest.custom).sort_by(&:created_at) + @form = Form::StatusBatch.new render template: 'admin/reports/show' end diff --git a/app/controllers/api/v1/admin/accounts_controller.rb b/app/controllers/api/v1/admin/accounts_controller.rb index c306180ca..c35ea5ab2 100644 --- a/app/controllers/api/v1/admin/accounts_controller.rb +++ b/app/controllers/api/v1/admin/accounts_controller.rb @@ -58,7 +58,7 @@ class Api::V1::Admin::AccountsController < Api::BaseController def reject authorize @account.user, :reject? - SuspendAccountService.new.call(@account, including_user: true, destroy: true, skip_distribution: true) + SuspendAccountService.new.call(@account, reserve_email: false, reserve_username: false) render json: @account, serializer: REST::Admin::AccountSerializer end diff --git a/app/lib/activitypub/activity/delete.rb b/app/lib/activitypub/activity/delete.rb index 345060462..dc9ff580c 100644 --- a/app/lib/activitypub/activity/delete.rb +++ b/app/lib/activitypub/activity/delete.rb @@ -13,8 +13,7 @@ class ActivityPub::Activity::Delete < ActivityPub::Activity def delete_person lock_or_return("delete_in_progress:#{@account.id}") do - SuspendAccountService.new.call(@account) - @account.destroy! + SuspendAccountService.new.call(@account, reserve_username: false) end end diff --git a/app/models/account.rb b/app/models/account.rb index 8c9388b95..55fe53fae 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -115,6 +115,7 @@ class Account < ApplicationRecord :approved?, :pending?, :disabled?, + :unconfirmed_or_pending?, :role, :admin?, :moderator?, diff --git a/app/models/admin/account_action.rb b/app/models/admin/account_action.rb index c7da8b52c..b30a82369 100644 --- a/app/models/admin/account_action.rb +++ b/app/models/admin/account_action.rb @@ -83,19 +83,23 @@ class Admin::AccountAction # A log entry is only interesting if the warning contains # custom text from someone. Otherwise it's just noise. + log_action(:create, warning) if warning.text.present? end def process_reports! - return if report_id.blank? + # If we're doing "mark as resolved" on a single report, + # then we want to keep other reports open in case they + # contain new actionable information. + # + # Otherwise, we will mark all unresolved reports about + # the account as resolved. - authorize(report, :update?) + reports.each { |report| authorize(report, :update?) } - if type == 'none' + reports.each do |report| log_action(:resolve, report) report.resolve!(current_account) - else - Report.where(target_account: target_account).unresolved.update_all(action_taken: true, action_taken_by_account_id: current_account.id) end end @@ -141,6 +145,16 @@ class Admin::AccountAction @report.status_ids if @report && include_statuses end + def reports + @reports ||= begin + if type == 'none' && with_report? + [report] + else + Report.where(target_account: target_account).unresolved + end + end + end + def warning_preset @warning_preset ||= AccountWarningPreset.find(warning_preset_id) if warning_preset_id.present? end diff --git a/app/models/form/account_batch.rb b/app/models/form/account_batch.rb index f1b7a4566..0b285fde9 100644 --- a/app/models/form/account_batch.rb +++ b/app/models/form/account_batch.rb @@ -69,6 +69,6 @@ class Form::AccountBatch records = accounts.includes(:user) records.each { |account| authorize(account.user, :reject?) } - .each { |account| SuspendAccountService.new.call(account, including_user: true, destroy: true, skip_distribution: true) } + .each { |account| SuspendAccountService.new.call(account, reserve_email: false, reserve_username: false) } end end diff --git a/app/models/form/status_batch.rb b/app/models/form/status_batch.rb index e09cc2594..c4943a7ea 100644 --- a/app/models/form/status_batch.rb +++ b/app/models/form/status_batch.rb @@ -35,7 +35,7 @@ class Form::StatusBatch def delete_statuses Status.where(id: status_ids).reorder(nil).find_each do |status| status.discard - RemovalWorker.perform_async(status.id, redraft: false) + RemovalWorker.perform_async(status.id, immediate: true) Tombstone.find_or_create_by(uri: status.uri, account: status.account, by_moderator: true) log_action :destroy, status end diff --git a/app/models/report.rb b/app/models/report.rb index 1e707ff1c..fb2e040ee 100644 --- a/app/models/report.rb +++ b/app/models/report.rb @@ -59,6 +59,7 @@ class Report < ApplicationRecord end def resolve!(acting_account) + RemovalWorker.push_bulk(Status.with_discarded.discarded.where(id: status_ids).pluck(:id)) { |status_id| [status_id, { immediate: true }] } update!(action_taken: true, action_taken_by_account_id: acting_account.id) end diff --git a/app/models/status.rb b/app/models/status.rb index 9cfaddcec..471bb03b4 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -214,6 +214,10 @@ class Status < ApplicationRecord !sensitive? && with_media? end + def reported? + @reported ||= Report.where(target_account: account).unresolved.where('? = ANY(status_ids)', id).exists? + end + def emojis return @emojis if defined?(@emojis) diff --git a/app/models/user.rb b/app/models/user.rb index 95f1d8fc5..78b82a68f 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -171,6 +171,10 @@ class User < ApplicationRecord confirmed? && approved? && !disabled? && !account.suspended? end + def unconfirmed_or_pending? + !(confirmed? && approved?) + end + def inactive_message !approved? ? :pending : super end diff --git a/app/services/block_domain_service.rb b/app/services/block_domain_service.rb index 0ec6be503..ae461abf2 100644 --- a/app/services/block_domain_service.rb +++ b/app/services/block_domain_service.rb @@ -53,7 +53,7 @@ class BlockDomainService < BaseService def suspend_accounts! blocked_domain_accounts.without_suspended.reorder(nil).find_each do |account| - SuspendAccountService.new.call(account, suspended_at: @domain_block.created_at) + SuspendAccountService.new.call(account, reserve_username: true, suspended_at: @domain_block.created_at) end end diff --git a/app/services/remove_status_service.rb b/app/services/remove_status_service.rb index 685c1d4bf..f9352ed3d 100644 --- a/app/services/remove_status_service.rb +++ b/app/services/remove_status_service.rb @@ -8,7 +8,8 @@ class RemoveStatusService < BaseService # @param [Status] status # @param [Hash] options # @option [Boolean] :redraft - # @options [Boolean] :original_removed + # @option [Boolean] :immediate + # @option [Boolean] :original_removed def call(status, **options) @payload = Oj.dump(event: :delete, payload: status.id.to_s) @status = status @@ -31,7 +32,7 @@ class RemoveStatusService < BaseService remove_from_spam_check remove_media - @status.destroy! + @status.destroy! if @options[:immediate] || !@status.reported? else raise Mastodon::RaceConditionError end @@ -150,7 +151,7 @@ class RemoveStatusService < BaseService end def remove_media - return if @options[:redraft] + return if @options[:redraft] || (!@options[:immediate] && @status.reported?) @status.media_attachments.destroy_all end diff --git a/app/services/suspend_account_service.rb b/app/services/suspend_account_service.rb index 85da7e921..ecc893931 100644 --- a/app/services/suspend_account_service.rb +++ b/app/services/suspend_account_service.rb @@ -15,7 +15,6 @@ class SuspendAccountService < BaseService favourites follow_requests list_accounts - media_attachments mute_relationships muted_by_relationships notifications @@ -32,14 +31,26 @@ class SuspendAccountService < BaseService targeted_reports ).freeze - # Suspend an account and remove as much of its data as possible + # Suspend or remove an account and remove as much of its data + # as possible. If it's a local account and it has not been confirmed + # or never been approved, then side effects are skipped and both + # the user and account records are removed fully. Otherwise, + # it is controlled by options. # @param [Account] # @param [Hash] options - # @option [Boolean] :including_user Remove the user record as well - # @option [Boolean] :destroy Remove the account record instead of suspending + # @option [Boolean] :reserve_email Keep user record. Only applicable for local accounts + # @option [Boolean] :reserve_username Keep account record + # @option [Boolean] :skip_side_effects Side effects are ActivityPub and streaming API payloads + # @option [Time] :suspended_at Only applicable when :reserve_username is true def call(account, **options) @account = account - @options = options + @options = { reserve_username: true, reserve_email: true }.merge(options) + + if @account.local? && @account.user_unconfirmed_or_pending? + @options[:reserve_email] = false + @options[:reserve_username] = false + @options[:skip_side_effects] = true + end reject_follows! purge_user! @@ -60,27 +71,39 @@ class SuspendAccountService < BaseService def purge_user! return if !@account.local? || @account.user.nil? - if @options[:including_user] - @options[:destroy] = true if !@account.user_confirmed? || @account.user_pending? - @account.user.destroy - else + if @options[:reserve_email] @account.user.disable! @account.user.invites.where(uses: 0).destroy_all + else + @account.user.destroy end end def purge_content! - distribute_delete_actor! if @account.local? && !@options[:skip_distribution] + distribute_delete_actor! if @account.local? && !@options[:skip_side_effects] @account.statuses.reorder(nil).find_in_batches do |statuses| - BatchedRemoveStatusService.new.call(statuses, skip_side_effects: @options[:destroy]) + statuses.reject! { |status| reported_status_ids.include?(status.id) } if @options[:reserve_username] + BatchedRemoveStatusService.new.call(statuses, skip_side_effects: @options[:skip_side_effects]) + end + + @account.media_attachments.reorder(nil).find_each do |media_attachment| + next if @options[:reserve_username] && reported_status_ids.include?(media_attachment.status_id) + + media_attachment.destroy + end + + @account.polls.reorder(nil).find_each do |poll| + next if @options[:reserve_username] && reported_status_ids.include?(poll.status_id) + + poll.destroy end associations_for_destruction.each do |association_name| destroy_all(@account.public_send(association_name)) end - @account.destroy if @options[:destroy] + @account.destroy unless @options[:reserve_username] end def purge_profile! @@ -88,11 +111,13 @@ class SuspendAccountService < BaseService # there is no point wasting time updating # its values first - return if @options[:destroy] + return unless @options[:reserve_username] @account.silenced_at = nil @account.suspended_at = @options[:suspended_at] || Time.now.utc @account.locked = false + @account.memorial = false + @account.discoverable = false @account.display_name = '' @account.note = '' @account.fields = [] @@ -100,6 +125,7 @@ class SuspendAccountService < BaseService @account.followers_count = 0 @account.following_count = 0 @account.moved_to_account = nil + @account.trust_level = :untrusted @account.avatar.destroy @account.header.destroy @account.save! @@ -135,11 +161,15 @@ class SuspendAccountService < BaseService Account.inboxes - delivery_inboxes end + def reported_status_ids + @reported_status_ids ||= Report.where(target_account: @account).unresolved.pluck(:status_ids).flatten.uniq + end + def associations_for_destruction - if @options[:destroy] - ASSOCIATIONS_ON_SUSPEND + ASSOCIATIONS_ON_DESTROY - else + if @options[:reserve_username] ASSOCIATIONS_ON_SUSPEND + else + ASSOCIATIONS_ON_SUSPEND + ASSOCIATIONS_ON_DESTROY end end end diff --git a/app/services/unallow_domain_service.rb b/app/services/unallow_domain_service.rb index d4387c1a1..bd1ad328d 100644 --- a/app/services/unallow_domain_service.rb +++ b/app/services/unallow_domain_service.rb @@ -3,7 +3,7 @@ class UnallowDomainService < BaseService def call(domain_allow) Account.where(domain: domain_allow.domain).find_each do |account| - SuspendAccountService.new.call(account, destroy: true) + SuspendAccountService.new.call(account, reserve_username: false) end domain_allow.destroy diff --git a/app/workers/admin/suspension_worker.rb b/app/workers/admin/suspension_worker.rb index ae8b24d8c..83c815efd 100644 --- a/app/workers/admin/suspension_worker.rb +++ b/app/workers/admin/suspension_worker.rb @@ -6,6 +6,6 @@ class Admin::SuspensionWorker sidekiq_options queue: 'pull' def perform(account_id, remove_user = false) - SuspendAccountService.new.call(Account.find(account_id), including_user: remove_user) + SuspendAccountService.new.call(Account.find(account_id), reserve_username: true, reserve_email: !remove_user) end end diff --git a/lib/mastodon/accounts_cli.rb b/lib/mastodon/accounts_cli.rb index b16bf2e38..a09a6ab04 100644 --- a/lib/mastodon/accounts_cli.rb +++ b/lib/mastodon/accounts_cli.rb @@ -185,7 +185,7 @@ module Mastodon end say("Deleting user with #{account.statuses_count} statuses, this might take a while...") - SuspendAccountService.new.call(account, including_user: true) + SuspendAccountService.new.call(account, reserve_email: false) say('OK', :green) end @@ -239,7 +239,7 @@ module Mastodon end if [404, 410].include?(code) - SuspendAccountService.new.call(account, destroy: true) unless options[:dry_run] + SuspendAccountService.new.call(account, reserve_username: false) unless options[:dry_run] 1 else # Touch account even during dry run to avoid getting the account into the window again diff --git a/lib/mastodon/domains_cli.rb b/lib/mastodon/domains_cli.rb index c612c2d72..8e52de1c3 100644 --- a/lib/mastodon/domains_cli.rb +++ b/lib/mastodon/domains_cli.rb @@ -42,7 +42,7 @@ module Mastodon end processed, = parallelize_with_progress(scope) do |account| - SuspendAccountService.new.call(account, destroy: true) unless options[:dry_run] + SuspendAccountService.new.call(account, reserve_username: false, skip_side_effects: true) unless options[:dry_run] end DomainBlock.where(domain: domain).destroy_all unless options[:dry_run] diff --git a/spec/controllers/admin/reported_statuses_controller_spec.rb b/spec/controllers/admin/reported_statuses_controller_spec.rb index bd146b795..2a1598123 100644 --- a/spec/controllers/admin/reported_statuses_controller_spec.rb +++ b/spec/controllers/admin/reported_statuses_controller_spec.rb @@ -47,7 +47,7 @@ describe Admin::ReportedStatusesController do it 'removes a status' do allow(RemovalWorker).to receive(:perform_async) subject.call - expect(RemovalWorker).to have_received(:perform_async).with(status_ids.first, redraft: false) + expect(RemovalWorker).to have_received(:perform_async).with(status_ids.first, immediate: true) end end diff --git a/spec/controllers/admin/statuses_controller_spec.rb b/spec/controllers/admin/statuses_controller_spec.rb index 6b06343ef..d9690d83f 100644 --- a/spec/controllers/admin/statuses_controller_spec.rb +++ b/spec/controllers/admin/statuses_controller_spec.rb @@ -65,7 +65,7 @@ describe Admin::StatusesController do it 'removes a status' do allow(RemovalWorker).to receive(:perform_async) subject.call - expect(RemovalWorker).to have_received(:perform_async).with(status_ids.first, redraft: false) + expect(RemovalWorker).to have_received(:perform_async).with(status_ids.first, immediate: true) end end diff --git a/spec/models/form/status_batch_spec.rb b/spec/models/form/status_batch_spec.rb index f9c58c90f..68d84a737 100644 --- a/spec/models/form/status_batch_spec.rb +++ b/spec/models/form/status_batch_spec.rb @@ -41,12 +41,12 @@ describe Form::StatusBatch do it 'call RemovalWorker' do form.save - expect(RemovalWorker).to have_received(:perform_async).with(status.id, redraft: false) + expect(RemovalWorker).to have_received(:perform_async).with(status.id, immediate: true) end it 'do not call RemovalWorker' do form.save - expect(RemovalWorker).not_to have_received(:perform_async).with(another_status.id, redraft: false) + expect(RemovalWorker).not_to have_received(:perform_async).with(another_status.id, immediate: true) end end end -- cgit