discourse/app/serializers/post_serializer.rb
Martin Brennan b8828d4a2d
FEATURE: Polymorphic bookmarks pt. 1 (CRUD) (#16308)
This commit introduces a new use_polymorphic_bookmarks site setting
that is default false and hidden, that will be used to help continuous
development of polymorphic bookmarks. This setting **should not** be
enabled anywhere in production yet, it is purely for local development.

This commit uses the setting to enable create/update/delete actions
for polymorphic bookmarks on the server and client side. The bookmark
interactions on topics/posts are all usable. Listing, searching,
sending bookmark reminders, and other edge cases will be handled
in subsequent PRs.

Comprehensive UI tests will be added in the final PR -- we already
have them for regular bookmarks, so it will just be a matter of
changing them to be for polymorphic bookmarks.
2022-03-30 12:43:11 +10:00

584 lines
12 KiB
Ruby

# frozen_string_literal: true
class PostSerializer < BasicPostSerializer
# To pass in additional information we might need
INSTANCE_VARS ||= [
:parent_post,
:add_raw,
:add_title,
:single_post_link_counts,
:draft_sequence,
:post_actions,
:all_post_actions,
:add_excerpt
]
INSTANCE_VARS.each do |v|
self.public_send(:attr_accessor, v)
end
attributes :post_number,
:post_type,
:updated_at,
:reply_count,
:reply_to_post_number,
:quote_count,
:incoming_link_count,
:reads,
:readers_count,
:score,
:yours,
:topic_id,
:topic_slug,
:topic_title,
:topic_html_title,
:category_id,
:display_username,
:primary_group_name,
:flair_name,
:flair_url,
:flair_bg_color,
:flair_color,
:version,
:can_edit,
:can_delete,
:can_permanently_delete,
:can_recover,
:can_wiki,
:link_counts,
:read,
:user_title,
:title_is_group,
:reply_to_user,
:bookmarked,
:bookmark_reminder_at,
:bookmark_id,
:bookmark_name,
:bookmark_auto_delete_preference,
:raw,
:actions_summary,
:moderator?,
:admin?,
:staff?,
:group_moderator,
:user_id,
:draft_sequence,
:hidden,
:hidden_reason_id,
:trust_level,
:deleted_at,
:deleted_by,
:user_deleted,
:edit_reason,
:can_view_edit_history,
:wiki,
:user_custom_fields,
:static_doc,
:via_email,
:is_auto_generated,
:action_code,
:action_code_who,
:action_code_path,
:notice,
:last_wiki_edit,
:locked,
:excerpt,
:reviewable_id,
:reviewable_score_count,
:reviewable_score_pending_count
def initialize(object, opts)
super(object, opts)
PostSerializer::INSTANCE_VARS.each do |name|
if opts.include? name
self.public_send("#{name}=", opts[name])
end
end
end
def topic_slug
topic&.slug
end
def include_topic_title?
@add_title
end
def include_topic_html_title?
@add_title
end
def include_category_id?
@add_title
end
def include_excerpt?
@add_excerpt
end
def topic_title
topic&.title
end
def topic_html_title
topic&.fancy_title
end
def category_id
topic&.category_id
end
def moderator?
!!(object&.user&.moderator?)
end
def admin?
!!(object&.user&.admin?)
end
def staff?
!!(object&.user&.staff?)
end
def group_moderator
!!@group_moderator
end
def include_group_moderator?
@group_moderator ||= begin
if @topic_view
@topic_view.category_group_moderator_user_ids.include?(object.user_id)
else
object&.user&.guardian&.is_category_group_moderator?(object&.topic&.category)
end
end
end
def yours
scope.user == object.user
end
def can_edit
scope.can_edit?(object)
end
def can_delete
scope.can_delete?(object)
end
def can_permanently_delete
true
end
def include_can_permanently_delete?
SiteSetting.can_permanently_delete && object.deleted_at
end
def can_recover
scope.can_recover_post?(object)
end
def can_wiki
scope.can_wiki?(object)
end
def display_username
object.user&.name
end
def primary_group_name
return nil unless object.user && object.user.primary_group_id
if @topic_view
@topic_view.primary_group_names[object.user.primary_group_id]
else
object.user.primary_group.name if object.user.primary_group
end
end
def flair_name
object.user&.flair_group&.name
end
def flair_url
object.user&.flair_group&.flair_url
end
def flair_bg_color
object.user&.flair_group&.flair_bg_color
end
def flair_color
object.user&.flair_group&.flair_color
end
def link_counts
return @single_post_link_counts if @single_post_link_counts.present?
# TODO: This could be better, just porting the old one over
@topic_view.link_counts[object.id].map do |link|
result = {}
result[:url] = link[:url]
result[:internal] = link[:internal]
result[:reflection] = link[:reflection]
result[:title] = link[:title] if link[:title].present?
result[:clicks] = link[:clicks] || 0
result
end
end
def read
@topic_view.read?(object.post_number)
end
def score
object.score || 0
end
def user_title
object&.user&.title
end
def title_is_group
object&.user&.title == object.user&.primary_group&.title
end
def include_title_is_group?
object&.user&.title.present?
end
def trust_level
object&.user&.trust_level
end
def reply_to_user
{
username: object.reply_to_user.username,
avatar_template: object.reply_to_user.avatar_template
}
end
def deleted_by
BasicUserSerializer.new(object.deleted_by, root: false).as_json
end
def include_deleted_by?
scope.is_staff? && object.deleted_by.present?
end
# Helper function to decide between #post_actions and @all_post_actions
def actions
return post_actions if post_actions.present?
return all_post_actions[object.id] if all_post_actions.present?
nil
end
# Summary of the actions taken on this post
def actions_summary
result = []
can_see_post = scope.can_see_post?(object)
PostActionType.types.except(:bookmark).each do |sym, id|
count_col = "#{sym}_count".to_sym
count = object.public_send(count_col) if object.respond_to?(count_col)
summary = { id: id, count: count }
if scope.post_can_act?(object, sym, opts: { taken_actions: actions }, can_see_post: can_see_post)
summary[:can_act] = true
end
if sym == :notify_user &&
(
(scope.current_user.present? && scope.current_user == object.user) ||
(object.user && object.user.bot?)
)
summary.delete(:can_act)
end
if actions.present? && actions.has_key?(id)
summary[:acted] = true
summary[:can_undo] = true if scope.can_delete?(actions[id])
end
# only show public data
unless scope.is_staff? || PostActionType.public_types.values.include?(id)
summary[:count] = summary[:acted] ? 1 : 0
end
summary.delete(:count) if summary[:count] == 0
# Only include it if the user can do it or it has a count
if summary[:can_act] || summary[:count]
result << summary
end
end
result
end
def include_draft_sequence?
@draft_sequence.present?
end
def include_slug_title?
@topic_slug.present?
end
def include_raw?
@add_raw.present? && (!object.hidden || scope.user&.staff? || yours)
end
def include_link_counts?
return true if @single_post_link_counts.present?
@topic_view.present? && @topic_view.link_counts.present? && @topic_view.link_counts[object.id].present?
end
def include_read?
@topic_view.present?
end
def include_reply_to_user?
!(SiteSetting.suppress_reply_when_quoting && object.reply_quoted?) && object.reply_to_user
end
def bookmarked
@bookmarked ||= post_bookmark.present?
end
def include_bookmark_reminder_at?
bookmarked
end
def include_bookmark_name?
bookmarked
end
def include_bookmark_auto_delete_preference?
bookmarked
end
def include_bookmark_id?
bookmarked
end
def post_bookmark
if SiteSetting.use_polymorphic_bookmarks
if @topic_view.present?
@post_bookmark ||= @topic_view.bookmarks.find { |bookmark| bookmark.bookmarkable == object }
else
@post_bookmark ||= Bookmark.find_by(user: scope.user, bookmarkable: object)
end
else
if @topic_view.present?
@post_bookmark ||= @topic_view.bookmarks.find { |bookmark| bookmark.post_id == object.id && !bookmark.for_topic }
else
@post_bookmark ||= object.bookmarks.find_by(user: scope.user, for_topic: false)
end
end
end
def bookmark_reminder_at
post_bookmark&.reminder_at
end
def bookmark_name
post_bookmark&.name
end
def bookmark_auto_delete_preference
post_bookmark&.auto_delete_preference
end
def bookmark_id
post_bookmark&.id
end
def include_display_username?
SiteSetting.enable_names?
end
def can_view_edit_history
scope.can_view_edit_history?(object)
end
def user_custom_fields
user_custom_fields_object[object.user_id]
end
def include_user_custom_fields?
user_custom_fields_object[object.user_id]
end
def static_doc
true
end
def include_static_doc?
object.is_first_post? && Discourse.static_doc_topic_ids.include?(object.topic_id)
end
def include_via_email?
object.via_email?
end
def is_auto_generated
object.incoming_email&.is_auto_generated
end
def include_is_auto_generated?
object.via_email? && is_auto_generated
end
def version
return 1 if object.hidden && !scope.can_view_hidden_post_revisions?
scope.is_staff? ? object.version : object.public_version
end
def action_code
return "open_topic" if object.action_code == "public_topic" && SiteSetting.login_required?
object.action_code
end
def include_action_code?
object.action_code.present?
end
def action_code_who
post_custom_fields["action_code_who"]
end
def include_action_code_who?
include_action_code? && action_code_who.present?
end
def action_code_path
post_custom_fields["action_code_path"]
end
def include_action_code_path?
include_action_code? && action_code_path.present?
end
def notice
post_custom_fields[Post::NOTICE]
end
def include_notice?
return false if notice.blank?
case notice["type"]
when Post.notices[:custom]
return true
when Post.notices[:new_user]
min_trust_level = SiteSetting.new_user_notice_tl
when Post.notices[:returning_user]
min_trust_level = SiteSetting.returning_user_notice_tl
else
return false
end
scope.user && scope.user.id != object.user_id && scope.user.has_trust_level?(min_trust_level)
end
def locked
true
end
# Only show locked posts to the users who made the post and staff
def include_locked?
object.locked? && (yours || scope.is_staff?)
end
def last_wiki_edit
object.revisions.last.updated_at
end
def include_last_wiki_edit?
object.wiki &&
object.post_number == 1 &&
object.revisions.size > 0
end
def include_hidden_reason_id?
object.hidden
end
# If we have a topic view, it has bulk values for the reviewable content we can use
def reviewable_id
if @topic_view.present?
for_post = @topic_view.reviewable_counts[object.id]
return for_post ? for_post[:reviewable_id] : 0
end
reviewable&.id
end
def include_reviewable_id?
can_review_topic?
end
def reviewable_score_count
if @topic_view.present?
for_post = @topic_view.reviewable_counts[object.id]
return for_post ? for_post[:total] : 0
end
reviewable_scores.size
end
def include_reviewable_score_count?
can_review_topic?
end
def reviewable_score_pending_count
if @topic_view.present?
for_post = @topic_view.reviewable_counts[object.id]
return for_post ? for_post[:pending] : 0
end
reviewable_scores.count { |rs| rs.pending? }
end
def include_reviewable_score_pending_count?
can_review_topic?
end
private
def can_review_topic?
return @can_review_topic unless @can_review_topic.nil?
@can_review_topic = @topic_view&.can_review_topic
@can_review_topic ||= scope.can_review_topic?(object.topic)
@can_review_topic
end
def reviewable
@reviewable ||= Reviewable.where(target: object).includes(:reviewable_scores).first
end
def reviewable_scores
reviewable&.reviewable_scores&.to_a || []
end
def user_custom_fields_object
(@topic_view&.user_custom_fields || @options[:user_custom_fields] || {})
end
def topic
@topic = object.topic
@topic ||= Topic.with_deleted.find_by(id: object.topic_id) if scope.is_staff?
@topic
end
def post_actions
@post_actions ||= (@topic_view&.all_post_actions || {})[object.id]
end
end