discourse/app/services/base_bookmarkable.rb
Martin Brennan 222c8d9b6a
FEATURE: Polymorphic bookmarks pt. 3 (reminders, imports, exports, refactors) (#16591)
A bit of a mixed bag, this addresses several edge areas of bookmarks and makes them compatible with polymorphic bookmarks (hidden behind the `use_polymorphic_bookmarks` site setting). The main ones are:

* ExportUserArchive compatibility
* SyncTopicUserBookmarked job compatibility
* Sending different notifications for the bookmark reminders based on the bookmarkable type
* Import scripts compatibility
* BookmarkReminderNotificationHandler compatibility

This PR also refactors the `register_bookmarkable` API so it accepts a class descended from a `BaseBookmarkable` class instead. This was done because we kept having to add more and more lambdas/properties inline and it was very messy, so a factory pattern is cleaner. The classes can be tested independently as well.

Some later PRs will address some other areas like the discourse narrative bot, advanced search, reports, and the .ics endpoint for bookmarks.
2022-05-09 09:37:23 +10:00

113 lines
5.1 KiB
Ruby

# frozen_string_literal: true
##
# Anything that we want to be able to bookmark must be registered as a
# bookmarkable type using Bookmark.register_bookmarkable(bookmarkable_klass),
# where the bookmarkable_klass is a class that implements this BaseBookmarkable
# interface. Some examples are TopicBookmarkable and PostBookmarkable.
#
# These methods are then called by the RegisteredBookmarkable class through a public
# interface, used in places where we need to list, send reminders for,
# or otherwise interact with bookmarks in a way that is unique to the
# bookmarkable type.
#
# See RegisteredBookmarkable for additional documentation.
class BaseBookmarkable
attr_reader :model, :serializer, :preload_associations
# @return [ActiveRecord::Base] The ActiveRecord model class which will be used to denote
# the type of the bookmarkable upon registration along with
# querying.
def self.model
raise NotImplementedError
end
# @return [ApplicationSerializer] The serializer class inheriting from UserBookmarkBaseSerializer
def self.serializer
raise NotImplementedError
end
# @return [Array] Used for preloading associations on the bookmarks for listing
# purposes. Should be in the same format used for .includes() e.g.
#
# [{ topic: [:topic_users, :tags] }, :user]
def self.preload_associations
nil
end
def self.has_preloads?
preload_associations.present?
end
##
# This is where the main query to filter the bookmarks by the provided bookmarkable
# type should occur. This should join on additional tables that are required later
# on to preload additional data for serializers, and also is the place where the
# bookmarks should be filtered based on security checks, which is why the Guardian
# instance is provided.
#
# @param [User] user The user to perform the query for, this scopes the bookmarks returned.
# @param [Guardian] guardian An instance of Guardian for the user to be used for security filters.
# @return [Bookmark::ActiveRecord_AssociationRelation] Should be an approprialely scoped list of bookmarks for the user.
def self.list_query(user, guardian)
raise NotImplementedError
end
##
# Called from BookmarkQuery when the initial results have been returned by
# perform_list_query. The search_query should join additional tables required
# to filter the bookmarks further, as well as defining a string used for
# where_sql, which can include comparisons with the :q parameter.
#
# @param [Bookmark::ActiveRecord_Relation] bookmarks The bookmark records returned by perform_list_query
# @param [String] query The search query from the user surrounded by the %% wildcards
# @param [String] ts_query The postgres TSQUERY string used for comparisons with full text search columns
# @param [Block] bookmarkable_search This block _must_ be called with the additional WHERE clause SQL relevant
# for the bookmarkable to be searched, as well as the bookmarks relation
# with any additional joins applied.
# @return [Bookmark::ActiveRecord_AssociationRelation] The list of bookmarks from perform_list_query filtered further by
# the query parameter.
def self.search_query(bookmarks, query, ts_query, &bookmarkable_search)
raise NotImplementedError
end
##
# When sending bookmark reminders, we want to make sure that whatever we
# are sending the reminder for has not been deleted or is otherwise inaccessible.
# Most of the time we can just check if the bookmarkable record is present
# because it will be trashable, though in some cases there will be additional
# conditions in the form of a lambda that we should use instead.
#
# The logic around whether it is the right time to send a reminder does not belong
# here, that is done in the BookmarkReminderNotifications job.
#
# @param [Bookmark] bookmark The bookmark that we are considering sending a reminder for.
# @return [Boolean]
def self.reminder_conditions(bookmark)
raise NotImplementedError
end
##
# Different bookmarkables may have different ways of notifying a user or presenting
# the reminder and what it is for, so it is up to the bookmarkable to register
# its preferred method of sending the reminder.
#
# @param [Bookmark] bookmark The bookmark that we are sending the reminder notification for.
# @return [void]
def self.reminder_handler(bookmark)
raise NotImplementedError
end
##
# Access control is dependent on what has been bookmarked, the appropriate guardian
# can_see_X? method should be called from the bookmarkable class to determine
# whether the bookmarkable record (e.g. Post, Topic) is accessible by the guardian user.
#
# @param [Guardian] guardian The guardian class for the user that we are performing the access check for.
# @param [Bookmark] bookmark The bookmark which we are checking access for using the bookmarkable association.
# @return [Boolean]
def self.can_see?(guardian, bookmark)
raise NotImplementedError
end
end