FEATURE: intoduces recurrence support
For now: every_week/every_day/every_month/every_weekday (monday to friday)
This commit is contained in:
parent
7b45032561
commit
019847948c
|
@ -143,6 +143,7 @@ module DiscoursePostEvent
|
|||
:ends_at,
|
||||
:status,
|
||||
:url,
|
||||
:recurrence,
|
||||
custom_fields: allowed_custom_fields,
|
||||
raw_invitees: [],
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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", {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -344,6 +344,9 @@ en:
|
|||
add_reminder: Add reminder
|
||||
reminders:
|
||||
label: Reminders
|
||||
recurrence:
|
||||
label: Recurrence
|
||||
none: No recurrence
|
||||
url:
|
||||
label: URL
|
||||
placeholder: Optional
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
390
plugin.rb
|
@ -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
|
||||
|
|
|
@ -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 'doesn’t 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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
Loading…
Reference in New Issue