docbrown/app/queries/articles/active_threads_query.rb
Jeremy Friesen c75dd69d09
Favoring common method not previously available (#15855)
With this refactor, I'm adding documentation and favoring using a common
method that wasn't available at the time of implementation.

In [this commit][1] we had logic that said "if we have a singular tag
use the cache" otherwise use the join.  At that time, the implementation
of [Article.cached_tag_with][previous] was as follows, allowing only a
singular tag:

```ruby
scope :cached_tagged_with, ->(tag) { where("cached_tag_list ~* ?",
"^#{tag},| #{tag},|, #{tag}$|^#{tag}$") }
```

The [current implementation][current], as of writing this, allows for
multiple tags and is as follows:

```ruby
scope :cached_tagged_with, lambda { |tag|
  case tag
  when String, Symbol
    # In Postgres regexes, the [[:<:]] and [[:>:]] are equivalent to "start of
    # word" and "end of word", respectively. They're similar to `\b` in Perl-
    # compatible regexes (PCRE), but that matches at either end of a word.
    # They're more comparable to how vim's `\<` and `\>` work.
    where("cached_tag_list ~ ?", "[[:<:]]#{tag}[[:>:]]")
  when Array
    tag.reduce(self) { |acc, elem| acc.cached_tagged_with(elem) }
  when Tag
    cached_tagged_with(tag.name)
  else
    raise TypeError, "Cannot search tags for: #{tag.inspect}"
  end
}
```

Given that we are content to use the cached tag in the singular tag
case, it seems safe to say that we're comfortable using it in the
multiple tag case.

[1]:af5a391429 (diff-24503fd25ed68e6ebceee4951bc4f9b255b197278d4aa9d86ef9d5afe3f26bea)
[previous]:af5a391429/app/models/article.rb (L151)
[current]:98e97e7aa8/app/models/article.rb (L212-L227)
2021-12-22 11:00:54 -05:00

48 lines
2 KiB
Ruby

module Articles
class ActiveThreadsQuery
DEFAULT_OPTIONS = {
tags: ["discuss"],
time_ago: nil,
count: 10
}.with_indifferent_access.freeze
MINIMUM_SCORE = -4
# Get the "plucked" attribute information for the article thread.
#
# @param relation [ActiveRecord::Relation] the original Article scope
# @param options [Hash]
# @option options [Array<String>] :tags which tags to select
# @option options [NilClass, String, ActiveSupport::TimeWithZone] :time_ago
# @option options [Integer] :count the number of records to pluck
#
# @return [Array<Array>] The inner array is the plucked attribute
# values for the selected articles. Which means be mindful
# of the order you pass for attributes.
#
# @note The order of attributes and behavior of this method is from
# past implementations. A refactor to consider would be to
# create a data structure.
#
# @see `./app/views/articles/_widget_list_item.html.erb` for the
# importance of maintaining position of these parameters.
def self.call(relation: Article.published, **options)
options = DEFAULT_OPTIONS.merge(options)
tags, time_ago, count = options.values_at(:tags, :time_ago, :count)
relation = relation.limit(count)
relation = relation.cached_tagged_with(tags)
relation = if time_ago == "latest"
relation = relation.where(score: MINIMUM_SCORE..).presence || relation
relation.order(published_at: :desc)
elsif time_ago
relation = relation.where(published_at: time_ago.., score: MINIMUM_SCORE..).presence || relation
relation.order(comments_count: :desc)
else
relation = relation.where(published_at: 3.days.ago.., score: MINIMUM_SCORE..).presence || relation
relation.order("last_comment_at DESC NULLS LAST")
end
relation.pluck(:path, :title, :comments_count, :created_at)
end
end
end