2014-11-11 18:01:41 -05:00
|
|
|
require_dependency 'distributed_cache'
|
|
|
|
|
2013-02-05 14:16:51 -05:00
|
|
|
class Category < ActiveRecord::Base
|
2017-08-15 11:46:57 -04:00
|
|
|
include Searchable
|
2014-04-19 00:00:40 -04:00
|
|
|
include Positionable
|
2014-04-28 04:31:51 -04:00
|
|
|
include HasCustomFields
|
2016-01-12 03:40:36 -05:00
|
|
|
include CategoryHashtag
|
2016-12-22 00:46:22 -05:00
|
|
|
include AnonCacheInvalidator
|
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"
|
2016-12-02 02:15:34 -05:00
|
|
|
belongs_to :uploaded_logo, class_name: "Upload"
|
|
|
|
belongs_to :uploaded_background, class_name: "Upload"
|
2013-02-05 14:16:51 -05:00
|
|
|
|
|
|
|
has_many :topics
|
2016-07-06 15:56:40 -04:00
|
|
|
has_many :category_users
|
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
|
|
|
|
|
2014-08-31 16:10:38 -04:00
|
|
|
has_many :category_groups, dependent: :destroy
|
2013-04-29 02:33:24 -04:00
|
|
|
has_many :groups, through: :category_groups
|
|
|
|
|
2016-06-15 13:49:57 -04:00
|
|
|
has_and_belongs_to_many :web_hooks
|
|
|
|
|
2013-03-02 03:57:02 -05:00
|
|
|
validates :user_id, presence: true
|
2017-08-31 00:06:56 -04:00
|
|
|
validates :name, if: Proc.new { |c| c.new_record? || c.will_save_change_to_name? },
|
2014-08-11 16:55:26 -04:00
|
|
|
presence: true,
|
|
|
|
uniqueness: { scope: :parent_category_id, case_sensitive: false },
|
|
|
|
length: { in: 1..50 }
|
2017-03-01 12:03:12 -05:00
|
|
|
validates :num_featured_topics, numericality: { only_integer: true, greater_than: 0 }
|
2013-10-23 12:58:11 -04:00
|
|
|
validate :parent_category_validator
|
2013-02-05 14:16:51 -05:00
|
|
|
|
2016-02-24 13:47:58 -05:00
|
|
|
validate :email_in_validator
|
|
|
|
|
2014-12-03 19:23:59 -05:00
|
|
|
validate :ensure_slug
|
2016-12-21 21:13:14 -05:00
|
|
|
|
|
|
|
after_create :create_category_definition
|
|
|
|
|
2013-07-13 21:24:16 -04:00
|
|
|
before_save :apply_permissions
|
2014-07-14 10:16:24 -04:00
|
|
|
before_save :downcase_email
|
2014-08-18 11:07:32 -04:00
|
|
|
before_save :downcase_name
|
2015-07-09 22:09:43 -04:00
|
|
|
|
2016-12-21 21:13:14 -05:00
|
|
|
after_save :publish_discourse_stylesheet
|
2015-07-09 22:09:43 -04:00
|
|
|
after_save :publish_category
|
2016-12-21 21:13:14 -05:00
|
|
|
after_save :reset_topic_ids_cache
|
|
|
|
after_save :clear_url_cache
|
|
|
|
after_save :index_search
|
2015-07-09 22:09:43 -04:00
|
|
|
|
2016-12-21 21:13:14 -05:00
|
|
|
after_destroy :reset_topic_ids_cache
|
|
|
|
after_destroy :publish_category_deletion
|
2018-03-13 15:59:12 -04:00
|
|
|
after_destroy :remove_site_settings
|
2013-02-05 14:16:51 -05:00
|
|
|
|
2016-04-27 07:04:44 -04:00
|
|
|
after_create :delete_category_permalink
|
|
|
|
|
2017-08-31 00:06:56 -04:00
|
|
|
after_update :rename_category_definition, if: :saved_change_to_name?
|
|
|
|
after_update :create_category_permalink, if: :saved_change_to_slug?
|
2014-11-10 23:32:44 -05:00
|
|
|
|
2018-03-27 02:23:35 -04:00
|
|
|
after_commit :trigger_category_created_event, on: :create
|
|
|
|
after_commit :trigger_category_destroyed_event, on: :destroy
|
|
|
|
|
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
|
|
|
|
2016-06-07 13:08:59 -04:00
|
|
|
has_many :category_tags, dependent: :destroy
|
2016-05-30 16:37:06 -04:00
|
|
|
has_many :tags, through: :category_tags
|
2016-06-07 13:08:59 -04:00
|
|
|
has_many :category_tag_groups, dependent: :destroy
|
|
|
|
has_many :tag_groups, through: :category_tag_groups
|
2016-05-30 16:37:06 -04:00
|
|
|
|
2016-06-26 13:25:45 -04:00
|
|
|
scope :latest, -> { order('topic_count DESC') }
|
2013-03-02 03:57:02 -05:00
|
|
|
|
2016-06-26 13:25:45 -04:00
|
|
|
scope :secured, -> (guardian = nil) {
|
2013-05-13 04:04:03 -04:00
|
|
|
ids = guardian.secure_category_ids if guardian
|
2017-08-31 00:06:56 -04:00
|
|
|
|
2013-05-13 04:04:03 -04:00
|
|
|
if ids.present?
|
2016-06-26 13:25:45 -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
|
|
|
|
}
|
|
|
|
|
2016-06-26 13:25:45 -04:00
|
|
|
TOPIC_CREATION_PERMISSIONS ||= [:full]
|
|
|
|
POST_CREATION_PERMISSIONS ||= [:create_post, :full]
|
|
|
|
scope :topic_create_allowed, -> (guardian) { scoped_to_permissions(guardian, TOPIC_CREATION_PERMISSIONS) }
|
|
|
|
scope :post_create_allowed, -> (guardian) { scoped_to_permissions(guardian, POST_CREATION_PERMISSIONS) }
|
2016-01-12 06:06:51 -05: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
|
2016-08-18 19:47:00 -04:00
|
|
|
attr_accessor :displayable_topics, :permission, :subcategory_ids, :notification_level, :has_children
|
2013-06-05 02:10:26 -04:00
|
|
|
|
2016-07-27 20:20:14 -04:00
|
|
|
@topic_id_cache = DistributedCache.new('category_topic_ids')
|
|
|
|
|
2016-07-18 22:34:54 -04:00
|
|
|
def self.topic_ids
|
2016-07-27 20:20:14 -04:00
|
|
|
@topic_id_cache['ids'] || reset_topic_ids_cache
|
2016-07-18 22:34:54 -04:00
|
|
|
end
|
|
|
|
|
2016-07-27 20:20:14 -04:00
|
|
|
def self.reset_topic_ids_cache
|
|
|
|
@topic_id_cache['ids'] = Set.new(Category.pluck(:topic_id).compact)
|
2016-07-18 22:34:54 -04:00
|
|
|
end
|
|
|
|
|
2016-07-27 20:20:14 -04:00
|
|
|
def reset_topic_ids_cache
|
|
|
|
Category.reset_topic_ids_cache
|
2016-07-18 22:34:54 -04:00
|
|
|
end
|
|
|
|
|
2013-07-13 21:24:16 -04:00
|
|
|
def self.scoped_to_permissions(guardian, permission_types)
|
2016-06-26 13:25:45 -04:00
|
|
|
if guardian.try(:is_admin?)
|
2014-02-17 11:44:28 -05:00
|
|
|
all
|
2015-05-13 22:19:22 -04:00
|
|
|
elsif !guardian || guardian.anonymous?
|
|
|
|
if permission_types.include?(:readonly)
|
|
|
|
where("NOT categories.read_restricted")
|
|
|
|
else
|
|
|
|
where("1 = 0")
|
|
|
|
end
|
2013-07-13 21:24:16 -04:00
|
|
|
else
|
2016-06-26 13:25:45 -04:00
|
|
|
permissions = permission_types.map { |p| CategoryGroup.permission_types[p] }
|
|
|
|
where("(:staged AND LENGTH(COALESCE(email_in, '')) > 0 AND email_in_allow_strangers)
|
|
|
|
OR categories.id NOT IN (SELECT category_id FROM category_groups)
|
|
|
|
OR categories.id IN (
|
|
|
|
SELECT category_id
|
|
|
|
FROM category_groups
|
|
|
|
WHERE permission_type IN (:permissions)
|
|
|
|
AND (group_id = :everyone OR group_id IN (SELECT group_id FROM group_users WHERE user_id = :user_id))
|
|
|
|
)",
|
|
|
|
staged: guardian.is_staged?,
|
|
|
|
permissions: permissions,
|
|
|
|
user_id: guardian.user.id,
|
|
|
|
everyone: 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
|
2017-07-27 21:20:09 -04:00
|
|
|
.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
|
2014-03-19 10:14:05 -04:00
|
|
|
|
2016-06-26 13:25:45 -04:00
|
|
|
Category.exec_sql <<-SQL
|
2014-03-19 10:14:05 -04:00
|
|
|
UPDATE categories c
|
2016-06-26 13:25:45 -04:00
|
|
|
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)
|
2014-03-19 10:14:05 -04:00
|
|
|
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|
|
2014-08-27 15:58:05 -04:00
|
|
|
topics = c.topics.visible
|
|
|
|
topics = topics.where(['topics.id <> ?', c.topic_id]) if c.topic_id
|
2014-03-19 10:14:05 -04:00
|
|
|
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)
|
2017-07-27 21:20:09 -04:00
|
|
|
.where(['topics.category_id = ?', self.id])
|
|
|
|
.where('topics.visible = true')
|
|
|
|
.where('posts.deleted_at IS NULL')
|
|
|
|
.where('posts.user_deleted = false')
|
2013-12-13 15:15:51 -05:00
|
|
|
self.topic_id ? query.where(['topics.id <> ?', self.topic_id]) : query
|
|
|
|
end
|
|
|
|
|
2016-08-17 17:23:16 -04:00
|
|
|
# Internal: Generate the text of post prompting to enter category description.
|
2013-04-29 02:33:24 -04:00
|
|
|
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
|
2014-10-10 12:21:44 -04:00
|
|
|
t.ignore_category_auto_close = true
|
2017-08-22 02:22:48 -04:00
|
|
|
t.delete_topic_timer(TopicTimer.types[:close])
|
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)
|
2018-03-07 12:10:15 -05:00
|
|
|
t.posts.create(raw: description || post_template, user: user)
|
2013-03-02 03:57:02 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
def topic_url
|
2015-09-28 02:43:38 -04:00
|
|
|
if has_attribute?("topic_slug")
|
2015-10-01 22:27:38 -04:00
|
|
|
Topic.relative_url(topic_id, read_attribute(:topic_slug))
|
2015-09-28 02:43:38 -04:00
|
|
|
else
|
|
|
|
topic_only_relative_url.try(:relative_url)
|
|
|
|
end
|
2013-03-02 03:57:02 -05:00
|
|
|
end
|
|
|
|
|
2014-10-22 00:48:18 -04:00
|
|
|
def description_text
|
2016-12-07 17:05:14 -05:00
|
|
|
return nil unless self.description
|
2014-10-22 00:48:18 -04:00
|
|
|
|
2015-09-28 02:41:16 -04:00
|
|
|
@@cache ||= LruRedux::ThreadSafeCache.new(1000)
|
2014-10-22 00:48:18 -04:00
|
|
|
@@cache.getset(self.description) do
|
2016-12-07 17:05:14 -05:00
|
|
|
Nokogiri::HTML.fragment(self.description).text.strip
|
2014-10-22 00:48:18 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2014-12-03 19:23:59 -05:00
|
|
|
def duplicate_slug?
|
|
|
|
Category.where(slug: self.slug, parent_category_id: parent_category_id).where.not(id: id).any?
|
|
|
|
end
|
|
|
|
|
2013-03-02 03:57:02 -05:00
|
|
|
def ensure_slug
|
2014-12-20 09:07:29 -05:00
|
|
|
return unless name.present?
|
|
|
|
|
|
|
|
self.name.strip!
|
|
|
|
|
|
|
|
if slug.present?
|
|
|
|
# santized custom slug
|
2015-05-13 04:52:48 -04:00
|
|
|
self.slug = Slug.sanitize(slug)
|
2014-12-20 09:07:29 -05:00
|
|
|
errors.add(:slug, 'is already in use') if duplicate_slug?
|
|
|
|
else
|
|
|
|
# auto slug
|
2015-04-13 10:50:41 -04:00
|
|
|
self.slug = Slug.for(name, '')
|
2014-12-20 09:07:29 -05:00
|
|
|
self.slug = '' if duplicate_slug?
|
2013-04-01 12:26:51 -04:00
|
|
|
end
|
2015-04-13 10:50:41 -04:00
|
|
|
# only allow to use category itself id. new_record doesn't have a id.
|
|
|
|
unless new_record?
|
2016-01-07 01:36:45 -05:00
|
|
|
match_id = /^(\d+)-category/.match(self.slug)
|
2015-04-13 10:50:41 -04:00
|
|
|
errors.add(:slug, :invalid) if match_id && match_id[1] && match_id[1] != self.id.to_s
|
|
|
|
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
|
|
|
|
|
2015-07-09 22:09:43 -04:00
|
|
|
def publish_category
|
|
|
|
group_ids = self.groups.pluck(:id) if self.read_restricted
|
2017-07-27 21:20:09 -04:00
|
|
|
MessageBus.publish('/categories', { categories: ActiveModel::ArraySerializer.new([self]).as_json }, group_ids: group_ids)
|
2015-07-09 22:09:43 -04:00
|
|
|
end
|
|
|
|
|
2018-03-13 15:59:12 -04:00
|
|
|
def remove_site_settings
|
|
|
|
SiteSetting.all_settings.each do |s|
|
|
|
|
if s[:type] == 'category' && s[:value].to_i == self.id
|
|
|
|
SiteSetting.send("#{s[:setting]}=", '')
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
end
|
|
|
|
|
2015-07-09 22:09:43 -04:00
|
|
|
def publish_category_deletion
|
2017-07-27 21:20:09 -04:00
|
|
|
MessageBus.publish('/categories', deleted_categories: [self.id])
|
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
|
|
|
|
|
2015-09-17 03:51:32 -04:00
|
|
|
def permissions_params
|
|
|
|
hash = {}
|
|
|
|
category_groups.includes(:group).each do |category_group|
|
|
|
|
hash[category_group.group_name] = category_group.permission_type
|
|
|
|
end
|
|
|
|
hash
|
|
|
|
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
|
|
|
|
2018-03-26 17:04:55 -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|
|
|
|
|
group_id = Group.group_id_from_param(group)
|
|
|
|
permission = CategoryGroup.permission_types[permission] unless permission.is_a?(Integer)
|
|
|
|
|
|
|
|
[group_id, 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
|
|
|
|
|
2016-06-01 17:05:15 -04:00
|
|
|
def allowed_tags=(tag_names_arg)
|
2017-07-27 21:20:09 -04:00
|
|
|
DiscourseTagging.add_or_create_tags_by_name(self, tag_names_arg, unlimited: true)
|
2016-05-30 16:37:06 -04:00
|
|
|
end
|
|
|
|
|
2016-06-07 13:08:59 -04:00
|
|
|
def allowed_tag_groups=(group_names)
|
|
|
|
self.tag_groups = TagGroup.where(name: group_names).all.to_a
|
|
|
|
end
|
|
|
|
|
2014-07-14 10:16:24 -04:00
|
|
|
def downcase_email
|
2016-02-24 13:47:58 -05:00
|
|
|
self.email_in = (email_in || "").strip.downcase.presence
|
|
|
|
end
|
|
|
|
|
|
|
|
def email_in_validator
|
|
|
|
return if self.email_in.blank?
|
|
|
|
email_in.split("|").each do |email|
|
2016-07-28 11:57:30 -04:00
|
|
|
|
|
|
|
escaped = Rack::Utils.escape_html(email)
|
2016-03-08 14:52:04 -05:00
|
|
|
if !Email.is_valid?(email)
|
2016-07-28 11:57:30 -04:00
|
|
|
self.errors.add(:base, I18n.t('category.errors.invalid_email_in', email: escaped))
|
2016-03-08 14:52:04 -05:00
|
|
|
elsif group = Group.find_by_email(email)
|
2016-07-28 11:57:30 -04:00
|
|
|
self.errors.add(:base, I18n.t('category.errors.email_already_used_in_group', email: escaped, group_name: Rack::Utils.escape_html(group.name)))
|
2016-03-08 14:52:04 -05:00
|
|
|
elsif category = Category.where.not(id: self.id).find_by_email(email)
|
2016-07-28 11:57:30 -04:00
|
|
|
self.errors.add(:base, I18n.t('category.errors.email_already_used_in_category', email: escaped, category_name: Rack::Utils.escape_html(category.name)))
|
2016-02-24 13:47:58 -05:00
|
|
|
end
|
|
|
|
end
|
2014-07-14 10:16:24 -04:00
|
|
|
end
|
|
|
|
|
2014-08-18 11:07:32 -04:00
|
|
|
def downcase_name
|
|
|
|
self.name_lower = name.downcase if self.name
|
|
|
|
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
|
2017-07-27 21:20:09 -04:00
|
|
|
.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
|
2013-10-17 02:44:56 -04:00
|
|
|
|
|
|
|
latest_topic_id = Topic
|
2017-07-27 21:20:09 -04:00
|
|
|
.order("topics.created_at desc")
|
|
|
|
.where("visible")
|
|
|
|
.where("topics.category_id = :id", id: self.id)
|
|
|
|
.limit(1)
|
|
|
|
.pluck("topics.id")
|
|
|
|
.first
|
2013-10-17 02:44:56 -04:00
|
|
|
|
|
|
|
self.update_attributes(latest_topic_id: latest_topic_id, latest_post_id: latest_post_id)
|
|
|
|
end
|
2013-07-13 21:24:16 -04: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
|
|
|
|
|
2015-02-12 12:21:07 -05:00
|
|
|
def self.query_category(slug_or_id, parent_category_id)
|
2018-03-05 17:27:30 -05:00
|
|
|
self.where(slug: slug_or_id, parent_category_id: parent_category_id).first ||
|
|
|
|
self.where(id: slug_or_id.to_i, parent_category_id: parent_category_id).first
|
2014-02-08 17:10:48 -05:00
|
|
|
end
|
|
|
|
|
2014-02-27 07:44:21 -05:00
|
|
|
def self.find_by_email(email)
|
2016-03-08 14:52:04 -05:00
|
|
|
self.where("string_to_array(email_in, '|') @> ARRAY[?]", Email.downcase(email)).first
|
2014-02-27 07:44:21 -05:00
|
|
|
end
|
|
|
|
|
2014-02-12 17:24:25 -05:00
|
|
|
def has_children?
|
2015-10-01 22:35:47 -04:00
|
|
|
@has_children ||= (id && Category.where(parent_category_id: id).exists?) ? :true : :false
|
|
|
|
@has_children == :true
|
2014-02-12 17:24:25 -05:00
|
|
|
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
|
|
|
|
2014-11-11 18:01:41 -05:00
|
|
|
@@url_cache = DistributedCache.new('category_url')
|
|
|
|
|
2016-12-21 21:13:14 -05:00
|
|
|
def clear_url_cache
|
2014-11-11 18:01:41 -05:00
|
|
|
@@url_cache.clear
|
|
|
|
end
|
|
|
|
|
2016-01-12 03:40:36 -05:00
|
|
|
def full_slug(separator = "-")
|
|
|
|
url[3..-1].gsub("/", separator)
|
2015-02-12 12:21:07 -05:00
|
|
|
end
|
|
|
|
|
2014-02-16 12:45:00 -05:00
|
|
|
def url
|
2014-11-11 18:01:41 -05:00
|
|
|
url = @@url_cache[self.id]
|
|
|
|
unless url
|
2015-04-30 12:46:19 -04:00
|
|
|
url = "#{Discourse.base_uri}/c"
|
2014-11-11 18:01:41 -05:00
|
|
|
url << "/#{parent_category.slug}" if parent_category_id
|
|
|
|
url << "/#{slug}"
|
|
|
|
url.freeze
|
|
|
|
|
|
|
|
@@url_cache[self.id] = url
|
|
|
|
end
|
|
|
|
|
|
|
|
url
|
2014-02-16 12:45:00 -05:00
|
|
|
end
|
2014-07-18 13:59:54 -04:00
|
|
|
|
2015-12-28 01:28:16 -05:00
|
|
|
def url_with_id
|
|
|
|
self.parent_category ? "#{url}/#{self.id}" : "#{Discourse.base_uri}/c/#{self.id}-#{self.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
|
2017-08-31 00:06:56 -04:00
|
|
|
old_name = saved_changes.transform_values(&:first)["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)
|
2017-03-26 02:59:53 -04:00
|
|
|
topic.update_attribute(:title, I18n.t("category.topic_prefix", category: name))
|
2014-07-18 13:59:54 -04:00
|
|
|
end
|
|
|
|
end
|
2014-11-10 23:32:44 -05:00
|
|
|
|
2016-04-27 07:04:44 -04:00
|
|
|
def create_category_permalink
|
2017-08-31 00:06:56 -04:00
|
|
|
old_slug = saved_changes.transform_values(&:first)["slug"]
|
2016-04-27 07:04:44 -04:00
|
|
|
if self.parent_category
|
2016-11-17 11:27:06 -05:00
|
|
|
url = "c/#{self.parent_category.slug}/#{old_slug}"
|
2016-04-27 07:04:44 -04:00
|
|
|
else
|
2016-11-17 11:27:06 -05:00
|
|
|
url = "c/#{old_slug}"
|
|
|
|
end
|
|
|
|
|
|
|
|
if Permalink.where(url: url).exists?
|
|
|
|
Permalink.where(url: url).update_all(category_id: id)
|
|
|
|
else
|
|
|
|
Permalink.create(url: url, category_id: id)
|
2016-04-27 07:04:44 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def delete_category_permalink
|
|
|
|
if self.parent_category
|
|
|
|
permalink = Permalink.find_by_url("c/#{self.parent_category.slug}/#{slug}")
|
|
|
|
else
|
|
|
|
permalink = Permalink.find_by_url("c/#{slug}")
|
|
|
|
end
|
|
|
|
permalink.destroy if permalink
|
|
|
|
end
|
|
|
|
|
2014-11-10 23:32:44 -05:00
|
|
|
def publish_discourse_stylesheet
|
2017-04-12 10:52:52 -04:00
|
|
|
Stylesheet::Manager.cache.clear
|
2014-11-10 23:32:44 -05:00
|
|
|
end
|
2016-03-14 12:38:29 -04:00
|
|
|
|
2016-12-21 21:13:14 -05:00
|
|
|
def index_search
|
|
|
|
SearchIndexer.index(self)
|
|
|
|
end
|
|
|
|
|
2017-07-27 21:20:09 -04:00
|
|
|
def self.find_by_slug(category_slug, parent_category_slug = nil)
|
2016-03-14 12:38:29 -04:00
|
|
|
if parent_category_slug
|
|
|
|
parent_category_id = self.where(slug: parent_category_slug, parent_category_id: nil).pluck(:id).first
|
|
|
|
self.where(slug: category_slug, parent_category_id: parent_category_id).first
|
|
|
|
else
|
|
|
|
self.where(slug: category_slug, parent_category_id: nil).first
|
|
|
|
end
|
|
|
|
end
|
2017-03-08 11:31:30 -05:00
|
|
|
|
|
|
|
def subcategory_list_includes_topics?
|
2017-03-15 17:34:38 -04:00
|
|
|
subcategory_list_style.end_with?("with_featured_topics")
|
2017-03-08 11:31:30 -05:00
|
|
|
end
|
2018-03-27 02:23:35 -04:00
|
|
|
|
|
|
|
def trigger_category_created_event
|
|
|
|
DiscourseEvent.trigger(:category_created, self)
|
|
|
|
true
|
|
|
|
end
|
|
|
|
|
|
|
|
def trigger_category_destroyed_event
|
|
|
|
DiscourseEvent.trigger(:category_destroyed, self)
|
|
|
|
true
|
|
|
|
end
|
2013-02-05 14:16:51 -05:00
|
|
|
end
|
2013-05-23 22:48:32 -04:00
|
|
|
|
|
|
|
# == Schema Information
|
|
|
|
#
|
|
|
|
# Table name: categories
|
|
|
|
#
|
2014-11-19 22:53:15 -05: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
|
|
|
|
# created_at :datetime not null
|
|
|
|
# updated_at :datetime not null
|
|
|
|
# user_id :integer not null
|
|
|
|
# topics_year :integer default(0)
|
|
|
|
# topics_month :integer default(0)
|
|
|
|
# topics_week :integer default(0)
|
2018-02-20 01:28:58 -05:00
|
|
|
# slug :string not null
|
2014-11-19 22:53:15 -05:00
|
|
|
# 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)
|
2018-02-20 01:28:58 -05:00
|
|
|
# email_in :string
|
2014-11-19 22:53:15 -05:00
|
|
|
# email_in_allow_strangers :boolean default(FALSE)
|
|
|
|
# topics_day :integer default(0)
|
|
|
|
# posts_day :integer default(0)
|
|
|
|
# allow_badges :boolean default(TRUE), not null
|
|
|
|
# name_lower :string(50) not null
|
|
|
|
# auto_close_based_on_last_post :boolean default(FALSE)
|
2015-09-17 20:41:10 -04:00
|
|
|
# topic_template :text
|
2016-02-22 18:33:53 -05:00
|
|
|
# contains_messages :boolean
|
2016-11-01 12:18:31 -04:00
|
|
|
# sort_order :string
|
|
|
|
# sort_ascending :boolean
|
2016-12-12 09:59:40 -05:00
|
|
|
# uploaded_logo_id :integer
|
|
|
|
# uploaded_background_id :integer
|
2017-03-22 02:26:53 -04:00
|
|
|
# topic_featured_link_allowed :boolean default(TRUE)
|
|
|
|
# all_topics_wiki :boolean default(FALSE), not null
|
|
|
|
# show_subcategory_list :boolean default(FALSE)
|
|
|
|
# num_featured_topics :integer default(3)
|
|
|
|
# default_view :string(50)
|
|
|
|
# subcategory_list_style :string(50) default("rows_with_featured_topics")
|
2017-03-22 16:54:12 -04:00
|
|
|
# default_top_period :string(20) default("all")
|
2017-11-15 11:36:52 -05:00
|
|
|
# mailinglist_mirror :boolean default(FALSE), not null
|
2018-03-04 19:06:45 -05:00
|
|
|
# suppress_from_latest :boolean default(FALSE)
|
2013-05-23 22:48:32 -04:00
|
|
|
#
|
|
|
|
# Indexes
|
|
|
|
#
|
2018-02-20 01:28:58 -05:00
|
|
|
# index_categories_on_email_in (email_in) UNIQUE
|
|
|
|
# index_categories_on_topic_count (topic_count)
|
2018-03-04 19:06:45 -05:00
|
|
|
# unique_index_categories_on_name ((COALESCE(parent_category_id, '-1'::integer)), name) UNIQUE
|
2013-05-23 22:48:32 -04:00
|
|
|
#
|