diff --git a/.rubocop.yml b/.rubocop.yml index 6a4974108..33c934fbc 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -156,3 +156,6 @@ Style/TrailingCommaInArrayLiteral: Style/TrailingCommaInHashLiteral: EnforcedStyleForMultiline: consistent_comma + +Rails/NotNullColumn: + Enabled: false diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index f26e4fa29..4ebf69619 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -498,7 +498,7 @@ Layout/SpaceInsideBlockBraces: # SupportedStylesForEmptyBraces: space, no_space Layout/SpaceInsideHashLiteralBraces: Exclude: - - 'app/controllers/admin/staff_notes_controller.rb' + - 'app/controllers/staff_notes_controller.rb' - 'app/controllers/admin/users_controller.rb' - 'app/controllers/application_controller.rb' - 'app/controllers/comment_votes_controller.rb' @@ -2416,7 +2416,7 @@ Style/StringLiterals: Exclude: - 'Gemfile' - 'Rakefile' - - 'app/controllers/admin/staff_notes_controller.rb' + - 'app/controllers/staff_notes_controller.rb' - 'app/controllers/blips_controller.rb' - 'app/controllers/comments_controller.rb' - 'app/controllers/edit_histories_controller.rb' diff --git a/app/controllers/admin/staff_notes_controller.rb b/app/controllers/admin/staff_notes_controller.rb deleted file mode 100644 index 2b5fad79f..000000000 --- a/app/controllers/admin/staff_notes_controller.rb +++ /dev/null @@ -1,41 +0,0 @@ -# frozen_string_literal: true - -module Admin - class StaffNotesController < ApplicationController - before_action :can_view_staff_notes_only - respond_to :html - - def index - @user = User.find_by(id: params[:user_id]) - @notes = StaffNote.search(search_params.merge({ user_id: params[:user_id] })).includes(:user, :creator).paginate(params[:page], limit: params[:limit]) - respond_with(@notes) - end - - def new - @user = User.find(params[:user_id]) - @staff_note = StaffNote.new(note_params) - respond_with(@note) - end - - def create - @user = User.find(params[:user_id]) - @staff_note = StaffNote.create(note_params.merge({creator: CurrentUser.user, user_id: @user.id})) - flash[:notice] = @staff_note.valid? ? "Staff Note added" : @staff_note.errors.full_messages.join("; ") - respond_with(@staff_note) do |format| - format.html do - redirect_back fallback_location: admin_staff_notes_path - end - end - end - - private - - def search_params - permit_search_params(%i[creator_id creator_name user_id user_name resolved body_matches without_system_user]) - end - - def note_params - params.fetch(:staff_note, {}).permit(%i[body]) - end - end -end diff --git a/app/controllers/mod_actions_controller.rb b/app/controllers/mod_actions_controller.rb index b3fd2076b..c13f61348 100644 --- a/app/controllers/mod_actions_controller.rb +++ b/app/controllers/mod_actions_controller.rb @@ -5,19 +5,20 @@ class ModActionsController < ApplicationController def index @mod_actions = ModActionDecorator.decorate_collection( - ModAction.includes(:creator).search(search_params).paginate(params[:page], limit: params[:limit]), + ModAction.visible(CurrentUser.user).includes(:creator).search(search_params).paginate(params[:page], limit: params[:limit]), ) - respond_with(@mod_actions) do |format| - format.json do - render json: @mod_actions.to_json - end - end + respond_with(@mod_actions) end def show @mod_action = ModAction.find(params[:id]) + check_permission(@mod_action) respond_with(@mod_action) do |fmt| fmt.html { redirect_to mod_actions_path(search: { id: @mod_action.id }) } end end + + def check_permission(mod_action) + raise(User::PrivilegeError) unless mod_action.can_view?(CurrentUser.user) + end end diff --git a/app/controllers/staff_notes_controller.rb b/app/controllers/staff_notes_controller.rb new file mode 100644 index 000000000..cefaac26b --- /dev/null +++ b/app/controllers/staff_notes_controller.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +class StaffNotesController < ApplicationController + before_action :can_view_staff_notes_only + before_action :load_staff_note, only: %i[show edit update delete undelete] + before_action :check_edit_privilege, only: %i[update] + before_action :check_delete_privilege, only: %i[delete undelete] + respond_to :html, :json + + def index + @user = User.find_by(id: params[:user_id]) + @notes = StaffNote.search(search_params.merge({ user_id: params[:user_id] })).includes(:user, :creator).paginate(params[:page], limit: params[:limit]) + respond_with(@notes) + end + + def show + respond_with(@staff_note) + end + + def new + @user = User.find(params[:user_id]) + @staff_note = StaffNote.new(staff_note_params) + respond_with(@note) + end + + def edit + respond_with(@staff_note) + end + + def create + @user = User.find(params[:user_id]) + @staff_note = StaffNote.create(staff_note_params.merge({ user_id: @user.id })) + flash[:notice] = @staff_note.valid? ? "Staff Note added" : @staff_note.errors.full_messages.join("; ") + respond_with(@staff_note) do |format| + format.html do + redirect_back fallback_location: staff_notes_path + end + end + end + + def update + @staff_note.update(staff_note_params) + redirect_back(fallback_location: staff_notes_path) + end + + def delete + @staff_note.update(is_deleted: true) + redirect_back(fallback_location: staff_notes_path) + end + + def undelete + @staff_note.update(is_deleted: false) + redirect_back(fallback_location: staff_notes_path) + end + + private + + def search_params + permit_search_params(%i[creator_id creator_name updater_id updater_name user_id user_name body_matches without_system_user include_deleted]) + end + + def staff_note_params + params.fetch(:staff_note, {}).permit(%i[body]) + end + + def load_staff_note + @staff_note = StaffNote.find(params[:id]) + end + + def check_edit_privilege + raise User::PrivilegeError unless @staff_note.can_edit?(CurrentUser.user) + end + + def check_delete_privilege + raise User::PrivilegeError unless @staff_note.can_delete?(CurrentUser.user) + end +end diff --git a/app/decorators/mod_action_decorator.rb b/app/decorators/mod_action_decorator.rb index 45d12b184..a0b7f71c0 100644 --- a/app/decorators/mod_action_decorator.rb +++ b/app/decorators/mod_action_decorator.rb @@ -55,7 +55,7 @@ class ModActionDecorator < ApplicationDecorator when "artist_delete" "Deleted artist ##{vals['artist_id']} (#{vals['artist_name']})" when "artist_page_rename" - "Renamed artist page (\"#{vals['old_name']}\":/artists/show_or_new?name=#{vals['old_name']} -> \"#{vals['new_name']}\":/artists/show_or_new?name=#{vals['new_name']})" + "Renamed artist page (\"#{vals['old_name']}\":/artists/show_or_new?name=#{vals['old_name']} โ \"#{vals['new_name']}\":/artists/show_or_new?name=#{vals['new_name']})" when "artist_page_lock" "Locked artist page artist ##{vals['artist_page']}" when "artist_page_unlock" @@ -77,6 +77,16 @@ class ModActionDecorator < ApplicationDecorator when "avoid_posting_undelete" "Undeleted \"avoid posting ##{vals['id']}\":/avoid_postings/#{vals['id']} for [[#{vals['artist_name']}]]" + ### Staff Note ### + when "staff_note_create" + "Created \"staff note ##{vals['id']}\":/staff_notes/#{vals['id']} for #{user}\n#{vals['body']}" + when "staff_note_update" + "Updated \"staff note ##{vals['id']}\":/staff_notes/#{vals['id']} for #{user}\n#{vals['body']}" + when "staff_note_delete" + "Deleted \"staff note ##{vals['id']}\":/staff_notes/#{vals['id']} for #{user}" + when "staff_note_undelete" + "Undeleted \"staff note ##{vals['id']}\":/staff_notes/#{vals['id']} for #{user}" + ### User ### when "user_delete" @@ -276,7 +286,7 @@ class ModActionDecorator < ApplicationDecorator ### BURs ### when "mass_update" - "Mass updated [[#{vals['antecedent']}]] -> [[#{vals['consequent']}]]" + "Mass updated [[#{vals['antecedent']}]] โ [[#{vals['consequent']}]]" when "nuke_tag" "Nuked tag [[#{vals['tag_name']}]]" @@ -319,7 +329,7 @@ class ModActionDecorator < ApplicationDecorator "Edited whitelist entry" else if vals['old_pattern'] && vals['old_pattern'] != vals['pattern'] && CurrentUser.is_admin? - "Edited whitelist entry '#{vals['old_pattern']}' -> '#{vals['pattern']}'" + "Edited whitelist entry '#{vals['old_pattern']}' โ '#{vals['pattern']}'" else "Edited whitelist entry '#{CurrentUser.is_admin? ? vals['pattern'] : vals['note']}'" end diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index 1d30f168f..b04f50f6d 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -47,6 +47,7 @@ export { default as PostReplacement } from '../src/javascripts/post_replacement. export { default as PostVersions } from '../src/javascripts/post_versions.js'; export { default as Replacer } from '../src/javascripts/replacer.js'; export { default as Shortcuts } from '../src/javascripts/shortcuts.js'; +export { default as StaffNote } from '../src/javascripts/staff_notes.js'; export { default as Utility } from '../src/javascripts/utility.js'; export { default as TagRelationships } from '../src/javascripts/tag_relationships.js'; export { default as Takedown } from '../src/javascripts/takedowns.js'; diff --git a/app/javascript/src/javascripts/staff_notes.js b/app/javascript/src/javascripts/staff_notes.js new file mode 100644 index 000000000..2122ad53b --- /dev/null +++ b/app/javascript/src/javascripts/staff_notes.js @@ -0,0 +1,25 @@ +const StaffNote = {}; + +StaffNote.initialize_all = function () { + $(".expand-new-staff-note").on("click", StaffNote.show_new_note_form); + $(".edit-staff-note-link").on("click", StaffNote.show_edit_form); +}; + +StaffNote.show_new_note_form = function (e) { + e.preventDefault(); + $(e.target).hide(); + var $form = $(e.target).closest("div.new-staff-note").find("form"); + $form.show(); + $form[0].scrollIntoView(false); +}; + +StaffNote.show_edit_form = function (e) { + e.preventDefault(); + $(this).closest(".staff-note").find(".edit_staff_note").show(); +}; + +$(document).ready(function () { + StaffNote.initialize_all(); +}); + +export default StaffNote; diff --git a/app/javascript/src/styles/specific/staff_notes.scss b/app/javascript/src/styles/specific/staff_notes.scss index 82213752a..f03111552 100644 --- a/app/javascript/src/styles/specific/staff_notes.scss +++ b/app/javascript/src/styles/specific/staff_notes.scss @@ -1,11 +1,15 @@ .staff-note-list { - .staff-note { - margin-bottom: $padding-050; - } - // This is getting overwritten through DText styling on user profiles, // because the whole list is wrapped in a collapsable section. h4.author-name { font-size: $h4-size; } } + +.staff-note { + margin-bottom: $padding-050; + + &[data-is-deleted="true"] { + background: $background-article-hidden; + } +} diff --git a/app/models/mod_action.rb b/app/models/mod_action.rb index 02b35eb36..0ee0cb942 100644 --- a/app/models/mod_action.rb +++ b/app/models/mod_action.rb @@ -16,6 +16,10 @@ class ModAction < ApplicationRecord avoid_posting_delete: %i[id artist_name], avoid_posting_undelete: %i[id artist_name], avoid_posting_destroy: %i[id artist_name], + staff_note_create: %i[id user_id body], + staff_note_update: %i[id user_id body old_body], + staff_note_delete: %i[id user_id], + staff_note_undelete: %i[id user_id], blip_delete: %i[blip_id user_id], blip_hide: %i[blip_id user_id], blip_unhide: %i[blip_id user_id], @@ -91,18 +95,38 @@ class ModAction < ApplicationRecord takedown_process: %i[takedown_id], }.freeze + ProtectedActionKeys = %w[staff_note_create staff_note_update staff_note_delete staff_note_undelete ip_ban_create ip_ban_delete].freeze + KnownActionKeys = KnownActions.keys.freeze - def self.search(params) - q = super - - q = q.where_user(:creator_id, :creator, params) - - if params[:action].present? - q = q.where("action = ?", params[:action]) + module SearchMethods + def visible(user) + if user.is_staff? + all + else + where.not(action: ProtectedActionKeys) + end end - q.apply_basic_order(params) + def search(params) + q = super + + q = q.where_user(:creator_id, :creator, params) + + if params[:action].present? + q = q.where("action = ?", params[:action]) + end + + q.apply_basic_order(params) + end + end + + def can_view?(user) + if user.is_staff? + true + else + ProtectedActionKeys.exclude?(action) + end end def values @@ -131,7 +155,7 @@ class ModAction < ApplicationRecord end def hidden_attributes - super + [:values] + super + %i[values values_old] end def method_attributes @@ -145,4 +169,6 @@ class ModAction < ApplicationRecord def initialize_creator self.creator_id = CurrentUser.id end + + extend SearchMethods end diff --git a/app/models/staff_note.rb b/app/models/staff_note.rb index 496ba8957..5b3dfe0f5 100644 --- a/app/models/staff_note.rb +++ b/app/models/staff_note.rb @@ -1,8 +1,33 @@ # frozen_string_literal: true class StaffNote < ApplicationRecord - belongs_to :creator, class_name: "User" + belongs_to_creator + belongs_to_updater belongs_to :user + after_create :log_create + after_update :log_update + + scope :active, -> { where(is_deleted: false) } + + module LogMethods + def log_create + ModAction.log(:staff_note_create, { id: id, user_id: user_id, body: body }) + end + + def log_update + if saved_change_to_body? + ModAction.log(:staff_note_update, { id: id, user_id: user_id, body: body, old_body: body_before_last_save }) + end + + if saved_change_to_is_deleted? + if is_deleted? + ModAction.log(:staff_note_delete, { id: id, user_id: user_id }) + else + ModAction.log(:staff_note_undelete, { id: id, user_id: user_id }) + end + end + end + end module SearchMethods def search(params) @@ -12,28 +37,45 @@ class StaffNote < ApplicationRecord q = q.attribute_matches(:body, params[:body_matches]) q = q.where_user(:user_id, :user, params) q = q.where_user(:creator_id, :creator, params) + q = q.where_user(:updater_id, :updater, params) if params[:without_system_user]&.truthy? q = q.where.not(creator: User.system) end + if params[:is_deleted].present? + q = q.attribute_matches(:is_deleted, params[:is_deleted]) + elsif !params[:include_deleted]&.truthy? + q = q.active + end + q.apply_basic_order(params) end def default_order - order("resolved asc, id desc") + order("id desc") end end + include LogMethods extend SearchMethods - def resolve! - self.resolved = true - save + def user_name + User.id_to_name(user_id) end - def unresolve! - self.resolved = false - save + def user_name=(name) + self.user_id = User.name_to_id(name) + end + + def can_edit?(user) + return false unless user.is_staff? + user.id == creator_id || user.is_admin? + end + + def can_delete?(user) + return false unless user.is_staff? + return true if creator_id == user.id || user.is_admin? + user_id != user.id end end diff --git a/app/models/user.rb b/app/models/user.rb index b03d0c410..0ad1ecde7 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -111,7 +111,7 @@ class User < ApplicationRecord has_many :post_sets, -> { order(name: :asc) }, foreign_key: :creator_id has_many :post_versions has_many :post_votes - has_many :staff_notes, -> { order("staff_notes.id desc") } + has_many :staff_notes, -> { active.order("staff_notes.id desc") } has_many :user_name_change_requests, -> { order(id: :asc) } belongs_to :avatar, class_name: 'Post', optional: true diff --git a/app/views/admin/staff_notes/partials/_for_user.html.erb b/app/views/admin/staff_notes/partials/_for_user.html.erb deleted file mode 100644 index ceb013bc3..000000000 --- a/app/views/admin/staff_notes/partials/_for_user.html.erb +++ /dev/null @@ -1,13 +0,0 @@ -<% if CurrentUser.can_view_staff_notes? %> -
<%= link_to "Create ยป", new_staff_note_path(search: { user_id: user.id }), class: "expand-new-staff-note" %>
+ <%= render "staff_notes/partials/new", user: user, staff_note: StaffNote.new(user_id: user.id), hidden: true %> +