2013-02-05 14:16:51 -05:00
|
|
|
class Category < ActiveRecord::Base
|
2013-10-18 03:09:30 -04:00
|
|
|
|
2014-04-19 00:00:40 -04:00
|
|
|
include Positionable
|
2014-04-28 04:31:51 -04:00
|
|
|
include HasCustomFields
|
2013-10-18 03:09:30 -04:00
|
|
|
|
2013-02-28 13:54:12 -05:00
|
|
|
belongs_to :topic, dependent: :destroy
|
2014-02-17 11:44:28 -05:00
|
|
|
belongs_to :topic_only_relative_url,
|
|
|
|
-> { select "id, title, slug" },
|
|
|
|
class_name: "Topic",
|
|
|
|
foreign_key: "topic_id"
|
2013-08-15 11:52:18 -04:00
|
|
|
|
2013-02-05 14:16:51 -05:00
|
|
|
belongs_to :user
|
2013-10-17 02:44:56 -04:00
|
|
|
belongs_to :latest_post, class_name: "Post"
|
2013-02-05 14:16:51 -05:00
|
|
|
|
|
|
|
has_many :topics
|
2013-02-07 10:45:24 -05:00
|
|
|
has_many :category_featured_topics
|
2013-02-05 14:16:51 -05:00
|
|
|
has_many :featured_topics, through: :category_featured_topics, source: :topic
|
|
|
|
|
|
|
|
has_many :category_featured_users
|
|
|
|
has_many :featured_users, through: :category_featured_users, source: :user
|
|
|
|
|
2013-04-29 02:33:24 -04:00
|
|
|
has_many :category_groups
|
|
|
|
has_many :groups, through: :category_groups
|
|
|
|
|
2013-03-02 03:57:02 -05:00
|
|
|
validates :user_id, presence: true
|
2014-08-11 16:55:26 -04:00
|
|
|
validates :name, if: Proc.new { |c| c.new_record? || c.name_changed? },
|
|
|
|
presence: true,
|
|
|
|
uniqueness: { scope: :parent_category_id, case_sensitive: false },
|
|
|
|
length: { in: 1..50 }
|
2013-10-23 12:58:11 -04:00
|
|
|
validate :parent_category_validator
|
2013-02-05 14:16:51 -05:00
|
|
|
|
2013-04-01 12:26:51 -04:00
|
|
|
before_validation :ensure_slug
|
2013-07-13 21:24:16 -04:00
|
|
|
before_save :apply_permissions
|
2014-07-14 10:16:24 -04:00
|
|
|
before_save :downcase_email
|
2013-03-02 03:57:02 -05:00
|
|
|
after_create :create_category_definition
|
2013-04-10 15:53:36 -04:00
|
|
|
after_create :publish_categories_list
|
|
|
|
after_destroy :publish_categories_list
|
2014-07-18 13:59:54 -04:00
|
|
|
after_update :rename_category_definition, if: :name_changed?
|
2013-02-05 14:16:51 -05:00
|
|
|
|
2013-05-22 15:33:33 -04:00
|
|
|
has_one :category_search_data
|
2013-10-23 12:58:11 -04:00
|
|
|
belongs_to :parent_category, class_name: 'Category'
|
2014-02-05 18:39:26 -05:00
|
|
|
has_many :subcategories, class_name: 'Category', foreign_key: 'parent_category_id'
|
2013-05-22 15:33:33 -04:00
|
|
|
|
2013-03-27 16:17:49 -04:00
|
|
|
scope :latest, ->{ order('topic_count desc') }
|
2013-03-02 03:57:02 -05:00
|
|
|
|
2013-05-15 16:45:52 -04:00
|
|
|
scope :secured, ->(guardian = nil) {
|
2013-05-13 04:04:03 -04:00
|
|
|
ids = guardian.secure_category_ids if guardian
|
|
|
|
if ids.present?
|
2013-08-25 17:18:11 -04:00
|
|
|
where("NOT categories.read_restricted or categories.id in (:cats)", cats: ids).references(:categories)
|
2013-05-13 04:04:03 -04:00
|
|
|
else
|
2013-08-25 17:18:11 -04:00
|
|
|
where("NOT categories.read_restricted").references(:categories)
|
2013-05-13 04:04:03 -04:00
|
|
|
end
|
|
|
|
}
|
|
|
|
|
2013-07-13 21:24:16 -04:00
|
|
|
scope :topic_create_allowed, ->(guardian) {
|
2013-10-12 18:54:48 -04:00
|
|
|
if guardian.anonymous?
|
|
|
|
where("1=0")
|
|
|
|
else
|
|
|
|
scoped_to_permissions(guardian, [:full])
|
|
|
|
end
|
2013-07-13 21:24:16 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
scope :post_create_allowed, ->(guardian) {
|
2013-10-12 18:54:48 -04:00
|
|
|
if guardian.anonymous?
|
|
|
|
where("1=0")
|
|
|
|
else
|
|
|
|
scoped_to_permissions(guardian, [:create_post, :full])
|
|
|
|
end
|
2013-07-13 21:24:16 -04:00
|
|
|
}
|
2013-03-02 03:57:02 -05:00
|
|
|
delegate :post_template, to: 'self.class'
|
|
|
|
|
2013-07-16 01:44:07 -04:00
|
|
|
# permission is just used by serialization
|
|
|
|
# we may consider wrapping this in another spot
|
2014-04-17 05:17:39 -04:00
|
|
|
attr_accessor :displayable_topics, :permission, :subcategory_ids, :notification_level
|
2013-06-05 02:10:26 -04:00
|
|
|
|
2014-06-27 17:06:59 -04:00
|
|
|
def self.last_updated_at
|
|
|
|
order('updated_at desc').limit(1).pluck(:updated_at).first.to_i
|
|
|
|
end
|
2013-07-13 21:24:16 -04:00
|
|
|
|
|
|
|
def self.scoped_to_permissions(guardian, permission_types)
|
|
|
|
if guardian && guardian.is_staff?
|
2014-02-17 11:44:28 -05:00
|
|
|
all
|
2013-07-13 21:24:16 -04:00
|
|
|
else
|
|
|
|
permission_types = permission_types.map{ |permission_type|
|
|
|
|
CategoryGroup.permission_types[permission_type]
|
|
|
|
}
|
|
|
|
where("categories.id in (
|
|
|
|
SELECT c.id FROM categories c
|
|
|
|
WHERE (
|
|
|
|
NOT c.read_restricted AND
|
2013-07-16 01:44:07 -04:00
|
|
|
(
|
|
|
|
NOT EXISTS(
|
|
|
|
SELECT 1 FROM category_groups cg WHERE cg.category_id = categories.id )
|
|
|
|
) OR EXISTS(
|
|
|
|
SELECT 1 FROM category_groups cg
|
|
|
|
WHERE permission_type in (?) AND
|
|
|
|
cg.category_id = categories.id AND
|
|
|
|
group_id IN (
|
|
|
|
SELECT g.group_id FROM group_users g where g.user_id = ? UNION SELECT ?
|
|
|
|
)
|
|
|
|
)
|
2013-07-13 21:24:16 -04:00
|
|
|
)
|
2013-07-16 01:44:07 -04:00
|
|
|
)", permission_types,(!guardian || guardian.user.blank?) ? -1 : guardian.user.id, Group[:everyone].id)
|
2013-07-13 21:24:16 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2014-03-19 10:14:05 -04:00
|
|
|
def self.update_stats
|
|
|
|
topics_with_post_count = Topic
|
|
|
|
.select("topics.category_id, COUNT(*) topic_count, SUM(topics.posts_count) post_count")
|
|
|
|
.where("topics.id NOT IN (select cc.topic_id from categories cc WHERE topic_id IS NOT NULL)")
|
|
|
|
.group("topics.category_id")
|
|
|
|
.visible.to_sql
|
|
|
|
|
|
|
|
Category.exec_sql <<SQL
|
|
|
|
UPDATE categories c
|
|
|
|
SET topic_count = x.topic_count,
|
|
|
|
post_count = x.post_count
|
|
|
|
FROM (#{topics_with_post_count}) x
|
|
|
|
WHERE x.category_id = c.id AND
|
|
|
|
(c.topic_count <> x.topic_count OR c.post_count <> x.post_count)
|
|
|
|
|
|
|
|
SQL
|
|
|
|
|
|
|
|
# Yes, there are a lot of queries happening below.
|
|
|
|
# Performing a lot of queries is actually faster than using one big update
|
|
|
|
# statement with sub-selects on large databases with many categories,
|
|
|
|
# topics, and posts.
|
|
|
|
#
|
|
|
|
# The old method with the one query is here:
|
|
|
|
# https://github.com/discourse/discourse/blob/5f34a621b5416a53a2e79a145e927fca7d5471e8/app/models/category.rb
|
|
|
|
#
|
|
|
|
# If you refactor this, test performance on a large database.
|
|
|
|
|
|
|
|
Category.all.each do |c|
|
|
|
|
topics = c.topics.where(['topics.id <> ?', c.topic_id]).visible
|
|
|
|
c.topics_year = topics.created_since(1.year.ago).count
|
|
|
|
c.topics_month = topics.created_since(1.month.ago).count
|
|
|
|
c.topics_week = topics.created_since(1.week.ago).count
|
|
|
|
c.topics_day = topics.created_since(1.day.ago).count
|
|
|
|
|
|
|
|
posts = c.visible_posts
|
|
|
|
c.posts_year = posts.created_since(1.year.ago).count
|
|
|
|
c.posts_month = posts.created_since(1.month.ago).count
|
|
|
|
c.posts_week = posts.created_since(1.week.ago).count
|
|
|
|
c.posts_day = posts.created_since(1.day.ago).count
|
|
|
|
|
|
|
|
c.save if c.changed?
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2013-12-13 15:15:51 -05:00
|
|
|
|
|
|
|
def visible_posts
|
|
|
|
query = Post.joins(:topic)
|
|
|
|
.where(['topics.category_id = ?', self.id])
|
|
|
|
.where('topics.visible = true')
|
|
|
|
.where('posts.deleted_at IS NULL')
|
|
|
|
.where('posts.user_deleted = false')
|
|
|
|
self.topic_id ? query.where(['topics.id <> ?', self.topic_id]) : query
|
|
|
|
end
|
|
|
|
|
2013-04-29 02:33:24 -04:00
|
|
|
|
|
|
|
# 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
|
|
|
|
|
2013-03-02 03:57:02 -05:00
|
|
|
def create_category_definition
|
2013-10-23 19:05:51 -04:00
|
|
|
t = Topic.new(title: I18n.t("category.topic_prefix", category: name), user: user, pinned_at: Time.now, category_id: id)
|
|
|
|
t.skip_callbacks = true
|
2013-12-06 16:39:35 -05:00
|
|
|
t.auto_close_hours = nil
|
2014-05-26 15:33:51 -04:00
|
|
|
t.save!(validate: false)
|
2013-10-23 19:05:51 -04:00
|
|
|
update_column(:topic_id, t.id)
|
|
|
|
t.posts.create(raw: post_template, user: user)
|
2013-03-02 03:57:02 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
def topic_url
|
2013-03-08 16:48:56 -05:00
|
|
|
topic_only_relative_url.try(:relative_url)
|
2013-03-02 03:57:02 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
def ensure_slug
|
2013-04-01 12:26:51 -04:00
|
|
|
if name.present?
|
2013-07-30 16:48:19 -04:00
|
|
|
self.name.strip!
|
2013-04-01 12:26:51 -04:00
|
|
|
self.slug = Slug.for(name)
|
|
|
|
|
2013-05-30 11:09:09 -04:00
|
|
|
return if self.slug.blank?
|
|
|
|
|
2013-04-01 12:26:51 -04:00
|
|
|
# If a category with that slug already exists, set the slug to nil so the category can be found
|
|
|
|
# another way.
|
2014-08-13 14:45:25 -04:00
|
|
|
category = Category.where(slug: self.slug, parent_category_id: parent_category_id)
|
2013-04-01 12:26:51 -04:00
|
|
|
category = category.where("id != ?", id) if id.present?
|
|
|
|
self.slug = '' if category.exists?
|
|
|
|
end
|
2013-03-02 03:57:02 -05:00
|
|
|
end
|
|
|
|
|
2014-03-24 13:36:23 -04:00
|
|
|
def slug_for_url
|
|
|
|
slug.present? ? self.slug : "#{self.id}-category"
|
|
|
|
end
|
|
|
|
|
2013-04-10 15:53:36 -04:00
|
|
|
def publish_categories_list
|
2013-07-22 14:44:11 -04:00
|
|
|
MessageBus.publish('/categories', {categories: ActiveModel::ArraySerializer.new(Category.latest).as_json})
|
2013-04-10 15:53:36 -04:00
|
|
|
end
|
|
|
|
|
2013-10-23 12:58:11 -04:00
|
|
|
def parent_category_validator
|
|
|
|
if parent_category_id
|
2014-07-15 15:19:17 -04:00
|
|
|
errors.add(:base, I18n.t("category.errors.self_parent")) if parent_category_id == id
|
|
|
|
errors.add(:base, I18n.t("category.errors.uncategorized_parent")) if uncategorized?
|
2013-10-23 12:58:11 -04:00
|
|
|
|
|
|
|
grandfather_id = Category.where(id: parent_category_id).pluck(:parent_category_id).first
|
2013-10-24 17:03:28 -04:00
|
|
|
errors.add(:base, I18n.t("category.errors.depth")) if grandfather_id
|
2013-10-23 12:58:11 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2013-05-10 02:47:47 -04:00
|
|
|
def group_names=(names)
|
|
|
|
# this line bothers me, destroying in AR can not seem to be queued, thinking of extending it
|
|
|
|
category_groups.destroy_all unless new_record?
|
2013-05-17 15:11:37 -04:00
|
|
|
ids = Group.where(name: names.split(",")).pluck(:id)
|
2013-05-10 02:47:47 -04:00
|
|
|
ids.each do |id|
|
|
|
|
category_groups.build(group_id: id)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2013-07-13 21:24:16 -04:00
|
|
|
# will reset permission on a topic to a particular
|
|
|
|
# set.
|
|
|
|
#
|
|
|
|
# Available permissions are, :full, :create_post, :readonly
|
|
|
|
# hash can be:
|
|
|
|
#
|
|
|
|
# :everyone => :full - everyone has everything
|
|
|
|
# :everyone => :readonly, :staff => :full
|
|
|
|
# 7 => 1 # you can pass a group_id and permission id
|
|
|
|
def set_permissions(permissions)
|
|
|
|
self.read_restricted, @permissions = Category.resolve_permissions(permissions)
|
|
|
|
|
|
|
|
# Ideally we can just call .clear here, but it runs SQL, we only want to run it
|
|
|
|
# on save.
|
2013-02-05 14:16:51 -05:00
|
|
|
end
|
|
|
|
|
2013-07-16 01:44:07 -04:00
|
|
|
def permissions=(permissions)
|
|
|
|
set_permissions(permissions)
|
|
|
|
end
|
|
|
|
|
2013-07-13 21:24:16 -04:00
|
|
|
def apply_permissions
|
|
|
|
if @permissions
|
|
|
|
category_groups.destroy_all
|
|
|
|
@permissions.each do |group_id, permission_type|
|
|
|
|
category_groups.build(group_id: group_id, permission_type: permission_type)
|
|
|
|
end
|
|
|
|
@permissions = nil
|
2013-04-29 02:33:24 -04:00
|
|
|
end
|
2013-02-21 18:09:56 -05:00
|
|
|
end
|
2013-04-29 02:33:24 -04:00
|
|
|
|
2014-07-14 10:16:24 -04:00
|
|
|
def downcase_email
|
|
|
|
self.email_in = email_in.downcase if self.email_in
|
|
|
|
end
|
|
|
|
|
2013-05-29 04:11:04 -04:00
|
|
|
def secure_group_ids
|
2013-07-13 21:24:16 -04:00
|
|
|
if self.read_restricted?
|
2013-05-29 04:11:04 -04:00
|
|
|
groups.pluck("groups.id")
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2013-10-17 02:44:56 -04:00
|
|
|
def update_latest
|
|
|
|
latest_post_id = Post
|
|
|
|
.order("posts.created_at desc")
|
|
|
|
.where("NOT hidden")
|
|
|
|
.joins("join topics on topics.id = topic_id")
|
|
|
|
.where("topics.category_id = :id", id: self.id)
|
|
|
|
.limit(1)
|
|
|
|
.pluck("posts.id")
|
|
|
|
.first
|
|
|
|
|
|
|
|
latest_topic_id = Topic
|
|
|
|
.order("topics.created_at desc")
|
|
|
|
.where("visible")
|
|
|
|
.where("topics.category_id = :id", id: self.id)
|
|
|
|
.limit(1)
|
|
|
|
.pluck("topics.id")
|
|
|
|
.first
|
|
|
|
|
|
|
|
self.update_attributes(latest_topic_id: latest_topic_id, latest_post_id: latest_post_id)
|
|
|
|
end
|
2013-07-13 21:24:16 -04:00
|
|
|
|
|
|
|
def self.resolve_permissions(permissions)
|
|
|
|
read_restricted = true
|
|
|
|
|
|
|
|
everyone = Group::AUTO_GROUPS[:everyone]
|
|
|
|
full = CategoryGroup.permission_types[:full]
|
|
|
|
|
|
|
|
mapped = permissions.map do |group,permission|
|
2014-03-26 15:20:41 -04:00
|
|
|
group = group.id if group.is_a?(Group)
|
2013-07-13 21:24:16 -04:00
|
|
|
|
|
|
|
# subtle, using Group[] ensures the group exists in the DB
|
2014-03-26 15:20:41 -04:00
|
|
|
group = Group[group.to_sym].id unless group.is_a?(Fixnum)
|
|
|
|
permission = CategoryGroup.permission_types[permission] unless permission.is_a?(Fixnum)
|
2013-07-13 21:24:16 -04:00
|
|
|
|
|
|
|
[group, permission]
|
|
|
|
end
|
|
|
|
|
|
|
|
mapped.each do |group, permission|
|
|
|
|
if group == everyone && permission == full
|
|
|
|
return [false, []]
|
|
|
|
end
|
|
|
|
|
|
|
|
read_restricted = false if group == everyone
|
|
|
|
end
|
|
|
|
|
|
|
|
[read_restricted, mapped]
|
|
|
|
end
|
2013-12-17 15:36:15 -05:00
|
|
|
|
2014-02-08 17:10:48 -05:00
|
|
|
def self.query_parent_category(parent_slug)
|
2014-08-13 15:24:28 -04:00
|
|
|
self.where(slug: parent_slug, parent_category_id: nil).pluck(:id).first ||
|
2014-02-08 17:10:48 -05:00
|
|
|
self.where(id: parent_slug.to_i).pluck(:id).first
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.query_category(slug, parent_category_id)
|
2014-03-12 16:33:05 -04:00
|
|
|
self.where(slug: slug, parent_category_id: parent_category_id).includes(:featured_users).first ||
|
|
|
|
self.where(id: slug.to_i, parent_category_id: parent_category_id).includes(:featured_users).first
|
2014-02-08 17:10:48 -05:00
|
|
|
end
|
|
|
|
|
2014-02-27 07:44:21 -05:00
|
|
|
def self.find_by_email(email)
|
2014-05-06 09:41:59 -04:00
|
|
|
self.find_by(email_in: Email.downcase(email))
|
2014-02-27 07:44:21 -05:00
|
|
|
end
|
|
|
|
|
2014-02-12 17:24:25 -05:00
|
|
|
def has_children?
|
|
|
|
id && Category.where(parent_category_id: id).exists?
|
|
|
|
end
|
|
|
|
|
2014-01-15 14:11:19 -05:00
|
|
|
def uncategorized?
|
2013-12-17 15:36:15 -05:00
|
|
|
id == SiteSetting.uncategorized_category_id
|
|
|
|
end
|
2014-02-16 12:45:00 -05:00
|
|
|
|
|
|
|
def url
|
|
|
|
url = "/category"
|
|
|
|
url << "/#{parent_category.slug}" if parent_category_id
|
|
|
|
url << "/#{slug}"
|
|
|
|
end
|
2014-07-18 13:59:54 -04:00
|
|
|
|
|
|
|
# If the name changes, try and update the category definition topic too if it's
|
|
|
|
# an exact match
|
|
|
|
def rename_category_definition
|
|
|
|
old_name = changed_attributes["name"]
|
2014-07-25 16:36:16 -04:00
|
|
|
return unless topic.present?
|
2014-07-18 13:59:54 -04:00
|
|
|
if topic.title == I18n.t("category.topic_prefix", category: old_name)
|
|
|
|
topic.update_column(:title, I18n.t("category.topic_prefix", category: name))
|
|
|
|
end
|
|
|
|
end
|
2013-02-05 14:16:51 -05:00
|
|
|
end
|
2013-05-23 22:48:32 -04:00
|
|
|
|
|
|
|
# == Schema Information
|
|
|
|
#
|
|
|
|
# Table name: categories
|
|
|
|
#
|
2014-03-20 00:35:51 -04:00
|
|
|
# id :integer not null, primary key
|
|
|
|
# name :string(50) not null
|
|
|
|
# color :string(6) default("AB9364"), not null
|
|
|
|
# topic_id :integer
|
|
|
|
# topic_count :integer default(0), not null
|
2014-05-27 21:49:50 -04:00
|
|
|
# created_at :datetime
|
|
|
|
# updated_at :datetime
|
2014-03-20 00:35:51 -04:00
|
|
|
# user_id :integer not null
|
|
|
|
# topics_year :integer default(0)
|
|
|
|
# topics_month :integer default(0)
|
|
|
|
# topics_week :integer default(0)
|
|
|
|
# slug :string(255) not null
|
|
|
|
# description :text
|
|
|
|
# text_color :string(6) default("FFFFFF"), not null
|
|
|
|
# read_restricted :boolean default(FALSE), not null
|
|
|
|
# auto_close_hours :float
|
|
|
|
# post_count :integer default(0), not null
|
|
|
|
# latest_post_id :integer
|
|
|
|
# latest_topic_id :integer
|
|
|
|
# position :integer
|
|
|
|
# parent_category_id :integer
|
|
|
|
# posts_year :integer default(0)
|
|
|
|
# posts_month :integer default(0)
|
|
|
|
# posts_week :integer default(0)
|
|
|
|
# email_in :string(255)
|
|
|
|
# email_in_allow_strangers :boolean default(FALSE)
|
|
|
|
# topics_day :integer default(0)
|
|
|
|
# posts_day :integer default(0)
|
2014-07-03 03:29:44 -04:00
|
|
|
# logo_url :string(255)
|
|
|
|
# background_url :string(255)
|
2014-07-14 21:29:44 -04:00
|
|
|
# allow_badges :boolean default(TRUE), not null
|
2013-05-23 22:48:32 -04:00
|
|
|
#
|
|
|
|
# Indexes
|
|
|
|
#
|
2014-07-03 03:29:44 -04:00
|
|
|
# index_categories_on_email_in (email_in) UNIQUE
|
|
|
|
# index_categories_on_topic_count (topic_count)
|
|
|
|
# unique_index_categories_on_name (name) UNIQUE
|
2013-05-23 22:48:32 -04:00
|
|
|
#
|