discourse/app/models/category.rb
Nick Borromeo b51bebb200 Extract queries to keep logic in the Categories Model
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
2014-02-08 14:10:48 -08:00

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
#