FEATURE: intoduces recurrence support

FEATURE: intoduces recurrence support

For now: every_week/every_day/every_month/every_weekday (monday to friday)

diff --git a/app/controllers/discourse_post_event/events_controller.rb b/app/controllers/discourse_post_event/events_controller.rb
index 21d8c5f..2c01a7f 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 8f97ac3..6b6d5b2 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

[... diff too long, it was truncated ...]

GitHub sha: 01984794