168 lines
5.9 KiB
Ruby
168 lines
5.9 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
##
|
|
# Allows us to query Bookmark records for lists. Used mainly
|
|
# in the user/activity/bookmarks page.
|
|
|
|
class BookmarkQuery
|
|
def self.on_preload(&blk)
|
|
(@preload ||= Set.new) << blk
|
|
end
|
|
|
|
def self.preload(bookmarks, object)
|
|
preload_polymorphic_associations(bookmarks, object.guardian)
|
|
@preload.each { |preload| preload.call(bookmarks, object) } if @preload
|
|
end
|
|
|
|
# These polymorphic associations are loaded to make the UserBookmarkListSerializer's
|
|
# life easier, which conditionally chooses the bookmark serializer to use based
|
|
# on the type, and we want the associations all loaded ahead of time to make
|
|
# sure we are not doing N+1s.
|
|
def self.preload_polymorphic_associations(bookmarks, guardian)
|
|
Bookmark.registered_bookmarkables.each do |registered_bookmarkable|
|
|
registered_bookmarkable.perform_preload(bookmarks, guardian)
|
|
end
|
|
end
|
|
|
|
attr_reader :guardian, :count
|
|
|
|
def initialize(user:, guardian: nil, search_term: nil, page: nil, per_page: nil)
|
|
@user = user
|
|
@search_term = search_term
|
|
@guardian = guardian || Guardian.new(@user)
|
|
@page = page ? page.to_i : 0
|
|
@per_page = per_page ? per_page.to_i : 20
|
|
@count = 0
|
|
end
|
|
|
|
def list_all(&blk)
|
|
ts_query = @search_term.present? ? Search.ts_query(term: @search_term) : nil
|
|
search_term_wildcard = @search_term.present? ? "%#{@search_term}%" : nil
|
|
|
|
queries =
|
|
Bookmark
|
|
.registered_bookmarkables
|
|
.map do |bookmarkable|
|
|
interim_results = bookmarkable.perform_list_query(@user, @guardian)
|
|
|
|
# this could occur if there is some security reason that the user cannot
|
|
# access the bookmarkables that they have bookmarked, e.g. if they had 1 bookmark
|
|
# on a topic and that topic was moved into a private category
|
|
next if interim_results.blank?
|
|
|
|
if @search_term.present?
|
|
interim_results =
|
|
bookmarkable.perform_search_query(interim_results, search_term_wildcard, ts_query)
|
|
end
|
|
|
|
# this is purely to make the query easy to read and debug, otherwise it's
|
|
# all mashed up into a massive ball in MiniProfiler :)
|
|
"---- #{bookmarkable.model} bookmarkable ---\n\n #{interim_results.to_sql}"
|
|
end
|
|
.compact
|
|
|
|
# same for interim results being blank, the user might have been locked out
|
|
# from all their various bookmarks, in which case they will see nothing and
|
|
# no further pagination/ordering/etc is required
|
|
return [] if queries.empty?
|
|
|
|
union_sql = queries.join("\n\nUNION\n\n")
|
|
results = Bookmark.select("bookmarks.*").from("(\n\n#{union_sql}\n\n) as bookmarks")
|
|
results =
|
|
results.order(
|
|
"(CASE WHEN bookmarks.pinned THEN 0 ELSE 1 END),
|
|
bookmarks.reminder_at ASC,
|
|
bookmarks.updated_at DESC",
|
|
)
|
|
|
|
@count = results.count
|
|
|
|
results = results.offset(@page * @per_page) if @page.positive?
|
|
|
|
if updated_results = blk&.call(results)
|
|
results = updated_results
|
|
end
|
|
|
|
results = results.limit(@per_page).to_a
|
|
|
|
BookmarkQuery.preload(results, self)
|
|
results
|
|
end
|
|
|
|
def unread_notifications(limit: 20)
|
|
reminder_notifications =
|
|
Notification
|
|
.for_user_menu(@user.id, limit: [limit, 100].min)
|
|
.unread
|
|
.where(notification_type: Notification.types[:bookmark_reminder])
|
|
|
|
reminder_bookmark_ids = reminder_notifications.map { |n| n.data_hash[:bookmark_id] }.compact
|
|
|
|
# We preload associations like we do above for the list to avoid
|
|
# N1s in the can_see? guardian calls for each bookmark.
|
|
bookmarks = Bookmark.where(user: @user, id: reminder_bookmark_ids)
|
|
BookmarkQuery.preload(bookmarks, self)
|
|
|
|
# Any bookmarks that no longer exist, we need to find the associated
|
|
# records using bookmarkable details.
|
|
#
|
|
# First we want to group these by type into a hash to reduce queries:
|
|
#
|
|
# {
|
|
# "Post": {
|
|
# 1234: <Post>,
|
|
# 566: <Post>,
|
|
# },
|
|
# "Topic": {
|
|
# 123: <Topic>,
|
|
# 99: <Topic>,
|
|
# }
|
|
# }
|
|
#
|
|
# We may not need to do this most of the time. It depends mostly on
|
|
# a user's auto_delete_preference for bookmarks.
|
|
deleted_bookmark_ids = reminder_bookmark_ids - bookmarks.map(&:id)
|
|
deleted_bookmarkables =
|
|
reminder_notifications
|
|
.select do |notif|
|
|
deleted_bookmark_ids.include?(notif.data_hash[:bookmark_id]) &&
|
|
notif.data_hash[:bookmarkable_type].present?
|
|
end
|
|
.inject({}) do |hash, notif|
|
|
hash[notif.data_hash[:bookmarkable_type]] ||= {}
|
|
hash[notif.data_hash[:bookmarkable_type]][notif.data_hash[:bookmarkable_id]] = nil
|
|
hash
|
|
end
|
|
|
|
# Then, we can actually find the associated records for each type in the database.
|
|
deleted_bookmarkables.each do |type, bookmarkable|
|
|
records = Bookmark.registered_bookmarkable_from_type(type).model.where(id: bookmarkable.keys)
|
|
records.each { |record| deleted_bookmarkables[type][record.id] = record }
|
|
end
|
|
|
|
reminder_notifications.select do |notif|
|
|
bookmark = bookmarks.find { |bm| bm.id == notif.data_hash[:bookmark_id] }
|
|
|
|
# This is the happy path, it's easiest to look up using a bookmark
|
|
# that hasn't been deleted.
|
|
if bookmark.present?
|
|
bookmarkable = Bookmark.registered_bookmarkable_from_type(bookmark.bookmarkable_type)
|
|
bookmarkable.can_see?(@guardian, bookmark)
|
|
else
|
|
# Otherwise, we have to use our cached records from the deleted
|
|
# bookmarks' related bookmarkable (e.g. Post, Topic) to determine
|
|
# secure access.
|
|
bookmarkable =
|
|
deleted_bookmarkables.dig(
|
|
notif.data_hash[:bookmarkable_type],
|
|
notif.data_hash[:bookmarkable_id],
|
|
)
|
|
bookmarkable.present? &&
|
|
Bookmark.registered_bookmarkable_from_type(
|
|
notif.data_hash[:bookmarkable_type],
|
|
).can_see_bookmarkable?(@guardian, bookmarkable)
|
|
end
|
|
end
|
|
end
|
|
end
|