diff --git a/app/models/ban.rb b/app/models/ban.rb new file mode 100644 index 000000000..370be1495 --- /dev/null +++ b/app/models/ban.rb @@ -0,0 +1,53 @@ +class Ban < ActiveRecord::Base + after_create :update_feedback + belongs_to :user + belongs_to :banner, :class_name => "User" + attr_accessible :reason, :duration, :user_id + validate :user_is_inferior + + def self.is_user_banned?(user) + exists?(["user_id = ? AND expires_at > ?", user.id, Time.now]) + end + + def self.is_ip_banned?(ip_addr) + exists?(["ip_addr = ? AND expires_at > ?", ip_addr, Time.now]) + end + + def user_is_inferior + if user + if user.is_admin? + errors[:base] << "You can never ban an admin." + false + elsif user.is_moderator? && banner.is_admin? + true + elsif user.is_moderator? + errors[:base] << "Only admins can ban moderators." + false + elsif banner.is_admin? || banner.is_moderator? + true + else + errors[:base] << "No one else can ban." + false + end + end + end + + def update_feedback + if user + feedback = user.feedback.build + feedback.is_positive = false + feedback.body = "Banned: #{reason}" + feedback.creator_id = banner_id + feedback.save + end + end + + def duration=(dur) + self.expires_at = dur.to_i.days.from_now + @duration = dur + end + + def duration + @duration + end +end diff --git a/app/models/user.rb b/app/models/user.rb index 248b1786c..73bbe238d 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -12,6 +12,8 @@ class User < ActiveRecord::Base validates_confirmation_of :password before_save :encrypt_password after_save :update_cache + before_create :normalize_level + has_many :feedback, :class_name => "UserFeedback", :dependent => :destroy scope :named, lambda {|name| where(["lower(name) = ?", name])} module NameMethods @@ -92,10 +94,29 @@ class User < ActiveRecord::Base end end + module LevelMethods + def normalize_level + if is_admin? + self.is_moderator = true + self.is_janitor = true + self.is_contributor = true + self.is_privileged = true + elsif is_moderator? + self.is_janitor = true + self.is_privileged = true + elsif is_janitor? + self.is_privileged = true + elsif is_contributor? + self.is_privileged = true + end + end + end + include NameMethods include PasswordMethods extend AuthenticationMethods include FavoriteMethods + include LevelMethods def can_update?(object, foreign_key = :user_id) is_moderator? || is_admin? || object.__send__(foreign_key) == id diff --git a/app/models/user_feedback.rb b/app/models/user_feedback.rb new file mode 100644 index 000000000..9a7b132b0 --- /dev/null +++ b/app/models/user_feedback.rb @@ -0,0 +1,5 @@ +class UserFeedback < ActiveRecord::Base + set_table_name "user_feedback" + belongs_to :user + attr_accessible :body, :user_id, :is_positive +end diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb index d531b8bb8..8422db461 100644 --- a/config/initializers/inflections.rb +++ b/config/initializers/inflections.rb @@ -1,10 +1,3 @@ -# Be sure to restart your server when you modify this file. - -# Add new inflection rules using the following format -# (all these examples are active by default): -# ActiveSupport::Inflector.inflections do |inflect| -# inflect.plural /^(ox)$/i, '\1en' -# inflect.singular /^(ox)en/i, '\1' -# inflect.irregular 'person', 'people' -# inflect.uncountable %w( fish sheep ) -# end +ActiveSupport::Inflector.inflections do |inflect| + inflect.uncountable %w( feedback ) +end diff --git a/db/development_structure.sql b/db/development_structure.sql index 780a59630..1b57cc3dd 100644 --- a/db/development_structure.sql +++ b/db/development_structure.sql @@ -250,6 +250,41 @@ CREATE SEQUENCE artists_id_seq ALTER SEQUENCE artists_id_seq OWNED BY artists.id; +-- +-- Name: bans; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE bans ( + id integer NOT NULL, + user_id integer, + ip_addr inet, + reason text NOT NULL, + banner_id integer NOT NULL, + expires_at timestamp without time zone NOT NULL, + created_at timestamp without time zone, + updated_at timestamp without time zone +); + + +-- +-- Name: bans_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE bans_id_seq + START WITH 1 + INCREMENT BY 1 + NO MAXVALUE + NO MINVALUE + CACHE 1; + + +-- +-- Name: bans_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE bans_id_seq OWNED BY bans.id; + + -- -- Name: comment_votes; Type: TABLE; Schema: public; Owner: -; Tablespace: -- @@ -1000,6 +1035,40 @@ CREATE SEQUENCE uploads_id_seq ALTER SEQUENCE uploads_id_seq OWNED BY uploads.id; +-- +-- Name: user_feedback; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE user_feedback ( + id integer NOT NULL, + user_id integer NOT NULL, + creator_id integer NOT NULL, + is_positive boolean NOT NULL, + body text NOT NULL, + created_at timestamp without time zone, + updated_at timestamp without time zone +); + + +-- +-- Name: user_feedback_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE user_feedback_id_seq + START WITH 1 + INCREMENT BY 1 + NO MAXVALUE + NO MINVALUE + CACHE 1; + + +-- +-- Name: user_feedback_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE user_feedback_id_seq OWNED BY user_feedback.id; + + -- -- Name: users; Type: TABLE; Schema: public; Owner: -; Tablespace: -- @@ -1155,6 +1224,13 @@ ALTER TABLE artist_versions ALTER COLUMN id SET DEFAULT nextval('artist_versions ALTER TABLE artists ALTER COLUMN id SET DEFAULT nextval('artists_id_seq'::regclass); +-- +-- Name: id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE bans ALTER COLUMN id SET DEFAULT nextval('bans_id_seq'::regclass); + + -- -- Name: id; Type: DEFAULT; Schema: public; Owner: - -- @@ -1309,6 +1385,13 @@ ALTER TABLE unapprovals ALTER COLUMN id SET DEFAULT nextval('unapprovals_id_seq' ALTER TABLE uploads ALTER COLUMN id SET DEFAULT nextval('uploads_id_seq'::regclass); +-- +-- Name: id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE user_feedback ALTER COLUMN id SET DEFAULT nextval('user_feedback_id_seq'::regclass); + + -- -- Name: id; Type: DEFAULT; Schema: public; Owner: - -- @@ -1370,6 +1453,14 @@ ALTER TABLE ONLY artists ADD CONSTRAINT artists_pkey PRIMARY KEY (id); +-- +-- Name: bans_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY bans + ADD CONSTRAINT bans_pkey PRIMARY KEY (id); + + -- -- Name: comment_votes_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: -- @@ -1546,6 +1637,14 @@ ALTER TABLE ONLY uploads ADD CONSTRAINT uploads_pkey PRIMARY KEY (id); +-- +-- Name: user_feedback_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY user_feedback + ADD CONSTRAINT user_feedback_pkey PRIMARY KEY (id); + + -- -- Name: users_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: -- @@ -1647,6 +1746,27 @@ CREATE UNIQUE INDEX index_artists_on_name ON artists USING btree (name); CREATE INDEX index_artists_on_other_names_index ON artists USING gin (other_names_index); +-- +-- Name: index_bans_on_expires_at; Type: INDEX; Schema: public; Owner: -; Tablespace: +-- + +CREATE INDEX index_bans_on_expires_at ON bans USING btree (expires_at); + + +-- +-- Name: index_bans_on_ip_addr; Type: INDEX; Schema: public; Owner: -; Tablespace: +-- + +CREATE INDEX index_bans_on_ip_addr ON bans USING btree (ip_addr); + + +-- +-- Name: index_bans_on_user_id; Type: INDEX; Schema: public; Owner: -; Tablespace: +-- + +CREATE INDEX index_bans_on_user_id ON bans USING btree (user_id); + + -- -- Name: index_comment_votes_on_user_id; Type: INDEX; Schema: public; Owner: -; Tablespace: -- @@ -1962,6 +2082,13 @@ CREATE UNIQUE INDEX index_tags_on_name ON tags USING btree (name); CREATE INDEX index_unapprovals_on_post_id ON unapprovals USING btree (post_id); +-- +-- Name: index_user_feedback_on_user_id; Type: INDEX; Schema: public; Owner: -; Tablespace: +-- + +CREATE INDEX index_user_feedback_on_user_id ON user_feedback USING btree (user_id); + + -- -- Name: index_users_on_email; Type: INDEX; Schema: public; Owner: -; Tablespace: -- @@ -2086,4 +2213,8 @@ INSERT INTO schema_migrations (version) VALUES ('20100215223541'); INSERT INTO schema_migrations (version) VALUES ('20100215224629'); -INSERT INTO schema_migrations (version) VALUES ('20100215224635'); \ No newline at end of file +INSERT INTO schema_migrations (version) VALUES ('20100215224635'); + +INSERT INTO schema_migrations (version) VALUES ('20100215225710'); + +INSERT INTO schema_migrations (version) VALUES ('20100215230642'); \ No newline at end of file diff --git a/db/migrate/20100215225710_create_bans.rb b/db/migrate/20100215225710_create_bans.rb new file mode 100644 index 000000000..2031e2caa --- /dev/null +++ b/db/migrate/20100215225710_create_bans.rb @@ -0,0 +1,20 @@ +class CreateBans < ActiveRecord::Migration + def self.up + create_table :bans do |t| + t.column :user_id, :integer + t.column :ip_addr, "inet" + t.column :reason, :text, :null => false + t.column :banner_id, :integer, :null => false + t.column :expires_at, :datetime, :null => false + t.timestamps + end + + add_index :bans, :user_id + add_index :bans, :ip_addr + add_index :bans, :expires_at + end + + def self.down + drop_table :bans + end +end diff --git a/db/migrate/20100215230642_create_user_feedback.rb b/db/migrate/20100215230642_create_user_feedback.rb new file mode 100644 index 000000000..27a7f8fbb --- /dev/null +++ b/db/migrate/20100215230642_create_user_feedback.rb @@ -0,0 +1,17 @@ +class CreateUserFeedback < ActiveRecord::Migration + def self.up + create_table :user_feedback do |t| + t.column :user_id, :integer, :null => false + t.column :creator_id, :integer, :null => false + t.column :is_positive, :boolean, :null => false + t.column :body, :text, :null => false + t.timestamps + end + + add_index :user_feedback, :user_id + end + + def self.down + drop_table :user_feedback + end +end diff --git a/test/factories/ban.rb b/test/factories/ban.rb new file mode 100644 index 000000000..c7a37f859 --- /dev/null +++ b/test/factories/ban.rb @@ -0,0 +1,6 @@ +Factory.define(:ban) do |f| + f.user {|x| x.association(:user)} + f.banner {|x| x.association(:admin_user)} + f.reason {Faker::Lorem.words} + f.duration 60 +end diff --git a/test/unit/ban_test.rb b/test/unit/ban_test.rb new file mode 100644 index 000000000..13bd8fa05 --- /dev/null +++ b/test/unit/ban_test.rb @@ -0,0 +1,170 @@ +require File.dirname(__FILE__) + '/../test_helper' + +class BanTest < ActiveSupport::TestCase + context "A ban" do + context "created by an admin" do + setup do + @banner = Factory.create(:admin_user) + end + + teardown do + @banner = nil + end + + should "not be valid against another admin" do + user = Factory.create(:admin_user) + ban = Factory.build(:ban, :user => user, :banner => @banner) + ban.save + assert(ban.errors.any?) + end + + should "be valid against anyone who is not an admin" do + user = Factory.create(:moderator_user) + ban = Factory.create(:ban, :user => user, :banner => @banner) + assert(ban.errors.empty?) + + user = Factory.create(:janitor_user) + ban = Factory.create(:ban, :user => user, :banner => @banner) + assert(ban.errors.empty?) + + user = Factory.create(:contributor_user) + ban = Factory.create(:ban, :user => user, :banner => @banner) + assert(ban.errors.empty?) + + user = Factory.create(:privileged_user) + ban = Factory.create(:ban, :user => user, :banner => @banner) + assert(ban.errors.empty?) + + user = Factory.create(:user) + ban = Factory.create(:ban, :user => user, :banner => @banner) + assert(ban.errors.empty?) + end + end + + context "created by a moderator" do + setup do + @banner = Factory.create(:moderator_user) + end + + teardown do + @banner = nil + end + + should "not be valid against an admin or moderator" do + user = Factory.create(:admin_user) + ban = Factory.build(:ban, :user => user, :banner => @banner) + ban.save + assert(ban.errors.any?) + + user = Factory.create(:moderator_user) + ban = Factory.build(:ban, :user => user, :banner => @banner) + ban.save + assert(ban.errors.any?) + end + + should "be valid against anyone who is not an admin or a moderator" do + user = Factory.create(:janitor_user) + ban = Factory.create(:ban, :user => user, :banner => @banner) + assert(ban.errors.empty?) + + user = Factory.create(:contributor_user) + ban = Factory.create(:ban, :user => user, :banner => @banner) + assert(ban.errors.empty?) + + user = Factory.create(:privileged_user) + ban = Factory.create(:ban, :user => user, :banner => @banner) + assert(ban.errors.empty?) + + user = Factory.create(:user) + ban = Factory.create(:ban, :user => user, :banner => @banner) + assert(ban.errors.empty?) + end + end + + context "created by a janitor" do + setup do + @banner = Factory.create(:janitor_user) + end + + teardown do + @banner = nil + end + + should "always be invalid" do + user = Factory.create(:admin_user) + ban = Factory.build(:ban, :user => user, :banner => @banner) + ban.save + assert(ban.errors.any?) + + user = Factory.create(:moderator_user) + ban = Factory.build(:ban, :user => user, :banner => @banner) + ban.save + assert(ban.errors.any?) + + user = Factory.create(:janitor_user) + ban = Factory.build(:ban, :user => user, :banner => @banner) + ban.save + assert(ban.errors.any?) + + user = Factory.create(:contributor_user) + ban = Factory.build(:ban, :user => user, :banner => @banner) + ban.save + assert(ban.errors.any?) + + user = Factory.create(:privileged_user) + ban = Factory.build(:ban, :user => user, :banner => @banner) + ban.save + assert(ban.errors.any?) + + user = Factory.create(:user) + ban = Factory.build(:ban, :user => user, :banner => @banner) + ban.save + assert(ban.errors.any?) + end + end + + should "initialize the expiration date" do + user = Factory.create(:user) + admin = Factory.create(:admin_user) + ban = Factory.create(:ban, :user => user, :banner => admin) + assert_not_nil(ban.expires_at) + end + + should "update the user's feedback" do + user = Factory.create(:user) + admin = Factory.create(:admin_user) + assert(user.feedback.empty?) + ban = Factory.create(:ban, :user => user, :banner => admin) + assert(!user.feedback.empty?) + assert(!user.feedback.last.is_positive?) + end + end + + context "Searching for a ban" do + context "by user id" do + should "not return expired bans" do + admin = Factory.create(:admin_user) + + user = Factory.create(:user) + ban = Factory.create(:ban, :user => user, :banner => admin, :duration => -1) + assert(!Ban.is_user_banned?(user)) + + user = Factory.create(:user) + ban = Factory.create(:ban, :user => user, :banner => admin, :duration => 1) + assert(Ban.is_user_banned?(user)) + end + end + + context "by ip address" do + should "not return expired bans" do + admin = Factory.create(:admin_user) + + ban = Factory.create(:ban, :ip_addr => "1.2.3.4", :banner => admin, :duration => -1) + assert(!Ban.is_ip_banned?("1.2.3.4")) + + ban = Factory.create(:ban, :ip_addr => "5.6.7.8", :banner => admin, :duration => 1) + assert(Ban.is_ip_banned?("5.6.7.8")) + end + end + end +end diff --git a/test/unit/user_feedback_test.rb b/test/unit/user_feedback_test.rb new file mode 100644 index 000000000..949cfe1b4 --- /dev/null +++ b/test/unit/user_feedback_test.rb @@ -0,0 +1,8 @@ +require 'test_helper' + +class UserFeedbackTest < ActiveSupport::TestCase + # Replace this with your real tests. + test "the truth" do + assert true + end +end diff --git a/test/unit/user_test.rb b/test/unit/user_test.rb index 9a5ac94a0..aa5dd7d2d 100644 --- a/test/unit/user_test.rb +++ b/test/unit/user_test.rb @@ -14,6 +14,49 @@ class UserTest < ActiveSupport::TestCase assert(!User.authenticate_hash(@user.name, "xxxx"), "Authentication should not have succeeded") end + should "normalize its level" do + user = Factory.create(:user, :is_admin => true) + assert(user.is_moderator?) + assert(user.is_janitor?) + assert(user.is_contributor?) + assert(user.is_privileged?) + + user = Factory.create(:user, :is_moderator => true) + assert(!user.is_admin?) + assert(user.is_moderator?) + assert(user.is_janitor?) + assert(!user.is_contributor?) + assert(user.is_privileged?) + + user = Factory.create(:user, :is_janitor => true) + assert(!user.is_admin?) + assert(!user.is_moderator?) + assert(user.is_janitor?) + assert(!user.is_contributor?) + assert(user.is_privileged?) + + user = Factory.create(:user, :is_contributor => true) + assert(!user.is_admin?) + assert(!user.is_moderator?) + assert(!user.is_janitor?) + assert(user.is_contributor?) + assert(user.is_privileged?) + + user = Factory.create(:user, :is_privileged => true) + assert(!user.is_admin?) + assert(!user.is_moderator?) + assert(!user.is_janitor?) + assert(!user.is_contributor?) + assert(user.is_privileged?) + + user = Factory.create(:user) + assert(!user.is_admin?) + assert(!user.is_moderator?) + assert(!user.is_janitor?) + assert(!user.is_contributor?) + assert(!user.is_privileged?) + end + context "name" do should "be #{Danbooru.config.default_guest_name} given an invalid user id" do assert_equal(Danbooru.config.default_guest_name, User.find_name(-1))