Add a /user_actions page. This page shows you a global timeline of (almost) all activity on the site, including uploads, comments, votes, edits, forum posts, and so on. The main things it doesn't include are post edits, pool edits, and favorites (posts and pools live in a separate database, and favorites don't have the timestamps we need for ordering). This page is useful for moderation purposes because it lets you see a history of almost all of a user's activity on a single page. Currently this page is mod-only. In the future it will be open to all users, so you can view the history of your own site activity, or the activity of others.
192 lines
4.8 KiB
Ruby
192 lines
4.8 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
class SavedSearch < ApplicationRecord
|
|
REDIS_EXPIRY = 1.hour
|
|
QUERY_LIMIT = 1000
|
|
|
|
attr_reader :disable_labels
|
|
|
|
belongs_to :user
|
|
|
|
normalize :query, :normalize_query
|
|
normalize :labels, :normalize_labels
|
|
|
|
validates :query, presence: true
|
|
validate :validate_count, on: :create
|
|
|
|
scope :labeled, ->(label) { where_array_includes_any_lower(:labels, [normalize_label(label)]) }
|
|
scope :has_tag, ->(name) { where_regex(:query, "(^| )[~-]?#{Regexp.escape(name)}( |$)", flags: "i") }
|
|
|
|
def self.visible(user)
|
|
if user.is_anonymous?
|
|
none
|
|
else
|
|
where(user: user)
|
|
end
|
|
end
|
|
|
|
concerning :Redis do
|
|
extend Memoist
|
|
|
|
class_methods do
|
|
extend Memoist
|
|
|
|
def redis
|
|
return nil if Danbooru.config.redis_url.blank?
|
|
::Redis.new(url: Danbooru.config.redis_url)
|
|
end
|
|
memoize :redis
|
|
|
|
def post_ids_for(user_id, label: nil)
|
|
return [] if redis.nil?
|
|
|
|
queries = queries_for(user_id, label: label)
|
|
post_ids = Set.new
|
|
queries.each do |query|
|
|
redis_key = "search:#{query}"
|
|
if redis.exists?(redis_key)
|
|
sub_ids = redis.smembers(redis_key).map(&:to_i)
|
|
post_ids.merge(sub_ids)
|
|
else
|
|
PopulateSavedSearchJob.perform_later(query)
|
|
end
|
|
end
|
|
post_ids.to_a.sort.last(QUERY_LIMIT)
|
|
end
|
|
end
|
|
|
|
def refreshed_at
|
|
return Time.zone.now if SavedSearch.redis.nil?
|
|
|
|
ttl = SavedSearch.redis.ttl("search:#{normalized_query}")
|
|
return nil if ttl < 0
|
|
(REDIS_EXPIRY.to_i - ttl).seconds.ago
|
|
end
|
|
memoize :refreshed_at
|
|
|
|
def cached_size
|
|
return 0 if SavedSearch.redis.nil?
|
|
|
|
SavedSearch.redis.scard("search:#{normalized_query}")
|
|
end
|
|
memoize :cached_size
|
|
end
|
|
|
|
concerning :Labels do
|
|
class_methods do
|
|
def normalize_labels(labels)
|
|
# XXX should sort and uniq, but will break some use cases.
|
|
labels.map { |label| normalize_label(label) }.reject(&:blank?)
|
|
end
|
|
|
|
def normalize_label(label)
|
|
label.to_s.unicode_normalize(:nfc).normalize_whitespace.downcase.gsub(/[[:space:]]+/, "_").squeeze("_").gsub(/\A_|_\z/, "")
|
|
end
|
|
|
|
def all_labels
|
|
select(Arel.sql("distinct unnest(labels) as label")).order(:label)
|
|
end
|
|
|
|
def labels_like(label)
|
|
all_labels.select { |ss| ss.label.ilike?(label) }.map(&:label)
|
|
end
|
|
|
|
def labels_for(user_id)
|
|
SavedSearch
|
|
.where(user_id: user_id)
|
|
.order(label: :asc)
|
|
.pluck(Arel.sql("distinct unnest(labels) as label"))
|
|
end
|
|
end
|
|
|
|
def label_string
|
|
labels.join(" ")
|
|
end
|
|
|
|
def label_string=(val)
|
|
self.labels = val.to_s.split(/[[:space:]]+/)
|
|
end
|
|
end
|
|
|
|
concerning :Search do
|
|
class_methods do
|
|
def search(params)
|
|
q = search_attributes(params, :id, :created_at, :updated_at, :query)
|
|
|
|
if params[:label]
|
|
q = q.labeled(params[:label])
|
|
end
|
|
|
|
case params[:order]
|
|
when "query"
|
|
q = q.order(:query).order(id: :desc)
|
|
when "label"
|
|
q = q.order(:labels).order(id: :desc)
|
|
else
|
|
q = q.apply_default_order(params)
|
|
end
|
|
|
|
q
|
|
end
|
|
|
|
def populate(query, timeout: 10_000)
|
|
return if redis.nil?
|
|
|
|
redis_key = "search:#{query}"
|
|
return if redis.exists?(redis_key)
|
|
|
|
post_ids = Post.with_timeout(timeout, [], query: query) do
|
|
Post.anon_tag_match(query).limit(QUERY_LIMIT).pluck(:id)
|
|
end
|
|
|
|
if post_ids.present?
|
|
redis.sadd(redis_key, post_ids)
|
|
redis.expire(redis_key, REDIS_EXPIRY.to_i)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
concerning :Queries do
|
|
class_methods do
|
|
def normalize_query(query)
|
|
PostQuery.new(query.to_s).replace_aliases.to_infix
|
|
end
|
|
|
|
def queries_for(user_id, label: nil)
|
|
searches = SavedSearch.where(user_id: user_id)
|
|
searches = searches.labeled(label) if label.present?
|
|
queries = searches.map(&:normalized_query)
|
|
queries.sort.uniq
|
|
end
|
|
|
|
def rewrite_queries!(old_name, new_name)
|
|
has_tag(old_name).find_each do |ss|
|
|
ss.lock!
|
|
ss.rewrite_query(old_name, new_name)
|
|
ss.save!
|
|
end
|
|
end
|
|
end
|
|
|
|
def normalized_query
|
|
@normalized_query ||= PostQuery.normalize(query).sort.to_s
|
|
end
|
|
|
|
def rewrite_query(old_name, new_name)
|
|
query.gsub!(/(?:\A| )([-~])?#{Regexp.escape(old_name)}(?: |\z)/i) { " #{$1}#{new_name} " }
|
|
query.strip!
|
|
end
|
|
end
|
|
|
|
def validate_count
|
|
if user.saved_searches.count >= user.max_saved_searches
|
|
errors.add(:user, "can only have up to #{user.max_saved_searches} " + "saved search".pluralize(user.max_saved_searches))
|
|
end
|
|
end
|
|
|
|
def disable_labels=(value)
|
|
user.update(disable_categorized_saved_searches: true) if value.to_s.truthy?
|
|
end
|
|
end
|