diff --git a/app/assets/stylesheets/specific/user_feedback.css.scss b/app/assets/stylesheets/specific/user_feedback.css.scss index abf100c9f..3495f1023 100644 --- a/app/assets/stylesheets/specific/user_feedback.css.scss +++ b/app/assets/stylesheets/specific/user_feedback.css.scss @@ -1,9 +1,9 @@ div#c-user-feedbacks { - tr.feedback-category-positive { + .feedback-category-positive { background: #DDFFDD !important; } - tr.feedback-category-negative { + .feedback-category-negative { background: #FFDDDD !important; } } \ No newline at end of file diff --git a/app/assets/stylesheets/specific/user_name_change_requests.css.scss b/app/assets/stylesheets/specific/user_name_change_requests.css.scss new file mode 100644 index 000000000..bfacee695 --- /dev/null +++ b/app/assets/stylesheets/specific/user_name_change_requests.css.scss @@ -0,0 +1,31 @@ +#c-user-name-change-requests { + .feedback-category-positive { + background: #DDFFDD !important; + } + + .feedback-category-negative { + background: #FFDDDD !important; + } + + li { + margin-bottom: 1em; + } + + form { + margin-bottom: 2em; + } + + p { + margin: 0; + } + + p.author { + font-size: 80%; + font-style: italic; + color: #AAA; + } + + section { + margin-bottom: 1em; + } +} \ No newline at end of file diff --git a/app/controllers/user_name_change_requests_controller.rb b/app/controllers/user_name_change_requests_controller.rb new file mode 100644 index 000000000..334112c88 --- /dev/null +++ b/app/controllers/user_name_change_requests_controller.rb @@ -0,0 +1,38 @@ +class UserNameChangeRequestsController < ApplicationController + before_filter :member_only, :only => [:new, :create, :show] + before_filter :admin_only, :only => [:index, :approve, :reject] + + def new + end + + def create + @change_request = UserNameChangeRequest.create( + :user_id => CurrentUser.user.id, + :original_name => CurrentUser.user.name, + :status => "pending", + :change_reason => params[:reason], + :desired_name => params[:desired_name] + ) + redirect_to user_name_change_request_path(@change_request), :notice => "Your request has been submitted and is pending admin review" + end + + def show + @change_request = UserNameChangeRequest.find(params[:id]) + end + + def index + @change_requests = UserNameChangeRequest.order("id desc").paginate(params[:page]) + end + + def approve + @change_request = UserNameChangeRequest.find(params[:id]) + @change_request.approve! + redirect_to user_name_change_request_path(@change_request), :notice => "Name change request approved" + end + + def reject + @change_request = UserNameChangeRequest.find(params[:id]) + @change_request.reject!(params[:reason]) + redirect_to user_name_change_request_path(@change_request), :notice => "Name change request rejected" + end +end diff --git a/app/models/tag_alias.rb b/app/models/tag_alias.rb index bef397702..c185c3b5f 100644 --- a/app/models/tag_alias.rb +++ b/app/models/tag_alias.rb @@ -3,6 +3,7 @@ class TagAlias < ActiveRecord::Base after_save :clear_all_cache after_destroy :clear_all_cache before_validation :initialize_creator, :on => :create + before_validation :normalize_names validates_presence_of :creator_id, :antecedent_name, :consequent_name validates_uniqueness_of :antecedent_name validate :absence_of_transitive_relation @@ -88,6 +89,11 @@ class TagAlias < ActiveRecord::Base def is_active? status == "active" end + + def normalize_names + self.antecedent_name = antecedent_name.mb_chars.downcase.tr(" ", "_").strip + self.consequent_name = consequent_name.downcase.tr(" ", "_").strip + end def initialize_creator self.creator_id = CurrentUser.user.id diff --git a/app/models/tag_implication.rb b/app/models/tag_implication.rb index 3384386b2..fb2948bcf 100644 --- a/app/models/tag_implication.rb +++ b/app/models/tag_implication.rb @@ -3,6 +3,7 @@ class TagImplication < ActiveRecord::Base after_save :update_descendant_names_for_parent belongs_to :creator, :class_name => "User" before_validation :initialize_creator, :on => :create + before_validation :normalize_names validates_presence_of :creator_id, :antecedent_name, :consequent_name validates_uniqueness_of :antecedent_name, :scope => :consequent_name validate :absence_of_circular_relation @@ -129,6 +130,11 @@ class TagImplication < ActiveRecord::Base end end end + + def normalize_names + self.antecedent_name = antecedent_name.downcase.tr(" ", "_").strip + self.consequent_name = consequent_name.downcase.tr(" ", "_").strip + end def is_pending? status == "pending" diff --git a/app/models/user.rb b/app/models/user.rb index c86cb3300..ddc8627de 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -531,7 +531,7 @@ class User < ActiveRecord::Base end def admins - where("is_admin = TRUE") + where("level = ?", Levels::ADMIN) end def with_email(email) diff --git a/app/models/user_name_change_request.rb b/app/models/user_name_change_request.rb new file mode 100644 index 000000000..6fd271077 --- /dev/null +++ b/app/models/user_name_change_request.rb @@ -0,0 +1,48 @@ +class UserNameChangeRequest < ActiveRecord::Base + validates_presence_of :user_id, :original_name, :desired_name + validates_inclusion_of :status, :in => %w(pending approved rejected) + belongs_to :user + belongs_to :approver, :class_name => "User" + validate :uniqueness_of_desired_name + validates_length_of :desired_name, :within => 2..100, :on => :create + validates_format_of :desired_name, :with => /\A[^\s:]+\Z/, :on => :create, :message => "cannot have whitespace or colons" + after_create :notify_admins + + def self.pending + where(:status => "pending") + end + + def feedback + UserFeedback.for_user(user_id).order("id desc").all + end + + def notify_admins + title = "#{original_name} is requesting a name change to #{desired_name}" + body = title + "\n\n\"See request\":/user_name_change_requests/#{id}" + User.admins.find_each do |user| + Dmail.create_split(:title => title, :body => body, :to_id => user.id) + end + end + + def approve! + update_attribute(:status, "approved") + user.update_attribute(:name, desired_name) + body = "Your name change request has been approved. Be sure to log in with your new user name." + Dmail.create_split(:title => "Name change request approved", :body => body, :to_id => user_id) + end + + def reject!(reason) + update_attributes(:status => "rejected", :rejection_reason => reason) + body = "Your name change request has been rejected for the following reason: #{rejection_reason}" + Dmail.create_split(:title => "Name change request rejected", :body => body, :to_id => user_id) + end + + def uniqueness_of_desired_name + if User.find_by_name(desired_name) + errors.add(:desired_name, "already exists") + return false + else + return true + end + end +end diff --git a/app/views/user_name_change_requests/new.html.erb b/app/views/user_name_change_requests/new.html.erb new file mode 100644 index 000000000..c112e827e --- /dev/null +++ b/app/views/user_name_change_requests/new.html.erb @@ -0,0 +1,21 @@ +

Name Change Request

+ +

You can request a name change but it must be approved. Factors that go into consideration include your upload and update history, and your user feedback.

+ +<%= error_messages_for "change_request" %> + +<%= form_tag(user_name_change_requests_path, :class => "simple_form") do %> +
+ + <%= text_field_tag "desired_name" %> +
+ +
+ + <%= text_field_tag "reason" %> +
+ +
+ <%= submit_tag %> +
+<% end %> \ No newline at end of file diff --git a/app/views/user_name_change_requests/show.html.erb b/app/views/user_name_change_requests/show.html.erb new file mode 100644 index 000000000..9f1d9cd78 --- /dev/null +++ b/app/views/user_name_change_requests/show.html.erb @@ -0,0 +1,56 @@ +
+

Name Change Request

+ +
+

<%= @change_request.original_name %> is requesting to change their name to <%= @change_request.desired_name %>.

+
+ +
+

Reason

+

<%= @change_request.change_reason %>

+
+ +
+

Feedback

+ +
+ +
+

Statistics

+ +
+ + <% if CurrentUser.user.is_admin? %> +
+

Options

+ <%= form_tag(approve_user_name_change_request_path(@change_request)) do %> + <%= submit_tag "Approve" %> + <% end %> + + <%= form_tag(reject_user_name_change_request_path(@change_request), :class => "simple_form") do %> +
+ + <%= text_field_tag "reason" %> +
+ +
+ <%= submit_tag "Reject" %> +
+ <% end %> +
+ <% end %> +
+ diff --git a/config/routes.rb b/config/routes.rb index faeb79bba..ff4410630 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -196,6 +196,12 @@ Danbooru::Application.routes.draw do end end resources :user_feedbacks + resources :user_name_change_requests do + member do + post :approve + post :reject + end + end resources :wiki_pages do member do put :revert diff --git a/db/migrate/20130326035904_create_user_name_change_requests.rb b/db/migrate/20130326035904_create_user_name_change_requests.rb new file mode 100644 index 000000000..4c8c02b96 --- /dev/null +++ b/db/migrate/20130326035904_create_user_name_change_requests.rb @@ -0,0 +1,21 @@ +class CreateUserNameChangeRequests < ActiveRecord::Migration + def up + create_table :user_name_change_requests do |t| + t.string :status, :null => false, :default => "pending" + t.integer :user_id, :null => false + t.integer :approver_id + t.string :original_name + t.string :desired_name + t.text :change_reason + t.text :rejection_reason + t.timestamps + end + + add_index :user_name_change_requests, :user_id + add_index :user_name_change_requests, :original_name + end + + def down + drop_table :user_name_change_requests + end +end diff --git a/db/structure.sql b/db/structure.sql index 9102bb82d..30197e5bd 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -2539,6 +2539,43 @@ CREATE SEQUENCE user_feedback_id_seq ALTER SEQUENCE user_feedback_id_seq OWNED BY user_feedback.id; +-- +-- Name: user_name_change_requests; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE user_name_change_requests ( + id integer NOT NULL, + status character varying(255) DEFAULT 'pending'::character varying NOT NULL, + user_id integer NOT NULL, + approver_id integer, + original_name character varying(255), + desired_name character varying(255), + change_reason text, + rejection_reason text, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL +); + + +-- +-- Name: user_name_change_requests_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE user_name_change_requests_id_seq + START WITH 1 + INCREMENT BY 1 + NO MAXVALUE + NO MINVALUE + CACHE 1; + + +-- +-- Name: user_name_change_requests_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE user_name_change_requests_id_seq OWNED BY user_name_change_requests.id; + + -- -- Name: user_password_reset_nonces; Type: TABLE; Schema: public; Owner: -; Tablespace: -- @@ -3641,6 +3678,13 @@ ALTER TABLE ONLY uploads ALTER COLUMN id SET DEFAULT nextval('uploads_id_seq'::r ALTER TABLE ONLY user_feedback ALTER COLUMN id SET DEFAULT nextval('user_feedback_id_seq'::regclass); +-- +-- Name: id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY user_name_change_requests ALTER COLUMN id SET DEFAULT nextval('user_name_change_requests_id_seq'::regclass); + + -- -- Name: id; Type: DEFAULT; Schema: public; Owner: - -- @@ -3941,6 +3985,14 @@ ALTER TABLE ONLY user_feedback ADD CONSTRAINT user_feedback_pkey PRIMARY KEY (id); +-- +-- Name: user_name_change_requests_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY user_name_change_requests + ADD CONSTRAINT user_name_change_requests_pkey PRIMARY KEY (id); + + -- -- Name: user_password_reset_nonces_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: -- @@ -6010,6 +6062,20 @@ CREATE INDEX index_user_feedback_on_creator_id ON user_feedback USING btree (cre CREATE INDEX index_user_feedback_on_user_id ON user_feedback USING btree (user_id); +-- +-- Name: index_user_name_change_requests_on_original_name; Type: INDEX; Schema: public; Owner: -; Tablespace: +-- + +CREATE INDEX index_user_name_change_requests_on_original_name ON user_name_change_requests USING btree (original_name); + + +-- +-- Name: index_user_name_change_requests_on_user_id; Type: INDEX; Schema: public; Owner: -; Tablespace: +-- + +CREATE INDEX index_user_name_change_requests_on_user_id ON user_name_change_requests USING btree (user_id); + + -- -- Name: index_users_on_email; Type: INDEX; Schema: public; Owner: -; Tablespace: -- @@ -6278,4 +6344,6 @@ INSERT INTO schema_migrations (version) VALUES ('20130322173202'); INSERT INTO schema_migrations (version) VALUES ('20130322173859'); -INSERT INTO schema_migrations (version) VALUES ('20130323160259'); \ No newline at end of file +INSERT INTO schema_migrations (version) VALUES ('20130323160259'); + +INSERT INTO schema_migrations (version) VALUES ('20130326035904'); \ No newline at end of file diff --git a/test/unit/user_name_change_request_test.rb b/test/unit/user_name_change_request_test.rb new file mode 100644 index 000000000..cbc62206f --- /dev/null +++ b/test/unit/user_name_change_request_test.rb @@ -0,0 +1,89 @@ +require 'test_helper' + +class UserNameChangeRequestTest < ActiveSupport::TestCase + context "in all cases" do + setup do + @admin = FactoryGirl.create(:admin_user) + @requester = FactoryGirl.create(:user) + CurrentUser.user = @requester + end + + context "approving a request" do + setup do + @change_request = UserNameChangeRequest.create( + :user_id => @requester.id, + :original_name => @requester.name, + :status => "pending", + :desired_name => "abc" + ) + CurrentUser.user = @admin + end + + should "create a dmail" do + assert_difference("Dmail.count", 2) do + @change_request.approve! + end + end + + should "change the user's name" do + @change_request.approve! + @requester.reload + assert_equal("abc", @requester.name) + end + + should "clear the user name cache" do + @change_request.approve! + assert_equal("abc", Cache.get("uin:#{@requester.id}")) + end + end + + context "rejecting a request" do + setup do + @change_request = UserNameChangeRequest.create( + :user_id => @requester.id, + :original_name => @requester.name, + :status => "pending", + :desired_name => "abc" + ) + CurrentUser.user = @admin + end + + should "create a dmail" do + assert_difference("Dmail.count", 2) do + @change_request.reject!("msg") + end + end + + should "preserve the username" do + @change_request.reject!("msg") + @requester.reload + assert_not_equal("abc", @requester.name) + end + end + + context "creating a new request" do + should "send dmails to the admin" do + assert_difference("Dmail.count", 2) do + UserNameChangeRequest.create( + :user_id => @requester.id, + :original_name => @requester.name, + :status => "pending", + :desired_name => "abc" + ) + end + end + + should "not validate if the desired name already exists" do + assert_difference("UserNameChangeRequest.count", 0) do + req = UserNameChangeRequest.create( + :user_id => @requester.id, + :original_name => @requester.name, + :status => "pending", + :desired_name => @requester.name + ) + assert_equal(["Desired name already exists"], req.errors.full_messages) + end + end + end + end +end