diff --git a/app/models/category.rb b/app/models/category.rb index ede0b27a485..92d5ff70d3a 100755 --- a/app/models/category.rb +++ b/app/models/category.rb @@ -13,6 +13,9 @@ class Category < ActiveRecord::Base has_many :category_featured_users has_many :featured_users, through: :category_featured_users, source: :user + has_many :category_groups + has_many :groups, through: :category_groups + validates :user_id, presence: true validates :name, presence: true, uniqueness: true, length: { in: 1..50 } validate :uncategorized_validator @@ -28,6 +31,32 @@ class Category < ActiveRecord::Base delegate :post_template, to: 'self.class' + # Internal: Update category stats: # of topics in past year, month, week for + # all categories. + def self.update_stats + topics = Topic + .select("COUNT(*)") + .where("topics.category_id = categories.id") + .where("categories.topic_id <> topics.id") + .visible + + topic_count = topics.to_sql + topics_year = topics.created_since(1.year.ago).to_sql + topics_month = topics.created_since(1.month.ago).to_sql + topics_week = topics.created_since(1.week.ago).to_sql + + Category.update_all("topic_count = (#{topic_count}), + topics_year = (#{topics_year}), + topics_month = (#{topics_month}), + topics_week = (#{topics_week})") + end + + # Internal: Generate the text of post prompting to enter category + # description. + def self.post_template + I18n.t("category.post_template", replace_paragraph: I18n.t("category.replace_paragraph")) + end + def create_category_definition create_topic!(title: I18n.t("category.topic_prefix", category: name), user: user, pinned_at: Time.now) update_column(:topic_id, topic.id) @@ -66,29 +95,23 @@ class Category < ActiveRecord::Base errors.add(:slug, I18n.t(:is_reserved)) if slug == SiteSetting.uncategorized_name end - # Internal: Update category stats: # of topics in past year, month, week for - # all categories. - def self.update_stats - topics = Topic - .select("COUNT(*)") - .where("topics.category_id = categories.id") - .where("categories.topic_id <> topics.id") - .visible - - topic_count = topics.to_sql - topics_year = topics.created_since(1.year.ago).to_sql - topics_month = topics.created_since(1.month.ago).to_sql - topics_week = topics.created_since(1.week.ago).to_sql - - Category.update_all("topic_count = (#{topic_count}), - topics_year = (#{topics_year}), - topics_month = (#{topics_month}), - topics_week = (#{topics_week})") + def secure? + self.secure end - # Internal: Generate the text of post prompting to enter category - # description. - def self.post_template - I18n.t("category.post_template", replace_paragraph: I18n.t("category.replace_paragraph")) + def deny(group) + if group == :all + self.secure = true + end end + + def allow(group) + if group == :all + self.secure = false + category_groups.clear + else + groups.push(group) + end + end + end diff --git a/app/models/category_group.rb b/app/models/category_group.rb new file mode 100644 index 00000000000..25781fca52e --- /dev/null +++ b/app/models/category_group.rb @@ -0,0 +1,4 @@ +class CategoryGroup < ActiveRecord::Base + belongs_to :category + belongs_to :group +end diff --git a/app/models/group.rb b/app/models/group.rb index 8e660a9e752..0c5c70b366d 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -1,5 +1,15 @@ class Group < ActiveRecord::Base + has_many :category_groups + has_many :group_users + + has_many :categories, through: :category_groups + has_many :users, through: :group_users + def self.builtin Enum.new(:moderators, :admins, :trust_level_1, :trust_level_2) end + + def add(user) + self.users.push(user) + end end diff --git a/app/models/group_user.rb b/app/models/group_user.rb index ed0c77ce23f..0c93905c8aa 100644 --- a/app/models/group_user.rb +++ b/app/models/group_user.rb @@ -1,2 +1,4 @@ class GroupUser < ActiveRecord::Base + belongs_to :group + belongs_to :user end diff --git a/app/models/topic.rb b/app/models/topic.rb index 6232b073b0b..2f7bc2584a5 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -68,7 +68,7 @@ class Topic < ActiveRecord::Base scope :listable_topics, lambda { where('topics.archetype <> ?', [Archetype.private_message]) } - scope :by_newest, order('created_at desc, id desc') + scope :by_newest, order('topics.created_at desc, topics.id desc') # Helps us limit how many favorites can be made in a day class FavoriteLimiter < RateLimiter diff --git a/app/models/user.rb b/app/models/user.rb index 2ef19ec5747..90813a20bc5 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -26,6 +26,10 @@ class User < ActiveRecord::Base has_one :github_user_info, dependent: :destroy belongs_to :approved_by, class_name: 'User' + has_many :group_users + has_many :groups, through: :group_users + has_many :secure_categories, through: :groups, source: :categories + validates_presence_of :username validates_presence_of :email validates_uniqueness_of :email @@ -150,10 +154,6 @@ class User < ActiveRecord::Base u.errors[:username].blank? end - def enqueue_welcome_message(message_type) - return unless SiteSetting.send_welcome_message? - Jobs.enqueue(:send_system_message, user_id: id, message_type: message_type) - end def self.suggest_name(email) return "" unless email @@ -162,6 +162,25 @@ class User < ActiveRecord::Base name.titleize end + # Find a user by temporary key, nil if not found or key is invalid + def self.find_by_temporary_key(key) + user_id = $redis.get("temporary_key:#{key}") + if user_id.present? + where(id: user_id.to_i).first + end + end + + def self.find_by_username_or_email(username_or_email) + lower_user = username_or_email.downcase + lower_email = Email.downcase(username_or_email) + where("username_lower = :user or lower(username) = :user or email = :email or lower(name) = :user", user: lower_user, email: lower_email) + end + + def enqueue_welcome_message(message_type) + return unless SiteSetting.send_welcome_message? + Jobs.enqueue(:send_system_message, user_id: id, message_type: message_type) + end + def change_username(new_username) current_username, self.username = username, new_username @@ -185,19 +204,6 @@ class User < ActiveRecord::Base key end - # Find a user by temporary key, nil if not found or key is invalid - def self.find_by_temporary_key(key) - user_id = $redis.get("temporary_key:#{key}") - if user_id.present? - where(id: user_id.to_i).first - end - end - - def self.find_by_username_or_email(username_or_email) - lower_user = username_or_email.downcase - lower_email = Email.downcase(username_or_email) - where("username_lower = :user or lower(username) = :user or email = :email or lower(name) = :user", user: lower_user, email: lower_email) - end # tricky, we need our bus to be subscribed from the right spot def sync_notification_channel_position @@ -510,6 +516,11 @@ class User < ActiveRecord::Base .count end + def secure_category_ids + cats = self.moderator? ? Category.select(:id).where(:secure, true) : secure_categories.select('categories.id') + cats.map{|c| c.id} + end + protected def cook @@ -591,4 +602,5 @@ class User < ActiveRecord::Base end end + end diff --git a/app/models/user_action.rb b/app/models/user_action.rb index e007962e9fa..a281bcbb967 100644 --- a/app/models/user_action.rb +++ b/app/models/user_action.rb @@ -35,27 +35,28 @@ class UserAction < ActiveRecord::Base EDIT ].each_with_index.to_a.flatten] + def self.stats(user_id, guardian) - results = UserAction.select("action_type, COUNT(*) count") - .joins(:target_topic) - .where(user_id: user_id) - .group('action_type') - # We apply similar filters in stream, might consider trying to consolidate somehow - unless guardian.can_see_private_messages?(user_id) - results = results.where('topics.archetype <> ?', Archetype::private_message) - end + # Sam: I tried this in AR and it got complex + builder = UserAction.sql_builder < ?", BOOKMARK) - end + SELECT action_type, COUNT(*) count + FROM user_actions a + JOIN topics t ON t.id = a.target_topic_id + LEFT JOIN posts p on p.id = a.target_post_id + JOIN posts p2 on p2.topic_id = a.target_topic_id and p2.post_number = 1 + LEFT JOIN categories c ON c.id = t.category_id + /*where*/ + GROUP BY action_type +SQL - unless guardian.can_see_deleted_posts? - results = results.where('topics.deleted_at IS NULL') - end - results = results.to_a + builder.where('a.user_id = :user_id', user_id: user_id) + apply_common_filters(builder, user_id, guardian) + + results = builder.exec.to_a results.sort! { |a,b| ORDER[a.action_type] <=> ORDER[b.action_type] } results @@ -93,23 +94,14 @@ JOIN posts p2 on p2.topic_id = a.target_topic_id and p2.post_number = 1 JOIN users u on u.id = a.acting_user_id JOIN users pu on pu.id = COALESCE(p.user_id, t.user_id) JOIN users au on au.id = a.user_id +LEFT JOIN categories c on c.id = t.category_id /*where*/ /*order_by*/ /*offset*/ /*limit*/ ") - unless guardian.can_see_deleted_posts? - builder.where("p.deleted_at is null and p2.deleted_at is null and t.deleted_at is null") - end - - unless guardian.user && guardian.user.id == user_id - builder.where("a.action_type not in (#{BOOKMARK})") - end - - if !guardian.can_see_private_messages?(user_id) || ignore_private_messages - builder.where("t.archetype != :archetype", archetype: Archetype::private_message) - end + apply_common_filters(builder, user_id, guardian, ignore_private_messages) if action_id builder.where("a.id = :id", id: action_id.to_i) @@ -182,6 +174,34 @@ JOIN users au on au.id = a.user_id end protected + + def self.apply_common_filters(builder,user_id,guardian,ignore_private_messages=false) + + + unless guardian.can_see_deleted_posts? + builder.where("p.deleted_at is null and p2.deleted_at is null and t.deleted_at is null") + end + + unless guardian.user && guardian.user.id == user_id + builder.where("a.action_type not in (#{BOOKMARK})") + end + + if !guardian.can_see_private_messages?(user_id) || ignore_private_messages + builder.where("t.archetype != :archetype", archetype: Archetype::private_message) + end + + unless guardian.is_moderator? + allowed = guardian.secure_category_ids + if allowed.present? + builder.where("( c.secure IS NULL OR + c.secure = 'f' OR + (c.secure = 't' and c.id in (:cats)) )", cats: guardian.secure_category_ids ) + else + builder.where("(c.secure IS NULL OR c.secure = 'f')") + end + end + end + def self.require_parameters(data, *params) params.each do |p| raise Discourse::InvalidParameters.new(p) if data[p].nil? diff --git a/db/migrate/20130429000101_add_security_to_categories.rb b/db/migrate/20130429000101_add_security_to_categories.rb new file mode 100644 index 00000000000..71272b68bfe --- /dev/null +++ b/db/migrate/20130429000101_add_security_to_categories.rb @@ -0,0 +1,11 @@ +class AddSecurityToCategories < ActiveRecord::Migration + def change + add_column :categories, :secure, :boolean, default: false, null: false + + create_table :category_groups do |t| + t.integer :category_id, null: false + t.integer :group_id, null: false + t.timestamps + end + end +end diff --git a/lib/guardian.rb b/lib/guardian.rb index e22181e54ae..3e4967f1868 100644 --- a/lib/guardian.rb +++ b/lib/guardian.rb @@ -12,7 +12,11 @@ class Guardian end def is_admin? - !@user.nil? && @user.admin? + @user && @user.admin? + end + + def is_moderator? + @user && @user.moderator? end # Can the user see the object? @@ -329,6 +333,15 @@ class Guardian end def can_see_topic?(topic) + return false unless topic + + return true if @user && @user.moderator? + return false if topic.deleted_at.present? + + if topic.category && topic.category.secure + return false unless @user && can_see_category?(topic.category) + end + if topic.private_message? return false if @user.blank? return true if topic.allowed_users.include?(@user) @@ -337,6 +350,22 @@ class Guardian true end + def can_see_post?(post) + return false unless post + + return true if @user && @user.moderator? + return false if post.deleted_at.present? + + can_see_topic?(post.topic) + end + + def can_see_category?(category) + return true unless category.secure + return false unless @user + + @user.secure_category_ids.include?(category.id) + end + def can_vote?(post, opts={}) post_can_act?(post,:vote, opts) end @@ -371,4 +400,7 @@ class Guardian return true end + def secure_category_ids + @user ? @user.secure_category_ids : [] + end end diff --git a/lib/topic_query.rb b/lib/topic_query.rb index e091b99dbb3..4b733e1bdc7 100644 --- a/lib/topic_query.rb +++ b/lib/topic_query.rb @@ -207,13 +207,13 @@ class TopicQuery # Start with a list of all topics result = Topic - if @user_id.present? + if @user_id result = result.joins("LEFT OUTER JOIN topic_users AS tu ON (topics.id = tu.topic_id AND tu.user_id = #{@user_id})") end unless query_opts[:unordered] # If we're logged in, we have to pay attention to our pinned settings - if @user_id.present? + if @user result = result.order(TopicQuery.order_nocategory_with_pinned_sql) else result = result.order(TopicQuery.order_nocategory_basic_bumped) @@ -227,6 +227,16 @@ class TopicQuery result = result.visible if @user.blank? or @user.regular? result = result.where('topics.id <> ?', query_opts[:except_topic_id]) if query_opts[:except_topic_id].present? result = result.offset(query_opts[:page].to_i * page_size) if query_opts[:page].present? + + unless @user && @user.moderator? + category_ids = @user.secure_category_ids if @user + if category_ids.present? + result = result.where('categories.secure IS NULL OR categories.secure = ? OR categories.id IN (?)', false, category_ids) + else + result = result.where('categories.secure IS NULL OR categories.secure = ?', false) + end + end + result end diff --git a/spec/components/guardian_spec.rb b/spec/components/guardian_spec.rb index ddf5922c1dd..5637a5ec551 100644 --- a/spec/components/guardian_spec.rb +++ b/spec/components/guardian_spec.rb @@ -144,28 +144,14 @@ describe Guardian do let(:topic) { Fabricate(:topic, user: coding_horror)} - it 'returns false when the post is nil' do - Guardian.new(user).can_see_post_actors?(nil, PostActionType.types[:like]).should be_false - end - - it 'returns true for likes' do - Guardian.new(user).can_see_post_actors?(topic, PostActionType.types[:like]).should be_true - end - - it 'returns false for bookmarks' do - Guardian.new(user).can_see_post_actors?(topic, PostActionType.types[:bookmark]).should be_false - end - - it 'returns false for off-topic flags' do - Guardian.new(user).can_see_post_actors?(topic, PostActionType.types[:off_topic]).should be_false - end - - it 'returns false for spam flags' do - Guardian.new(user).can_see_post_actors?(topic, PostActionType.types[:spam]).should be_false - end - - it 'returns true for public votes' do - Guardian.new(user).can_see_post_actors?(topic, PostActionType.types[:vote]).should be_true + it 'displays visibility correctly' do + guardian = Guardian.new(user) + guardian.can_see_post_actors?(nil, PostActionType.types[:like]).should be_false + guardian.can_see_post_actors?(topic, PostActionType.types[:like]).should be_true + guardian.can_see_post_actors?(topic, PostActionType.types[:bookmark]).should be_false + guardian.can_see_post_actors?(topic, PostActionType.types[:off_topic]).should be_false + guardian.can_see_post_actors?(topic, PostActionType.types[:spam]).should be_false + guardian.can_see_post_actors?(topic, PostActionType.types[:vote]).should be_true end it 'returns false for private votes' do @@ -176,34 +162,15 @@ describe Guardian do end describe 'can_impersonate?' do - it 'returns false when the target is nil' do + it 'allows impersonation correctly' do Guardian.new(admin).can_impersonate?(nil).should be_false - end - - it 'returns false when the user is nil' do Guardian.new.can_impersonate?(user).should be_false - end - - it "doesn't allow a non-admin to impersonate someone" do Guardian.new(coding_horror).can_impersonate?(user).should be_false - end - - it "doesn't allow an admin to impersonate themselves" do Guardian.new(admin).can_impersonate?(admin).should be_false - end - - it "doesn't allow an admin to impersonate another admin" do Guardian.new(admin).can_impersonate?(another_admin).should be_false - end - - it "allows an admin to impersonate a regular user" do Guardian.new(admin).can_impersonate?(user).should be_true - end - - it "allows an admin to impersonate a moderator" do Guardian.new(admin).can_impersonate?(moderator).should be_true end - end describe 'can_invite_to?' do @@ -211,16 +178,11 @@ describe Guardian do let(:user) { topic.user } let(:moderator) { Fabricate(:moderator) } - it 'returns false with a nil user' do + it 'handles invitation correctly' do Guardian.new(nil).can_invite_to?(topic).should be_false - end - - it 'returns false with a nil object' do Guardian.new(moderator).can_invite_to?(nil).should be_false - end - - it 'returns true for a moderator to invite' do Guardian.new(moderator).can_invite_to?(topic).should be_true + Guardian.new(user).can_invite_to?(topic).should be_false end it 'returns false when the site requires approving users' do @@ -228,10 +190,6 @@ describe Guardian do Guardian.new(moderator).can_invite_to?(topic).should be_false end - it 'returns false for a regular user to invite' do - Guardian.new(user).can_invite_to?(topic).should be_false - end - end describe 'can_see?' do @@ -244,6 +202,40 @@ describe Guardian do it 'allows non logged in users to view topics' do Guardian.new.can_see?(topic).should be_true end + + it 'correctly handles groups' do + group = Fabricate(:group) + category = Fabricate(:category, secure: true) + category.allow(group) + + topic = Fabricate(:topic, category: category) + + Guardian.new(user).can_see?(topic).should be_false + group.add(user) + group.save + + Guardian.new(user).can_see?(topic).should be_true + end + end + + describe 'a Post' do + it 'correctly handles post visibility' do + Guardian.new(user).can_see?(post).should be_true + + post.destroy + post.reload + Guardian.new(user).can_see?(post).should be_false + Guardian.new(admin).can_see?(post).should be_true + + post.recover + post.reload + topic.destroy + topic.reload + Guardian.new(user).can_see?(post).should be_false + Guardian.new(admin).can_see?(post).should be_true + end + + end end diff --git a/spec/components/topic_query_spec.rb b/spec/components/topic_query_spec.rb index 4c48fc3f133..c09548322eb 100644 --- a/spec/components/topic_query_spec.rb +++ b/spec/components/topic_query_spec.rb @@ -3,13 +3,38 @@ require 'topic_view' describe TopicQuery do - let!(:user) { Fabricate(:coding_horror) } + let(:user) { Fabricate(:coding_horror) } let(:creator) { Fabricate(:user) } let(:topic_query) { TopicQuery.new(user) } let(:moderator) { Fabricate(:moderator) } let(:admin) { Fabricate(:moderator) } + + context 'secure category' do + it "filters categories out correctly" do + category = Fabricate(:category) + category.deny(:all) + group = Fabricate(:group) + category.allow(group) + category.save + + topic = Fabricate(:topic, category: category) + + TopicQuery.new(nil).list_latest.topics.count.should == 0 + TopicQuery.new(user).list_latest.topics.count.should == 0 + + # mods can see every group + TopicQuery.new(moderator).list_latest.topics.count.should == 2 + + group.add(user) + group.save + + TopicQuery.new(user).list_latest.topics.count.should == 2 + end + + end + context 'a bunch of topics' do let!(:regular_topic) { Fabricate(:topic, title: 'this is a regular topic', user: creator, bumped_at: 15.minutes.ago) } let!(:pinned_topic) { Fabricate(:topic, title: 'this is a pinned topic', user: creator, pinned_at: 10.minutes.ago, bumped_at: 10.minutes.ago) } @@ -90,17 +115,11 @@ describe TopicQuery do context 'with no data' do - it "has no read topics" do - topic_query.list_unread.topics.should be_blank - end - it "has no unread topics" do topic_query.list_unread.topics.should be_blank - end - - it "has an unread count of 0" do topic_query.unread_count.should == 0 end + end context 'with read data' do @@ -115,9 +134,6 @@ describe TopicQuery do context 'list_unread' do it 'contains no topics' do topic_query.list_unread.topics.should == [] - end - - it "returns 0 as the unread count" do topic_query.unread_count.should == 0 end end diff --git a/spec/fabricators/group_fabricator.rb b/spec/fabricators/group_fabricator.rb new file mode 100644 index 00000000000..6aa21f3e542 --- /dev/null +++ b/spec/fabricators/group_fabricator.rb @@ -0,0 +1,3 @@ +Fabricator(:group) do + name 'my group' +end diff --git a/spec/models/category_spec.rb b/spec/models/category_spec.rb index fbc26929cce..c68981a3100 100644 --- a/spec/models/category_spec.rb +++ b/spec/models/category_spec.rb @@ -18,6 +18,34 @@ describe Category do it { should have_many :category_featured_topics } it { should have_many :featured_topics } + describe "security" do + it "secures categories correctly" do + category = Fabricate(:category) + + category.secure?.should be_false + + category.deny(:all) + category.secure?.should be_true + + category.allow(:all) + category.secure?.should be_false + + user = Fabricate(:user) + user.secure_categories.to_a.should == [] + + group = Fabricate(:group) + group.add(user) + group.save + + category.allow(group) + category.save + + user.reload + user.secure_categories.to_a.should == [category] + + end + end + describe "uncategorized name" do let(:category) { Fabricate.build(:category, name: SiteSetting.uncategorized_name) } @@ -71,47 +99,27 @@ describe Category do @topic = @category.topic end - it 'creates a slug' do + it 'is created correctly' do @category.slug.should == 'amazing-category' - end - it "has a hotness of 5.0 by default" do @category.hotness.should == 5.0 - end - it 'has a default description' do @category.description.should be_blank - end - it 'has one topic' do Topic.where(category_id: @category).count.should == 1 - end - it 'creates a topic post' do @topic.should be_present - end - it 'points back to itself' do @topic.category.should == @category - end - it 'is a visible topic' do @topic.should be_visible - end - it 'is pinned' do @topic.pinned_at.should be_present - end - it 'is an undeletable topic' do Guardian.new(@category.user).can_delete?(@topic).should be_false - end - it 'should have one post' do @topic.posts.count.should == 1 - end - it 'should have a topic url' do @category.topic_url.should be_present end @@ -129,17 +137,12 @@ describe Category do @category.reload end - it 'still has 0 forum topics' do + it 'does not cause changes' do @category.topic_count.should == 0 - end - - it "didn't change the category" do @topic.category.should == @category - end - - it "didn't change the category's forum topic" do @category.topic.should == @topic end + end end @@ -151,11 +154,8 @@ describe Category do @category.destroy end - it 'deletes the category' do + it 'is deleted correctly' do Category.exists?(id: @category_id).should be_false - end - - it 'deletes the forum topic' do Topic.exists?(id: @topic_id).should be_false end end @@ -172,21 +172,13 @@ describe Category do @category.reload end - it 'updates topics_week' do + it 'updates topic stats' do @category.topics_week.should == 1 - end - - it 'updates topics_month' do @category.topics_month.should == 1 - end - - it 'updates topics_year' do @category.topics_year.should == 1 - end - - it 'updates topic_count' do @category.topic_count.should == 1 end + end context 'with deleted topics' do @@ -197,21 +189,13 @@ describe Category do @category.reload end - it 'does not count deleted topics for topics_week' do + it 'does not count deleted topics' do @category.topics_week.should == 0 - end - - it 'does not count deleted topics for topics_month' do + @category.topic_count.should == 0 @category.topics_month.should == 0 - end - - it 'does not count deleted topics for topics_year' do @category.topics_year.should == 0 end - it 'does not count deleted topics for topic_count' do - @category.topic_count.should == 0 - end end end end diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb new file mode 100644 index 00000000000..32135450e4a --- /dev/null +++ b/spec/models/group_spec.rb @@ -0,0 +1,4 @@ +require 'spec_helper' + +describe Group do +end diff --git a/spec/models/user_action_spec.rb b/spec/models/user_action_spec.rb index 05bbfb9cf5b..84be5c8748c 100644 --- a/spec/models/user_action_spec.rb +++ b/spec/models/user_action_spec.rb @@ -37,41 +37,52 @@ describe UserAction do log_test_action(action_type: UserAction::BOOKMARK) end - describe 'stats' do - - let :mystats do - UserAction.stats(user.id, Guardian.new(user)) - end - - it 'include correct events' do - mystats.map{|r| r["action_type"].to_i}.should include(UserAction::NEW_TOPIC) - UserAction.stats(user.id,Guardian.new).map{|r| r["action_type"].to_i}.should_not include(UserAction::NEW_PRIVATE_MESSAGE) - UserAction.stats(user.id,Guardian.new).map{|r| r["action_type"].to_i}.should_not include(UserAction::GOT_PRIVATE_MESSAGE) - mystats.map{|r| r["action_type"].to_i}.should include(UserAction::NEW_PRIVATE_MESSAGE) - mystats.map{|r| r["action_type"].to_i}.should include(UserAction::GOT_PRIVATE_MESSAGE) - end - - it 'should not include new topic when topic is deleted' do - public_topic.destroy - mystats.map{|r| r["action_type"].to_i}.should_not include(UserAction::NEW_TOPIC) - end - + def stats_for_user(viewer=nil) + UserAction.stats(user.id, Guardian.new(viewer)).map{|r| r["action_type"].to_i}.sort end - describe 'stream' do + def stream_count(viewer=nil) + UserAction.stream(user_id: user.id, guardian: Guardian.new(viewer)).count + end - it 'should have 1 item for non owners' do - UserAction.stream(user_id: user.id, guardian: Guardian.new).count.should == 1 - end + it 'includes the events correctly' do - it 'should include no items for non owner when topic deleted' do - public_topic.destroy - UserAction.stream(user_id: user.id, guardian: Guardian.new).count.should == 0 - end + mystats = stats_for_user(user) + expecting = [UserAction::NEW_TOPIC, UserAction::NEW_PRIVATE_MESSAGE, UserAction::GOT_PRIVATE_MESSAGE, UserAction::BOOKMARK].sort + mystats.should == expecting + stream_count(user).should == 4 - it 'should have bookmarks and pms for owners' do - UserAction.stream(user_id: user.id, guardian: user.guardian).count.should == 4 - end + other_stats = stats_for_user + expecting = [UserAction::NEW_TOPIC] + stream_count.should == 1 + + other_stats.should == expecting + + public_topic.destroy + stats_for_user.should == [] + stream_count.should == 0 + + # groups + + category = Fabricate(:category, secure: true) + + public_topic.recover + public_topic.category = category + public_topic.save + + stats_for_user.should == [] + stream_count.should == 0 + + group = Fabricate(:group) + u = Fabricate(:coding_horror) + group.add(u) + group.save + + category.allow(group) + category.save + + stats_for_user(u).should == [UserAction::NEW_TOPIC] + stream_count(u).should == 1 end end @@ -146,8 +157,6 @@ describe UserAction do end it 'should exist' do @action.should_not be_nil - end - it 'shoule have the correct date' do @action.created_at.should be_within(1).of(@post.topic.created_at) end end @@ -164,16 +173,11 @@ describe UserAction do @response = Fabricate(:post, reply_to_post_number: 1, topic: @post.topic, user: @other_user, raw: "perhaps @#{@mentioned.username} knows how this works?") end - it 'should log a post action for the poster' do + it 'should log user actions correctly' do @response.user.user_actions.where(action_type: UserAction::POST).first.should_not be_nil - end - - it 'should log a post action for the original poster' do @post.user.user_actions.where(action_type: UserAction::RESPONSE).first.should_not be_nil - end - - it 'should log a mention for the mentioned' do @mentioned.user_actions.where(action_type: UserAction::MENTION).first.should_not be_nil + @post.user.user_actions.joins(:target_post).where('posts.post_number = 2').count.should == 1 end it 'should not log a double notification for a post edit' do @@ -182,12 +186,7 @@ describe UserAction do @response.user.user_actions.where(action_type: UserAction::POST).count.should == 1 end - it 'should not log topic reply and reply for a single post' do - @post.user.user_actions.joins(:target_post).where('posts.post_number = 2').count.should == 1 - end - end - end describe 'when user bookmarks' do @@ -198,19 +197,12 @@ describe UserAction do @action = @user.user_actions.where(action_type: UserAction::BOOKMARK).first end - it 'should create a bookmark action' do + it 'should create a bookmark action correctly' do @action.action_type.should == UserAction::BOOKMARK - end - it 'should point to the correct post' do @action.target_post_id.should == @post.id - end - it 'should have the right acting_user' do @action.acting_user_id.should == @user.id - end - it 'should target the correct user' do @action.user_id.should == @user.id - end - it 'should nuke the action when unbookmarked' do + PostAction.remove_act(@user, @post, PostActionType.types[:bookmark]) @user.user_actions.where(action_type: UserAction::BOOKMARK).first.should be_nil end