diff --git a/app/controllers/discourse_post_event/events_controller.rb b/app/controllers/discourse_post_event/events_controller.rb index 21d8c5f5..2c01a7fd 100644 --- a/app/controllers/discourse_post_event/events_controller.rb +++ b/app/controllers/discourse_post_event/events_controller.rb @@ -143,6 +143,7 @@ module DiscoursePostEvent :ends_at, :status, :url, + :recurrence, custom_fields: allowed_custom_fields, raw_invitees: [], ) diff --git a/app/models/discourse_post_event/event.rb b/app/models/discourse_post_event/event.rb index 8f97ac3d..6b6d5b2a 100644 --- a/app/models/discourse_post_event/event.rb +++ b/app/models/discourse_post_event/event.rb @@ -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 diff --git a/app/serializers/discourse_post_event/event_serializer.rb b/app/serializers/discourse_post_event/event_serializer.rb index ebe85ce4..e173bf23 100644 --- a/app/serializers/discourse_post_event/event_serializer.rb +++ b/app/serializers/discourse_post_event/event_serializer.rb @@ -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) diff --git a/assets/javascripts/discourse/controllers/discourse-post-event-builder.js.es6 b/assets/javascripts/discourse/controllers/discourse-post-event-builder.js.es6 index 3dd13772..c72ec733 100644 --- a/assets/javascripts/discourse/controllers/discourse-post-event-builder.js.es6 +++ b/assets/javascripts/discourse/controllers/discourse-post-event-builder.js.es6 @@ -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", { diff --git a/assets/javascripts/discourse/templates/modal/discourse-post-event-builder.hbs b/assets/javascripts/discourse/templates/modal/discourse-post-event-builder.hbs index d2bc0ff1..127503a4 100644 --- a/assets/javascripts/discourse/templates/modal/discourse-post-event-builder.hbs +++ b/assets/javascripts/discourse/templates/modal/discourse-post-event-builder.hbs @@ -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}} -