[AvoidPosting] Integrate the avoid posting list into the site (#582)

This commit is contained in:
Donovan Daniels 2024-08-03 16:15:26 -05:00 committed by GitHub
parent 516e3391be
commit 8a3e70e5ca
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
46 changed files with 1460 additions and 41 deletions

View File

@ -185,7 +185,7 @@ class ApplicationController < ActionController::Base
end end
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 define_method("#{role}_only") do
user_access_check("#{role}?") user_access_check("#{role}?")
end end

View File

@ -64,7 +64,7 @@ class ArtistsController < ApplicationController
@artist.destroy @artist.destroy
respond_with(@artist) do |format| respond_with(@artist) do |format|
format.html do 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 end
end end

View File

@ -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

View File

@ -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

View File

@ -2,19 +2,23 @@
class StaticController < ApplicationController class StaticController < ApplicationController
def privacy def privacy
@page = WikiPage.find_by(title: "e621:privacy_policy") @page = format_wiki_page("e621:privacy_policy")
end end
def terms_of_service def terms_of_service
@page = WikiPage.find_by(title: "e621:terms_of_service") @page = format_wiki_page("e621:terms_of_service")
end end
def contact def contact
@page = WikiPage.find_by(title: "e621:contact") @page = format_wiki_page("e621:contact")
end end
def takedown 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 end
def not_found def not_found
@ -64,4 +68,12 @@ class StaticController < ApplicationController
redirect_to(Danbooru.config.discord_site + user_hash, allow_other_host: true) redirect_to(Danbooru.config.discord_site + user_hash, allow_other_host: true)
end end
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 end

View File

@ -65,6 +65,18 @@ class ModActionDecorator < ApplicationDecorator
when "artist_user_unlinked" when "artist_user_unlinked"
"Unlinked #{user} from artist ##{vals['artist_page']}" "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 ### ### User ###
when "user_delete" when "user_delete"

View File

@ -1,21 +1,22 @@
# frozen_string_literal: true # frozen_string_literal: true
module ArtistsHelper module ArtistsHelper
def link_to_artist(name) def link_to_artist(name, hide_new_notice: false)
artist = Artist.find_by(name: name) artist = Artist.find_by(name: name)
if artist if artist
link_to(artist.name, artist_path(artist)) link_to(artist.name, artist_path(artist))
else else
link = link_to(name, new_artist_path(artist: { name: name })) 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.") notice = tag.span("*", class: "new-artist", title: "No artist with this name currently exists.")
"#{link} #{notice}".html_safe "#{link} #{notice}".html_safe
end end
end end
def link_to_artists(names) def link_to_artists(names, hide_new_notice: false)
names.map do |name| 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.join(", ").html_safe
end end
end end

View File

@ -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

View File

@ -38,6 +38,7 @@
@import "specific/admin_dashboards.scss"; @import "specific/admin_dashboards.scss";
@import "specific/api_keys.scss"; @import "specific/api_keys.scss";
@import "specific/artists.scss"; @import "specific/artists.scss";
@import "specific/avoid_posting.scss";
@import "specific/bans.scss"; @import "specific/bans.scss";
@import "specific/blips.scss"; @import "specific/blips.scss";
@import "specific/comments.scss"; @import "specific/comments.scss";

View File

@ -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;
}
}
}
}
}
}

View File

@ -210,7 +210,7 @@ div#c-posts {
#image-extra-controls { #image-extra-controls {
justify-content: center; justify-content: center;
} }
.fav-buttons, .fav-buttons,
#image-download-link { #image-download-link {
.button > i { .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 { div.quick-mod {
.quick-mod-group { .quick-mod-group {
@ -535,7 +563,7 @@ body[data-user-can-approve-posts="true"] .notice {
@media only screen and (min-width: 550px) { @media only screen and (min-width: 550px) {
display: flex; display: flex;
} }
// Overwrite some DText styles to make the header look better // Overwrite some DText styles to make the header look better
blockquote { blockquote {
border: 0; border: 0;
@ -546,25 +574,25 @@ body[data-user-can-approve-posts="true"] .notice {
} }
.flag-dialog-body { .flag-dialog-body {
// Option label // Option label
label { label {
font-weight: normal; font-weight: normal;
font-size: 1rem; font-size: 1rem;
cursor: pointer; cursor: pointer;
} }
// Option explanation // Option explanation
div.styled-dtext { div.styled-dtext {
margin: 0.125rem 1.25rem; margin: 0.125rem 1.25rem;
filter: brightness(85%); filter: brightness(85%);
} }
// Align label with the explanation // Align label with the explanation
input[type="radio"] { input[type="radio"] {
width: 1rem; width: 1rem;
} }
// Post ID input // Post ID input
form.simple_form div.input { form.simple_form div.input {
margin: -0.5rem 0 0 1.25rem; 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 } &.post_flag_parent_id { display: flex }
span.error { margin-left: 1rem; } span.error { margin-left: 1rem; }
} }
hr { margin: 0.75rem 1.25rem; } hr { margin: 0.75rem 1.25rem; }
h3 { h3 {
margin: 0.5rem 1.25rem; margin: 0.5rem 1.25rem;

View File

@ -65,7 +65,7 @@ class UploadService
p.has_cropped = upload.is_image? p.has_cropped = upload.is_image?
p.duration = upload.video_duration(upload.file.path) 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 p.is_pending = true
end end
end end

View File

@ -7,8 +7,9 @@ class Artist < ApplicationRecord
array_attribute :other_names array_attribute :other_names
belongs_to_creator belongs_to_creator
before_validation :normalize_name before_validation :normalize_name, unless: :destroyed?
before_validation :normalize_other_names before_validation :normalize_other_names, unless: :destroyed?
before_validation :validate_protected_properties_not_changed, if: :dnp_restricted?
validate :validate_user_can_edit validate :validate_user_can_edit
validate :wiki_page_not_locked validate :wiki_page_not_locked
validate :user_not_limited validate :user_not_limited
@ -28,6 +29,8 @@ class Artist < ApplicationRecord
has_one :wiki_page, foreign_key: "title", primary_key: "name" has_one :wiki_page, foreign_key: "title", primary_key: "name"
has_one :tag_alias, foreign_key: "antecedent_name", 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 :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 belongs_to :linked_user, class_name: "User", optional: true
attribute :notes, :string attribute :notes, :string
@ -496,6 +499,29 @@ class Artist < ApplicationRecord
end end
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 UrlMethods
include NameMethods include NameMethods
include GroupMethods include GroupMethods
@ -505,8 +531,10 @@ class Artist < ApplicationRecord
include LockMethods include LockMethods
extend SearchMethods extend SearchMethods
# due to technical limitations (foreign keys), artists with any
# dnp entry (active or inactive) cannot be deleted
def deletable_by?(user) def deletable_by?(user)
user.is_admin? !has_any_dnp? && user.is_admin?
end end
def editable_by?(user) def editable_by?(user)

133
app/models/avoid_posting.rb Normal file
View File

@ -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

View File

@ -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

View File

@ -11,6 +11,11 @@ class ModAction < ApplicationRecord
:artist_page_unlock, :artist_page_unlock,
:artist_user_linked, :artist_user_linked,
:artist_user_unlinked, :artist_user_unlinked,
:avoid_posting_create,
:avoid_posting_update,
:avoid_posting_delete,
:avoid_posting_undelete,
:avoid_posting_destroy,
:blip_delete, :blip_delete,
:blip_hide, :blip_hide,
:blip_unhide, :blip_unhide,

View File

@ -1754,9 +1754,12 @@ class Post < ApplicationRecord
save save
end end
def artist_tags
tags.select { |t| t.category == Tag.categories.artist }
end
def uploader_linked_artists def uploader_linked_artists
linked_artists ||= tags.select { |t| t.category == Tag.categories.artist }.filter_map(&:artist) artist_tags.filter_map(&:artist).select { |artist| artist.linked_user_id == uploader_id }
linked_artists.select { |artist| artist.linked_user_id == uploader_id }
end end
def flaggable_for_guidelines? def flaggable_for_guidelines?
@ -1768,4 +1771,8 @@ class Post < ApplicationRecord
def visible_comment_count(_user) def visible_comment_count(_user)
comment_count comment_count
end end
def avoid_posting_artists
AvoidPosting.active.joins(:artist).where("artists.name": artist_tags.map(&:name))
end
end end

View File

@ -319,6 +319,10 @@ class User < ApplicationRecord
is_bd_staff is_bd_staff
end end
def is_staff?
is_janitor?
end
def is_approver? def is_approver?
can_approve_posts? can_approve_posts?
end end
@ -508,13 +512,17 @@ class User < ApplicationRecord
end end
def can_view_staff_notes? def can_view_staff_notes?
is_janitor? is_staff?
end end
def can_handle_takedowns? def can_handle_takedowns?
is_bd_staff? is_bd_staff?
end end
def can_edit_avoid_posting_entries?
is_bd_staff?
end
def can_undo_post_versions? def can_undo_post_versions?
is_member? is_member?
end end

View File

@ -2,18 +2,23 @@
<% if @artist.new_record? %> <% if @artist.new_record? %>
<%# FIXME: Probably wrong type in the production database %> <%# FIXME: Probably wrong type in the production database %>
<%= f.input :name, as: :string, autocomplete: "tag" %> <%= f.input :name, as: :string, autocomplete: "tag" %>
<% elsif CurrentUser.user.is_janitor? %> <% elsif !@artist.dnp_restricted? && CurrentUser.user.is_janitor? %>
<%= f.input :name, autocomplete: "tag", hint: "Change to rename this artist entry and its wiki page" %> <%= f.input :name, autocomplete: "tag", hint: "Change to rename this artist entry and its wiki page." %>
<% else %> <% else %>
<p><%= @artist.name %></p> <p><%= @artist.name %></p>
<% if @artist.dnp_restricted? %>
<p>Name, other names, and group name cannot be edited for artists on the <%= link_to "Avoid Posting", avoid_postings_path %> list.</p>
<% end %>
<% end %> <% end %>
<% if CurrentUser.is_janitor? %> <% if CurrentUser.is_janitor? %>
<%= f.input :linked_user_id, label: "Linked User ID" %> <%= f.input :linked_user_id, label: "Linked User ID" %>
<%= f.input :is_locked, label: "Locked" %> <%= f.input :is_locked, label: "Locked" %>
<% end %> <% 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" } %> <% if @artist.new_record? || !@artist.dnp_restricted? %>
<%= f.input :group_name %> <%= 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 :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." %> <%= 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? %> <% if @artist.is_note_locked? %>
<p>Artist is note locked. Notes cannot be edited.</p> <p>Artist is note locked. Notes cannot be edited.</p>

View File

@ -1,7 +1,7 @@
<% content_for(:secondary_links) do %> <% content_for(:secondary_links) do %>
<li><%= render "artists/quick_search" %></li> <li><%= render "artists/quick_search" %></li>
<%= subnav_link_to "Listing", artists_path %> <%= 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? %> <% if CurrentUser.is_member? %>
<%= subnav_link_to "New", new_artist_path %> <%= subnav_link_to "New", new_artist_path %>
<% end %> <% end %>
@ -18,5 +18,10 @@
<% if @artist.deletable_by?(CurrentUser.user) %> <% 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." } %> <%= 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 %> <% end %>
<% if @artist.is_dnp? %>
<li>|</li>
<%= subnav_link_to "DNP", avoid_posting_path(@artist.avoid_posting) %>
<% end %>
<% else %>
<% end %> <% end %>
<% end %> <% end %>

View File

@ -7,6 +7,18 @@
<% end %> <% end %>
</h1> </h1>
<% if @artist.is_dnp? %>
<div id="avoid-posting">
<h2>This artist is on the <%= link_to "Avoid Posting", avoid_postings_path %> list.</h2>
<% if @artist.avoid_posting.pretty_details.present? %>
<div class="dtext-container">
<%= format_text(@artist.avoid_posting.pretty_details, inline: true) %>
</div>
<% end %>
<p><%= link_to "Open Avoid Posting entry", avoid_posting_path(@artist.avoid_posting) %></p>
</div>
<% end %>
<% if @artist.notes.present? && @artist.visible? %> <% if @artist.notes.present? && @artist.visible? %>
<div class="dtext-container"> <div class="dtext-container">
<%= format_text(@artist.notes, allow_color: true) %> <%= format_text(@artist.notes, allow_color: true) %>

View File

@ -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 %>

View File

@ -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 %>

View File

@ -0,0 +1,84 @@
<div id="c-avoid-posting-versions">
<div id="a-index">
<h1>Avoid Posting Versions</h1>
<%= render "search" %>
<table class="striped dnp-list">
<thead>
<tr>
<th class="dnp-artist">Artist</th>
<th class="dnp-details" width="60%">Details</th>
<th class="dnp-status">Status</th>
<th class="dnp-creator">Updater</th>
<th class="dnp-links"></th>
</tr>
</thead>
<tbody>
<% @avoid_posting_versions.each do |avoid_posting_version| %>
<% previous = avoid_posting_version.previous %>
<tr id="avoid-posting-version-<%= avoid_posting_version.id %>" data-artist="<%= avoid_posting_version.artist_name %>">
<td class="dnp-artist">
<%= link_to avoid_posting_version.artist_name, artist_path(avoid_posting_version.artist) %>
</td>
<td class="dnp-details">
<span class="avoid-posting-details">
<% 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 %>
</span>
<% if CurrentUser.is_staff? %>
<span class="avoid-posting-staff-notes">
<% if previous.present? && previous.staff_notes != avoid_posting_version.staff_notes %>
<b>Staff Notes</b>
<span>
<% if avoid_posting_version.staff_notes.blank? %>
(cleared)
<% else %>
<%= avoid_posting_version.staff_notes %>
<% end %>
</span>
<% end %>
</span>
<% end %>
</td>
<td class="dnp-status">
<%= avoid_posting_version.status %>
</td>
<td class="dnp-creator">
<%= link_to_user avoid_posting_version.updater %>
<%= link_to "»", avoid_posting_versions_path(search: { updater_name: avoid_posting_version.updater_name }) %>
<p>
<%= compact_time(avoid_posting_version.updated_at) %>
<% if CurrentUser.is_admin? %>
(<%= link_to_ip avoid_posting_version.updater_ip_addr %>)
<% end %>
</p>
</td>
<td class="dnp-links">
<%= 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 %>
</td>
</tr>
<% end %>
</tbody>
</table>
<%= numbered_paginator(@avoid_posting_versions) %>
</div>
</div>
<%= render "secondary_links" %>
<% content_for(:page_title) do %>
Avoid Posting Versions
<% end %>

View File

@ -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 %>

View File

@ -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 %>

View File

@ -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 %>
<li>|</li>
<%= subnav_link_to "Static", avoid_posting_static_path %>
<% end %>

View File

@ -0,0 +1,13 @@
<div class="c-avoid-postings">
<div class="a-edit">
<h1>Edit Avoid Posting for <%= link_to @avoid_posting.artist_name, avoid_posting_path(@avoid_posting), class: "tag-type-1" %></h1>
<%= error_messages_for :avoid_posting %>
<%= render "form" %>
</div>
</div>
<%= render "secondary_links" %>
<% content_for(:page_title) do %>
Edit Avoid Posting for <%= @avoid_posting.artist_name %>
<% end %>

View File

@ -0,0 +1,70 @@
<div id="c-avoid-postings">
<div id="a-index">
<h1>Avoid Postings</h1>
<%= render "search" %>
<table class="striped dnp-list">
<thead>
<tr>
<th class="dnp-artist">Artist</th>
<th class="dnp-details">Details</th>
<% if search_params.key?(:is_active) %>
<th class="dnp-status">Status</th>
<% end %>
<% if search_params.key?(:creator_id) || search_params.key?(:creator_name) %>
<th class="dnp-creator">Creator</th>
<% end %>
<th class="dnp-links"></th>
</tr>
</thead>
<tbody>
<% @avoid_postings.each do |avoid_posting| %>
<tr id="avoid-posting-<%= avoid_posting.id %>" data-artist="<%= avoid_posting.artist_name %>">
<td class="dnp-artist">
<span class="dnp-artist-names">
<%= link_to avoid_posting.artist_name, artist_path(avoid_posting.artist) %>
<% if avoid_posting.other_names.present? %>
<span>(<%= link_to_artists(avoid_posting.other_names, hide_new_notice: true) %>)</span>
<% end %>
</span>
</td>
<td class="dnp-details">
<span class="avoid-posting-details"><%= format_text(avoid_posting.details, inline: true) %></span>
<% if CurrentUser.is_staff? && avoid_posting.staff_notes.present? %>
<span class="avoid-posting-staff-notes">
<b>Staff Notes</b>
<span><%= format_text(avoid_posting.staff_notes, inline: true) %></span>
</span>
<% end %>
</td>
<% if search_params.key?(:is_active) %>
<td class="dnp-status">
<%= avoid_posting.status %>
</td>
<% end %>
<% if search_params.key?(:creator_id) || search_params.key?(:creator_name) %>
<td class="dnp-creator">
<%= link_to_user avoid_posting.creator %>
</td>
<% end %>
<td class="dnp-links">
<%= 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 %>
</td>
</tr>
<% end %>
</tbody>
</table>
<%= numbered_paginator(@avoid_postings) %>
</div>
</div>
<%= render "secondary_links" %>
<% content_for(:page_title) do %>
Avoid Postings
<% end %>

View File

@ -0,0 +1,13 @@
<div class="c-avoid-postings">
<div class="a-new">
<h1>New Avoid Posting</h1>
<%= error_messages_for :avoid_posting %>
<%= render "form" %>
</div>
</div>
<%= render "secondary_links" %>
<% content_for(:page_title) do %>
New Avoid Posting
<% end %>

View File

@ -0,0 +1,46 @@
<div id="c-avoid-postings">
<div id="a-index">
<h1>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? %></h1>
<ul>
<li><strong>Status</strong> <%= @avoid_posting.status %></li>
<% if @avoid_posting.linked_user_id && @avoid_posting.linked_user %>
<li><strong>User</strong> <%= link_to_user(@avoid_posting.linked_user) %></li>
<% end %>
<li><strong>Created</strong> <%= 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 %></li>
<% if @avoid_posting.updater.present? %>
<li><strong>Updated</strong> <%= 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 %></li>
<% end %>
<% if @avoid_posting.other_names.present? %>
<li><strong>Other Names</strong> <%= link_to_artists(@avoid_posting.other_names) %></li>
<% end %>
<% if @avoid_posting.group_name.present? %>
<li><strong>Group</strong> <%= link_to_artist(@avoid_posting.group_name) %></li>
<% end %>
<% if @avoid_posting.group_name.present? && @avoid_posting.artist.members.present? %>
<li><strong>Members</strong> <%= link_to_artists(@avoid_posting.artist.members.limit(25).map(&:name)) %></li>
<% end %>
<% if @avoid_posting.details.present? %>
<li>
<strong>Details</strong>
<div class="dtext-container">
<%= format_text(@avoid_posting.details) %>
</div>
</li>
<% end %>
<% if CurrentUser.is_staff? && @avoid_posting.staff_notes.present? %>
<li>
<strong>Staff Notes</strong>
<div class="dtext-container">
<%= format_text(@avoid_posting.staff_notes, inline: true) %>
</div>
</li>
<% end %>
</ul>
</div>
</div>
<%= render "secondary_links" %>
<% content_for(:page_title) do %>
Avoid Posting for <%= @avoid_posting.artist_name %>
<% end %>

View File

@ -0,0 +1,27 @@
<% if post.avoid_posting_artists.any? %>
<div class="notice" id="avoid-posting-notice">
<div class="avoid-posting">
<h3>Avoid Posting</h3>
<ul>
<% post.avoid_posting_artists.each do |dnp| %>
<li>
<% if dnp.pretty_details.blank? %>
<span class="artist"><%= link_to dnp.artist_name, avoid_posting_path(dnp) %></span>
<span class="separator">-</span>
<span class="details">Avoid posting.</span>
<% else %>
<span class="artist">
<%= link_to dnp.artist_name, avoid_posting_path(dnp) %>
<% if dnp.artist.try(:linked_user_id) == post.uploader_id %>
<i class="uploader-is-artist fa-regular fa-circle-check" title="The uploader is linked to this artist."></i>
<% end %>
</span>
<span class="separator">-</span>
<span class="details"><%= format_text(dnp.pretty_details, inline: true) %></span>
<% end %>
</li>
<% end %>
</ul>
</div>
</div>
<% end %>

View File

@ -49,6 +49,10 @@
</div> </div>
<% end %> <% 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? %> <% if post.replacements.pending.any? %>
<div class="notice notice-flagged"> <div class="notice notice-flagged">
<p>This post has <%= link_to "pending replacements.", post_replacements_path(search: {post_id: @post.id}) %></p> <p>This post has <%= link_to "pending replacements.", post_replacements_path(search: {post_id: @post.id}) %></p>

View File

@ -0,0 +1,15 @@
<div id="c-static">
<div id="a-avoid-posting">
<div class="dtext-container">
<%= format_text(@page.body, allow_color: true) %>
<%= format_avoid_posting_list %>
</div>
</div>
</div>
<%= render "avoid_postings/secondary_links" %>
<% content_for(:page_title) do %>
Avoid Posting List
<% end %>

View File

@ -37,7 +37,7 @@
<ul> <ul>
<li><h1>Artists</h1></li> <li><h1>Artists</h1></li>
<li><%= link_to("Listing", artists_path) %></li> <li><%= link_to("Listing", artists_path) %></li>
<li><%= link_to("Avoid Posting", help_page_path(id: "avoid_posting")) %></li> <li><%= link_to("Avoid Posting", avoid_postings_path) %></li>
<li><%= link_to("Changes", artist_versions_path) %></li> <li><%= link_to("Changes", artist_versions_path) %></li>
<li><%= link_to("Help", help_page_path(id: "artists")) %></li> <li><%= link_to("Help", help_page_path(id: "artists")) %></li>
</ul> </ul>

View File

@ -4,7 +4,7 @@
<h2>Before uploading, read <h2>Before uploading, read
the <%= link_to "how to upload guide", wiki_page_path(id: "howto:upload") %>.</h2> the <%= link_to "how to upload guide", wiki_page_path(id: "howto:upload") %>.</h2>
<p>Make sure you're not posting something on <p>Make sure you're not posting something on
the <%= link_to "Avoid Posting List", help_page_path(id: 'avoid_posting') %><br> the <%= link_to "Avoid Posting List", avoid_postings_path %><br>
Review the <%= link_to "Uploading Guidelines", help_page_path(id: "uploading_guidelines") %> Review the <%= link_to "Uploading Guidelines", help_page_path(id: "uploading_guidelines") %>
<br> <br>
Unsure what to tag your post Unsure what to tag your post

View File

@ -460,7 +460,7 @@ module Danbooru
}, },
{ {
name: "dnp_artist", 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", 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",
"Traced artwork (post #%PARENT_ID%)", "Traced artwork (post #%PARENT_ID%)",
"Takedown #%OTHER_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]] (Only the artist is allowed to post)",
"[[conditional_dnp|Conditional DNP]] (%OTHER_ID%)", "[[conditional_dnp|Conditional DNP]] (%OTHER_ID%)",
] ]

View File

@ -76,6 +76,16 @@ Rails.application.routes.draw do
end end
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 resources :tickets, except: %i[destroy] do
member do member do
post :claim post :claim
@ -448,6 +458,7 @@ Rails.application.routes.draw do
post "/static/discord" => "static#discord", as: "discord_post" post "/static/discord" => "static#discord", as: "discord_post"
get "/static/toggle_mobile_mode" => "static#disable_mobile_mode", as: "disable_mobile_mode" get "/static/toggle_mobile_mode" => "static#disable_mobile_mode", as: "disable_mobile_mode"
get "/static/theme" => "static#theme", as: "theme" 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" get "/meta_searches/tags" => "meta_searches#tags", :as => "meta_searches_tags"
root :to => "static#home" root :to => "static#home"

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -202,6 +202,79 @@ CREATE SEQUENCE public.artists_id_seq
ALTER SEQUENCE public.artists_id_seq OWNED BY public.artists.id; 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: - -- 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); 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: - -- Name: bans id; Type: DEFAULT; Schema: public; Owner: -
-- --
@ -2840,6 +2927,22 @@ ALTER TABLE ONLY public.artists
ADD CONSTRAINT artists_pkey PRIMARY KEY (id); 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: - -- 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); 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: - -- 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); 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: - -- 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); 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: - -- 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); 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: - -- 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); 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: - -- 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); 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 -- PostgreSQL database dump complete
-- --
@ -4501,6 +4679,8 @@ INSERT INTO "schema_migrations" (version) VALUES
('20240726170041'), ('20240726170041'),
('20240709134926'), ('20240709134926'),
('20240706061122'), ('20240706061122'),
('20240103002049'),
('20240103002040'),
('20240101042716'), ('20240101042716'),
('20230531080817'), ('20230531080817'),
('20230518182034'), ('20230518182034'),

View File

@ -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

View File

@ -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

View File

@ -44,15 +44,18 @@ class ArtistsControllerTest < ActionDispatch::IntegrationTest
assert_response :success assert_response :success
end end
should "create an artist" do context "when creating an artist" do
attributes = attributes_for(:artist) should "work" do
assert_difference("Artist.count", 1) do attributes = attributes_for(:artist)
attributes.delete(:is_active) assert_difference("Artist.count", 1) do
post_auth artists_path, @user, params: {artist: attributes} 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 end
artist = Artist.find_by_name(attributes[:name])
assert_not_nil(artist)
assert_redirected_to(artist_path(artist.id))
end end
context "with an artist that has notes" do context "with an artist that has notes" do
@ -134,5 +137,44 @@ class ArtistsControllerTest < ActionDispatch::IntegrationTest
assert_redirected_to(artist_path(@artist.id)) assert_redirected_to(artist_path(@artist.id))
end end
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
end end

View File

@ -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