mirror of
https://github.com/codeninjasllc/discourse.git
synced 2024-11-28 01:56:01 -05:00
b51bebb200
This creates two methods in the Category model. This moves the model logic to the model and just calls the Category class methods in ListController. This also adds tests for the two methods created in the Category model. The motivation for this refactor is the code climate score of the this class and readability of the code. Please enter the commit message for your changes. Lines starting
391 lines
12 KiB
Ruby
391 lines
12 KiB
Ruby
require_dependency "concern/positionable"
|
|
|
|
class Category < ActiveRecord::Base
|
|
|
|
include Concern::Positionable
|
|
|
|
belongs_to :topic, dependent: :destroy
|
|
if rails4?
|
|
belongs_to :topic_only_relative_url,
|
|
-> { select "id, title, slug" },
|
|
class_name: "Topic",
|
|
foreign_key: "topic_id"
|
|
else
|
|
belongs_to :topic_only_relative_url,
|
|
select: "id, title, slug",
|
|
class_name: "Topic",
|
|
foreign_key: "topic_id"
|
|
end
|
|
|
|
belongs_to :user
|
|
belongs_to :latest_post, class_name: "Post"
|
|
|
|
has_many :topics
|
|
has_many :category_featured_topics
|
|
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
|
|
|
|
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 :parent_category_validator
|
|
|
|
before_validation :ensure_slug
|
|
after_save :invalidate_site_cache
|
|
before_save :apply_permissions
|
|
after_create :create_category_definition
|
|
after_create :publish_categories_list
|
|
after_destroy :invalidate_site_cache
|
|
after_destroy :publish_categories_list
|
|
|
|
has_one :category_search_data
|
|
belongs_to :parent_category, class_name: 'Category'
|
|
has_many :subcategories, class_name: 'Category', foreign_key: 'parent_category_id'
|
|
|
|
scope :latest, ->{ order('topic_count desc') }
|
|
|
|
scope :secured, ->(guardian = nil) {
|
|
ids = guardian.secure_category_ids if guardian
|
|
if ids.present?
|
|
where("NOT categories.read_restricted or categories.id in (:cats)", cats: ids).references(:categories)
|
|
else
|
|
where("NOT categories.read_restricted").references(:categories)
|
|
end
|
|
}
|
|
|
|
scope :topic_create_allowed, ->(guardian) {
|
|
if guardian.anonymous?
|
|
where("1=0")
|
|
else
|
|
scoped_to_permissions(guardian, [:full])
|
|
end
|
|
}
|
|
|
|
scope :post_create_allowed, ->(guardian) {
|
|
if guardian.anonymous?
|
|
where("1=0")
|
|
else
|
|
scoped_to_permissions(guardian, [:create_post, :full])
|
|
end
|
|
}
|
|
delegate :post_template, to: 'self.class'
|
|
|
|
# permission is just used by serialization
|
|
# we may consider wrapping this in another spot
|
|
attr_accessor :displayable_topics, :permission, :subcategory_ids
|
|
|
|
|
|
def self.scoped_to_permissions(guardian, permission_types)
|
|
if guardian && guardian.is_staff?
|
|
rails4? ? all : scoped
|
|
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
|
|
(
|
|
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 ?
|
|
)
|
|
)
|
|
)
|
|
)", permission_types,(!guardian || guardian.user.blank?) ? -1 : guardian.user.id, Group[:everyone].id)
|
|
end
|
|
end
|
|
|
|
# Internal: Update category stats: # of topics and posts in past year, month, week for
|
|
# all categories.
|
|
def self.update_stats
|
|
topics = Topic
|
|
.select("COUNT(*) topic_count")
|
|
.where("topics.category_id = categories.id")
|
|
.where("categories.topic_id <> topics.id OR categories.topic_id is null")
|
|
.visible
|
|
|
|
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
|
|
|
|
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.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
|
|
|
|
posts = Post.select("count(*) post_count")
|
|
.joins(:topic)
|
|
.where('topics.category_id = categories.id')
|
|
.where('topics.visible = true')
|
|
.where("topics.id NOT IN (select cc.topic_id from categories cc WHERE topic_id IS NOT NULL)")
|
|
.where('posts.deleted_at IS NULL')
|
|
.where('posts.user_deleted = false')
|
|
|
|
posts_year = posts.created_since(1.year.ago).to_sql
|
|
posts_month = posts.created_since(1.month.ago).to_sql
|
|
posts_week = posts.created_since(1.week.ago).to_sql
|
|
|
|
# TODO don't update unchanged data
|
|
Category.update_all("topics_year = (#{topics_year}),
|
|
topics_month = (#{topics_month}),
|
|
topics_week = (#{topics_week}),
|
|
posts_year = (#{posts_year}),
|
|
posts_month = (#{posts_month}),
|
|
posts_week = (#{posts_week})")
|
|
end
|
|
|
|
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
|
|
|
|
def topics_day
|
|
if val = $redis.get(topics_day_key)
|
|
val.to_i
|
|
else
|
|
val = self.topics.where(['topics.id <> ?', self.topic_id]).created_since(1.day.ago).visible.count
|
|
$redis.setex topics_day_key, 30.minutes.to_i, val
|
|
val
|
|
end
|
|
end
|
|
|
|
def topics_day_key
|
|
"topics_day:cat-#{self.id}"
|
|
end
|
|
|
|
def posts_day
|
|
if val = $redis.get(posts_day_key)
|
|
val.to_i
|
|
else
|
|
val = self.visible_posts.created_since(1.day.ago).count
|
|
$redis.setex posts_day_key, 30.minutes.to_i, val
|
|
val
|
|
end
|
|
end
|
|
|
|
def posts_day_key
|
|
"posts_day:cat-#{self.id}"
|
|
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
|
|
t = Topic.new(title: I18n.t("category.topic_prefix", category: name), user: user, pinned_at: Time.now, category_id: id)
|
|
t.skip_callbacks = true
|
|
t.auto_close_hours = nil
|
|
t.save!
|
|
update_column(:topic_id, t.id)
|
|
t.posts.create(raw: post_template, user: user)
|
|
end
|
|
|
|
def topic_url
|
|
topic_only_relative_url.try(:relative_url)
|
|
end
|
|
|
|
def ensure_slug
|
|
if name.present?
|
|
self.name.strip!
|
|
self.slug = Slug.for(name)
|
|
|
|
return if self.slug.blank?
|
|
|
|
# If a category with that slug already exists, set the slug to nil so the category can be found
|
|
# another way.
|
|
category = Category.where(slug: self.slug)
|
|
category = category.where("id != ?", id) if id.present?
|
|
self.slug = '' if category.exists?
|
|
end
|
|
end
|
|
|
|
# Categories are cached in the site json, so the caches need to be
|
|
# invalidated whenever the category changes.
|
|
def invalidate_site_cache
|
|
Site.invalidate_cache
|
|
end
|
|
|
|
def publish_categories_list
|
|
MessageBus.publish('/categories', {categories: ActiveModel::ArraySerializer.new(Category.latest).as_json})
|
|
end
|
|
|
|
def parent_category_validator
|
|
if parent_category_id
|
|
errors.add(:parent_category_id, I18n.t("category.errors.self_parent")) if parent_category_id == id
|
|
|
|
grandfather_id = Category.where(id: parent_category_id).pluck(:parent_category_id).first
|
|
errors.add(:base, I18n.t("category.errors.depth")) if grandfather_id
|
|
end
|
|
end
|
|
|
|
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?
|
|
ids = Group.where(name: names.split(",")).pluck(:id)
|
|
ids.each do |id|
|
|
category_groups.build(group_id: id)
|
|
end
|
|
end
|
|
|
|
# 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.
|
|
end
|
|
|
|
def permissions=(permissions)
|
|
set_permissions(permissions)
|
|
end
|
|
|
|
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
|
|
end
|
|
end
|
|
|
|
def secure_group_ids
|
|
if self.read_restricted?
|
|
groups.pluck("groups.id")
|
|
end
|
|
end
|
|
|
|
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
|
|
|
|
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 = group.id if Group === group
|
|
|
|
# subtle, using Group[] ensures the group exists in the DB
|
|
group = Group[group.to_sym].id unless Fixnum === group
|
|
permission = CategoryGroup.permission_types[permission] unless Fixnum === permission
|
|
|
|
[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
|
|
|
|
def self.query_parent_category(parent_slug)
|
|
self.where(slug: parent_slug).pluck(:id).first ||
|
|
self.where(id: parent_slug.to_i).pluck(:id).first
|
|
end
|
|
|
|
def self.query_category(slug, parent_category_id)
|
|
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
|
|
end
|
|
|
|
def uncategorized?
|
|
id == SiteSetting.uncategorized_category_id
|
|
end
|
|
end
|
|
|
|
# == Schema Information
|
|
#
|
|
# Table name: categories
|
|
#
|
|
# 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
|
|
# topics_month :integer
|
|
# topics_week :integer
|
|
# 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
|
|
# posts_month :integer
|
|
# posts_week :integer
|
|
#
|
|
# Indexes
|
|
#
|
|
# index_categories_on_forum_thread_count (topic_count)
|
|
# index_categories_on_name (name) UNIQUE
|
|
#
|