Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FEATURE: Show all upcoming events for the next year #483

Merged
merged 2 commits into from
Nov 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 13 additions & 32 deletions app/models/discourse_post_event/event.rb
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,10 @@ def private?
status == Event.statuses[:private]
end

def recurring?
recurrence.present?
end

def most_likely_going(limit = SiteSetting.displayed_invitees_limit)
going = self.invitees.order(%i[status user_id]).limit(limit)

Expand Down Expand Up @@ -360,42 +364,19 @@ def update_with_params!(params)
self.publish_update!
end

def calculate_next_date
def calculate_next_date(start_date: nil)
localized_start = start_date || original_starts_at.in_time_zone(timezone)

if self.recurrence.blank? || original_starts_at > Time.current
return { starts_at: original_starts_at, ends_at: original_ends_at, rescheduled: false }
end

localized_start = original_starts_at.in_time_zone(timezone)

recurrence = nil

case self.recurrence
when "every_day"
recurrence = "FREQ=DAILY"
when "every_month"
start_date = localized_start.beginning_of_month.to_date
end_date = localized_start.end_of_month.to_date
weekday = localized_start.strftime("%A")

count = 0
(start_date..end_date).each do |date|
count += 1 if date.strftime("%A") == weekday
break if date.day == localized_start.day
end

recurrence = "FREQ=MONTHLY;BYDAY=#{count}#{weekday.upcase[0, 2]}"
when "every_weekday"
recurrence = "FREQ=DAILY;BYDAY=MO,TU,WE,TH,FR"
when "every_two_weeks"
recurrence = "FREQ=WEEKLY;INTERVAL=2;"
when "every_four_weeks"
recurrence = "FREQ=WEEKLY;INTERVAL=4;"
else
byday = localized_start.strftime("%A").upcase[0, 2]
recurrence = "FREQ=WEEKLY;BYDAY=#{byday}"
end

next_starts_at = RRuleGenerator.generate(recurrence, localized_start, tzid: self.timezone)
next_starts_at =
RRuleGenerator.generate(
localized_start,
tzid: self.timezone,
recurrence_type: self.recurrence,
).first

if original_ends_at
difference = original_ends_at - original_starts_at
Expand Down
11 changes: 11 additions & 0 deletions app/serializers/discourse_post_event/event_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class EventSerializer < ApplicationSerializer
attributes :recurrence
attributes :minimal
attributes :category_id
attributes :recurrence_rule

def can_act_on_discourse_post_event
scope.can_act_on_discourse_post_event?(object)
Expand Down Expand Up @@ -129,8 +130,18 @@ def should_display_invitees
(object.public? && object.invitees.count > 0) ||
(object.private? && object.raw_invitees.count > 0)
end

def category_id
object.post.topic.category_id
end

def include_recurrence_rule?
object.recurring?
end

def recurrence_rule
localized_start ||= self.starts_at.in_time_zone(self.timezone)
RRuleConfigurator.rule(object.recurrence, localized_start)
end
end
end
18 changes: 18 additions & 0 deletions app/serializers/discourse_post_event/event_summary_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ class EventSummarySerializer < ApplicationSerializer
attributes :post
attributes :name
attributes :category_id
attributes :upcoming_dates

# lightweight post object containing
# only needed info for client
Expand All @@ -27,5 +28,22 @@ def post
def category_id
object.post.topic.category_id
end

def include_upcoming_dates?
object.recurring?
end

def upcoming_dates
difference = object.original_ends_at ? object.original_ends_at - object.original_starts_at : 0

RRuleGenerator
.generate(
object.starts_at.in_time_zone(object.timezone),
tzid: object.timezone,
max_years: 1,
recurrence_type: object.recurrence,
)
.map { |date| { starts_at: date, ends_at: date + difference.seconds } }
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,22 @@ export default Component.extend({
this._renderCalendar();
},

addRecurrentEvents(events) {
events.forEach((event) => {
event.upcoming_dates?.forEach((upcomingDate) => {
events.push(
Object.assign({}, event, {
starts_at: upcomingDate.starts_at,
ends_at: upcomingDate.ends_at,
upcoming_dates: [],
})
);
});
});

return events;
},

_renderCalendar() {
const calendarNode = document.getElementById("upcoming-events-calendar");
if (!calendarNode) {
Expand All @@ -40,7 +56,11 @@ export default Component.extend({
this._loadCalendar().then(() => {
this._calendar = new window.FullCalendar.Calendar(calendarNode, {});

(this.events || []).forEach((event) => {
const originalEventAndRecurrents = this.addRecurrentEvents(
this.events.content
);

(originalEventAndRecurrents || []).forEach((event) => {
const { starts_at, ends_at, post, category_id } = event;
const categoryColor = this.site.categoriesById[category_id]?.color;
const backgroundColor = categoryColor ? `#${categoryColor}` : undefined;
Expand Down
21 changes: 15 additions & 6 deletions assets/javascripts/discourse/widgets/discourse-post-event.js
Original file line number Diff line number Diff line change
Expand Up @@ -157,12 +157,21 @@ export default createWidget("discourse-post-event", {

addToCalendar() {
const event = this.state.eventModel;
this.attrs.api.downloadCalendar(event.name || event.post.topic.title, [
{
startsAt: event.starts_at,
endsAt: event.ends_at,
},
]);
this.attrs.api.downloadCalendar(
event.name || event.post.topic.title,
[
{
startsAt: event.starts_at,
endsAt: event.ends_at,
},
],
event.recurrence_rule
);
},

upcomingEvents() {
const router = this.register.lookup("service:router")._router;
router.transitionTo("discourse-post-event-upcoming-events");
},

leaveEvent(postId) {
Expand Down
8 changes: 8 additions & 0 deletions assets/javascripts/discourse/widgets/more-dropdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,14 @@ export default createWidget("more-dropdown", {
});
}

if (attrs.eventModel.recurrence) {
content.push({
id: "upcomingEvents",
icon: "far-calendar-plus",
label: "discourse_post_event.event_ui.upcoming_events",
});
}

if (attrs.canActOnEvent) {
content.push("separator");

Expand Down
1 change: 1 addition & 0 deletions config/locales/client.en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,7 @@ en:
other: "%{count} users participated."
invite: "Notify user"
add_to_calendar: "Add to calendar"
upcoming_events: "Upcoming events"
send_pm_to_creator: "Send PM to %{username}"
leave: "Leave event"
edit_event: "Edit event"
Expand Down
51 changes: 51 additions & 0 deletions lib/discourse_post_event/rrule_configurator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# frozen_string_literal: true

class RRuleConfigurator
def self.rule(recurrence_type, localized_start)
case recurrence_type
when "every_day"
"FREQ=DAILY"
when "every_month"
start_date = localized_start.beginning_of_month.to_date
end_date = localized_start.end_of_month.to_date
weekday = localized_start.strftime("%A")

count = 0
(start_date..end_date).each do |date|
count += 1 if date.strftime("%A") == weekday
break if date.day == localized_start.day
end

"FREQ=MONTHLY;BYDAY=#{count}#{weekday.upcase[0, 2]}"
when "every_weekday"
"FREQ=DAILY;BYDAY=MO,TU,WE,TH,FR"
when "every_two_weeks"
"FREQ=WEEKLY;INTERVAL=2;"
when "every_four_weeks"
"FREQ=WEEKLY;INTERVAL=4;"
else
byday = localized_start.strftime("%A").upcase[0, 2]
"FREQ=WEEKLY;BYDAY=#{byday}"
end
end

def self.how_many_recurring_events(recurrence_type, max_years)
return 1 if !max_years
per_year =
case recurrence_type
when "every_month"
12
when "every_four_weeks"
13
when "every_two_weeks"
26
when "every_weekday"
260
when "every_week"
52
when "every_day"
365
end
per_year * max_years
end
end
8 changes: 4 additions & 4 deletions lib/discourse_post_event/rrule_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@
require "rrule"

class RRuleGenerator
def self.generate(base_rrule, starts_at, tzid: nil)
def self.generate(starts_at, tzid: nil, max_years: nil, recurrence_type: "every_week")
tzid ||= "UTC"

rrule = generate_hash(base_rrule)
rrule = generate_hash(RRuleConfigurator.rule(recurrence_type, starts_at))
rrule = set_mandatory_options(rrule, starts_at)

::RRule::Rule
.new(stringify(rrule), dtstart: starts_at, exdate: [starts_at], tzid: tzid)
.between(Time.current, Time.current + 2.months)
.first
.between(Time.current, Time.current + 14.months)
.first(RRuleConfigurator.how_many_recurring_events(recurrence_type, max_years))
end

private
Expand Down
1 change: 1 addition & 0 deletions plugin.rb
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ module ::DiscoursePostEvent
require_relative "lib/discourse_post_event/export_csv_file_extension"
require_relative "lib/discourse_post_event/post_extension"
require_relative "lib/discourse_post_event/rrule_generator"
require_relative "lib/discourse_post_event/rrule_configurator"

::ActionController::Base.prepend_view_path File.expand_path("../app/views", __FILE__)

Expand Down
31 changes: 9 additions & 22 deletions spec/lib/discourse_post_event/rrule_generator_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,58 +8,45 @@
before { freeze_time Time.utc(2020, 8, 12, 16, 32) }

describe "every week" do
let(:sample_rrule) { "FREQ=WEEKLY;BYDAY=MO" }

context "when a rule and time are given" do
let(:time) { Time.utc(2020, 8, 10, 16, 32) }

it "generates the rule" do
rrule = RRuleGenerator.generate(sample_rrule, time)
rrule = RRuleGenerator.generate(time, recurrence_type: "every_week").first
expect(rrule.to_s).to eq("2020-08-17 16:32:00 UTC")
end

context "when the given time is a valid next" do
let(:time) { Time.utc(2020, 8, 10, 16, 32) }

it "returns the next valid after given time" do
rrule = RRuleGenerator.generate(sample_rrule, time)
expect(rrule.to_s).to eq("2020-08-17 16:32:00 UTC")
end
end
end
end

context "when tzid given" do
let(:sample_rrule) { "FREQ=WEEKLY;BYDAY=MO" }

it "it correctly computes the next date using the timezone" do
tzid = "Europe/Paris"
time = Time.utc(2020, 1, 25, 15, 36)

freeze_time DateTime.parse("2020-02-25 15:36")

rrule = RRuleGenerator.generate(sample_rrule, time, tzid: tzid)
expect(rrule.to_s).to eq("2020-03-02 15:36:00 +0100")
rrule = RRuleGenerator.generate(time, tzid: tzid, recurrence_type: "every_week").first
expect(rrule.to_s).to eq("2020-02-29 15:36:00 +0100")

freeze_time DateTime.parse("2020-09-25 15:36")

rrule = RRuleGenerator.generate(sample_rrule, time, tzid: tzid)
expect(rrule.to_s).to eq("2020-09-28 15:36:00 +0200")
rrule = RRuleGenerator.generate(time, tzid: tzid).first
expect(rrule.to_s).to eq("2020-09-26 15:36:00 +0200")
end
end

describe "every day" do
let(:sample_rrule) { "FREQ=DAILY" }

context "when a rule and time are given" do
it "generates the rule" do
rrule = RRuleGenerator.generate(sample_rrule, time)
rrule = RRuleGenerator.generate(time, recurrence_type: "every_day").first
expect(rrule.to_s).to eq("2020-08-13 16:32:00 UTC")
end

context "when the given time is a valid next" do
let(:time) { Time.utc(2020, 8, 10, 16, 32) }

it "returns the next valid after given time and in the future" do
rrule = RRuleGenerator.generate(sample_rrule, time)
rrule = RRuleGenerator.generate(time, recurrence_type: "every_day").first
expect(rrule.to_s).to eq("2020-08-12 16:32:00 UTC")
end
end
Expand Down
Loading