FEATURE: intoduces recurrence support

For now: every_week/every_day/every_month/every_weekday (monday to friday)
This commit is contained in:
jjaffeux 2020-08-13 10:19:13 +02:00
parent 7b45032561
commit 019847948c
15 changed files with 685 additions and 305 deletions

View File

@ -143,6 +143,7 @@ module DiscoursePostEvent
:ends_at,
:status,
:url,
:recurrence,
custom_fields: allowed_custom_fields,
raw_invitees: [],
)

View File

@ -7,68 +7,45 @@ module DiscoursePostEvent
self.table_name = 'discourse_post_event_events'
def self.attributes_protected_by_default
super - ['id']
super - %w[id]
end
after_commit :destroy_topic_custom_field, on: [:destroy]
after_commit :destroy_topic_custom_field, on: %i[destroy]
def destroy_topic_custom_field
if self.post && self.post.is_first_post?
TopicCustomField
.where(
topic_id: self.post.topic_id,
name: TOPIC_POST_EVENT_STARTS_AT,
)
.delete_all
TopicCustomField.where(
topic_id: self.post.topic_id, name: TOPIC_POST_EVENT_STARTS_AT
).delete_all
end
end
after_commit :upsert_topic_custom_field, on: [:create, :update]
after_commit :upsert_topic_custom_field, on: %i[create update]
def upsert_topic_custom_field
if self.post && self.post.is_first_post?
TopicCustomField
.upsert({
TopicCustomField.upsert(
{
topic_id: self.post.topic_id,
name: TOPIC_POST_EVENT_STARTS_AT,
value: self.starts_at,
created_at: Time.now,
updated_at: Time.now,
}, unique_by: [:name, :topic_id])
updated_at: Time.now
},
unique_by: %i[name topic_id]
)
end
end
after_commit :setup_handlers, on: [:create, :update]
after_commit :setup_handlers, on: %i[create update]
def setup_handlers
starts_at_changes = saved_change_to_starts_at
if starts_at_changes
new_starts_at = starts_at_changes[1]
Jobs.cancel_scheduled_job(:discourse_post_event_event_started, event_id: self.id)
Jobs.cancel_scheduled_job(:discourse_post_event_event_will_start, event_id: self.id)
if new_starts_at > Time.now
Jobs.enqueue_at(new_starts_at, :discourse_post_event_event_started, event_id: self.id)
will_start_at = new_starts_at - 1.hour
if will_start_at > Time.now
Jobs.enqueue_at(will_start_at, :discourse_post_event_event_will_start, event_id: self.id)
end
end
end
self.refresh_starts_at_handlers!(starts_at_changes) if starts_at_changes
if saved_change_to_starts_at || saved_change_to_reminders
self.refresh_reminders!
end
ends_at_changes = saved_change_to_ends_at
if ends_at_changes
new_ends_at = ends_at_changes[1]
Jobs.cancel_scheduled_job(:discourse_post_event_event_ended, event_id: self.id)
if new_ends_at && new_ends_at > Time.now
Jobs.enqueue_at(new_ends_at, :discourse_post_event_event_ended, event_id: self.id)
end
end
self.refresh_ends_at_handlers!(ends_at_changes) if ends_at_changes
end
has_many :invitees, foreign_key: :post_id, dependent: :delete_all
@ -76,7 +53,8 @@ module DiscoursePostEvent
scope :visible, -> { where(deleted_at: nil) }
scope :expired, -> { where('ends_at IS NOT NULL AND ends_at < ?', Time.now) }
scope :expired,
-> { where('ends_at IS NOT NULL AND ends_at < ?', Time.now) }
scope :not_expired, -> { where('ends_at IS NULL OR ends_at > ?', Time.now) }
def is_expired?
@ -86,15 +64,14 @@ module DiscoursePostEvent
validates :starts_at, presence: true
def on_going_event_invitees
if !self.ends_at && self.starts_at < Time.now
return []
end
return [] if !self.ends_at && self.starts_at < Time.now
if self.ends_at
extended_ends_at = self.ends_at + SiteSetting.discourse_post_event_edit_notifications_time_extension.minutes
if !(self.starts_at..extended_ends_at).cover?(Time.now)
return []
end
extended_ends_at =
self.ends_at +
SiteSetting.discourse_post_event_edit_notifications_time_extension
.minutes
return [] if !(self.starts_at..extended_ends_at).cover?(Time.now)
end
invitees.where(status: DiscoursePostEvent::Invitee.statuses[:going])
@ -103,30 +80,48 @@ module DiscoursePostEvent
MIN_NAME_LENGTH = 5
MAX_NAME_LENGTH = 30
validates :name,
length: { in: MIN_NAME_LENGTH..MAX_NAME_LENGTH },
unless: -> (event) { event.name.blank? }
length: { in: MIN_NAME_LENGTH..MAX_NAME_LENGTH },
unless: ->(event) { event.name.blank? }
validate :raw_invitees_length
def raw_invitees_length
if self.raw_invitees && self.raw_invitees.length > 10
errors.add(:base, I18n.t("discourse_post_event.errors.models.event.raw_invitees_length
", count: 10))
errors.add(
:base,
I18n.t(
'discourse_post_event.errors.models.event.raw_invitees_length
',
count: 10
)
)
end
end
validate :ends_before_start
def ends_before_start
if self.starts_at && self.ends_at && self.starts_at >= self.ends_at
errors.add(:base, I18n.t("discourse_post_event.errors.models.event.ends_at_before_starts_at"))
errors.add(
:base,
I18n.t(
'discourse_post_event.errors.models.event.ends_at_before_starts_at'
)
)
end
end
validate :allowed_custom_fields
def allowed_custom_fields
allowed_custom_fields = SiteSetting.discourse_post_event_allowed_custom_fields.split('|')
allowed_custom_fields =
SiteSetting.discourse_post_event_allowed_custom_fields.split('|')
self.custom_fields.each do |key, value|
if !allowed_custom_fields.include?(key)
errors.add(:base, I18n.t("discourse_post_event.errors.models.event.custom_field_is_invalid", field: key))
errors.add(
:base,
I18n.t(
'discourse_post_event.errors.models.event.custom_field_is_invalid',
field: key
)
)
end
end
end
@ -135,9 +130,7 @@ module DiscoursePostEvent
timestamp = Time.now
attrs.map! do |attr|
{
post_id: self.id,
created_at: timestamp,
updated_at: timestamp
post_id: self.id, created_at: timestamp, updated_at: timestamp
}.merge(attr)
end
@ -146,15 +139,22 @@ module DiscoursePostEvent
def notify_invitees!(predefined_attendance: false)
self.invitees.where(notified: false).each do |invitee|
create_notification!(invitee.user, self.post, predefined_attendance: predefined_attendance)
create_notification!(
invitee.user,
self.post,
predefined_attendance: predefined_attendance
)
invitee.update!(notified: true)
end
end
def create_notification!(user, post, predefined_attendance: false)
message = predefined_attendance ?
'discourse_post_event.notifications.invite_user_predefined_attendance_notification' :
'discourse_post_event.notifications.invite_user_notification'
message =
if predefined_attendance
'discourse_post_event.notifications.invite_user_predefined_attendance_notification'
else
'discourse_post_event.notifications.invite_user_notification'
end
user.notifications.create!(
notification_type: Notification.types[:custom],
@ -185,26 +185,28 @@ module DiscoursePostEvent
end
def most_likely_going(limit = SiteSetting.displayed_invitees_limit)
going = self.invitees
.order([:status, :user_id])
.limit(limit)
going = self.invitees.order(%i[status user_id]).limit(limit)
if self.private? && going.count < limit
# invitees are only created when an attendance is set
# so we create a dummy invitee object with only what's needed for serializer
going = going + GroupUser
.includes(:group, :user)
.where('groups.name' => self.raw_invitees)
.where.not('users.id' => going.pluck(:user_id))
.limit(limit - going.count)
.map { |gu| Invitee.new(user: gu.user, post_id: self.id) }
going =
going +
GroupUser.includes(:group, :user).where(
'groups.name' => self.raw_invitees
).where.not('users.id' => going.pluck(:user_id)).limit(
limit - going.count
).map { |gu| Invitee.new(user: gu.user, post_id: self.id) }
end
going
end
def publish_update!
self.post.publish_message!("/discourse-post-event/#{self.post.topic_id}", id: self.id)
self.post.publish_message!(
"/discourse-post-event/#{self.post.topic_id}",
id: self.id
)
end
def fetch_users
@ -218,16 +220,16 @@ module DiscoursePostEvent
def can_user_update_attendance(user)
!self.is_expired? &&
(
self.public? ||
(
self.private? &&
(
self.invitees.exists?(user_id: user.id) ||
(user.groups.pluck(:name) & self.raw_invitees).any?
)
self.public? ||
(
self.private? &&
(
self.invitees.exists?(user_id: user.id) ||
(user.groups.pluck(:name) & self.raw_invitees).any?
)
)
)
)
end
def self.update_from_raw(post)
@ -241,9 +243,20 @@ module DiscoursePostEvent
starts_at: event_params[:start] || event.starts_at,
ends_at: event_params[:end],
url: event_params[:url],
status: event_params[:status].present? ? Event.statuses[event_params[:status].to_sym] : event.status,
recurrence: event_params[:recurrence],
status:
if event_params[:status].present?
Event.statuses[event_params[:status].to_sym]
else
event.status
end,
reminders: event_params[:reminders],
raw_invitees: event_params[:"allowed-groups"] ? event_params[:"allowed-groups"].split(',') : nil
raw_invitees:
if event_params[:"allowed-groups"]
event_params[:"allowed-groups"].split(',')
else
nil
end
}
event.update_with_params!(params)
@ -253,11 +266,16 @@ module DiscoursePostEvent
end
def update_with_params!(params)
params[:custom_fields] = (params[:custom_fields] || {}).reject { |_, value| value.blank? }
params[:custom_fields] =
(params[:custom_fields] || {}).reject { |_, value| value.blank? }
case params[:status] ? params[:status].to_i : self.status
when Event.statuses[:private]
raw_invitees = Set.new(Array(self.raw_invitees) + Array(params[:raw_invitees]) - [PUBLIC_GROUP]).to_a
raw_invitees =
Set.new(
Array(self.raw_invitees) + Array(params[:raw_invitees]) -
[PUBLIC_GROUP]
).to_a
self.update!(params.merge(raw_invitees: raw_invitees))
self.enforce_private_invitees!
when Event.statuses[:public]
@ -270,17 +288,74 @@ module DiscoursePostEvent
self.publish_update!
end
def refresh_ends_at_handlers!(ends_at_changes)
new_ends_at = ends_at_changes[1]
Jobs.cancel_scheduled_job(
:discourse_post_event_event_ended,
event_id: self.id
)
if new_ends_at
if new_ends_at > Time.now
Jobs.enqueue_at(
new_ends_at,
:discourse_post_event_event_ended,
event_id: self.id
)
else
DiscourseEvent.trigger(:discourse_post_event_event_ended, self)
end
end
end
def refresh_starts_at_handlers!(starts_at_changes)
new_starts_at = starts_at_changes[1]
Jobs.cancel_scheduled_job(
:discourse_post_event_event_started,
event_id: self.id
)
Jobs.cancel_scheduled_job(
:discourse_post_event_event_will_start,
event_id: self.id
)
if new_starts_at > Time.now
Jobs.enqueue_at(
new_starts_at,
:discourse_post_event_event_started,
event_id: self.id
)
will_start_at = new_starts_at - 1.hour
if will_start_at > Time.now
Jobs.enqueue_at(
will_start_at,
:discourse_post_event_event_will_start,
event_id: self.id
)
end
end
end
def refresh_reminders!
(self.reminders || '').split(',').map do |reminder|
value, unit = reminder.split('.')
if transaction_include_any_action?([:update])
Jobs.cancel_scheduled_job(:discourse_post_event_send_reminder, event_id: self.id, reminder: reminder)
if transaction_include_any_action?(%i[update])
Jobs.cancel_scheduled_job(
:discourse_post_event_send_reminder,
event_id: self.id, reminder: reminder
)
end
enqueue_at = self.starts_at - value.to_i.send(unit)
if enqueue_at > Time.now
Jobs.enqueue_at(enqueue_at, :discourse_post_event_send_reminder, event_id: self.id, reminder: reminder)
Jobs.enqueue_at(
enqueue_at,
:discourse_post_event_send_reminder,
event_id: self.id, reminder: reminder
)
end
end
end

View File

@ -23,6 +23,7 @@ module DiscoursePostEvent
attributes :is_private
attributes :is_standalone
attributes :reminders
attributes :recurrence
def can_act_on_discourse_post_event
scope.can_act_on_discourse_post_event?(object)

View File

@ -20,6 +20,12 @@ export default Controller.extend(ModalFunctionality, {
this.set("reminderUnits", ["minutes", "hours", "days", "weeks"]);
this.set("reminderPeriods", ["before", "after"]);
this.set("availableRecurrences", [
"every_day",
"every_month",
"every_weekday",
"every_week"
]);
},
modalTitle: computed("model.eventModel.isNew", {

View File

@ -114,6 +114,18 @@
}}
{{/event-field}}
{{#event-field class="recurrence" label="discourse_post_event.builder_modal.recurrence.label"}}
{{combo-box
class="available-recurrences"
value=(readonly model.eventModel.recurrence)
nameProperty=null
valueProperty=null
content=availableRecurrences
onChange=(action (mut model.eventModel.recurrence))
options=(hash none="discourse_post_event.builder_modal.recurrence.none")
}}
{{/event-field}}
{{#if model.eventModel.custom_fields}}
{{#if allowedCustomFields.length}}
{{#event-field label="discourse_post_event.builder_modal.custom_fields.label"}}
@ -134,7 +146,6 @@
{{/conditional-loading-section}}
{{/d-modal-body}}
<div class="modal-footer">
{{#if model.eventModel.isNew}}
{{d-button

View File

@ -35,6 +35,10 @@ const rule = {
token.attrs.push(["data-reminders", info.attrs.reminders]);
}
if (info.attrs.recurrence) {
token.attrs.push(["data-recurrence", info.attrs.recurrence]);
}
return true;
}
};

View File

@ -23,6 +23,10 @@ export function buildParams(startsAt, endsAt, eventModel) {
params.url = eventModel.url;
}
if (eventModel.recurrence) {
params.recurrence = eventModel.recurrence;
}
if (endsAt) {
params.end = moment(endsAt)
.utc()

View File

@ -344,6 +344,9 @@ en:
add_reminder: Add reminder
reminders:
label: Reminders
recurrence:
label: Recurrence
none: No recurrence
url:
label: URL
placeholder: Optional

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
class AddRecurrenceToEvents < ActiveRecord::Migration[6.0]
def up
add_column :discourse_post_event_events, :recurrence, :string
end
def down
remove_column :discourse_post_event_events, :recurrence
end
end

View File

@ -7,7 +7,8 @@ VALID_OPTIONS = [
:"allowed-groups",
:url,
:name,
:reminders
:reminders,
:recurrence
]
module DiscoursePostEvent
@ -24,7 +25,7 @@ module DiscoursePostEvent
if valid_options.include?(name) && value
event ||= {}
event[name["data-".length..-1].to_sym] = CGI.escapeHTML(value)
event[name['data-'.length..-1].to_sym] = CGI.escapeHTML(value)
end
end
event

View File

@ -0,0 +1,35 @@
# frozen_string_literal: true
require 'rrule'
class RRuleGenerator
def self.generate(base_rrule, starts_at)
rrule = generate_hash(base_rrule)
rrule = set_mandatory_options(rrule, starts_at)
::RRule::Rule.new(stringify(rrule), dtstart: starts_at, exdate: [starts_at])
.first
end
private
def self.stringify(rrule)
rrule.map { |k, v| "#{k}=#{v}" }.join(';')
end
def self.generate_hash(rrule)
rrule.split(';').each_with_object({}) do |rr, h|
key, value = rr.split('=')
h[key] = value
end
end
def self.set_mandatory_options(rrule, time)
rrule['COUNT'] = 2 # we compute 2 in case the first is equal to exdate
rrule['BYHOUR'] = time.strftime('%H')
rrule['BYMINUTE'] = time.strftime('%M')
rrule['INTERVAL'] = 1
rrule['WKST'] = 'MO' # considers Monday as the first day of the week
rrule
end
end

390
plugin.rb
View File

@ -6,50 +6,51 @@
# author: Daniel Waterworth, Joffrey Jaffeux
# url: https://github.com/discourse/discourse-calendar
gem "holidays", "8.2.0", require: false
gem 'holidays', '8.2.0', require: false
gem 'rrule', '0.4.2', require: false
load File.expand_path("../lib/calendar_settings_validator.rb", __FILE__)
load File.expand_path('../lib/calendar_settings_validator.rb', __FILE__)
enabled_site_setting :calendar_enabled
register_asset "stylesheets/vendor/fullcalendar.min.css"
register_asset "stylesheets/common/discourse-calendar.scss"
register_asset "stylesheets/common/upcoming-events-calendar.scss"
register_asset "stylesheets/common/discourse-post-event.scss"
register_asset "stylesheets/common/discourse-post-event-preview.scss"
register_asset "stylesheets/common/discourse-post-event-builder.scss"
register_asset "stylesheets/common/discourse-post-event-invitees.scss"
register_asset "stylesheets/common/discourse-post-event-upcoming-events.scss"
register_asset "stylesheets/common/discourse-post-event-core-ext.scss"
register_asset "stylesheets/common/discourse-post-event-bulk-invite-modal.scss"
register_asset "stylesheets/mobile/discourse-calendar.scss", :mobile
register_asset "stylesheets/mobile/discourse-post-event.scss", :mobile
register_asset "stylesheets/desktop/discourse-calendar.scss", :desktop
register_asset "stylesheets/colors.scss", :color_definitions
register_svg_icon "fas fa-calendar-day"
register_svg_icon "fas fa-clock"
register_svg_icon "fas fa-file-csv"
register_svg_icon "fas fa-star"
register_svg_icon "fas fa-file-upload"
register_asset 'stylesheets/vendor/fullcalendar.min.css'
register_asset 'stylesheets/common/discourse-calendar.scss'
register_asset 'stylesheets/common/upcoming-events-calendar.scss'
register_asset 'stylesheets/common/discourse-post-event.scss'
register_asset 'stylesheets/common/discourse-post-event-preview.scss'
register_asset 'stylesheets/common/discourse-post-event-builder.scss'
register_asset 'stylesheets/common/discourse-post-event-invitees.scss'
register_asset 'stylesheets/common/discourse-post-event-upcoming-events.scss'
register_asset 'stylesheets/common/discourse-post-event-core-ext.scss'
register_asset 'stylesheets/common/discourse-post-event-bulk-invite-modal.scss'
register_asset 'stylesheets/mobile/discourse-calendar.scss', :mobile
register_asset 'stylesheets/mobile/discourse-post-event.scss', :mobile
register_asset 'stylesheets/desktop/discourse-calendar.scss', :desktop
register_asset 'stylesheets/colors.scss', :color_definitions
register_svg_icon 'fas fa-calendar-day'
register_svg_icon 'fas fa-clock'
register_svg_icon 'fas fa-file-csv'
register_svg_icon 'fas fa-star'
register_svg_icon 'fas fa-file-upload'
after_initialize do
module ::DiscourseCalendar
PLUGIN_NAME ||= "discourse-calendar"
PLUGIN_NAME ||= 'discourse-calendar'
# Type of calendar ('static' or 'dynamic')
CALENDAR_CUSTOM_FIELD ||= "calendar"
CALENDAR_CUSTOM_FIELD ||= 'calendar'
# User custom field set when user is on holiday
HOLIDAY_CUSTOM_FIELD ||= "on_holiday"
HOLIDAY_CUSTOM_FIELD ||= 'on_holiday'
# List of all users on holiday
USERS_ON_HOLIDAY_KEY ||= "users_on_holiday"
USERS_ON_HOLIDAY_KEY ||= 'users_on_holiday'
# User region used in finding holidays
REGION_CUSTOM_FIELD ||= "holidays-region"
REGION_CUSTOM_FIELD ||= 'holidays-region'
# List of groups
GROUP_TIMEZONES_CUSTOM_FIELD ||= "group-timezones"
GROUP_TIMEZONES_CUSTOM_FIELD ||= 'group-timezones'
def self.users_on_holiday
PluginStore.get(PLUGIN_NAME, USERS_ON_HOLIDAY_KEY)
@ -66,10 +67,10 @@ after_initialize do
end
module ::DiscoursePostEvent
PLUGIN_NAME ||= "discourse-post-event"
PLUGIN_NAME ||= 'discourse-post-event'
# Topic where op has a post event custom field
TOPIC_POST_EVENT_STARTS_AT ||= "TopicEventStartsAt"
TOPIC_POST_EVENT_STARTS_AT ||= 'TopicEventStartsAt'
class Engine < ::Rails::Engine
engine_name PLUGIN_NAME
@ -79,44 +80,53 @@ after_initialize do
# DISCOURSE POST EVENT
[
"../app/controllers/discourse_post_event_controller.rb",
"../app/controllers/discourse_post_event/invitees_controller.rb",
"../app/controllers/discourse_post_event/events_controller.rb",
"../app/controllers/discourse_post_event/upcoming_events_controller.rb",
"../app/models/discourse_post_event/event.rb",
"../app/models/discourse_post_event/invitee.rb",
"../lib/discourse_post_event/event_parser.rb",
"../lib/discourse_post_event/event_validator.rb",
"../jobs/regular/discourse_post_event/bulk_invite.rb",
"../jobs/regular/discourse_post_event/event_will_start.rb",
"../jobs/regular/discourse_post_event/event_started.rb",
"../jobs/regular/discourse_post_event/event_ended.rb",
"../jobs/regular/discourse_post_event/send_reminder.rb",
"../lib/discourse_post_event/event_finder.rb",
"../app/serializers/discourse_post_event/invitee_serializer.rb",
"../app/serializers/discourse_post_event/event_serializer.rb"
%w[
../app/controllers/discourse_post_event_controller.rb
../app/controllers/discourse_post_event/invitees_controller.rb
../app/controllers/discourse_post_event/events_controller.rb
../app/controllers/discourse_post_event/upcoming_events_controller.rb
../app/models/discourse_post_event/event.rb
../app/models/discourse_post_event/invitee.rb
../lib/discourse_post_event/event_parser.rb
../lib/discourse_post_event/event_validator.rb
../lib/discourse_post_event/rrule_generator.rb
../jobs/regular/discourse_post_event/bulk_invite.rb
../jobs/regular/discourse_post_event/event_will_start.rb
../jobs/regular/discourse_post_event/event_started.rb
../jobs/regular/discourse_post_event/event_ended.rb
../jobs/regular/discourse_post_event/send_reminder.rb
../lib/discourse_post_event/event_finder.rb
../app/serializers/discourse_post_event/invitee_serializer.rb
../app/serializers/discourse_post_event/event_serializer.rb
].each { |path| load File.expand_path(path, __FILE__) }
::ActionController::Base.prepend_view_path File.expand_path("../app/views", __FILE__)
::ActionController::Base.prepend_view_path File.expand_path(
'../app/views',
__FILE__
)
Discourse::Application.routes.append do
mount ::DiscoursePostEvent::Engine, at: '/'
end
DiscoursePostEvent::Engine.routes.draw do
get '/discourse-post-event/events' => 'events#index', constraints: { format: /(json|ics)/ }
get '/discourse-post-event/events' => 'events#index',
constraints: { format: /(json|ics)/ }
get '/discourse-post-event/events/:id' => 'events#show'
delete '/discourse-post-event/events/:id' => 'events#destroy'
post '/discourse-post-event/events' => 'events#create'
put '/discourse-post-event/events/:id' => 'events#update', format: :json
post '/discourse-post-event/events/:id/csv-bulk-invite' => 'events#csv_bulk_invite'
post '/discourse-post-event/events/:id/bulk-invite' => 'events#bulk_invite', format: :json
post '/discourse-post-event/events/:id/csv-bulk-invite' =>
'events#csv_bulk_invite'
post '/discourse-post-event/events/:id/bulk-invite' => 'events#bulk_invite',
format: :json
post '/discourse-post-event/events/:id/invite' => 'events#invite'
put '/discourse-post-event/events/:post_id/invitees/:id' => 'invitees#update'
put '/discourse-post-event/events/:post_id/invitees/:id' =>
'invitees#update'
post '/discourse-post-event/events/:post_id/invitees' => 'invitees#create'
get '/discourse-post-event/events/:post_id/invitees' => 'invitees#index'
delete '/discourse-post-event/events/:post_id/invitees/:id' => 'invitees#destroy'
delete '/discourse-post-event/events/:post_id/invitees/:id' =>
'invitees#destroy'
get '/upcoming-events' => 'upcoming_events#index'
end
@ -125,9 +135,9 @@ after_initialize do
class ::Post
has_one :event,
dependent: :destroy,
class_name: 'DiscoursePostEvent::Event',
foreign_key: :id
dependent: :destroy,
class_name: 'DiscoursePostEvent::Event',
foreign_key: :id
validate :valid_event
def valid_event
@ -139,48 +149,65 @@ after_initialize do
end
add_to_class(:user, :can_create_discourse_post_event?) do
return @can_create_discourse_post_event if defined?(@can_create_discourse_post_event)
@can_create_discourse_post_event = begin
return true if staff?
allowed_groups = SiteSetting.discourse_post_event_allowed_on_groups.split('|').compact
allowed_groups.present? && groups.where(id: allowed_groups).exists?
rescue
false
if defined?(@can_create_discourse_post_event)
return @can_create_discourse_post_event
end
@can_create_discourse_post_event =
begin
return true if staff?
allowed_groups =
SiteSetting.discourse_post_event_allowed_on_groups.split('|').compact
allowed_groups.present? && groups.where(id: allowed_groups).exists?
rescue StandardError
false
end
end
add_to_class(:guardian, :can_act_on_invitee?) do |invitee|
user && (user.staff? || user.id == invitee.user_id)
end
add_to_class(:guardian, :can_create_discourse_post_event?) { user && user.can_create_discourse_post_event? }
add_to_class(:guardian, :can_create_discourse_post_event?) do
user && user.can_create_discourse_post_event?
end
add_to_serializer(:current_user, :can_create_discourse_post_event) do
object.can_create_discourse_post_event?
end
add_to_class(:user, :can_act_on_discourse_post_event?) do |event|
return @can_act_on_discourse_post_event if defined?(@can_act_on_discourse_post_event)
@can_act_on_discourse_post_event = begin
return true if admin?
can_create_discourse_post_event? || event.post.user_id == id
rescue
false
if defined?(@can_act_on_discourse_post_event)
return @can_act_on_discourse_post_event
end
@can_act_on_discourse_post_event =
begin
return true if admin?
can_create_discourse_post_event? || event.post.user_id == id
rescue StandardError
false
end
end
add_to_class(:guardian, :can_act_on_discourse_post_event?) { |event| user && user.can_act_on_discourse_post_event?(event) }
add_to_class(:guardian, :can_act_on_discourse_post_event?) do |event|
user && user.can_act_on_discourse_post_event?(event)
end
add_class_method(:group, :discourse_post_event_allowed_groups) do
where(id: SiteSetting.discourse_post_event_allowed_on_groups.split('|').compact)
where(
id: SiteSetting.discourse_post_event_allowed_on_groups.split('|').compact
)
end
add_to_serializer(:post, :event) do
DiscoursePostEvent::EventSerializer.new(object.event, scope: scope, root: false)
DiscoursePostEvent::EventSerializer.new(
object.event,
scope: scope, root: false
)
end
add_to_serializer(:post, :include_event?) do
SiteSetting.discourse_post_event_enabled && !object.nil? && !object.deleted_at.present?
SiteSetting.discourse_post_event_enabled && !object.nil? &&
!object.deleted_at.present?
end
on(:post_process_cooked) do |doc, post|
@ -199,7 +226,8 @@ after_initialize do
end
end
TopicList.preloaded_custom_fields << DiscoursePostEvent::TOPIC_POST_EVENT_STARTS_AT
TopicList.preloaded_custom_fields <<
DiscoursePostEvent::TOPIC_POST_EVENT_STARTS_AT
add_to_serializer(:topic_view, :event_starts_at, false) do
object.topic.custom_fields[DiscoursePostEvent::TOPIC_POST_EVENT_STARTS_AT]
@ -207,16 +235,15 @@ after_initialize do
add_to_serializer(:topic_view, 'include_event_starts_at?') do
SiteSetting.discourse_post_event_enabled &&
SiteSetting.display_post_event_date_on_topic_title &&
object
.topic
.custom_fields
.keys
.include?(DiscoursePostEvent::TOPIC_POST_EVENT_STARTS_AT)
SiteSetting.display_post_event_date_on_topic_title &&
object.topic.custom_fields.keys.include?(
DiscoursePostEvent::TOPIC_POST_EVENT_STARTS_AT
)
end
add_to_class(:topic, :event_starts_at) do
@event_starts_at ||= custom_fields[DiscoursePostEvent::TOPIC_POST_EVENT_STARTS_AT]
@event_starts_at ||=
custom_fields[DiscoursePostEvent::TOPIC_POST_EVENT_STARTS_AT]
end
add_to_serializer(:topic_list_item, :event_starts_at, false) do
@ -225,30 +252,40 @@ after_initialize do
add_to_serializer(:topic_list_item, 'include_event_starts_at?') do
SiteSetting.discourse_post_event_enabled &&
SiteSetting.display_post_event_date_on_topic_title &&
object.event_starts_at
SiteSetting.display_post_event_date_on_topic_title &&
object.event_starts_at
end
# DISCOURSE CALENDAR
[
"../app/models/calendar_event.rb",
"../app/serializers/user_timezone_serializer.rb",
"../jobs/scheduled/create_holiday_events.rb",
"../jobs/scheduled/destroy_past_events.rb",
"../jobs/scheduled/update_holiday_usernames.rb",
"../lib/calendar_validator.rb",
"../lib/calendar.rb",
"../lib/event_validator.rb",
"../lib/group_timezones.rb",
"../lib/time_sniffer.rb",
%w[
../app/models/calendar_event.rb
../app/serializers/user_timezone_serializer.rb
../jobs/scheduled/create_holiday_events.rb
../jobs/scheduled/destroy_past_events.rb
../jobs/scheduled/update_holiday_usernames.rb
../lib/calendar_validator.rb
../lib/calendar.rb
../lib/event_validator.rb
../lib/group_timezones.rb
../lib/time_sniffer.rb
].each { |path| load File.expand_path(path, __FILE__) }
register_post_custom_field_type(DiscourseCalendar::CALENDAR_CUSTOM_FIELD, :string)
register_post_custom_field_type(DiscourseCalendar::GROUP_TIMEZONES_CUSTOM_FIELD, :json)
TopicView.default_post_custom_fields << DiscourseCalendar::GROUP_TIMEZONES_CUSTOM_FIELD
register_post_custom_field_type(
DiscourseCalendar::CALENDAR_CUSTOM_FIELD,
:string
)
register_post_custom_field_type(
DiscourseCalendar::GROUP_TIMEZONES_CUSTOM_FIELD,
:json
)
TopicView.default_post_custom_fields <<
DiscourseCalendar::GROUP_TIMEZONES_CUSTOM_FIELD
register_user_custom_field_type(DiscourseCalendar::HOLIDAY_CUSTOM_FIELD, :boolean)
register_user_custom_field_type(
DiscourseCalendar::HOLIDAY_CUSTOM_FIELD,
:boolean
)
# TODO Drop after Discourse 2.6.0 release
if respond_to?(:allow_staff_user_custom_field)
@ -267,7 +304,9 @@ after_initialize do
end
on(:site_setting_changed) do |name, old_value, new_value|
next unless [:all_day_event_start_time, :all_day_event_end_time].include? name
unless %i[all_day_event_start_time all_day_event_end_time].include? name
next
end
Post.where(id: CalendarEvent.select(:post_id).distinct).each do |post|
CalendarEvent.update(post)
@ -303,7 +342,11 @@ after_initialize do
return if self.is_first_post?
# Skip if not a calendar topic
return if !self&.topic&.first_post&.custom_fields&.[](DiscourseCalendar::CALENDAR_CUSTOM_FIELD)
if !self&.topic&.first_post&.custom_fields&.[](
DiscourseCalendar::CALENDAR_CUSTOM_FIELD
)
return
end
validator = DiscourseCalendar::EventValidator.new(self)
validator.validate_event
@ -330,20 +373,25 @@ after_initialize do
grouped_events = {}
CalendarEvent.where(topic_id: object.topic_id).each do |event|
# Events with no `post_id` are holidays
if event.post_id
result << {
type: :standalone,
post_number: event.post_number,
message: event.description,
from: event.start_date,
to: event.end_date,
username: event.username,
recurring: event.recurrence,
post_url: Post.url('-', event.topic_id, event.post_number)
}
# Events with no `post_id` are holidays
result <<
{
type: :standalone,
post_number: event.post_number,
message: event.description,
from: event.start_date,
to: event.end_date,
username: event.username,
recurring: event.recurrence,
post_url: Post.url('-', event.topic_id, event.post_number)
}
else
identifier = "#{event.region.split("_").first}-#{event.start_date.strftime("%W")}-#{(event.end_date || event.start_date).strftime("%W")}"
identifier =
"#{event.region.split('_').first}-#{
event.start_date.strftime('%W')
}-#{(event.end_date || event.start_date).strftime('%W')}"
if grouped_events[identifier]
grouped_events[identifier][:to] = event.start_date
@ -365,23 +413,25 @@ after_initialize do
result.concat(grouped_events.values)
end
add_to_serializer(:post, :include_calendar_details?) do
object.is_first_post?
end
add_to_serializer(:post, :include_calendar_details?) { object.is_first_post? }
add_to_serializer(:post, :group_timezones) do
result = {}
group_names = object.group_timezones["groups"] || []
group_names = object.group_timezones['groups'] || []
if group_names.present?
users = User
.joins(:groups, :user_option)
.where("groups.name": group_names)
.select("users.*", "groups.name AS group_name", "user_options.timezone")
users =
User.joins(:groups, :user_option).where("groups.name": group_names)
.select(
'users.*',
'groups.name AS group_name',
'user_options.timezone'
)
users.each do |u|
result[u.group_name] ||= []
result[u.group_name] << UserTimezoneSerializer.new(u, root: false).as_json
result[u.group_name] <<
UserTimezoneSerializer.new(u, root: false).as_json
end
end
@ -396,16 +446,21 @@ after_initialize do
DiscourseCalendar.users_on_holiday
end
add_to_serializer(:site, :include_users_on_holiday?) do
scope.is_staff?
end
add_to_serializer(:site, :include_users_on_holiday?) { scope.is_staff? }
reloadable_patch do
module DiscoursePostEvent::ExportCsvControllerExtension
def export_entity
if post_event_export? && ensure_can_export_post_event
Jobs.enqueue(:export_csv_file, entity: export_params[:entity], user_id: current_user.id, args: export_params[:args])
StaffActionLogger.new(current_user).log_entity_export(export_params[:entity])
Jobs.enqueue(
:export_csv_file,
entity: export_params[:entity],
user_id: current_user.id,
args: export_params[:args]
)
StaffActionLogger.new(current_user).log_entity_export(
export_params[:entity]
)
render json: success_json
else
super
@ -416,10 +471,11 @@ after_initialize do
def export_params
if post_event_export?
@_export_params ||= begin
params.require(:entity)
params.permit(:entity, args: [:id]).to_h
end
@_export_params ||=
begin
params.require(:entity)
params.permit(:entity, args: %i[id]).to_h
end
else
super
end
@ -448,31 +504,24 @@ after_initialize do
guardian = Guardian.new(current_user)
event = DiscoursePostEvent::Event
.includes(invitees: :user)
.find(@extra[:id])
event =
DiscoursePostEvent::Event.includes(invitees: :user).find(@extra[:id])
guardian.ensure_can_act_on_discourse_post_event!(event)
event.invitees
.each do |invitee|
yield [
invitee.user.username,
DiscoursePostEvent::Invitee.statuses[invitee.status],
invitee.created_at,
invitee.updated_at,
]
end
event.invitees.each do |invitee|
yield [
invitee.user.username,
DiscoursePostEvent::Invitee.statuses[invitee.status],
invitee.created_at,
invitee.updated_at
]
end
end
def get_header(entity)
if SiteSetting.discourse_post_event_enabled && entity === 'post_event'
[
'username',
'status',
'first_answered_at',
'last_updated_at',
]
%w[username status first_answered_at last_updated_at]
else
super
end
@ -489,15 +538,17 @@ after_initialize do
starts_at = event_node['data-start']
ends_at = event_node['data-end']
dates = "#{starts_at} (UTC)"
if ends_at
dates = "#{dates}#{ends_at} (UTC)"
end
dates = "#{dates}#{ends_at} (UTC)" if ends_at
event_name = event_node['data-name'] || post.topic.title
event_node.replace <<~TXT
<div style='border:1px solid #dedede'>
<p><a href="#{Discourse.base_url}#{post.url}">#{event_name}</a></p>
<p>#{dates}</p>
<p><a href="#{
Discourse.base_url
}#{post.url}">#{event_name}</a></p>
<p>#{
dates
}</p>
</div>
TXT
end
@ -531,11 +582,38 @@ after_initialize do
next if removed_fields.empty?
DiscoursePostEvent::Event.all.find_each do |event|
removed_fields.each do |field|
event.custom_fields.delete(field)
end
removed_fields.each { |field| event.custom_fields.delete(field) }
event.save
end
end
on(:discourse_post_event_event_ended) do |event|
next if !event.ends_at
if event.recurrence.present?
recurrence = nil
case event.recurrence
when 'every_day'
recurrence = 'FREQ=DAILY'
when 'every_month'
recurrence = 'FREQ=MONTHLY'
when 'every_weekday'
recurrence = 'FREQ=DAILY;BYDAY=MO,TU,WE,TH,FR'
else
byday = event.starts_at.strftime('%A').upcase[0, 2]
recurrence = "FREQ=WEEKLY;BYDAY=#{byday}"
end
next_starts_at = RRuleGenerator.generate(recurrence, event.starts_at)
difference = event.ends_at - event.starts_at
next_ends_at = next_starts_at + difference.seconds
event.update!(starts_at: next_starts_at, ends_at: next_ends_at)
event.invitees.update_all(status: nil)
event.publish_update!
end
end
end
end

View File

@ -18,7 +18,9 @@ describe Post do
let(:user) { Fabricate(:user, admin: true) }
let(:topic) { Fabricate(:topic, user: user) }
let(:post1) { Fabricate(:post, topic: topic) }
let!(:post_event) { Fabricate(:event, post: post1, status: Event.statuses[:public]) }
let!(:post_event) do
Fabricate(:event, post: post1, status: Event.statuses[:public])
end
context 'when a post is updated' do
context 'when the post has a valid event' do
@ -26,16 +28,15 @@ describe Post do
it 'destroys the associated event' do
start = Time.now.utc.iso8601(3)
post = PostCreator.create!(
user,
title: 'Sell a boat party',
raw: "[event start=\"#{start}\"]\n[/event]",
)
post = create_post_with_event(user)
expect(post.reload.event.persisted?).to eq(true)
revisor = PostRevisor.new(post, post.topic)
revisor.revise!(user, raw: 'The event is over. Come back another day.')
revisor.revise!(
user,
raw: 'The event is over. Come back another day.'
)
expect(post.reload.event).to be(nil)
end
@ -49,17 +50,24 @@ describe Post do
before do
SiteSetting.editing_grace_period = 1.minute
PostActionNotifier.enable
SiteSetting.discourse_post_event_edit_notifications_time_extension = 180
SiteSetting.discourse_post_event_edit_notifications_time_extension =
180
end
context 'when in edit grace period' do
before do
post_1.reload.event.update_with_params!(starts_at: 3.hours.ago, ends_at: 2.hours.ago)
post_1.reload.event.update_with_params!(
starts_at: 3.hours.ago, ends_at: 2.hours.ago
)
end
it 'sends a post revision to going invitees' do
Invitee.create_attendance!(going_user.id, post_1.id, :going)
Invitee.create_attendance!(interested_user.id, post_1.id, :interested)
Invitee.create_attendance!(
interested_user.id,
post_1.id,
:interested
)
revisor = PostRevisor.new(post_1)
revisor.revise!(
@ -80,7 +88,11 @@ describe Post do
it 'doesnt send a post revision to anyone' do
Invitee.create_attendance!(going_user.id, post_1.id, :going)
Invitee.create_attendance!(interested_user.id, post_1.id, :interested)
Invitee.create_attendance!(
interested_user.id,
post_1.id,
:interested
)
revisor = PostRevisor.new(post_1)
revisor.revise!(
@ -93,6 +105,55 @@ describe Post do
expect(interested_user.notifications.count).to eq(0)
end
end
context 'an event with recurrence' do
let(:event_1) do
create_post_with_event(user, 'recurrence=FREQ=WEEKLY;BYDAY=MO')
.event
end
before do
freeze_time Time.utc(2020, 8, 12, 16, 32)
event_1.update_with_params!(
recurrence: 'FREQ=WEEKLY;BYDAY=MO',
starts_at: 3.hours.ago,
ends_at: nil
)
Invitee.create_attendance!(going_user.id, event_1.id, :going)
Invitee.create_attendance!(
interested_user.id,
event_1.id,
:interested
)
event_1.reload
# we stop processing jobs immediately at this point to prevent infinite loop
# as future event ended job would finish now, trigger next recurrence, and anodther job...
Jobs.run_later!
end
context 'when the event ends' do
it 'sets the next dates' do
event_1.update_with_params!(ends_at: Time.now)
expect(event_1.starts_at.to_s).to eq('2020-08-19 13:32:00 UTC')
expect(event_1.ends_at.to_s).to eq('2020-08-19 16:32:00 UTC')
end
it 'it removes status from every invitees' do
expect(event_1.invitees.pluck(:status)).to match_array(
[Invitee.statuses[:going], Invitee.statuses[:interested]]
)
event_1.update_with_params!(ends_at: Time.now)
expect(event_1.invitees.pluck(:status)).to eq([])
end
end
end
end
end
end
@ -103,11 +164,12 @@ describe Post do
it 'creates the post event' do
start = Time.now.utc.iso8601(3)
post = PostCreator.create!(
user,
title: 'Sell a boat party',
raw: "[event start=\"#{start}\"]\n[/event]",
)
post =
PostCreator.create!(
user,
title: 'Sell a boat party',
raw: "[event start=\"#{start}\"]\n[/event]"
)
expect(post.reload.persisted?).to eq(true)
expect(post.event.persisted?).to eq(true)
@ -116,7 +178,7 @@ describe Post do
it 'works with name attribute' do
post = create_post_with_event(user, 'name="foo bar"').reload
expect(post.event.name).to eq("foo bar")
expect(post.event.name).to eq('foo bar')
post = create_post_with_event(user, 'name=""').reload
expect(post.event.name).to be_blank
@ -140,35 +202,59 @@ describe Post do
it 'works with status attribute' do
post = create_post_with_event(user, 'status="private"').reload
expect(post.event.status).to eq(DiscoursePostEvent::Event.statuses[:private])
expect(post.event.status).to eq(
DiscoursePostEvent::Event.statuses[:private]
)
post = create_post_with_event(user, 'status=""').reload
expect(post.event.status).to eq(DiscoursePostEvent::Event.statuses[:standalone])
expect(post.event.status).to eq(
DiscoursePostEvent::Event.statuses[:standalone]
)
post = create_post_with_event(user, 'status=').reload
expect(post.event.status).to eq(DiscoursePostEvent::Event.statuses[:standalone])
expect(post.event.status).to eq(
DiscoursePostEvent::Event.statuses[:standalone]
)
end
it 'works with allowedGroups attribute' do
post = create_post_with_event(user, 'allowedGroups="euro"').reload
expect(post.event.raw_invitees).to eq([])
post = create_post_with_event(user, 'status="public" allowedGroups="euro"').reload
expect(post.event.raw_invitees).to eq(['trust_level_0'])
post =
create_post_with_event(user, 'status="public" allowedGroups="euro"')
.reload
expect(post.event.raw_invitees).to eq(%w[trust_level_0])
post = create_post_with_event(user, 'status="standalone" allowedGroups="euro"').reload
post =
create_post_with_event(
user,
'status="standalone" allowedGroups="euro"'
).reload
expect(post.event.raw_invitees).to eq([])
post = create_post_with_event(user, 'status="private" allowedGroups="euro"').reload
expect(post.event.raw_invitees).to eq(['euro'])
post =
create_post_with_event(
user,
'status="private" allowedGroups="euro"'
).reload
expect(post.event.raw_invitees).to eq(%w[euro])
post = create_post_with_event(user, 'status="private" allowedGroups="euro,america"').reload
expect(post.event.raw_invitees).to match_array(['euro', 'america'])
post =
create_post_with_event(
user,
'status="private" allowedGroups="euro,america"'
).reload
expect(post.event.raw_invitees).to match_array(%w[euro america])
post = create_post_with_event(user, 'status="private" allowedGroups=""').reload
post =
create_post_with_event(user, 'status="private" allowedGroups=""')
.reload
expect(post.event.raw_invitees).to eq([])
post = create_post_with_event(user, 'status="private" allowedGroups=').reload
post =
create_post_with_event(user, 'status="private" allowedGroups=')
.reload
expect(post.event.raw_invitees).to eq([])
end
@ -176,7 +262,8 @@ describe Post do
post = create_post_with_event(user).reload
expect(post.event.reminders).to eq(nil)
post = create_post_with_event(user, 'reminders="1.hours,-3.days"').reload
post =
create_post_with_event(user, 'reminders="1.hours,-3.days"').reload
expect(post.event.reminders).to eq('1.hours,-3.days')
end
end
@ -192,11 +279,12 @@ describe Post do
it 'creates the post event' do
start = Time.now.utc.iso8601(3)
post = PostCreator.create!(
user_with_rights,
title: 'Sell a boat party',
raw: "[event start=\"#{start}\"]\n[/event]",
)
post =
PostCreator.create!(
user_with_rights,
title: 'Sell a boat party',
raw: "[event start=\"#{start}\"]\n[/event]"
)
expect(post.reload.persisted?).to eq(true)
expect(post.event.persisted?).to eq(true)
@ -215,15 +303,18 @@ describe Post do
it 'raises an error' do
start = Time.now.utc.iso8601(3)
expect {
expect do
PostCreator.create!(
user_without_rights,
title: 'Sell a boat party',
raw: "[event start=\"#{start}\"]\n[/event]",
raw: "[event start=\"#{start}\"]\n[/event]"
)
end.to(
raise_error(ActiveRecord::RecordNotSaved).with_message(
I18n.t(
'discourse_post_event.errors.models.event.acting_user_not_allowed_to_create_event'
)
)
}.to(
raise_error(ActiveRecord::RecordNotSaved)
.with_message(I18n.t("discourse_post_event.errors.models.event.acting_user_not_allowed_to_create_event"))
)
end
end
@ -232,29 +323,32 @@ describe Post do
context 'when the post contains one invalid event' do
context 'when start is invalid' do
it 'raises an error' do
expect {
expect do
PostCreator.create!(
user,
title: 'Sell a boat party',
raw: "[event start=\"x\"]\n[/event]",
title: 'Sell a boat party', raw: "[event start=\"x\"]\n[/event]"
)
end.to(
raise_error(ActiveRecord::RecordNotSaved).with_message(
I18n.t(
'discourse_post_event.errors.models.event.start_must_be_present_and_a_valid_date'
)
)
}.to(
raise_error(ActiveRecord::RecordNotSaved)
.with_message(I18n.t("discourse_post_event.errors.models.event.start_must_be_present_and_a_valid_date"))
)
end
end
context 'when start is not provided or' do
it 'is not cooked' do
post = PostCreator.create!(
user,
title: 'Sell a boat party',
raw: <<~TXT
post =
PostCreator.create!(
user,
title: 'Sell a boat party',
raw: <<~TXT
[event end=\"1\"]
[/event]
TXT
)
)
expect(!post.cooked.include?('discourse-post-event')).to be(true)
end
@ -262,15 +356,21 @@ describe Post do
context 'when end is provided and is invalid' do
it 'raises an error' do
expect {
expect do
PostCreator.create!(
user,
title: 'Sell a boat party',
raw: "[event start=\"#{Time.now.utc.iso8601(3)}\" end=\"d\"]\n[/event]",
raw:
"[event start=\"#{
Time.now.utc.iso8601(3)
}\" end=\"d\"]\n[/event]"
)
end.to(
raise_error(ActiveRecord::RecordNotSaved).with_message(
I18n.t(
'discourse_post_event.errors.models.event.end_must_be_a_valid_date'
)
)
}.to(
raise_error(ActiveRecord::RecordNotSaved)
.with_message(I18n.t("discourse_post_event.errors.models.event.end_must_be_a_valid_date"))
)
end
end
@ -278,7 +378,7 @@ describe Post do
context 'when the post contains multiple events' do
it 'raises an error' do
expect {
expect do
PostCreator.create!(
user,
title: 'Sell a boat party',
@ -290,9 +390,10 @@ describe Post do
[/event]
TXT
)
}.to(
raise_error(ActiveRecord::RecordNotSaved)
.with_message(I18n.t("discourse_post_event.errors.models.event.only_one_event"))
end.to(
raise_error(ActiveRecord::RecordNotSaved).with_message(
I18n.t('discourse_post_event.errors.models.event.only_one_event')
)
)
end
end

View File

@ -21,5 +21,5 @@ def create_post_with_event(user, extra_raw = '')
user,
title: "Sell a boat party ##{SecureRandom.alphanumeric}",
raw: "[event start=\"#{start}\" #{extra_raw}]\n[/event]",
)
).reload
end

View File

@ -0,0 +1,49 @@
# frozen_string_literal: true
require 'rails_helper'
describe RRuleGenerator do
let(:time) { Time.now }
before { freeze_time Time.utc(2020, 8, 12, 16, 32) }
context 'every week' do
let(:sample_rrule) { 'FREQ=WEEKLY;BYDAY=MO' }
context 'a rule and time are given' do
it 'generates the rule' do
rrule = RRuleGenerator.generate(sample_rrule, time)
expect(rrule.to_s).to eq('2020-08-17 16:32:00 UTC')
end
context '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 'every day' do
let(:sample_rrule) { 'FREQ=DAILY' }
context 'a rule and time are given' do
it 'generates the rule' do
rrule = RRuleGenerator.generate(sample_rrule, time)
expect(rrule.to_s).to eq('2020-08-13 16:32:00 UTC')
end
context '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-11 16:32:00 UTC')
end
end
end
end
end