<commit_before>require_dependency 'distributed_cache'
require_dependency 'sass/discourse_stylesheets'

class Category < ActiveRecord::Base

  include Positionable
  include HasCustomFields

  belongs_to :topic, dependent: :destroy
  belongs_to :topic_only_relative_url,
              -> { select "id, title, slug" },
              class_name: "Topic",
              foreign_key: "topic_id"

  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, dependent: :destroy
  has_many :groups, through: :category_groups

  validates :user_id, presence: true
  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 }
  validate :parent_category_validator

  validate :ensure_slug
  before_save :apply_permissions
  before_save :downcase_email
  before_save :downcase_name
  after_create :create_category_definition

  after_save :publish_category
  after_destroy :publish_category_deletion

  after_update :rename_category_definition, if: :name_changed?

  after_save :publish_discourse_stylesheet

  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, :notification_level, :has_children

  def self.last_updated_at
    order('updated_at desc').limit(1).pluck(:updated_at).first.to_i
  end

  def self.scoped_to_permissions(guardian, permission_types)
    if guardian && guardian.is_staff?
      all
    elsif !guardian || guardian.anonymous?
      if permission_types.include?(:readonly)
        where("NOT categories.read_restricted")
      else
        where("1 = 0")
      end
    else
      permission_types = permission_types.map{ |permission_type|
        CategoryGroup.permission_types[permission_type]
      }
      where("categories.id in (
                  SELECT cg.category_id FROM category_groups cg
                    WHERE permission_type in (:permissions) AND
                    (
                      group_id IN (
                        SELECT g.group_id FROM group_users g where g.user_id = :user_id
                      )
                    )
                )
                OR
                categories.id in (
                  SELECT cg.category_id FROM category_groups cg
                    WHERE permission_type in (:permissions) AND group_id = :everyone
                  )
                OR
                categories.id NOT in (SELECT cg.category_id FROM category_groups cg)
            ", permissions: permission_types,
               user_id: guardian.user.id,
               everyone: Group[:everyone].id)
    end
  end

  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.visible
      topics = topics.where(['topics.id <> ?', c.topic_id]) if c.topic_id
      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


  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


  # 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.ignore_category_auto_close = true
    t.set_auto_close(nil)
    t.save!(validate: false)
    update_column(:topic_id, t.id)
    t.posts.create(raw: post_template, user: user)
  end

  def topic_url
    if has_attribute?("topic_slug")
      Topic.relative_url(topic_id, topic_slug)
    else
      topic_only_relative_url.try(:relative_url)
    end
  end

  def description_text
    return nil unless description

    @@cache ||= LruRedux::ThreadSafeCache.new(1000)
    @@cache.getset(self.description) do
      Nokogiri::HTML(self.description).text
    end

  end

  def duplicate_slug?
    Category.where(slug: self.slug, parent_category_id: parent_category_id).where.not(id: id).any?
  end

  def ensure_slug
    return unless name.present?

    self.name.strip!

    if slug.present?
      # santized custom slug<commit_msg>PERF: avoid method_missing<commit_after>require_dependency 'distributed_cache'
require_dependency 'sass/discourse_stylesheets'

class Category < ActiveRecord::Base

  include Positionable
  include HasCustomFields

  belongs_to :topic, dependent: :destroy
  belongs_to :topic_only_relative_url,
              -> { select "id, title, slug" },
              class_name: "Topic",
              foreign_key: "topic_id"

  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, dependent: :destroy
  has_many :groups, through: :category_groups

  validates :user_id, presence: true
  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 }
  validate :parent_category_validator

  validate :ensure_slug
  before_save :apply_permissions
  before_save :downcase_email
  before_save :downcase_name
  after_create :create_category_definition

  after_save :publish_category
  after_destroy :publish_category_deletion

  after_update :rename_category_definition, if: :name_changed?

  after_save :publish_discourse_stylesheet

  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, :notification_level, :has_children

  def self.last_updated_at
    order('updated_at desc').limit(1).pluck(:updated_at).first.to_i
  end

  def self.scoped_to_permissions(guardian, permission_types)
    if guardian && guardian.is_staff?
      all
    elsif !guardian || guardian.anonymous?
      if permission_types.include?(:readonly)
        where("NOT categories.read_restricted")
      else
        where("1 = 0")
      end
    else
      permission_types = permission_types.map{ |permission_type|
        CategoryGroup.permission_types[permission_type]
      }
      where("categories.id in (
                  SELECT cg.category_id FROM category_groups cg
                    WHERE permission_type in (:permissions) AND
                    (
                      group_id IN (
                        SELECT g.group_id FROM group_users g where g.user_id = :user_id
                      )
                    )
                )
                OR
                categories.id in (
                  SELECT cg.category_id FROM category_groups cg
                    WHERE permission_type in (:permissions) AND group_id = :everyone
                  )
                OR
                categories.id NOT in (SELECT cg.category_id FROM category_groups cg)
            ", permissions: permission_types,
               user_id: guardian.user.id,
               everyone: Group[:everyone].id)
    end
  end

  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.visible
      topics = topics.where(['topics.id <> ?', c.topic_id]) if c.topic_id
      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


  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


  # 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.ignore_category_auto_close = true
    t.set_auto_close(nil)
    t.save!(validate: false)
    update_column(:topic_id, t.id)
    t.posts.create(raw: post_template, user: user)
  end

  def topic_url
    if has_attribute?("topic_slug")
      Topic.relative_url(topic_id, read_attribute(:topic_slug))
    else
      topic_only_relative_url.try(:relative_url)
    end
  end

  def description_text
    return nil unless description

    @@cache ||= LruRedux::ThreadSafeCache.new(1000)
    @@cache.getset(self.description) do
      Nokogiri::HTML(self.description).text
    end

  end

  def duplicate_slug?
    Category.where(slug: self.slug, parent_category_id: parent_category_id).where.not(id: id).any?
  end

  def ensure_slug
    return unless name.present?

    self.name.strip!

    if slug.present?
      # santized custom slug
{'fraction_non_alphanumeric': 0.09274030012167095, 'fraction_numerical': 0.007435446802757875, 'mean_word_length': 2.75412331895458, 'pattern_counts': {'":': 0, '<': 17, '<?xml version=': 0, '>': 21, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '198819', 'n_tokens_mistral': 4715, 'n_tokens_neox': 4502, 'n_words': 1007}