FIX: Allow .ics for polymorphic bookmarks (#16694)

We have a .ics endpoint for user bookmarks, this
commit makes it so polymorphic bookmarks work on
that endpoint, using the serializer associated with
the RegisteredBookmarkable.
This commit is contained in:
Martin Brennan
2022-05-11 09:29:24 +10:00
committed by GitHub
parent cc0e53cabd
commit 4037cdb6db
9 changed files with 153 additions and 55 deletions

View File

@ -1704,6 +1704,7 @@ class UsersController < ApplicationController
def bookmarks def bookmarks
user = fetch_user_from_params user = fetch_user_from_params
guardian.ensure_can_edit!(user) guardian.ensure_can_edit!(user)
user_guardian = Guardian.new(user)
respond_to do |format| respond_to do |format|
format.json do format.json do
@ -1720,12 +1721,22 @@ class UsersController < ApplicationController
render_serialized(bookmark_list, UserBookmarkListSerializer) render_serialized(bookmark_list, UserBookmarkListSerializer)
end end
end end
# TODO (martin) Make a separate PR for .ics reminders for polymorphic bookmarks
format.ics do format.ics do
@bookmark_reminders = Bookmark.with_reminders if SiteSetting.use_polymorphic_bookmarks
.where(user_id: user.id) @bookmark_reminders = Bookmark.with_reminders
.includes(:topic) .where(user_id: user.id)
.order(:reminder_at) .order(:reminder_at)
.map do |bookmark|
bookmark.registered_bookmarkable.serializer.new(
bookmark, scope: user_guardian, root: false
)
end
else
@bookmark_reminders = Bookmark.with_reminders
.where(user_id: user.id)
.includes(:topic)
.order(:reminder_at)
end
end end
end end
end end

View File

@ -6,6 +6,8 @@ class UserBookmarkBaseSerializer < ApplicationSerializer
:updated_at, :updated_at,
:name, :name,
:reminder_at, :reminder_at,
:reminder_at_ics_start,
:reminder_at_ics_end,
:pinned, :pinned,
:title, :title,
:fancy_title, :fancy_title,
@ -34,6 +36,22 @@ class UserBookmarkBaseSerializer < ApplicationSerializer
raise NotImplementedError raise NotImplementedError
end end
def include_reminder_at_ics_start?
reminder_at.present?
end
def include_reminder_at_ics_end?
reminder_at.present?
end
def reminder_at_ics_start
object.reminder_at_ics
end
def reminder_at_ics_end
object.reminder_at_ics(offset: 1.hour)
end
# Note: This assumes that the bookmarkable has a user attached to it, # Note: This assumes that the bookmarkable has a user attached to it,
# we may need to revisit this assumption at some point. # we may need to revisit this assumption at some point.
has_one :user, serializer: BasicUserSerializer, embed: :objects has_one :user, serializer: BasicUserSerializer, embed: :objects
@ -41,4 +59,10 @@ class UserBookmarkBaseSerializer < ApplicationSerializer
def user def user
bookmarkable_user bookmarkable_user
end end
private
def bookmarkable
object.bookmarkable
end
end end

View File

@ -6,7 +6,7 @@ class UserBookmarkListSerializer < ApplicationSerializer
def bookmarks def bookmarks
if SiteSetting.use_polymorphic_bookmarks if SiteSetting.use_polymorphic_bookmarks
object.bookmarks.map do |bm| object.bookmarks.map do |bm|
serialize_registered_type(bm) bm.registered_bookmarkable.serializer.new(bm, scope: scope, root: false)
end end
else else
object.bookmarks.map { |bm| UserBookmarkSerializer.new(bm, scope: scope, root: false) } object.bookmarks.map { |bm| UserBookmarkSerializer.new(bm, scope: scope, root: false) }
@ -16,12 +16,4 @@ class UserBookmarkListSerializer < ApplicationSerializer
def include_more_bookmarks_url? def include_more_bookmarks_url?
@include_more_bookmarks_url ||= object.bookmarks.size == object.per_page @include_more_bookmarks_url ||= object.bookmarks.size == object.per_page
end end
private
def serialize_registered_type(bookmark)
Bookmark.registered_bookmarkable_from_type(
bookmark.bookmarkable_type
).serializer.new(bookmark, scope: scope, root: false)
end
end end

View File

@ -31,6 +31,12 @@ class UserPostBookmarkSerializer < UserPostTopicBookmarkBaseSerializer
@bookmarkable_user ||= post.user @bookmarkable_user ||= post.user
end end
# NOTE: In the UI there are special topic-status and topic-link components to
# display the topic URL, this is only used for certain routes like the .ics bookmarks.
def bookmarkable_url
post.full_url
end
private private
def topic def topic

View File

@ -63,12 +63,6 @@ class UserPostTopicBookmarkBaseSerializer < UserBookmarkBaseSerializer
topic.slug topic.slug
end end
# Note: This is nil because in the UI there are special topic-status and
# topic-link components to display the topic URL, and this is not used.
def bookmarkable_url
nil
end
private private
def topic_user def topic_user

View File

@ -53,6 +53,12 @@ class UserTopicBookmarkSerializer < UserPostTopicBookmarkBaseSerializer
@bookmarkable_user ||= first_post.user @bookmarkable_user ||= first_post.user
end end
# NOTE: In the UI there are special topic-status and topic-link components to
# display the topic URL, this is only used for certain routes like the .ics bookmarks.
def bookmarkable_url
topic.url
end
private private
def topic def topic

View File

@ -1,3 +1,21 @@
<% if SiteSetting.use_polymorphic_bookmarks %>
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Discourse//<%= Discourse.current_hostname %>//<%= Discourse.full_version %>//EN
<% @bookmark_reminders.each do |bookmark| %>
BEGIN:VEVENT
UID:bookmark_reminder_#<%= bookmark.id %>@<%= Discourse.current_hostname %>
DTSTAMP:<%= bookmark.updated_at.strftime(I18n.t("datetime_formats.formats.calendar_ics")) %>
DTSTART:<%= bookmark.reminder_at_ics_start %>
DTEND:<%= bookmark.reminder_at_ics_end %>
SUMMARY:<%= bookmark.name.presence || bookmark.title %>
DESCRIPTION:<%= bookmark.bookmarkable_url %>
URL:<%= bookmark.bookmarkable_url %>
END:VEVENT
<% end %>
END:VCALENDAR
<% else %>
BEGIN:VCALENDAR BEGIN:VCALENDAR
VERSION:2.0 VERSION:2.0
PRODID:-//Discourse//<%= Discourse.current_hostname %>//<%= Discourse.full_version %>//EN PRODID:-//Discourse//<%= Discourse.current_hostname %>//<%= Discourse.full_version %>//EN
@ -13,3 +31,4 @@ URL:<%= Discourse.base_url %>/t/-/<%= bookmark.topic_id %>
END:VEVENT END:VEVENT
<% end %> <% end %>
END:VCALENDAR END:VCALENDAR
<% end %>

View File

@ -5254,7 +5254,9 @@ describe UsersController do
sign_in(user1) sign_in(user1)
get "/u/#{user1.username}/bookmarks.json" get "/u/#{user1.username}/bookmarks.json"
expect(response.status).to eq(200) expect(response.status).to eq(200)
expect(response.parsed_body['user_bookmark_list']['bookmarks'].map { |b| b['id'] }).to match_array([bookmark1.id, bookmark2.id]) expect(response.parsed_body['user_bookmark_list']['bookmarks'].map { |b| b['id'] }).to match_array(
[bookmark1.id, bookmark2.id]
)
end end
it "returns an .ics file of bookmark reminders for the user in date order" do it "returns an .ics file of bookmark reminders for the user in date order" do
@ -5317,39 +5319,6 @@ describe UsersController do
end end
context "for polymorphic bookmarks" do context "for polymorphic bookmarks" do
class UserTestBookmarkSerializer < UserBookmarkBaseSerializer
def title
fancy_title
end
def fancy_title
@fancy_title ||= user.username
end
def cooked
"<p>Something cooked</p>"
end
def bookmarkable_user
@bookmarkable_user ||= user
end
def bookmarkable_url
"#{Discourse.base_url}/u/#{user.username}"
end
def excerpt
return nil unless cooked
@excerpt ||= PrettyText.excerpt(cooked, 300, keep_emoji_images: true)
end
private
def user
object.bookmarkable
end
end
before do before do
SiteSetting.use_polymorphic_bookmarks = true SiteSetting.use_polymorphic_bookmarks = true
register_test_bookmarkable register_test_bookmarkable
@ -5370,6 +5339,8 @@ describe UsersController do
it "returns a list of serialized bookmarks for the user including custom registered bookmarkables" do it "returns a list of serialized bookmarks for the user including custom registered bookmarkables" do
sign_in(user1) sign_in(user1)
bookmark3.bookmarkable.user_profile.update!(bio_raw: "<p>Something cooked</p>")
bookmark3.bookmarkable.user_profile.rebake!
get "/u/#{user1.username}/bookmarks.json" get "/u/#{user1.username}/bookmarks.json"
expect(response.status).to eq(200) expect(response.status).to eq(200)
response_bookmarks = response.parsed_body['user_bookmark_list']['bookmarks'] response_bookmarks = response.parsed_body['user_bookmark_list']['bookmarks']
@ -5378,6 +5349,49 @@ describe UsersController do
) )
expect(response_bookmarks.find { |b| b['id'] == bookmark3.id }['excerpt']).to eq('Something cooked') expect(response_bookmarks.find { |b| b['id'] == bookmark3.id }['excerpt']).to eq('Something cooked')
end end
it "returns an .ics file of bookmark reminders for the user in date order" do
bookmark1.update!(name: nil, reminder_at: 1.day.from_now)
bookmark2.update!(name: "Some bookmark note", reminder_at: 1.week.from_now)
bookmark3.update!(name: nil, reminder_at: 2.weeks.from_now)
sign_in(user1)
get "/u/#{user1.username}/bookmarks.ics"
expect(response.status).to eq(200)
expect(response.body.chomp).to eq(<<~ICS)
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Discourse//#{Discourse.current_hostname}//#{Discourse.full_version}//EN
BEGIN:VEVENT
UID:bookmark_reminder_##{bookmark1.id}@#{Discourse.current_hostname}
DTSTAMP:#{bookmark1.updated_at.strftime(I18n.t("datetime_formats.formats.calendar_ics"))}
DTSTART:#{bookmark1.reminder_at_ics}
DTEND:#{bookmark1.reminder_at_ics(offset: 1.hour)}
SUMMARY:#{bookmark1.bookmarkable.topic.title}
DESCRIPTION:#{bookmark1.bookmarkable.full_url}
URL:#{bookmark1.bookmarkable.full_url}
END:VEVENT
BEGIN:VEVENT
UID:bookmark_reminder_##{bookmark2.id}@#{Discourse.current_hostname}
DTSTAMP:#{bookmark2.updated_at.strftime(I18n.t("datetime_formats.formats.calendar_ics"))}
DTSTART:#{bookmark2.reminder_at_ics}
DTEND:#{bookmark2.reminder_at_ics(offset: 1.hour)}
SUMMARY:Some bookmark note
DESCRIPTION:#{bookmark2.bookmarkable.url}
URL:#{bookmark2.bookmarkable.url}
END:VEVENT
BEGIN:VEVENT
UID:bookmark_reminder_##{bookmark3.id}@#{Discourse.current_hostname}
DTSTAMP:#{bookmark3.updated_at.strftime(I18n.t("datetime_formats.formats.calendar_ics"))}
DTSTART:#{bookmark3.reminder_at_ics}
DTEND:#{bookmark3.reminder_at_ics(offset: 1.hour)}
SUMMARY:#{bookmark3.bookmarkable.username}
DESCRIPTION:#{Discourse.base_url}/u/#{bookmark3.bookmarkable.username}
URL:#{Discourse.base_url}/u/#{bookmark3.bookmarkable.username}
END:VEVENT
END:VCALENDAR
ICS
end
end end
end end

View File

@ -1,6 +1,38 @@
# frozen_string_literal: true # frozen_string_literal: true
class UserTestBookmarkSerializer < UserBookmarkBaseSerializer; end class UserTestBookmarkSerializer < UserBookmarkBaseSerializer
def title
fancy_title
end
def fancy_title
@fancy_title ||= user.username
end
def cooked
user.user_profile&.bio_cooked
end
def bookmarkable_user
@bookmarkable_user ||= user
end
def bookmarkable_url
"#{Discourse.base_url}/u/#{user.username}"
end
def excerpt
return nil unless cooked
@excerpt ||= PrettyText.excerpt(cooked, 300, keep_emoji_images: true)
end
private
def user
object.bookmarkable
end
end
class UserTestBookmarkable < BaseBookmarkable class UserTestBookmarkable < BaseBookmarkable
def self.model def self.model
User User