Files
leadchat/app/models/article.rb
Pranav d79d9e8b46 fix: Include uncategorized articles in the all article section to allow edit/delete (#10153)
Fixes https://github.com/chatwoot/chatwoot/issues/9935
Fixes https://github.com/chatwoot/chatwoot/issues/8213

The articles were grouped by category, with locale being a derived
attribute from the category. If a category was deleted, the article
wouldn't appear on the dashboard. However, due to a bug, it would show
up in the uncategorized section on the public portal, leaving agents
unable to edit or update the article.

To address this issue, I've added a locale attribute directly to the
article. This attribute is automatically set from the category or the
portal's default locale if not supplied. The API parameters now use this
attribute to filter articles. As a result, the dashboard will display
articles even if they're not associated with a category, improving the
overall workflow.

**Main updates:** 
- Add locale attribute to the Article model. Add db migration to back
fill the data based on the above logic.
- Add a new scope search_by_locale and use it instead of
search_by_category_locale.
- Update the ERB template to include the locale filter.
- Move from `joins` to `left_outer_joins` to include the articles with
no categories.

---------

Co-authored-by: Sojan <sojan@pepalo.com>
2024-09-23 23:39:03 -07:00

185 lines
5.8 KiB
Ruby

# == Schema Information
#
# Table name: articles
#
# id :bigint not null, primary key
# content :text
# description :text
# locale :string default("en"), not null
# meta :jsonb
# position :integer
# slug :string not null
# status :integer
# title :string
# views :integer
# created_at :datetime not null
# updated_at :datetime not null
# account_id :integer not null
# associated_article_id :bigint
# author_id :bigint
# category_id :integer
# folder_id :integer
# portal_id :integer not null
#
# Indexes
#
# index_articles_on_associated_article_id (associated_article_id)
# index_articles_on_author_id (author_id)
# index_articles_on_slug (slug) UNIQUE
#
class Article < ApplicationRecord
include PgSearch::Model
has_many :associated_articles,
class_name: :Article,
foreign_key: :associated_article_id,
dependent: :nullify,
inverse_of: 'root_article'
belongs_to :root_article,
class_name: :Article,
foreign_key: :associated_article_id,
inverse_of: :associated_articles,
optional: true
belongs_to :account
belongs_to :category, optional: true
belongs_to :portal
belongs_to :author, class_name: 'User', inverse_of: :articles
before_validation :ensure_account_id
before_validation :ensure_article_slug
validates :account_id, presence: true
validates :author_id, presence: true
validates :title, presence: true
validates :content, presence: true
# ensuring that the position is always set correctly
before_create :add_position_to_article
before_create :add_locale_to_article
after_save :category_id_changed_action, if: :saved_change_to_category_id?
enum status: { draft: 0, published: 1, archived: 2 }
scope :search_by_category_slug, ->(category_slug) { where(categories: { slug: category_slug }) if category_slug.present? }
scope :search_by_category_locale, ->(locale) { where(categories: { locale: locale }) if locale.present? }
scope :search_by_locale, ->(locale) { where(locale: locale) if locale.present? }
scope :search_by_author, ->(author_id) { where(author_id: author_id) if author_id.present? }
scope :search_by_status, ->(status) { where(status: status) if status.present? }
scope :order_by_updated_at, -> { reorder(updated_at: :desc) }
scope :order_by_position, -> { reorder(position: :asc) }
scope :order_by_views, -> { reorder(views: :desc) }
# TODO: if text search slows down https://www.postgresql.org/docs/current/textsearch-features.html#TEXTSEARCH-UPDATE-TRIGGERS
pg_search_scope(
:text_search,
against: %i[
title
description
content
],
using: {
tsearch: {
prefix: true
}
}
)
def self.search(params)
records = left_outer_joins(
:category
).search_by_category_slug(
params[:category_slug]
).search_by_locale(params[:locale]).search_by_author(params[:author_id]).search_by_status(params[:status])
records = records.text_search(params[:query]) if params[:query].present?
records
end
def associate_root_article(associated_article_id)
article = portal.articles.find(associated_article_id) if associated_article_id.present?
return if article.nil?
root_article_id = self.class.find_root_article_id(article)
update(associated_article_id: root_article_id) if root_article_id.present?
end
# Make sure we always associate the parent's associated id to avoid the deeper associations od articles.
def self.find_root_article_id(article)
article.associated_article_id || article.id
end
def draft!
update(status: :draft)
end
def increment_view_count
# rubocop:disable Rails/SkipsModelValidations
update_column(:views, views? ? views + 1 : 1)
# rubocop:enable Rails/SkipsModelValidations
end
def self.update_positions(positions_hash)
positions_hash.each do |article_id, new_position|
# Find the article by its ID and update its position
article = Article.find(article_id)
article.update!(position: new_position)
end
end
private
def category_id_changed_action
# We need to update the position of the article in the new category
return unless persisted?
# this means the article is just created
# and the category_id is newly set
# and the position is already present
return if created_at_before_last_save.nil? && position.present? && category_id_before_last_save.nil?
update_article_position_in_category
end
def add_locale_to_article
self.locale = if category.present?
category.locale
else
portal.default_locale
end
end
def add_position_to_article
# on creation if a position is already present, ignore it
return if position.present?
update_article_position_in_category
end
def update_article_position_in_category
max_position = Article.where(category_id: category_id, account_id: account_id).maximum(:position)
new_position = max_position.present? ? max_position + 10 : 10
# update column to avoid validations if the article is already persisted
if persisted?
# rubocop:disable Rails/SkipsModelValidations
update_column(:position, new_position)
# rubocop:enable Rails/SkipsModelValidations
else
self.position = new_position
end
end
def ensure_account_id
self.account_id = portal&.account_id
end
def ensure_article_slug
self.slug ||= "#{Time.now.utc.to_i}-#{title.underscore.parameterize(separator: '-')}" if title.present?
end
end
Article.include_mod_with('Concerns::Article')