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 %>
  • <%= render "artists/quick_search" %>
  • <%= subnav_link_to "Listing", artists_path %> - <%= subnav_link_to "Avoid Posting", wiki_page_path(id: "avoid_posting") %> + <%= subnav_link_to "Avoid Posting", avoid_postings_path %> <% if CurrentUser.is_member? %> <%= subnav_link_to "New", new_artist_path %> <% end %> @@ -18,5 +18,10 @@ <% if @artist.deletable_by?(CurrentUser.user) %> <%= subnav_link_to "Delete", artist_path(@artist), method: :delete, data: { confirm: "Are you sure you want to delete this artist? This cannot be undone." } %> <% end %> + <% if @artist.is_dnp? %> +
  • |
  • + <%= subnav_link_to "DNP", avoid_posting_path(@artist.avoid_posting) %> + <% end %> + <% else %> <% end %> <% end %> diff --git a/app/views/artists/_show.html.erb b/app/views/artists/_show.html.erb index e273bcd8d..529babc85 100644 --- a/app/views/artists/_show.html.erb +++ b/app/views/artists/_show.html.erb @@ -7,6 +7,18 @@ <% end %> + <% if @artist.is_dnp? %> +
    +

    This artist is on the <%= link_to "Avoid Posting", avoid_postings_path %> list.

    + <% if @artist.avoid_posting.pretty_details.present? %> +
    + <%= format_text(@artist.avoid_posting.pretty_details, inline: true) %> +
    + <% end %> +

    <%= link_to "Open Avoid Posting entry", avoid_posting_path(@artist.avoid_posting) %>

    +
    + <% end %> + <% if @artist.notes.present? && @artist.visible? %>
    <%= format_text(@artist.notes, allow_color: true) %> diff --git a/app/views/avoid_posting_versions/_search.html.erb b/app/views/avoid_posting_versions/_search.html.erb new file mode 100644 index 000000000..f032caf74 --- /dev/null +++ b/app/views/avoid_posting_versions/_search.html.erb @@ -0,0 +1,11 @@ +<%= form_search(path: avoid_posting_versions_path) do |f| %> + <%= f.user :updater %> + <% if CurrentUser.is_admin? %> + <%= f.input :updater_ip_addr, label: "Updater IP Address" %> + <% end %> + <%= f.input :any_name_matches, label: "Name", hint: "Use * for wildcard", autocomplete: "artist" %> + <%= f.input :artist_name, label: "Artist Name", hide_unless_value: true %> + <%= f.input :artist_id, label: "Artist ID", hide_unless_value: true %> + <%= f.input :any_other_name_matches, label: "Other Names", hide_unless_value: true %> + <%= f.input :is_active, label: "Active?", collection: [%w[Yes true], %w[No false], %w[Any any]] %> +<% end %> diff --git a/app/views/avoid_posting_versions/_secondary_links.html.erb b/app/views/avoid_posting_versions/_secondary_links.html.erb new file mode 100644 index 000000000..6c8f511ac --- /dev/null +++ b/app/views/avoid_posting_versions/_secondary_links.html.erb @@ -0,0 +1,4 @@ +<% content_for(:secondary_links) do %> + <%= subnav_link_to "Listing", avoid_posting_versions_path %> + <%= subnav_link_to "Avoid Postings", avoid_postings_path %> +<% end %> diff --git a/app/views/avoid_posting_versions/index.html.erb b/app/views/avoid_posting_versions/index.html.erb new file mode 100644 index 000000000..5b89e37af --- /dev/null +++ b/app/views/avoid_posting_versions/index.html.erb @@ -0,0 +1,84 @@ +
    +
    +

    Avoid Posting Versions

    + + <%= render "search" %> + + + + + + + + + + + + + <% @avoid_posting_versions.each do |avoid_posting_version| %> + <% previous = avoid_posting_version.previous %> + + + + + + + + <% end %> + +
    ArtistDetailsStatusUpdater
    + <%= 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 %> +

    +
    + + <%= numbered_paginator(@avoid_posting_versions) %> +
    +
    + +<%= render "secondary_links" %> +<% content_for(:page_title) do %> + Avoid Posting Versions +<% end %> diff --git a/app/views/avoid_postings/_form.html.erb b/app/views/avoid_postings/_form.html.erb new file mode 100644 index 000000000..05a41807e --- /dev/null +++ b/app/views/avoid_postings/_form.html.erb @@ -0,0 +1,11 @@ +<%= custom_form_for(@avoid_posting) do |f| %> + <%= f.simple_fields_for(:artist) do |af| %> + <%= af.input :name, label: "Artist name", autocomplete: "artist", hint: params[:action] == "new" ? "An artist will be created if one does not exist." : "The artist will be renamed." %> + <%= af.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" } %> + <%= af.input :group_name %> + <%= af.input :linked_user_id, label: "Linked User ID" %> + <% end %> + <%= f.input :details, label: "Details", as: :dtext, hint: "Details shown to all users. Conditions should be listed here." %> + <%= f.input :staff_notes, label: "Staff Notes", as: :dtext, hint: "Notes shown only to staff members." %> + <%= f.button :submit, "Submit" %> +<% end %> diff --git a/app/views/avoid_postings/_search.html.erb b/app/views/avoid_postings/_search.html.erb new file mode 100644 index 000000000..adcbe1228 --- /dev/null +++ b/app/views/avoid_postings/_search.html.erb @@ -0,0 +1,16 @@ +<%= form_search(path: avoid_postings_path, always_display: true) do |f| %> + <%= f.user :creator %> + <% if CurrentUser.is_admin? %> + <%= f.input :creator_ip_addr, label: "Creator IP Address" %> + <% end %> + <%= f.input :any_name_matches, label: "Name", hint: "Use * for wildcard", autocomplete: "artist" %> + <%= f.input :artist_name, label: "Artist Name", hide_unless_value: true %> + <%= f.input :artist_id, label: "Artist ID", hide_unless_value: true %> + <%= f.input :any_other_name_matches, label: "Other Names", hide_unless_value: true %> + <%= f.input :group_name, label: "Group Name", hide_unless_value: true %> + <%= f.input :details, label: "Details" %> + <% if CurrentUser.is_staff? %> + <%= f.input :staff_notes %> + <% end %> + <%= f.input :is_active, label: "Active?", collection: [%w[Yes true], %w[No false], %w[Any any]] %> +<% end %> diff --git a/app/views/avoid_postings/_secondary_links.html.erb b/app/views/avoid_postings/_secondary_links.html.erb new file mode 100644 index 000000000..536b761e1 --- /dev/null +++ b/app/views/avoid_postings/_secondary_links.html.erb @@ -0,0 +1,23 @@ +<% content_for(:secondary_links) do %> + <%= subnav_link_to "Listing", avoid_postings_path %> + <% if CurrentUser.can_edit_avoid_posting_entries? %> + <%= subnav_link_to "New", new_avoid_posting_path %> + <% end %> + <% if params[:action] == "show" %> + <% if CurrentUser.can_edit_avoid_posting_entries? %> + <%= subnav_link_to "Edit", edit_avoid_posting_path(@avoid_posting) %> + <% if @avoid_posting.is_active? %> + <%= subnav_link_to "Delete", delete_avoid_posting_path(@avoid_posting), method: :put, data: { confirm: "Are you sure you want to delete this avoid posting entry?" } %> + <% else %> + <%= subnav_link_to "Undelete", undelete_avoid_posting_path(@avoid_posting), method: :put, data: { confirm: "Are you sure you want to undelete this avoid posting entry?" } %> + <% end %> + <%= subnav_link_to "Destroy", avoid_posting_path(@avoid_posting), method: :delete, data: { confirm: "Are you sure you want to destroy this avoid posting entry? This will remove all related history, and cannot be undone." } %> + <% end %> + <%= subnav_link_to "Implications", tag_implications_path(search: { antecedent_name: @avoid_posting.artist_name }) %> + <%= subnav_link_to "History", avoid_posting_versions_path(search: { artist_name: @avoid_posting.artist_name }) %> + <% else %> + <%= subnav_link_to "History", avoid_posting_versions_path %> + <% end %> +
  • |
  • + <%= subnav_link_to "Static", avoid_posting_static_path %> +<% end %> diff --git a/app/views/avoid_postings/edit.html.erb b/app/views/avoid_postings/edit.html.erb new file mode 100644 index 000000000..d8d0c56fb --- /dev/null +++ b/app/views/avoid_postings/edit.html.erb @@ -0,0 +1,13 @@ +
    +
    +

    Edit Avoid Posting for <%= link_to @avoid_posting.artist_name, avoid_posting_path(@avoid_posting), class: "tag-type-1" %>

    + + <%= error_messages_for :avoid_posting %> + <%= render "form" %> +
    +
    + +<%= render "secondary_links" %> +<% content_for(:page_title) do %> + Edit Avoid Posting for <%= @avoid_posting.artist_name %> +<% end %> diff --git a/app/views/avoid_postings/index.html.erb b/app/views/avoid_postings/index.html.erb new file mode 100644 index 000000000..98f8897d2 --- /dev/null +++ b/app/views/avoid_postings/index.html.erb @@ -0,0 +1,70 @@ +
    +
    +

    Avoid Postings

    + + <%= render "search" %> + + + + + + + <% if search_params.key?(:is_active) %> + + <% end %> + <% if search_params.key?(:creator_id) || search_params.key?(:creator_name) %> + + <% end %> + + + + + <% @avoid_postings.each do |avoid_posting| %> + + + + <% if search_params.key?(:is_active) %> + + <% end %> + <% if search_params.key?(:creator_id) || search_params.key?(:creator_name) %> + + <% end %> + + + <% end %> + +
    ArtistDetailsStatusCreator
    + + <%= 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 %> + + <%= avoid_posting.status %> + + <%= link_to_user avoid_posting.creator %> +
    + + <%= numbered_paginator(@avoid_postings) %> +
    +
    + +<%= render "secondary_links" %> +<% content_for(:page_title) do %> + Avoid Postings +<% end %> diff --git a/app/views/avoid_postings/new.html.erb b/app/views/avoid_postings/new.html.erb new file mode 100644 index 000000000..c46f0359f --- /dev/null +++ b/app/views/avoid_postings/new.html.erb @@ -0,0 +1,13 @@ +
    +
    +

    New Avoid Posting

    + + <%= error_messages_for :avoid_posting %> + <%= render "form" %> +
    +
    + +<%= render "secondary_links" %> +<% content_for(:page_title) do %> + New Avoid Posting +<% end %> diff --git a/app/views/avoid_postings/show.html.erb b/app/views/avoid_postings/show.html.erb new file mode 100644 index 000000000..6a237a0ac --- /dev/null +++ b/app/views/avoid_postings/show.html.erb @@ -0,0 +1,46 @@ +
    +
    +

    Avoid Posting: <%= link_to @avoid_posting.artist_name, show_or_new_artists_path(name: @avoid_posting.artist_name), class: "tag-type-1" %><%= " (inactive)" unless @avoid_posting.is_active? %>

    +
      +
    • Status <%= @avoid_posting.status %>
    • + + <% if @avoid_posting.linked_user_id && @avoid_posting.linked_user %> +
    • User <%= link_to_user(@avoid_posting.linked_user) %>
    • + <% end %> +
    • Created <%= compact_time @avoid_posting.created_at %> by <%= link_to_user(@avoid_posting.creator) %><% if CurrentUser.is_admin? %> (<%= link_to_ip @avoid_posting.creator_ip_addr %>)<% end %>
    • + <% if @avoid_posting.updater.present? %> +
    • Updated <%= compact_time @avoid_posting.updated_at %> by <%= link_to_user(@avoid_posting.updater) %><% if CurrentUser.is_admin? %> (<%= link_to_ip @avoid_posting.updater_ip_addr %>)<% end %>
    • + <% end %> + <% if @avoid_posting.other_names.present? %> +
    • Other Names <%= link_to_artists(@avoid_posting.other_names) %>
    • + <% end %> + <% if @avoid_posting.group_name.present? %> +
    • Group <%= link_to_artist(@avoid_posting.group_name) %>
    • + <% end %> + <% if @avoid_posting.group_name.present? && @avoid_posting.artist.members.present? %> +
    • Members <%= link_to_artists(@avoid_posting.artist.members.limit(25).map(&:name)) %>
    • + <% end %> + <% if @avoid_posting.details.present? %> +
    • + Details +
      + <%= format_text(@avoid_posting.details) %> +
      +
    • + <% end %> + <% if CurrentUser.is_staff? && @avoid_posting.staff_notes.present? %> +
    • + Staff Notes +
      + <%= format_text(@avoid_posting.staff_notes, inline: true) %> +
      +
    • + <% end %> +
    +
    +
    + +<%= render "secondary_links" %> +<% content_for(:page_title) do %> + Avoid Posting for <%= @avoid_posting.artist_name %> +<% end %> diff --git a/app/views/posts/partials/show/_avoid_posting.html.erb b/app/views/posts/partials/show/_avoid_posting.html.erb new file mode 100644 index 000000000..de4edfbe1 --- /dev/null +++ b/app/views/posts/partials/show/_avoid_posting.html.erb @@ -0,0 +1,27 @@ +<% if post.avoid_posting_artists.any? %> +
    +
    +

    Avoid Posting

    +
      + <% post.avoid_posting_artists.each do |dnp| %> +
    • + <% if dnp.pretty_details.blank? %> + <%= link_to dnp.artist_name, avoid_posting_path(dnp) %> + - + Avoid posting. + <% else %> + + <%= link_to dnp.artist_name, avoid_posting_path(dnp) %> + <% if dnp.artist.try(:linked_user_id) == post.uploader_id %> + + <% end %> + + - + <%= format_text(dnp.pretty_details, inline: true) %> + <% end %> +
    • + <% end %> +
    +
    +
    +<% end %> diff --git a/app/views/posts/partials/show/_notices.html.erb b/app/views/posts/partials/show/_notices.html.erb index 415ae1d77..6b3593c2e 100644 --- a/app/views/posts/partials/show/_notices.html.erb +++ b/app/views/posts/partials/show/_notices.html.erb @@ -49,6 +49,10 @@
    <% end %> +<% if post.is_pending? || (post.is_flagged? && post.flags.any?) %> + <%= render "posts/partials/show/avoid_posting", post: post %> +<% end %> + <% if post.replacements.pending.any? %>

    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 @@ +
    +
    + +
    + <%= format_text(@page.body, allow_color: true) %> + <%= format_avoid_posting_list %> +
    +
    +
    + +<%= render "avoid_postings/secondary_links" %> + +<% content_for(:page_title) do %> + Avoid Posting List +<% end %> diff --git a/app/views/static/site_map.html.erb b/app/views/static/site_map.html.erb index 5920a5b2a..eda0277b8 100644 --- a/app/views/static/site_map.html.erb +++ b/app/views/static/site_map.html.erb @@ -37,7 +37,7 @@ diff --git a/app/views/uploads/new.html.erb b/app/views/uploads/new.html.erb index 5e9f3825e..e10135630 100644 --- a/app/views/uploads/new.html.erb +++ b/app/views/uploads/new.html.erb @@ -4,7 +4,7 @@

    Before uploading, read the <%= link_to "how to upload guide", wiki_page_path(id: "howto:upload") %>.

    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