diff --git a/app/controllers/admin/staff_notes_controller.rb b/app/controllers/admin/staff_notes_controller.rb new file mode 100644 index 000000000..7e8426f2a --- /dev/null +++ b/app/controllers/admin/staff_notes_controller.rb @@ -0,0 +1,35 @@ +module Admin + class StaffNotesController < ApplicationController + before_action :moderator_only + respond_to :html + + def index + @user = User.where('id = ?', params[:user_id]).first + @notes = StaffNote.search(search_params).paginate(params[:page]) + 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 note_params + params.fetch(:staff_note, {}).permit([:body]) + end + end +end diff --git a/app/models/staff_audit_log.rb b/app/models/staff_audit_log.rb new file mode 100644 index 000000000..33d658b72 --- /dev/null +++ b/app/models/staff_audit_log.rb @@ -0,0 +1,7 @@ +class StaffAuditLog < ApplicationRecord + belongs_to :user, :class_name => "User" + + def self.log(category, user, details) + create(user: user, action: category.to_s, values: details) + end +end diff --git a/app/models/staff_note.rb b/app/models/staff_note.rb new file mode 100644 index 000000000..7544745f5 --- /dev/null +++ b/app/models/staff_note.rb @@ -0,0 +1,62 @@ +class StaffNote < ApplicationRecord + belongs_to :creator, :class_name => "User" + belongs_to :user + + after_create :add_audit_entry + + module SearchMethods + def for_creator(user_id) + user_id.present? ? where("creator_id = ?", user_id) : none + end + + def for_creator_name(user_name) + for_creator(User.name_to_id(user_name)) + end + + def for_user(user_id) + user_id.present? ? where('creator_id = ?', user_id) : none + end + + def for_user_name(user_name) + for_user(User.name_to_id(user_name)) + end + + def search(params) + q = super + + if params[:resolved] + q = q.attribute_matches(:resolved, params[:resolved]) + end + + if params[:user_name].present? + q = q.for_user_name(params[:user_name]) + end + + if params[:creator_name].present? + q = q.for_creator_name(params[:creator_name]) + end + q.apply_default_order(params) + end + + def default_order + order("resolved asc, id desc") + end + end + + extend SearchMethods + + + def add_audit_entry + StaffAuditLog.log(:staff_note_add, creator, {user_id: user_id}) + end + + def resolve! + self.resolved = true + save + end + + def unresolve! + self.resolved = false + save + end +end diff --git a/app/presenters/user_presenter.rb b/app/presenters/user_presenter.rb index b09f1f79a..afe402dc5 100644 --- a/app/presenters/user_presenter.rb +++ b/app/presenters/user_presenter.rb @@ -183,4 +183,16 @@ class UserPresenter return false if user.enable_privacy_mode? && !CurrentUser.is_admin? true end + + def show_staff_notes? + CurrentUser.is_moderator? + end + + def staff_notes + StaffNote.where(user_id: user.id).order(id: :desc).limit(15) + end + + def new_staff_note + StaffNote.new(user_id: user.id) + end end diff --git a/app/views/admin/staff_notes/_search.html.erb b/app/views/admin/staff_notes/_search.html.erb new file mode 100644 index 000000000..75912f00e --- /dev/null +++ b/app/views/admin/staff_notes/_search.html.erb @@ -0,0 +1,13 @@ +
+ <%= search_show_link %> +
+
+ <%= simple_form_for(:search, :method => :get, url: admin_staff_notes_path, defaults: {required: false}, html: {class: "inline-form"}) do |f| %> + <%= f.input :creator_name, label: "Creator Name", input_html: {data: {autocomplete: "user"}} %> + <%= f.input :user_name, label: "User Name", input_html: {data: {autocomplete: "user"}} %> + <%= f.input :body_matches, label: "Body" %> + <%= f.submit "Search" %> + <% end %> + <%= search_hide_link %> +
diff --git a/app/views/admin/staff_notes/index.html.erb b/app/views/admin/staff_notes/index.html.erb new file mode 100644 index 000000000..abf9ad86c --- /dev/null +++ b/app/views/admin/staff_notes/index.html.erb @@ -0,0 +1,15 @@ +
+
+

<%= @user ? "Staff Notes for #{@user.name}" : "Staff Notes" %>

+ <%= render "search" %> + + <%= render "admin/staff_notes/partials/list_of_notes", staff_notes: @notes %> + <%= numbered_paginator(@notes) %> +
+
+ +<%= render "users/secondary_links" %> + +<% content_for(:page_title) do %> + <%= @user ? "#{@user.name} - Staff Notes" : "Staff Notes" %> +<% end %> diff --git a/app/views/admin/staff_notes/new.html.erb b/app/views/admin/staff_notes/new.html.erb new file mode 100644 index 000000000..afd4e5915 --- /dev/null +++ b/app/views/admin/staff_notes/new.html.erb @@ -0,0 +1,13 @@ +
+
+

New Staff Note for <%= link_to_user(@user) %>

+ + <%= render "admin/staff_notes/partials/new", staff_note: @staff_note, user: @user %> +
+
+ +<%= render "users/secondary_links" %> + +<% content_for(:page_title) do %> + New Staff Note +<% end %> diff --git a/app/views/admin/staff_notes/partials/_list_of_notes.html.erb b/app/views/admin/staff_notes/partials/_list_of_notes.html.erb new file mode 100644 index 000000000..5be6ce20f --- /dev/null +++ b/app/views/admin/staff_notes/partials/_list_of_notes.html.erb @@ -0,0 +1,3 @@ +
+ <%= render :partial => "admin/staff_notes/partials/staff_note", :collection => staff_notes %> +
diff --git a/app/views/admin/staff_notes/partials/_new.html.erb b/app/views/admin/staff_notes/partials/_new.html.erb new file mode 100644 index 000000000..f1193ad70 --- /dev/null +++ b/app/views/admin/staff_notes/partials/_new.html.erb @@ -0,0 +1,7 @@ +<%= error_messages_for :staff_note %> + +<%= simple_form_for(staff_note, url: user_staff_notes_path(user_id: user.id), method: :post) do |f| %> + <%= dtext_field "staff_note", "body", name: "", :value => staff_note.body, :input_id => "staff_note_body_for_#{staff_note.id}", :preview_id => "dtext-preview-for-#{staff_note.id}" %> + <%= f.button :submit, "Submit" %> + <%= dtext_preview_button "staff_note", "body", :input_id => "staff_note_body_for_#{staff_note.id}", :preview_id => "dtext-preview-for-#{staff_note.id}" %> +<% end %> diff --git a/app/views/admin/staff_notes/partials/_staff_note.html.erb b/app/views/admin/staff_notes/partials/_staff_note.html.erb new file mode 100644 index 000000000..3c4b4f241 --- /dev/null +++ b/app/views/admin/staff_notes/partials/_staff_note.html.erb @@ -0,0 +1,19 @@ +
+
+
+

<%= link_to_user staff_note.creator %>

+ <%= staff_note.creator.level_string %> +
+
+ <%= user_avatar(staff_note.creator) %> +
+
+ <%= link_to time_ago_in_words_tagged(staff_note.created_at), user_staff_notes_path(user_id: staff_note.user_id, anchor: "staff-note-#{staff_note.id}") %> +
+
+
+
+ <%= format_text(staff_note.body, allow_color: true) %> +
+
+
diff --git a/app/views/users/show.html.erb b/app/views/users/show.html.erb index 476699213..47fd4abeb 100644 --- a/app/views/users/show.html.erb +++ b/app/views/users/show.html.erb @@ -3,14 +3,31 @@
<%= render "statistics", :presenter => @presenter, :user => @user %> + <% if @presenter.show_staff_notes? %> +
+ +
+ <% end %> +
<% if @presenter.can_view_favorites? %>
- <%= render "posts/partials/common/inline_blacklist" %> + <%= render "posts/partials/common/inline_blacklist" %>
- <%= render "post_summary", :presenter => @presenter, :user => @user %> + <%= render "post_summary", :presenter => @presenter, :user => @user %>
<% end %>
diff --git a/config/routes.rb b/config/routes.rb index 52487cb54..939939dae 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -21,6 +21,7 @@ Rails.application.routes.draw do resource :dashboard, :only => [:show] resources :exceptions, only: [:index, :show] resource :reowner, controller: 'reowner', only: [:new, :create] + resources :staff_notes, only: [:index] end resources :edit_histories namespace :moderator do @@ -336,6 +337,7 @@ Rails.application.routes.draw do resource :api_key, :only => [:show, :view, :update, :destroy], :controller => "maintenance/user/api_keys" do post :view end + resources :staff_notes, only: [:index, :new, :create], controller: "admin/staff_notes" collection do get :home diff --git a/db/migrate/20210426025625_add_staff_notes_table.rb b/db/migrate/20210426025625_add_staff_notes_table.rb new file mode 100644 index 000000000..edcccc5f7 --- /dev/null +++ b/db/migrate/20210426025625_add_staff_notes_table.rb @@ -0,0 +1,11 @@ +class AddStaffNotesTable < ActiveRecord::Migration[6.1] + def change + create_table :staff_notes do |t| + t.timestamps + t.references :user, null: false, foreign_key: true, index: true + t.integer :creator_id, null: false, index: true + t.string :body + t.boolean :resolved, null: false, default: false + end + end +end diff --git a/db/migrate/20210506235640_add_staff_audit_logs_table.rb b/db/migrate/20210506235640_add_staff_audit_logs_table.rb new file mode 100644 index 000000000..fecb9067f --- /dev/null +++ b/db/migrate/20210506235640_add_staff_audit_logs_table.rb @@ -0,0 +1,10 @@ +class AddStaffAuditLogsTable < ActiveRecord::Migration[6.1] + def change + create_table :staff_audit_logs do |t| + t.timestamps + t.references :user, null: false, foreign_key: true, index: true + t.string :action, null: false, default: 'unknown_action' + t.json :values + end + end +end diff --git a/db/structure.sql b/db/structure.sql index c2f46229b..83e6625af 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -2022,6 +2022,73 @@ CREATE TABLE public.schema_migrations ( ); +-- +-- Name: staff_audit_logs; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.staff_audit_logs ( + id bigint NOT NULL, + created_at timestamp(6) without time zone NOT NULL, + updated_at timestamp(6) without time zone NOT NULL, + user_id bigint NOT NULL, + action character varying DEFAULT 'unknown_action'::character varying NOT NULL, + "values" json +); + + +-- +-- Name: staff_audit_logs_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.staff_audit_logs_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: staff_audit_logs_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.staff_audit_logs_id_seq OWNED BY public.staff_audit_logs.id; + + +-- +-- Name: staff_notes; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.staff_notes ( + id bigint NOT NULL, + created_at timestamp(6) without time zone NOT NULL, + updated_at timestamp(6) without time zone NOT NULL, + user_id bigint NOT NULL, + creator_id integer NOT NULL, + body character varying, + resolved boolean DEFAULT false NOT NULL +); + + +-- +-- Name: staff_notes_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.staff_notes_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: staff_notes_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.staff_notes_id_seq OWNED BY public.staff_notes.id; + + -- -- Name: tag_aliases; Type: TABLE; Schema: public; Owner: - -- @@ -3025,6 +3092,20 @@ ALTER TABLE ONLY public.posts ALTER COLUMN change_seq SET DEFAULT nextval('publi ALTER TABLE ONLY public.saved_searches ALTER COLUMN id SET DEFAULT nextval('public.saved_searches_id_seq'::regclass); +-- +-- Name: staff_audit_logs id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.staff_audit_logs ALTER COLUMN id SET DEFAULT nextval('public.staff_audit_logs_id_seq'::regclass); + + +-- +-- Name: staff_notes id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.staff_notes ALTER COLUMN id SET DEFAULT nextval('public.staff_notes_id_seq'::regclass); + + -- -- Name: tag_aliases id; Type: DEFAULT; Schema: public; Owner: - -- @@ -3547,6 +3628,22 @@ ALTER TABLE ONLY public.schema_migrations ADD CONSTRAINT schema_migrations_pkey PRIMARY KEY (version); +-- +-- Name: staff_audit_logs staff_audit_logs_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.staff_audit_logs + ADD CONSTRAINT staff_audit_logs_pkey PRIMARY KEY (id); + + +-- +-- Name: staff_notes staff_notes_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.staff_notes + ADD CONSTRAINT staff_notes_pkey PRIMARY KEY (id); + + -- -- Name: tag_aliases tag_aliases_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -4522,6 +4619,27 @@ CREATE INDEX index_saved_searches_on_query ON public.saved_searches USING btree CREATE INDEX index_saved_searches_on_user_id ON public.saved_searches USING btree (user_id); +-- +-- Name: index_staff_audit_logs_on_user_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_staff_audit_logs_on_user_id ON public.staff_audit_logs USING btree (user_id); + + +-- +-- Name: index_staff_notes_on_creator_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_staff_notes_on_creator_id ON public.staff_notes USING btree (creator_id); + + +-- +-- Name: index_staff_notes_on_user_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_staff_notes_on_user_id ON public.staff_notes USING btree (user_id); + + -- -- Name: index_tag_aliases_on_antecedent_name; Type: INDEX; Schema: public; Owner: - -- @@ -4865,6 +4983,14 @@ CREATE TRIGGER trigger_posts_on_tag_index_update BEFORE INSERT OR UPDATE ON publ CREATE TRIGGER trigger_wiki_pages_on_update BEFORE INSERT OR UPDATE ON public.wiki_pages FOR EACH ROW EXECUTE PROCEDURE tsvector_update_trigger('body_index', 'public.danbooru', 'body', 'title'); +-- +-- Name: staff_audit_logs fk_rails_02329e5ef9; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.staff_audit_logs + ADD CONSTRAINT fk_rails_02329e5ef9 FOREIGN KEY (user_id) REFERENCES public.users(id); + + -- -- Name: post_image_hashes fk_rails_2b7afcc2f0; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -4881,6 +5007,14 @@ ALTER TABLE ONLY public.favorites ADD CONSTRAINT fk_rails_a7668ef613 FOREIGN KEY (user_id) REFERENCES public.users(id); +-- +-- Name: staff_notes fk_rails_bab7e2d92a; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.staff_notes + ADD CONSTRAINT fk_rails_bab7e2d92a FOREIGN KEY (user_id) REFERENCES public.users(id); + + -- -- Name: favorites fk_rails_d20e53bb68; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -5128,6 +5262,8 @@ INSERT INTO "schema_migrations" (version) VALUES ('20210117173030'), ('20210405040522'), ('20210425020131'), -('20210430201028'); +('20210426025625'), +('20210430201028'), +('20210506235640');