diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index a0d71c23c..490cafc3d 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -185,7 +185,7 @@ class ApplicationController < ActionController::Base end end - %i[is_bd_staff can_view_staff_notes can_handle_takedowns].each do |role| + %i[is_bd_staff can_view_staff_notes can_handle_takedowns can_edit_avoid_posting_entries].each do |role| define_method("#{role}_only") do user_access_check("#{role}?") end diff --git a/app/controllers/artists_controller.rb b/app/controllers/artists_controller.rb index 06983d9e2..7f8db910e 100644 --- a/app/controllers/artists_controller.rb +++ b/app/controllers/artists_controller.rb @@ -64,7 +64,7 @@ class ArtistsController < ApplicationController @artist.destroy respond_with(@artist) do |format| format.html do - redirect_to(artists_path, notice: "Artist deleted") + redirect_to(artists_path, notice: @artist.valid? ? "Artist deleted" : @artist.errors.full_messages.join("; ")) end end end diff --git a/app/controllers/avoid_posting_versions_controller.rb b/app/controllers/avoid_posting_versions_controller.rb new file mode 100644 index 000000000..5a887f57c --- /dev/null +++ b/app/controllers/avoid_posting_versions_controller.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class AvoidPostingVersionsController < ApplicationController + respond_to :html, :json + + def index + @avoid_posting_versions = AvoidPostingVersion.search(search_params).paginate(params[:page], limit: params[:limit]) + respond_with(@avoid_posting_versions) + end + + def search_params + permitted_params = %i[updater_name updater_id any_name_matches artist_name artist_id any_other_name_matches group_name is_active] + permitted_params += %i[updater_ip_addr] if CurrentUser.is_admin? + permit_search_params permitted_params + end +end diff --git a/app/controllers/avoid_postings_controller.rb b/app/controllers/avoid_postings_controller.rb new file mode 100644 index 000000000..65c3fd8f1 --- /dev/null +++ b/app/controllers/avoid_postings_controller.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true + +class AvoidPostingsController < ApplicationController + respond_to :html, :json + before_action :can_edit_avoid_posting_entries_only, except: %i[index show] + before_action :load_avoid_posting, only: %i[edit update destroy show delete undelete] + helper_method :search_params + + def index + @avoid_postings = AvoidPosting.search(search_params).paginate(params[:page], limit: params[:limit]) + respond_with(@avoid_postings) + end + + def show + respond_with(@avoid_posting) + end + + def new + @avoid_posting = AvoidPosting.new(avoid_posting_params) + @avoid_posting.artist = Artist.new(avoid_posting_params[:artist_attributes]) + respond_with(@artist) + end + + def edit + end + + def create + @avoid_posting = AvoidPosting.new(avoid_posting_params) + artparams = avoid_posting_params.try(:[], :artist_attributes) + if artparams.present? && (artist = Artist.find_by(name: Artist.normalize_name(artparams[:name]))) + @avoid_posting.artist = artist + notices = [] + if artist.other_names.present? && (artparams.key?(:other_names_string) || artparams.key?(:other_names)) + on = artparams[:other_names_string].try(:split) || artparams[:other_names] + artparams.delete(:other_names_string) + artparams.delete(:other_names) + if on.present? + artparams[:other_names] = (artist.other_names + on).uniq + notices << "Artist already had other names, the provided names were merged into the existing names." + end + end + if artist.group_name.present? && artparams.key?(:group_name) + if artparams[:group_name].blank? + artparams.delete(:group_name) + else + notices << "Artist's original group name was replaced." + end + end + if artist.linked_user_id.present? && artparams.key?(:linked_user_id) + if artparams[:linked_user_id].present? + notices << "Artist is already linked to \"#{artist.linked_user.name}\":/users/#{artist.linked_user_id}, no change was made." + end + artparams.delete(:linked_user_id) + end + notices = notices.join("\n") + # Remove period from last notice + flash[:notice] = notices[0..-2] if notices.present? + artist.update(artparams) + end + @avoid_posting.save + respond_with(@avoid_posting) + end + + def update + @avoid_posting.update(avoid_posting_params) + flash[:notice] = @avoid_posting.valid? ? "Avoid posting entry updated" : @avoid_posting.errors.full_messages.join("; ") + respond_with(@avoid_posting) + end + + def destroy + @avoid_posting.destroy + redirect_to artist_path(@avoid_posting.artist), notice: "Avoid posting entry destroyed" + end + + def delete + @avoid_posting.update(is_active: false) + redirect_to avoid_posting_path(@avoid_posting), notice: "Avoid posting entry deleted" + end + + def undelete + @avoid_posting.update(is_active: true) + redirect_to avoid_posting_path(@avoid_posting), notice: "Avoid posting entry undeleted" + end + + private + + def load_avoid_posting + id = params[:id] + if id =~ /\A\d+\z/ + @avoid_posting = AvoidPosting.find(id) + else + @avoid_posting = AvoidPosting.find_by!(artist_name: id) + end + end + + def search_params + permitted_params = %i[creator_name creator_id any_name_matches artist_id artist_name any_other_name_matches group_name details is_active] + permitted_params += %i[staff_notes] if CurrentUser.is_staff? + permitted_params += %i[creator_ip_addr] if CurrentUser.is_admin? + permit_search_params permitted_params + end + + def avoid_posting_params + permitted_params = %i[details staff_notes is_active] + permitted_params += [artist_attributes: [:id, :name, :other_names_string, :group_name, :linked_user_id, { other_names: [] }]] + + params.fetch(:avoid_posting, {}).permit(permitted_params) + end +end diff --git a/app/controllers/static_controller.rb b/app/controllers/static_controller.rb index 64161eb14..1290c18d1 100644 --- a/app/controllers/static_controller.rb +++ b/app/controllers/static_controller.rb @@ -2,19 +2,23 @@ class StaticController < ApplicationController def privacy - @page = WikiPage.find_by(title: "e621:privacy_policy") + @page = format_wiki_page("e621:privacy_policy") end def terms_of_service - @page = WikiPage.find_by(title: "e621:terms_of_service") + @page = format_wiki_page("e621:terms_of_service") end def contact - @page = WikiPage.find_by(title: "e621:contact") + @page = format_wiki_page("e621:contact") end def takedown - @page = WikiPage.find_by(title: "e621:takedown") + @page = format_wiki_page("e621:takedown") + end + + def avoid_posting + @page = format_wiki_page("e621:avoid_posting_notice") end def not_found @@ -64,4 +68,12 @@ class StaticController < ApplicationController redirect_to(Danbooru.config.discord_site + user_hash, allow_other_host: true) end end + + private + + def format_wiki_page(name) + wiki = WikiPage.find_by(title: name) + return WikiPage.new(body: "Wiki page \"#{name}\" not found.") if wiki.blank? + wiki + end end diff --git a/app/decorators/mod_action_decorator.rb b/app/decorators/mod_action_decorator.rb index 40cb0a8b3..beda3b46a 100644 --- a/app/decorators/mod_action_decorator.rb +++ b/app/decorators/mod_action_decorator.rb @@ -65,6 +65,18 @@ class ModActionDecorator < ApplicationDecorator when "artist_user_unlinked" "Unlinked #{user} from artist ##{vals['artist_page']}" + ### Avoid Posting ### + when "avoid_posting_create" + "Created avoid posting entry for artist \"#{vals['artist_name']}\":/artists/show_or_new?name=#{vals['artist_name']}" + when "avoid_posting_update" + "Updated avoid posting entry for artist \"#{vals['artist_name']}\":/artists/show_or_new?name=#{vals['artist_name']}" + when "avoid_posting_destroy" + "Destroyed avoid posting entry for artist \"#{vals['artist_name']}\":/artists/show_or_new?name=#{vals['artist_name']}" + when "avoid_posting_delete" + "Deleted avoid posting entry for artist \"#{vals['artist_name']}\":/artists/show_or_new?name=#{vals['artist_name']}" + when "avoid_posting_undelete" + "Undeleted avoid posting entry for artist \"#{vals['artist_name']}\":/artists/show_or_new?name=#{vals['artist_name']}" + ### User ### when "user_delete" diff --git a/app/helpers/artists_helper.rb b/app/helpers/artists_helper.rb index 1df3428c2..b64d35964 100644 --- a/app/helpers/artists_helper.rb +++ b/app/helpers/artists_helper.rb @@ -1,21 +1,22 @@ # frozen_string_literal: true module ArtistsHelper - def link_to_artist(name) + def link_to_artist(name, hide_new_notice: false) artist = Artist.find_by(name: name) if artist link_to(artist.name, artist_path(artist)) else link = link_to(name, new_artist_path(artist: { name: name })) + return link.html_safe if hide_new_notice notice = tag.span("*", class: "new-artist", title: "No artist with this name currently exists.") "#{link} #{notice}".html_safe end end - def link_to_artists(names) + def link_to_artists(names, hide_new_notice: false) names.map do |name| - link_to_artist(name.downcase) + link_to_artist(name.downcase, hide_new_notice: hide_new_notice) end.join(", ").html_safe end end diff --git a/app/helpers/avoid_posting_helper.rb b/app/helpers/avoid_posting_helper.rb new file mode 100644 index 000000000..2a437e1a4 --- /dev/null +++ b/app/helpers/avoid_posting_helper.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module AvoidPostingHelper + def format_avoid_posting_list + avoid_postings = AvoidPosting.active.joins(:artist).order("artists.name ASC").group_by(&:header) + text = "" + avoid_postings.each do |header, entries| + text += "h2. #{header} [##{anchor(header)}]\n" + entries.each do |dnp| + text += "* #{dnp.all_names}" + if dnp.pretty_details.present? + text += " - #{dnp.pretty_details}" + end + text += "\n" + end + text += "\n" + end + format_text(text) + end + + private + + def anchor(header) + case header + when "#" + "number" + when "?" + "other" + else + header.downcase + end + end +end diff --git a/app/javascript/src/styles/base.scss b/app/javascript/src/styles/base.scss index 8d116788d..27cc3134a 100644 --- a/app/javascript/src/styles/base.scss +++ b/app/javascript/src/styles/base.scss @@ -38,6 +38,7 @@ @import "specific/admin_dashboards.scss"; @import "specific/api_keys.scss"; @import "specific/artists.scss"; +@import "specific/avoid_posting.scss"; @import "specific/bans.scss"; @import "specific/blips.scss"; @import "specific/comments.scss"; diff --git a/app/javascript/src/styles/specific/avoid_posting.scss b/app/javascript/src/styles/specific/avoid_posting.scss new file mode 100644 index 000000000..43aeaa806 --- /dev/null +++ b/app/javascript/src/styles/specific/avoid_posting.scss @@ -0,0 +1,87 @@ +#c-static #a-avoid-posting { + margin-left: 0.25em; + + #avoid-posting-list { + h2 { + margin-top: 1.25em; + font-size: 1.8em; + line-height: 1em; + margin-bottom: .25em; + } + + ul { + margin-left: 1em; + + li { + list-style-type: disc; + } + } + } +} + +#c-avoid-postings #a-index, #c-avoid-posting-versions #a-index { + + table.dnp-list { + td.dnp-artist { + grid-area: artist; + + .dnp-artist-names { + display: flex; + flex-wrap: wrap; + gap: 0.25em 0.5em; + span { text-wrap: pretty; } + } + } + td.dnp-details { + grid-area: details; + width: 60%; + + span.avoid-posting-staff-notes { + display: flex; + flex-flow: column; + margin-top: 0.5em; + } + } + td.dnp-creator { + grid-area: creator; + text-wrap: nowrap; + } + td.dnp-status { + grid-area: status; + } + td.dnp-links { + grid-area: links; + white-space: nowrap; + } + } + + @media screen and (max-width: 800px) { + table.dnp-list { + thead { + display: block; + tr { + display: flex; + justify-content: space-around; + th.dnp-links { display: none; } + } + } + tbody { + display: block; + tr { + display: grid; + width: 100%; + grid-template-columns: 1fr min-content min-content; + grid-template-areas: + "artist status creator links" + "details details details details"; + grid-column-gap: 1em; + + td.dnp-details { + width: unset; + padding-top: 0; + } + } + } + } + } +} diff --git a/app/javascript/src/styles/specific/posts.scss b/app/javascript/src/styles/specific/posts.scss index 660a86b3d..96afaca5e 100644 --- a/app/javascript/src/styles/specific/posts.scss +++ b/app/javascript/src/styles/specific/posts.scss @@ -210,7 +210,7 @@ div#c-posts { #image-extra-controls { justify-content: center; } - + .fav-buttons, #image-download-link { .button > i { @@ -328,6 +328,34 @@ div#c-posts { } } + div#avoid-posting-notice { + font-size: 1.25rem; + line-height: 1.5rem; + padding: $padding-025 $padding-050; + + background-color: themed("color-section"); + border: 1px solid themed("color-foreground"); + + /*.artist-separator { + color: var(--color-text-muted); + }*/ + + ul { + list-style: disc; + } + + li { + .artist, .separator, .details { + display: table-cell; + } + + .separator { + color: var(--color-text-muted); + padding: 0 0.3rem; + } + } + } + div.quick-mod { .quick-mod-group { @@ -535,7 +563,7 @@ body[data-user-can-approve-posts="true"] .notice { @media only screen and (min-width: 550px) { display: flex; } - + // Overwrite some DText styles to make the header look better blockquote { border: 0; @@ -546,25 +574,25 @@ body[data-user-can-approve-posts="true"] .notice { } .flag-dialog-body { - + // Option label label { font-weight: normal; font-size: 1rem; cursor: pointer; } - + // Option explanation div.styled-dtext { margin: 0.125rem 1.25rem; filter: brightness(85%); } - + // Align label with the explanation input[type="radio"] { width: 1rem; } - + // Post ID input form.simple_form div.input { margin: -0.5rem 0 0 1.25rem; @@ -576,7 +604,7 @@ body[data-user-can-approve-posts="true"] .notice { &.post_flag_parent_id { display: flex } span.error { margin-left: 1rem; } } - + hr { margin: 0.75rem 1.25rem; } h3 { margin: 0.5rem 1.25rem; diff --git a/app/logical/upload_service.rb b/app/logical/upload_service.rb index 20e5eb61e..a0423331e 100644 --- a/app/logical/upload_service.rb +++ b/app/logical/upload_service.rb @@ -65,7 +65,7 @@ class UploadService p.has_cropped = upload.is_image? p.duration = upload.video_duration(upload.file.path) - if !upload.uploader.can_upload_free? || upload.upload_as_pending? + if !upload.uploader.can_upload_free? || (!upload.uploader.can_approve_posts? && p.avoid_posting_artists.any?) || upload.upload_as_pending? p.is_pending = true end end diff --git a/app/models/artist.rb b/app/models/artist.rb index 67a69437b..05b1feba9 100644 --- a/app/models/artist.rb +++ b/app/models/artist.rb @@ -7,8 +7,9 @@ class Artist < ApplicationRecord array_attribute :other_names belongs_to_creator - before_validation :normalize_name - before_validation :normalize_other_names + before_validation :normalize_name, unless: :destroyed? + before_validation :normalize_other_names, unless: :destroyed? + before_validation :validate_protected_properties_not_changed, if: :dnp_restricted? validate :validate_user_can_edit validate :wiki_page_not_locked validate :user_not_limited @@ -28,6 +29,8 @@ class Artist < ApplicationRecord has_one :wiki_page, foreign_key: "title", primary_key: "name" has_one :tag_alias, foreign_key: "antecedent_name", primary_key: "name" has_one :tag, foreign_key: "name", primary_key: "name" + has_one :avoid_posting, -> { active } + has_one :inactive_dnp, -> { deleted }, class_name: "AvoidPosting" belongs_to :linked_user, class_name: "User", optional: true attribute :notes, :string @@ -496,6 +499,29 @@ class Artist < ApplicationRecord end end + module AvoidPostingMethods + def validate_protected_properties_not_changed + errors.add(:name, "cannot be changed while the artist is on the avoid posting list") if will_save_change_to_name? + errors.add(:group_name, "cannot be changed while the artist is on the avoid posting list") if will_save_change_to_group_name? + errors.add(:other_names, "cannot be changed while the artist is on the avoid posting list") if will_save_change_to_other_names? + throw(:abort) if errors.any? + end + + def is_dnp? + avoid_posting.present? + end + + def has_any_dnp? + is_dnp? || inactive_dnp.present? + end + + def dnp_restricted? + is_dnp? && !CurrentUser.can_edit_avoid_posting_entries? + end + end + + include AvoidPostingMethods + include UrlMethods include NameMethods include GroupMethods @@ -505,8 +531,10 @@ class Artist < ApplicationRecord include LockMethods extend SearchMethods + # due to technical limitations (foreign keys), artists with any + # dnp entry (active or inactive) cannot be deleted def deletable_by?(user) - user.is_admin? + !has_any_dnp? && user.is_admin? end def editable_by?(user) diff --git a/app/models/avoid_posting.rb b/app/models/avoid_posting.rb new file mode 100644 index 000000000..410759a5a --- /dev/null +++ b/app/models/avoid_posting.rb @@ -0,0 +1,133 @@ +# frozen_string_literal: true + +class AvoidPosting < ApplicationRecord + belongs_to_creator + belongs_to_updater + belongs_to :artist + has_many :versions, -> { order("avoid_posting_versions.id ASC") }, class_name: "AvoidPostingVersion", dependent: :destroy + validates :artist_id, uniqueness: { message: "already has an avoid posting entry" } + after_create :log_create + after_create :create_version + after_update :log_update, if: :saved_change_to_watched_attributes? + after_update :create_version, if: :saved_change_to_watched_attributes? + after_destroy :log_destroy + validates_associated :artist + accepts_nested_attributes_for :artist + + scope :active, -> { where(is_active: true) } + scope :deleted, -> { where(is_active: false) } + + module LogMethods + def log_create + ModAction.log(:avoid_posting_create, { id: id, artist_name: artist_name }) + end + + def saved_change_to_watched_attributes? + saved_change_to_is_active? || saved_change_to_details? || saved_change_to_staff_notes? + end + + def log_update + entry = { id: id, artist_name: artist_name } + if saved_change_to_is_active? + action = is_active? ? :avoid_posting_undelete : :avoid_posting_delete + ModAction.log(action, entry) + # only log delete/undelete if only is_active changed (checking for 2 because of updated_at) + return if previous_changes.length == 2 + end + entry = entry.merge({ details: details, old_details: details_before_last_save }) if saved_change_to_details? + entry = entry.merge({ staff_notes: staff_notes, old_staff_notes: staff_notes_before_last_save }) if saved_change_to_staff_notes? + + ModAction.log(:avoid_posting_update, entry) + end + + def log_destroy + ModAction.log(:avoid_posting_destroy, { id: id, artist_name: artist_name }) + end + end + + def create_version + AvoidPostingVersion.create({ + avoid_posting: self, + details: details, + staff_notes: staff_notes, + is_active: is_active, + }) + end + + def status + if is_active? + "Active" + else + "Deleted" + end + end + + module ArtistMethods + delegate :group_name, :other_names, :other_names_string, :linked_user_id, :linked_user, :any_name_matches, to: :artist + delegate :name, to: :artist, prefix: true, allow_nil: true + end + + module ApiMethods + def hidden_attributes + attr = super + attr += %i[staff_notes] unless CurrentUser.is_staff? + attr + end + end + + module SearchMethods + def for_artist(name) + active.find_by(artist_name: name) + end + + def artist_search(params) + Artist.search(params.slice(:any_name_matches, :any_other_name_matches).merge({ id: params[:artist_id], name: params[:artist_name] })) + end + + def search(params) + q = super + artist_keys = %i[artist_id artist_name any_name_matches any_other_name_matches] + q = q.joins(:artist).merge(artist_search(params)) if artist_keys.any? { |key| params.key?(key) } + + if params[:is_active].present? + q = q.active if params[:is_active].to_s.truthy? + q = q.deleted if params[:is_active].to_s.falsy? + else + q = q.active + end + + q = q.attribute_matches(:details, params[:details]) + q = q.attribute_matches(:staff_notes, params[:staff_notes]) + q = q.where_user(:creator_id, :creator, params) + q = q.where("creator_ip_addr <<= ?", params[:creator_ip_addr]) if params[:creator_ip_addr].present? + q.apply_basic_order(params) + end + end + + def header + first = artist_name[0] + if first =~ /\d/ + "#" + elsif first =~ /[a-z]/ + first.upcase + else + "?" + end + end + + def all_names + return artist_name.tr("_", " ") if other_names.blank? + "#{artist_name} / #{other_names.join(' / ')}".tr("_", " ") + end + + def pretty_details + return details if details.present? + return "Only the artist is allowed to post." if linked_user_id.present? + "" + end + + include LogMethods + include ApiMethods + include ArtistMethods + extend SearchMethods +end diff --git a/app/models/avoid_posting_version.rb b/app/models/avoid_posting_version.rb new file mode 100644 index 000000000..613e4aee0 --- /dev/null +++ b/app/models/avoid_posting_version.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +class AvoidPostingVersion < ApplicationRecord + belongs_to_updater + belongs_to :avoid_posting + has_one :artist, through: :avoid_posting + delegate :artist_id, :artist_name, to: :avoid_posting + + def status + if is_active? + "Active" + else + "Deleted" + end + end + + def previous + AvoidPostingVersion.joins(:avoid_posting).where(id: ...id).order(id: :desc).first + end + + module ApiMethods + def hidden_attributes + attr = super + attr += %i[staff_notes] unless CurrentUser.is_janitor? + attr + end + end + + module SearchMethods + def artist_search(params) + Artist.search(params.slice(:any_name_matches, :any_other_name_matches).merge({ id: params[:artist_id], name: params[:artist_name] })) + end + + def search(params) + q = super + artist_keys = %i[artist_id artist_name any_name_matches any_other_name_matches] + q = q.joins(:artist).merge(artist_search(params)) if artist_keys.any? { |key| params.key?(key) } + + q = q.attribute_matches(:is_active, params[:is_active]) + q = q.where_user(:updater_id, :updater, params) + q = q.where("updater_ip_addr <<= ?", params[:updater_ip_addr]) if params[:updater_ip_addr].present? + q.apply_basic_order(params) + end + end + + include ApiMethods + extend SearchMethods +end diff --git a/app/models/mod_action.rb b/app/models/mod_action.rb index 6ac0a579d..33ce523ce 100644 --- a/app/models/mod_action.rb +++ b/app/models/mod_action.rb @@ -11,6 +11,11 @@ class ModAction < ApplicationRecord :artist_page_unlock, :artist_user_linked, :artist_user_unlinked, + :avoid_posting_create, + :avoid_posting_update, + :avoid_posting_delete, + :avoid_posting_undelete, + :avoid_posting_destroy, :blip_delete, :blip_hide, :blip_unhide, diff --git a/app/models/post.rb b/app/models/post.rb index 66f655dde..53a53f271 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -1754,9 +1754,12 @@ class Post < ApplicationRecord save end + def artist_tags + tags.select { |t| t.category == Tag.categories.artist } + end + def uploader_linked_artists - linked_artists ||= tags.select { |t| t.category == Tag.categories.artist }.filter_map(&:artist) - linked_artists.select { |artist| artist.linked_user_id == uploader_id } + artist_tags.filter_map(&:artist).select { |artist| artist.linked_user_id == uploader_id } end def flaggable_for_guidelines? @@ -1768,4 +1771,8 @@ class Post < ApplicationRecord def visible_comment_count(_user) comment_count end + + def avoid_posting_artists + AvoidPosting.active.joins(:artist).where("artists.name": artist_tags.map(&:name)) + end end diff --git a/app/models/user.rb b/app/models/user.rb index ba9338c61..b0f84cc9c 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -319,6 +319,10 @@ class User < ApplicationRecord is_bd_staff end + def is_staff? + is_janitor? + end + def is_approver? can_approve_posts? end @@ -508,13 +512,17 @@ class User < ApplicationRecord end def can_view_staff_notes? - is_janitor? + is_staff? end def can_handle_takedowns? is_bd_staff? end + def can_edit_avoid_posting_entries? + is_bd_staff? + end + def can_undo_post_versions? is_member? end diff --git a/app/views/artists/_form.html.erb b/app/views/artists/_form.html.erb index 6e394c0d5..acedfd1e9 100644 --- a/app/views/artists/_form.html.erb +++ b/app/views/artists/_form.html.erb @@ -2,18 +2,23 @@ <% if @artist.new_record? %> <%# FIXME: Probably wrong type in the production database %> <%= f.input :name, as: :string, autocomplete: "tag" %> - <% elsif CurrentUser.user.is_janitor? %> - <%= f.input :name, autocomplete: "tag", hint: "Change to rename this artist entry and its wiki page" %> + <% elsif !@artist.dnp_restricted? && CurrentUser.user.is_janitor? %> + <%= f.input :name, autocomplete: "tag", hint: "Change to rename this artist entry and its wiki page." %> <% else %>
<%= @artist.name %>
+ <% if @artist.dnp_restricted? %> +Name, other names, and group name cannot be edited for artists on the <%= link_to "Avoid Posting", avoid_postings_path %> list.
+ <% end %> <% end %> <% if CurrentUser.is_janitor? %> <%= f.input :linked_user_id, label: "Linked User ID" %> <%= f.input :is_locked, label: "Locked" %> <% end %> - <%= f.input :other_names_string, label: "Other names", hint: 'Separate names with spaces, not commas. Use underscores for spaces inside names. Limit 25.', input_html: { size: "80" } %> - <%= f.input :group_name %> - <%= f.input :url_string, :label => "URLs", as: :text, input_html: { size: "80x10", value: params.dig(:artist, :url_string) || @artist.urls.join("\n")}, hint: "You can prefix a URL with - to mark it as dead." %> + <% if @artist.new_record? || !@artist.dnp_restricted? %> + <%= f.input :other_names_string, label: "Other names", hint: 'Separate names with spaces, not commas. Use underscores for spaces inside names. Limit 25.', input_html: { size: "80" } %> + <%= f.input :group_name %> + <% end %> + <%= f.input :url_string, label: "URLs", as: :text, input_html: { size: "80x10", value: params.dig(:artist, :url_string) || @artist.urls.join("\n")}, hint: "You can prefix a URL with - to mark it as dead." %> <% if @artist.is_note_locked? %>Artist is note locked. Notes cannot be edited.
diff --git a/app/views/artists/_secondary_links.html.erb b/app/views/artists/_secondary_links.html.erb index 5ba797f34..1875300c4 100644 --- a/app/views/artists/_secondary_links.html.erb +++ b/app/views/artists/_secondary_links.html.erb @@ -1,7 +1,7 @@ <% content_for(:secondary_links) do %><%= link_to "Open Avoid Posting entry", avoid_posting_path(@artist.avoid_posting) %>
+Artist | +Details | +Status | +Updater | ++ |
---|---|---|---|---|
+ <%= link_to avoid_posting_version.artist_name, artist_path(avoid_posting_version.artist) %> + | ++ + <% if previous.present? %> + <% if previous.details == avoid_posting_version.details %> + (no changes) + <% elsif avoid_posting_version.details.blank? %> + (cleared) + <% else %> + <%= avoid_posting_version.details %> + <% end %> + <% else %> + <%= avoid_posting_version.details %> + <% end %> + + <% if CurrentUser.is_staff? %> + + <% if previous.present? && previous.staff_notes != avoid_posting_version.staff_notes %> + Staff Notes + + <% if avoid_posting_version.staff_notes.blank? %> + (cleared) + <% else %> + <%= avoid_posting_version.staff_notes %> + <% end %> + + <% end %> + + <% end %> + | ++ <%= avoid_posting_version.status %> + | +
+ <%= link_to_user avoid_posting_version.updater %>
+ <%= link_to "ยป", avoid_posting_versions_path(search: { updater_name: avoid_posting_version.updater_name }) %>
+ + <%= compact_time(avoid_posting_version.updated_at) %> + <% if CurrentUser.is_admin? %> + (<%= link_to_ip avoid_posting_version.updater_ip_addr %>) + <% end %> + + |
+ + <%= link_to "Show", avoid_posting_path(avoid_posting_version.avoid_posting) %> + <% if CurrentUser.can_edit_avoid_posting_entries? %> + | <%= link_to "Edit", edit_avoid_posting_path(avoid_posting_version.avoid_posting) %> + <% end %> + | +
Artist | +Details | + <% if search_params.key?(:is_active) %> +Status | + <% end %> + <% if search_params.key?(:creator_id) || search_params.key?(:creator_name) %> +Creator | + <% end %> ++ |
---|---|---|---|---|
+ + <%= link_to avoid_posting.artist_name, artist_path(avoid_posting.artist) %> + <% if avoid_posting.other_names.present? %> + (<%= link_to_artists(avoid_posting.other_names, hide_new_notice: true) %>) + <% end %> + + | ++ <%= format_text(avoid_posting.details, inline: true) %> + <% if CurrentUser.is_staff? && avoid_posting.staff_notes.present? %> + + Staff Notes + <%= format_text(avoid_posting.staff_notes, inline: true) %> + + <% end %> + | + <% if search_params.key?(:is_active) %> ++ <%= avoid_posting.status %> + | + <% end %> + <% if search_params.key?(:creator_id) || search_params.key?(:creator_name) %> ++ <%= link_to_user avoid_posting.creator %> + | + <% end %> ++ <%= link_to "Show", avoid_posting_path(avoid_posting) %> + <% if CurrentUser.can_edit_avoid_posting_entries? %> + | <%= link_to "Edit", edit_avoid_posting_path(avoid_posting) %> + | <%= link_to "Delete", delete_avoid_posting_path(avoid_posting), method: :put, data: { confirm: "Are you sure you want to delete this avoid posting?" } %> + <% end %> + | +
This post has <%= link_to "pending replacements.", post_replacements_path(search: {post_id: @post.id}) %>
diff --git a/app/views/static/avoid_posting.html.erb b/app/views/static/avoid_posting.html.erb new file mode 100644 index 000000000..dc9bbff60 --- /dev/null +++ b/app/views/static/avoid_posting.html.erb @@ -0,0 +1,15 @@ +Make sure you're not posting something on
- the <%= link_to "Avoid Posting List", help_page_path(id: 'avoid_posting') %>
+ the <%= link_to "Avoid Posting List", avoid_postings_path %>
Review the <%= link_to "Uploading Guidelines", help_page_path(id: "uploading_guidelines") %>
Unsure what to tag your post
diff --git a/config/danbooru_default_config.rb b/config/danbooru_default_config.rb
index fd3cadda5..9d30b5683 100644
--- a/config/danbooru_default_config.rb
+++ b/config/danbooru_default_config.rb
@@ -460,7 +460,7 @@ module Danbooru
},
{
name: "dnp_artist",
- reason: "The artist of this post is on the [[avoid_posting|avoid posting list]]",
+ reason: "The artist of this post is on the \"avoid posting list\":/avoid_postings",
text: "Certain artists have requested that their work is not to be published on this site, and were granted [[avoid_posting|Do Not Post]] status.\nSometimes, that status comes with conditions; see [[conditional_dnp]] for more information",
},
{
@@ -527,7 +527,7 @@ module Danbooru
"Traced artwork",
"Traced artwork (post #%PARENT_ID%)",
"Takedown #%OTHER_ID%",
- "The artist of this post is on the [[avoid_posting|avoid posting list]]",
+ "The artist of this post is on the \"avoid posting list\":/avoid_postings",
"[[conditional_dnp|Conditional DNP]] (Only the artist is allowed to post)",
"[[conditional_dnp|Conditional DNP]] (%OTHER_ID%)",
]
diff --git a/config/routes.rb b/config/routes.rb
index e982b8d82..3d529365d 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -76,6 +76,16 @@ Rails.application.routes.draw do
end
end
+ resources :avoid_postings, constraints: id_name_constraint do
+ member do
+ put :delete
+ put :undelete
+ end
+
+ end
+
+ resources :avoid_posting_versions, only: %i[index]
+
resources :tickets, except: %i[destroy] do
member do
post :claim
@@ -448,6 +458,7 @@ Rails.application.routes.draw do
post "/static/discord" => "static#discord", as: "discord_post"
get "/static/toggle_mobile_mode" => "static#disable_mobile_mode", as: "disable_mobile_mode"
get "/static/theme" => "static#theme", as: "theme"
+ get "/static/avoid_posting" => "static#avoid_posting", as: "avoid_posting_static"
get "/meta_searches/tags" => "meta_searches#tags", :as => "meta_searches_tags"
root :to => "static#home"
diff --git a/db/fixes/118_create_avoid_postings_from_implications.rb b/db/fixes/118_create_avoid_postings_from_implications.rb
new file mode 100755
index 000000000..df245f897
--- /dev/null
+++ b/db/fixes/118_create_avoid_postings_from_implications.rb
@@ -0,0 +1,19 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+require File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "config", "environment"))
+
+CurrentUser.as_system do
+ TagImplication.order(created_at: :asc).approved.where(consequent_name: %w[avoid_posting conditional_dnp]).find_each do |implication|
+ artist = Artist.find_or_create_by!(name: implication.antecedent_name)
+ dnp = CurrentUser.scoped(implication.creator, implication.creator_ip_addr) do
+ AvoidPosting.create(artist: artist, created_at: implication.created_at, updated_at: implication.created_at)
+ end
+ if dnp.valid?
+ puts artist.name
+ else
+ puts "Failed to create dnp for #{artist.name}"
+ puts dnp.errors.full_messages
+ end
+ end
+end
diff --git a/db/migrate/20240103002040_create_avoid_postings.rb b/db/migrate/20240103002040_create_avoid_postings.rb
new file mode 100644
index 000000000..9e63687a5
--- /dev/null
+++ b/db/migrate/20240103002040_create_avoid_postings.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class CreateAvoidPostings < ActiveRecord::Migration[7.0]
+ def change
+ create_table(:avoid_postings) do |t|
+ t.references(:creator, foreign_key: { to_table: :users }, null: false)
+ t.references(:updater, foreign_key: { to_table: :users }, null: false)
+ t.references(:artist, foreign_key: true, null: false, index: { unique: true })
+ t.inet(:creator_ip_addr, null: false)
+ t.inet(:updater_ip_addr, null: false)
+ t.string(:details, null: false, default: "")
+ t.string(:staff_notes, null: false, default: "")
+ t.boolean(:is_active, null: false, default: true)
+ t.timestamps
+ end
+ end
+end
diff --git a/db/migrate/20240103002049_create_avoid_posting_versions.rb b/db/migrate/20240103002049_create_avoid_posting_versions.rb
new file mode 100644
index 000000000..acbdd5ed3
--- /dev/null
+++ b/db/migrate/20240103002049_create_avoid_posting_versions.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class CreateAvoidPostingVersions < ActiveRecord::Migration[7.0]
+ def change
+ create_table(:avoid_posting_versions) do |t|
+ t.references(:updater, foreign_key: { to_table: :users }, null: false)
+ t.references(:avoid_posting, foreign_key: { to_table: :avoid_postings }, null: false)
+ t.inet(:updater_ip_addr, null: false)
+ t.string(:details, null: false, default: "")
+ t.string(:staff_notes, null: false, default: "")
+ t.boolean(:is_active, null: false, default: true)
+ t.datetime(:updated_at, null: false)
+ end
+ end
+end
diff --git a/db/structure.sql b/db/structure.sql
index 87cb2f052..dae4db36a 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -202,6 +202,79 @@ CREATE SEQUENCE public.artists_id_seq
ALTER SEQUENCE public.artists_id_seq OWNED BY public.artists.id;
+--
+-- Name: avoid_posting_versions; Type: TABLE; Schema: public; Owner: -
+--
+
+CREATE TABLE public.avoid_posting_versions (
+ id bigint NOT NULL,
+ updater_id bigint NOT NULL,
+ avoid_posting_id bigint NOT NULL,
+ updater_ip_addr inet NOT NULL,
+ details character varying DEFAULT ''::character varying NOT NULL,
+ staff_notes character varying DEFAULT ''::character varying NOT NULL,
+ is_active boolean DEFAULT true NOT NULL,
+ updated_at timestamp(6) without time zone NOT NULL
+);
+
+
+--
+-- Name: avoid_posting_versions_id_seq; Type: SEQUENCE; Schema: public; Owner: -
+--
+
+CREATE SEQUENCE public.avoid_posting_versions_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+
+--
+-- Name: avoid_posting_versions_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
+--
+
+ALTER SEQUENCE public.avoid_posting_versions_id_seq OWNED BY public.avoid_posting_versions.id;
+
+
+--
+-- Name: avoid_postings; Type: TABLE; Schema: public; Owner: -
+--
+
+CREATE TABLE public.avoid_postings (
+ id bigint NOT NULL,
+ creator_id bigint NOT NULL,
+ updater_id bigint NOT NULL,
+ artist_id bigint NOT NULL,
+ creator_ip_addr inet NOT NULL,
+ updater_ip_addr inet NOT NULL,
+ details character varying DEFAULT ''::character varying NOT NULL,
+ staff_notes character varying DEFAULT ''::character varying NOT NULL,
+ is_active boolean DEFAULT true NOT NULL,
+ created_at timestamp(6) without time zone NOT NULL,
+ updated_at timestamp(6) without time zone NOT NULL
+);
+
+
+--
+-- Name: avoid_postings_id_seq; Type: SEQUENCE; Schema: public; Owner: -
+--
+
+CREATE SEQUENCE public.avoid_postings_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+
+--
+-- Name: avoid_postings_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
+--
+
+ALTER SEQUENCE public.avoid_postings_id_seq OWNED BY public.avoid_postings.id;
+
+
--
-- Name: bans; Type: TABLE; Schema: public; Owner: -
--
@@ -2408,6 +2481,20 @@ ALTER TABLE ONLY public.artist_versions ALTER COLUMN id SET DEFAULT nextval('pub
ALTER TABLE ONLY public.artists ALTER COLUMN id SET DEFAULT nextval('public.artists_id_seq'::regclass);
+--
+-- Name: avoid_posting_versions id; Type: DEFAULT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.avoid_posting_versions ALTER COLUMN id SET DEFAULT nextval('public.avoid_posting_versions_id_seq'::regclass);
+
+
+--
+-- Name: avoid_postings id; Type: DEFAULT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.avoid_postings ALTER COLUMN id SET DEFAULT nextval('public.avoid_postings_id_seq'::regclass);
+
+
--
-- Name: bans id; Type: DEFAULT; Schema: public; Owner: -
--
@@ -2840,6 +2927,22 @@ ALTER TABLE ONLY public.artists
ADD CONSTRAINT artists_pkey PRIMARY KEY (id);
+--
+-- Name: avoid_posting_versions avoid_posting_versions_pkey; Type: CONSTRAINT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.avoid_posting_versions
+ ADD CONSTRAINT avoid_posting_versions_pkey PRIMARY KEY (id);
+
+
+--
+-- Name: avoid_postings avoid_postings_pkey; Type: CONSTRAINT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.avoid_postings
+ ADD CONSTRAINT avoid_postings_pkey PRIMARY KEY (id);
+
+
--
-- Name: bans bans_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
@@ -3410,6 +3513,41 @@ CREATE INDEX index_artists_on_name_trgm ON public.artists USING gin (name public
CREATE INDEX index_artists_on_other_names ON public.artists USING gin (other_names);
+--
+-- Name: index_avoid_posting_versions_on_avoid_posting_id; Type: INDEX; Schema: public; Owner: -
+--
+
+CREATE INDEX index_avoid_posting_versions_on_avoid_posting_id ON public.avoid_posting_versions USING btree (avoid_posting_id);
+
+
+--
+-- Name: index_avoid_posting_versions_on_updater_id; Type: INDEX; Schema: public; Owner: -
+--
+
+CREATE INDEX index_avoid_posting_versions_on_updater_id ON public.avoid_posting_versions USING btree (updater_id);
+
+
+--
+-- Name: index_avoid_postings_on_artist_id; Type: INDEX; Schema: public; Owner: -
+--
+
+CREATE UNIQUE INDEX index_avoid_postings_on_artist_id ON public.avoid_postings USING btree (artist_id);
+
+
+--
+-- Name: index_avoid_postings_on_creator_id; Type: INDEX; Schema: public; Owner: -
+--
+
+CREATE INDEX index_avoid_postings_on_creator_id ON public.avoid_postings USING btree (creator_id);
+
+
+--
+-- Name: index_avoid_postings_on_updater_id; Type: INDEX; Schema: public; Owner: -
+--
+
+CREATE INDEX index_avoid_postings_on_updater_id ON public.avoid_postings USING btree (updater_id);
+
+
--
-- Name: index_bans_on_banner_id; Type: INDEX; Schema: public; Owner: -
--
@@ -4427,6 +4565,14 @@ ALTER TABLE ONLY public.staff_audit_logs
ADD CONSTRAINT fk_rails_02329e5ef9 FOREIGN KEY (user_id) REFERENCES public.users(id);
+--
+-- Name: avoid_posting_versions fk_rails_1d1f54e17a; Type: FK CONSTRAINT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.avoid_posting_versions
+ ADD CONSTRAINT fk_rails_1d1f54e17a FOREIGN KEY (updater_id) REFERENCES public.users(id);
+
+
--
-- Name: blips fk_rails_23e7479aac; Type: FK CONSTRAINT; Schema: public; Owner: -
--
@@ -4443,6 +4589,14 @@ ALTER TABLE ONLY public.tickets
ADD CONSTRAINT fk_rails_45cd696dba FOREIGN KEY (accused_id) REFERENCES public.users(id);
+--
+-- Name: avoid_posting_versions fk_rails_4c48affea5; Type: FK CONSTRAINT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.avoid_posting_versions
+ ADD CONSTRAINT fk_rails_4c48affea5 FOREIGN KEY (avoid_posting_id) REFERENCES public.avoid_postings(id);
+
+
--
-- Name: user_feedback fk_rails_9329a36823; Type: FK CONSTRAINT; Schema: public; Owner: -
--
@@ -4467,6 +4621,14 @@ ALTER TABLE ONLY public.favorites
ADD CONSTRAINT fk_rails_a7668ef613 FOREIGN KEY (user_id) REFERENCES public.users(id);
+--
+-- Name: avoid_postings fk_rails_b2ebf2bc30; Type: FK CONSTRAINT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.avoid_postings
+ ADD CONSTRAINT fk_rails_b2ebf2bc30 FOREIGN KEY (artist_id) REFERENCES public.artists(id);
+
+
--
-- Name: staff_notes fk_rails_bab7e2d92a; Type: FK CONSTRAINT; Schema: public; Owner: -
--
@@ -4483,6 +4645,14 @@ ALTER TABLE ONLY public.post_events
ADD CONSTRAINT fk_rails_bd327ccee6 FOREIGN KEY (creator_id) REFERENCES public.users(id);
+--
+-- Name: avoid_postings fk_rails_cccc6419c8; Type: FK CONSTRAINT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.avoid_postings
+ ADD CONSTRAINT fk_rails_cccc6419c8 FOREIGN KEY (updater_id) REFERENCES public.users(id);
+
+
--
-- Name: favorites fk_rails_d20e53bb68; Type: FK CONSTRAINT; Schema: public; Owner: -
--
@@ -4491,6 +4661,14 @@ ALTER TABLE ONLY public.favorites
ADD CONSTRAINT fk_rails_d20e53bb68 FOREIGN KEY (post_id) REFERENCES public.posts(id);
+--
+-- Name: avoid_postings fk_rails_d45cc0f1a1; Type: FK CONSTRAINT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.avoid_postings
+ ADD CONSTRAINT fk_rails_d45cc0f1a1 FOREIGN KEY (creator_id) REFERENCES public.users(id);
+
+
--
-- PostgreSQL database dump complete
--
@@ -4501,6 +4679,8 @@ INSERT INTO "schema_migrations" (version) VALUES
('20240726170041'),
('20240709134926'),
('20240706061122'),
+('20240103002049'),
+('20240103002040'),
('20240101042716'),
('20230531080817'),
('20230518182034'),
diff --git a/test/controllers/avoid_postings_controller_test.rb b/test/controllers/avoid_postings_controller_test.rb
new file mode 100644
index 000000000..4f6ae78e2
--- /dev/null
+++ b/test/controllers/avoid_postings_controller_test.rb
@@ -0,0 +1,169 @@
+# frozen_string_literal: true
+
+require "test_helper"
+
+class AvoidPostingsControllerTest < ActionDispatch::IntegrationTest
+ context "The avoid postings controller" do
+ setup do
+ @user = create(:user)
+ @bd_user = create(:bd_staff_user)
+ CurrentUser.user = @user
+
+ as(@bd_user) do
+ @avoid_posting = create(:avoid_posting)
+ @artist = @avoid_posting.artist
+ end
+ end
+
+ context "index action" do
+ should "render" do
+ get_auth avoid_postings_path, @user
+ assert_response :success
+ end
+ end
+
+ context "show action" do
+ should "render" do
+ get_auth avoid_posting_path(@avoid_posting), @user
+ assert_response :success
+ end
+ end
+
+ context "edit action" do
+ should "render" do
+ get_auth edit_avoid_posting_path(@avoid_posting), @bd_user
+ assert_response :success
+ end
+ end
+
+ context "new action" do
+ should "render" do
+ get_auth new_avoid_posting_path, @bd_user
+ assert_response :success
+ end
+ end
+
+ context "create action" do
+ should "work and create artist" do
+ assert_difference(%w[AvoidPosting.count AvoidPostingVersion.count Artist.count], 1) do
+ post_auth avoid_postings_path, @bd_user, params: { avoid_posting: { artist_attributes: { name: "another_artist" } } }
+ end
+
+ artist = Artist.find_by(name: "another_artist")
+ assert_not_nil(artist)
+ avoid_posting = AvoidPosting.find_by(artist: artist)
+ assert_not_nil(avoid_posting)
+ assert_redirected_to(avoid_posting_path(avoid_posting))
+ end
+
+ should "work with existing artist" do
+ @artist = create(:artist)
+ assert_difference(%w[AvoidPosting.count AvoidPostingVersion.count], 1) do
+ post_auth avoid_postings_path, @bd_user, params: { avoid_posting: { artist_attributes: { name: @artist.name } } }
+ end
+
+ avoid_posting = AvoidPosting.find_by(artist: @artist)
+ assert_not_nil(avoid_posting)
+ assert_redirected_to(avoid_posting_path(avoid_posting))
+ end
+
+ should "merge other_names if already set" do
+ @artist = create(:artist, other_names: %w[test1 test2])
+ assert_difference(%w[AvoidPosting.count AvoidPostingVersion.count], 1) do
+ post_auth avoid_postings_path, @bd_user, params: { avoid_posting: { artist_attributes: { name: @artist.name, other_names_string: "test2 test3" } } }
+ end
+
+ @artist.reload
+ avoid_posting = AvoidPosting.find_by(artist: @artist)
+ assert_not_nil(avoid_posting)
+ assert_equal(%w[test1 test2 test3], @artist.other_names)
+ assert_redirected_to(avoid_posting_path(avoid_posting))
+ end
+
+ should "reject linked_user_id if already set" do
+ @artist = create(:artist, linked_user: @bd_user)
+ assert_difference(%w[AvoidPosting.count AvoidPostingVersion.count], 1) do
+ post_auth avoid_postings_path, @bd_user, params: { avoid_posting: { artist_attributes: { name: @artist.name, linked_user_id: create(:user).id } } }
+ end
+
+ @artist.reload
+ avoid_posting = AvoidPosting.find_by(artist: @artist)
+ assert_not_nil(avoid_posting)
+ assert_equal(@bd_user, @artist.linked_user)
+ assert_redirected_to(avoid_posting_path(avoid_posting))
+ end
+
+ should "not override existing artist properties with empty fields" do
+ @artist = create(:artist, other_names: %w[test1 test2], group_name: "foobar", linked_user: @bd_user)
+ assert_difference(%w[AvoidPosting.count AvoidPostingVersion.count], 1) do
+ post_auth avoid_postings_path, @bd_user, params: { avoid_posting: { artist_attributes: { name: @artist.name, other_names: [], other_names_string: "", group_name: "", linked_user_id: "" } } }
+ end
+
+ @artist.reload
+ avoid_posting = AvoidPosting.find_by(artist: @artist)
+ assert_not_nil(avoid_posting)
+ assert_equal(%w[test1 test2], @artist.other_names)
+ assert_equal("foobar", @artist.group_name)
+ assert_equal(@bd_user, @artist.linked_user)
+ assert_redirected_to(avoid_posting_path(avoid_posting))
+ end
+ end
+
+ context "update action" do
+ should "work" do
+ assert_difference(%w[ModAction.count AvoidPostingVersion.count], 1) do
+ put_auth avoid_posting_path(@avoid_posting), @bd_user, params: { avoid_posting: { details: "test" } }
+ end
+
+ assert_redirected_to(avoid_posting_path(@avoid_posting))
+ assert_equal("avoid_posting_update", ModAction.last.action)
+ assert_equal("test", @avoid_posting.reload.details)
+ end
+
+ should "work with nested attributes" do
+ assert_difference({ "ModAction.count" => 1, "AvoidPostingVersion.count" => 0 }) do
+ put_auth avoid_posting_path(@avoid_posting), @bd_user, params: { avoid_posting: { artist_attributes: { id: @avoid_posting.artist.id, name: "foobar" } } }
+ end
+
+ assert_redirected_to(avoid_posting_path(@avoid_posting))
+ assert_equal("artist_page_rename", ModAction.last.action)
+ assert_equal("foobar", @avoid_posting.artist.reload.name)
+ end
+ end
+
+ context "delete action" do
+ should "work" do
+ assert_difference(%w[ModAction.count AvoidPostingVersion.count], 1) do
+ put_auth delete_avoid_posting_path(@avoid_posting), @bd_user
+ end
+
+ assert_equal(false, @avoid_posting.reload.is_active?)
+ assert_equal("avoid_posting_delete", ModAction.last.action)
+ end
+ end
+
+ context "undelete action" do
+ should "work" do
+ @avoid_posting.update_column(:is_active, false)
+
+ assert_difference(%w[ModAction.count AvoidPostingVersion.count], 1) do
+ put_auth undelete_avoid_posting_path(@avoid_posting), @bd_user
+ end
+
+ assert_equal(true, @avoid_posting.reload.is_active?)
+ assert_equal("avoid_posting_undelete", ModAction.last.action)
+ end
+ end
+
+ context "destroy action" do
+ should "work" do
+ assert_difference({ "ModAction.count" => 1, "AvoidPosting.count" => -1 }) do
+ delete_auth avoid_posting_path(@avoid_posting), @bd_user
+ end
+
+ assert_nil(AvoidPosting.find_by(id: @avoid_posting.id))
+ assert_equal("avoid_posting_destroy", ModAction.last.action)
+ end
+ end
+ end
+end
diff --git a/test/factories/avoid_posting.rb b/test/factories/avoid_posting.rb
new file mode 100644
index 000000000..435127956
--- /dev/null
+++ b/test/factories/avoid_posting.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory(:avoid_posting) do
+ association :artist
+ is_active { true }
+ association :creator, factory: :bd_staff_user
+ creator_ip_addr { "127.0.0.1" }
+ end
+end
diff --git a/test/functional/artists_controller_test.rb b/test/functional/artists_controller_test.rb
index bb9164b21..f4ba30ccf 100644
--- a/test/functional/artists_controller_test.rb
+++ b/test/functional/artists_controller_test.rb
@@ -44,15 +44,18 @@ class ArtistsControllerTest < ActionDispatch::IntegrationTest
assert_response :success
end
- should "create an artist" do
- attributes = attributes_for(:artist)
- assert_difference("Artist.count", 1) do
- attributes.delete(:is_active)
- post_auth artists_path, @user, params: {artist: attributes}
+ context "when creating an artist" do
+ should "work" do
+ attributes = attributes_for(:artist)
+ assert_difference("Artist.count", 1) do
+ attributes.delete(:is_active)
+ post_auth artists_path, @user, params: { artist: attributes }
+ end
+
+ artist = Artist.find_by_name(attributes[:name])
+ assert_not_nil(artist)
+ assert_redirected_to(artist_path(artist.id))
end
- artist = Artist.find_by_name(attributes[:name])
- assert_not_nil(artist)
- assert_redirected_to(artist_path(artist.id))
end
context "with an artist that has notes" do
@@ -134,5 +137,44 @@ class ArtistsControllerTest < ActionDispatch::IntegrationTest
assert_redirected_to(artist_path(@artist.id))
end
end
+
+ context "with a dnp entry" do
+ setup do
+ @bd_user = create(:bd_staff_user)
+ CurrentUser.user = @bd_user
+ @avoid_posting = create(:avoid_posting, artist: @artist)
+ end
+
+ should "not allow destroying" do
+ assert_no_difference("Artist.count") do
+ delete_auth artist_path(@artist), @bd_user
+ end
+ end
+
+ # technical restriction
+ should "not allow destroying even if the dnp is inactive" do
+ @avoid_posting.update(is_active: false)
+ assert_no_difference("Artist.count") do
+ delete_auth artist_path(@artist), @bd_user
+ end
+ end
+
+ should "not allow editing protected properties" do
+ @janitor = create(:janitor_user)
+ name = @artist.name
+ group_name = @artist.group_name
+ other_names = @artist.other_names
+ assert_no_difference("ModAction.count") do
+ put_auth artist_path(@artist), @janitor, params: { artist: { name: "another_name", group_name: "some_group", other_names: "some other names" } }
+ end
+
+ @artist.reload
+ assert_equal(name, @artist.name)
+ assert_equal(group_name, @artist.group_name)
+ assert_equal(other_names, @artist.other_names)
+ assert_equal(name, @artist.wiki_page.reload.title)
+ assert_equal(name, @avoid_posting.reload.artist_name)
+ end
+ end
end
end
diff --git a/test/unit/avoid_posting_test.rb b/test/unit/avoid_posting_test.rb
new file mode 100644
index 000000000..44c416fdd
--- /dev/null
+++ b/test/unit/avoid_posting_test.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+require "test_helper"
+
+class AvoidPostingTest < ActiveSupport::TestCase
+ context "An avoid posting entry" do
+ setup do
+ @bd_user = create(:bd_staff_user)
+ CurrentUser.user = @bd_user
+ @avoid_posting = create(:avoid_posting)
+ end
+
+ should "create an artist" do
+ assert_difference("Artist.count", 1) do
+ create(:avoid_posting)
+ end
+ end
+
+ should "create a create modaction" do
+ assert_difference("ModAction.count", 1) do
+ create(:avoid_posting)
+ end
+
+ assert_equal("avoid_posting_create", ModAction.last.action)
+ end
+
+ should "create an update modaction" do
+ assert_difference("ModAction.count", 1) do
+ @avoid_posting.update(details: "test")
+ end
+
+ assert_equal("avoid_posting_update", ModAction.last.action)
+ end
+
+ should "create a delete modaction" do
+ assert_difference("ModAction.count", 1) do
+ @avoid_posting.update(is_active: false)
+ end
+
+ assert_equal("avoid_posting_delete", ModAction.last.action)
+ end
+
+ should "create an undelete modaction" do
+ @avoid_posting.update_column(:is_active, false)
+
+ assert_difference("ModAction.count", 1) do
+ @avoid_posting.update(is_active: true)
+ end
+
+ assert_equal("avoid_posting_undelete", ModAction.last.action)
+ end
+
+ should "create a destroy modaction" do
+ assert_difference("ModAction.count", 1) do
+ @avoid_posting.destroy
+ end
+
+ assert_equal("avoid_posting_destroy", ModAction.last.action)
+ end
+
+ should "create a version when updated" do
+ assert_difference("AvoidPostingVersion.count", 1) do
+ @avoid_posting.update(details: "test")
+ end
+
+ assert_equal("test", AvoidPostingVersion.last.details)
+ end
+ end
+end