mirror of
https://github.com/discourse/discourse.git
synced 2026-08-09 21:45:25 +08:00
When a link to an event topic is oneboxed from another post, it will now render the event's interactive card instead of a generic quote, which would sometimes be empty if the only content of the topic was the event. This covers replies, the composer markdown preview, and the composer rich text editor. When a card can't render it falls back to the standard onebox quote. This mirrors core's `localized_oneboxes` pattern for the event data, and uses a cooked decorator to swap the quote for `<DiscoursePostEvent>`. In the rich text editor a ProseMirror NodeView renders the read-only card. Before: <img width="828" height="298" alt="image" src="https://github.com/user-attachments/assets/41df1ece-ca38-4747-8c5a-3dda0b06998a" /> After: <img width="757" height="540" alt="image" src="https://github.com/user-attachments/assets/6312242d-f7c1-43e3-b641-0d471680c107" /> A pared down read-only card is rendered in the composer for preview. <img width="600" alt="image" src="https://github.com/user-attachments/assets/c06cd020-6acb-4a45-87be-4621ff2bf308" /> <img width="800" alt="image" src="https://github.com/user-attachments/assets/8250458b-0082-4be8-980a-bfb69a5191d3" />
203 lines
6.8 KiB
Ruby
Vendored
203 lines
6.8 KiB
Ruby
Vendored
# frozen_string_literal: true
|
|
|
|
module DiscoursePostEvent
|
|
class EventFinder
|
|
def self.search(user, params = {})
|
|
guardian = Guardian.new(user)
|
|
|
|
build_base_query(guardian, user, params)
|
|
.then { |query| filter_by_post_id(query, params) }
|
|
.then { |query| filter_by_topic_id(query, params) }
|
|
.then { |query| filter_by_attending_user(query, params, guardian, user) }
|
|
.then { |query| filter_by_dates(query, params) }
|
|
.then { |query| filter_by_category(query, params) }
|
|
.then { |query| apply_ordering(query, params) }
|
|
.then { |query| apply_limit(query, params) }
|
|
end
|
|
|
|
private
|
|
|
|
def self.build_base_query(guardian, user, params)
|
|
topics = listable_topics(guardian)
|
|
pms = private_messages(user)
|
|
|
|
scope = DiscoursePostEvent::Event.visible
|
|
scope = scope.open if params[:include_closed].blank?
|
|
|
|
scope
|
|
.joins(post: :topic)
|
|
.merge(Post.secured(guardian))
|
|
.merge(topics.or(pms))
|
|
.joins(latest_event_date_join)
|
|
.select(
|
|
"discourse_post_event_events.*, latest_event_dates.starts_at, latest_event_dates.ends_at, latest_event_dates.finished_at",
|
|
)
|
|
.where(
|
|
"(discourse_post_event_events.recurrence IS NOT NULL) OR (latest_event_dates.starts_at IS NOT NULL) OR (discourse_post_event_events.original_starts_at IS NOT NULL)",
|
|
)
|
|
.group(
|
|
"discourse_post_event_events.id, latest_event_dates.starts_at, latest_event_dates.ends_at, latest_event_dates.finished_at",
|
|
)
|
|
end
|
|
|
|
def self.latest_event_date_join
|
|
<<~SQL
|
|
LEFT JOIN (
|
|
SELECT DISTINCT ON (event_id)
|
|
event_id,
|
|
starts_at,
|
|
ends_at,
|
|
finished_at
|
|
FROM discourse_calendar_post_event_dates
|
|
ORDER BY event_id, #{DiscoursePostEvent::EventDate.current_ordering_sql}
|
|
) latest_event_dates ON latest_event_dates.event_id = discourse_post_event_events.id
|
|
SQL
|
|
end
|
|
|
|
def self.filter_by_post_id(events, params)
|
|
return events if params[:post_id].blank?
|
|
events.where(id: params[:post_id])
|
|
end
|
|
|
|
def self.filter_by_topic_id(events, params)
|
|
return events if params[:topic_id].blank?
|
|
events.where(topics: { id: params[:topic_id] })
|
|
end
|
|
|
|
def self.filter_by_attending_user(events, params, guardian, user)
|
|
return events if params[:attending_user].blank?
|
|
|
|
attending_user = User.find_by(username_lower: params[:attending_user].downcase)
|
|
return events.none if !attending_user
|
|
|
|
statuses = [DiscoursePostEvent::Invitee.statuses[:going]]
|
|
if params[:include_interested].present? &&
|
|
can_include_interested?(guardian, user, attending_user)
|
|
statuses << DiscoursePostEvent::Invitee.statuses[:interested]
|
|
end
|
|
|
|
events =
|
|
events.joins(:invitees).where(
|
|
discourse_post_event_invitees: {
|
|
user_id: attending_user.id,
|
|
status: statuses,
|
|
},
|
|
)
|
|
|
|
guardian.is_admin? ? events : apply_privacy_restrictions(events, user)
|
|
end
|
|
|
|
def self.can_include_interested?(guardian, user, attending_user)
|
|
guardian.is_admin? || user&.id == attending_user.id
|
|
end
|
|
|
|
def self.apply_privacy_restrictions(events, user)
|
|
private_status = DiscoursePostEvent::Event.statuses[:private]
|
|
|
|
# If no user, can only see non-private events
|
|
return events.where.not(status: private_status) if user.nil?
|
|
|
|
# User can see private events if they are invited to them
|
|
events.where(
|
|
"discourse_post_event_events.status != ? OR (discourse_post_event_events.status = ? AND EXISTS (SELECT 1 FROM discourse_post_event_invitees WHERE post_id = discourse_post_event_events.id AND user_id = ?))",
|
|
private_status,
|
|
private_status,
|
|
user.id,
|
|
)
|
|
end
|
|
|
|
def self.filter_by_dates(events, params)
|
|
return events if params[:before].blank? && params[:after].blank?
|
|
|
|
before_date = params[:before] == "now" ? Time.current : params[:before]&.to_datetime
|
|
after_date = params[:after] == "now" ? Time.current : params[:after]&.to_datetime
|
|
include_ongoing = params[:include_ongoing].present?
|
|
|
|
recurring_scope = build_recurring_date_scope(after_date, before_date)
|
|
non_recurring_scope =
|
|
build_non_recurring_date_scope(after_date, before_date, include_ongoing:)
|
|
|
|
# Apply the combined scope using OR logic
|
|
events.merge(recurring_scope.or(non_recurring_scope))
|
|
end
|
|
|
|
private
|
|
|
|
def self.build_recurring_date_scope(after_date, before_date)
|
|
scope = DiscoursePostEvent::Event.where.not(recurrence: nil)
|
|
|
|
if after_date
|
|
# For recurring events: original start date OR recurrence_until should be >= after_date
|
|
scope =
|
|
scope.where(
|
|
"original_starts_at >= ? OR recurrence_until IS NULL OR recurrence_until >= ?",
|
|
after_date,
|
|
after_date,
|
|
)
|
|
end
|
|
|
|
if before_date
|
|
# For recurring events: original start date should be < before_date
|
|
scope = scope.where("original_starts_at < ?", before_date)
|
|
end
|
|
|
|
scope
|
|
end
|
|
|
|
def self.build_non_recurring_date_scope(after_date, before_date, include_ongoing: false)
|
|
scope = DiscoursePostEvent::Event.where(recurrence: nil)
|
|
if after_date
|
|
if include_ongoing
|
|
scope =
|
|
scope.where(
|
|
"latest_event_dates.starts_at >= ? OR (latest_event_dates.ends_at IS NOT NULL AND latest_event_dates.ends_at >= ?)",
|
|
after_date,
|
|
after_date,
|
|
)
|
|
else
|
|
scope = scope.where("latest_event_dates.starts_at >= ?", after_date)
|
|
end
|
|
end
|
|
scope = scope.where("latest_event_dates.starts_at < ?", before_date) if before_date
|
|
scope
|
|
end
|
|
|
|
def self.filter_by_category(events, params)
|
|
return events if params[:category_id].blank?
|
|
|
|
category_id = params[:category_id].to_i
|
|
category_ids =
|
|
(
|
|
if params[:include_subcategories].present?
|
|
Category.subcategory_ids(category_id)
|
|
else
|
|
[category_id]
|
|
end
|
|
)
|
|
|
|
events.where(topics: { category_id: category_ids })
|
|
end
|
|
|
|
def self.apply_ordering(events, params)
|
|
order_direction = params[:order] == "desc" ? "DESC" : "ASC"
|
|
events.order(
|
|
"latest_event_dates.starts_at #{order_direction}, discourse_post_event_events.id #{order_direction}",
|
|
)
|
|
end
|
|
|
|
def self.apply_limit(events, params)
|
|
limit = params[:limit]&.to_i || 200
|
|
events.limit(limit.clamp(1, 200))
|
|
end
|
|
|
|
def self.listable_topics(guardian)
|
|
topics = Topic.listable_topics.secured(guardian)
|
|
topics = topics.visible unless guardian.can_see_unlisted_topics?
|
|
topics
|
|
end
|
|
|
|
def self.private_messages(user)
|
|
user ? Topic.private_messages_for_user(user) : Topic.none
|
|
end
|
|
end
|
|
end
|