FEATURE: implements initial support for post events (#24)

FEATURE: implements initial support for post events (#24)

diff --git a/app/controllers/discourse_calendar/invitees_controller.rb b/app/controllers/discourse_calendar/invitees_controller.rb
new file mode 100644
index 0000000..d6f1d2b
--- /dev/null
+++ b/app/controllers/discourse_calendar/invitees_controller.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module DiscourseCalendar
+  class InviteesController < ::ApplicationController
+    before_action :ensure_logged_in
+
+    def index
+      post_event_invitees = PostEvent.find(params['post-event-id']).invitees
+
+      if params[:filter]
+        post_event_invitees = post_event_invitees.joins(:user).where("users.username LIKE '%#{params[:filter]}%'")
+      end
+
+      render json: ActiveModel::ArraySerializer.new(post_event_invitees.limit(10), each_serializer: InviteeSerializer).as_json
+    end
+
+    def update
+      invitee = Invitee.find(params[:id])
+      guardian.ensure_can_act_on_invitee!(invitee)
+      status = Invitee.statuses[invitee_params[:status].to_sym]
+      invitee.update_attendance(status: status)
+      invitee.post_event.publish_update!
+      render json: InviteeSerializer.new(invitee)
+    end
+
+    def create
+      status = Invitee.statuses[invitee_params[:status].to_sym]
+      post_event = PostEvent.find(invitee_params[:post_id])
+      guardian.ensure_can_act_on_post_event!(post_event)
+      invitee = Invitee.create!(
+        status: status,
+        post_id: invitee_params[:post_id],
+        user_id: current_user.id,
+      )
+      invitee.post_event.publish_update!
+      render json: InviteeSerializer.new(invitee)
+    end
+
+    private
+
+    def invitee_params
+      params.require(:invitee).permit(:status, :post_id)
+    end
+  end
+end
diff --git a/app/controllers/discourse_calendar/post_events_controller.rb b/app/controllers/discourse_calendar/post_events_controller.rb
new file mode 100644
index 0000000..52891cd
--- /dev/null
+++ b/app/controllers/discourse_calendar/post_events_controller.rb
@@ -0,0 +1,92 @@
+# frozen_string_literal: true
+
+module DiscourseCalendar
+  class PostEventsController < ::ApplicationController
+    before_action :ensure_logged_in
+
+    def index
+      post_events = PostEvent.visible.where("starts_at > ?", Time.now).limit(10)
+      render json: ActiveModel::ArraySerializer.new(
+        post_events,
+        each_serializer: PostEventSerializer,
+        scope: guardian).as_json
+    end
+
+    def show
+      post_event = DiscourseCalendar::PostEvent.find(params[:id])
+      guardian.ensure_can_see!(post_event.post)
+      serializer = PostEventSerializer.new(post_event, scope: guardian)
+      render_json_dump(serializer)
+    end
+
+    def destroy
+      post_event = DiscourseCalendar::PostEvent.find(params[:id])
+      guardian.ensure_can_act_on_post_event!(post_event)
+      post_event.publish_update!
+      post_event.destroy
+      render json: success_json
+    end
+
+    def update
+      DistributedMutex.synchronize("discourse-calendar[post-event-invitee-update]") do
+        post_event = DiscourseCalendar::PostEvent.find(params[:id])
+        guardian.ensure_can_edit!(post_event.post)
+        guardian.ensure_can_act_on_post_event!(post_event)
+        post_event.enforce_utc!(post_event_params)
+
+        case post_event_params[:status].to_i
+        when PostEvent.statuses[:private]
+          raw_invitees = Array(post_event_params[:raw_invitees])
+          post_event.update!(post_event_params.merge(raw_invitees: raw_invitees))
+          post_event.enforce_raw_invitees!
+        when PostEvent.statuses[:public]
+          post_event.update!(post_event_params.merge(raw_invitees: []))
+        when PostEvent.statuses[:standalone]
+          post_event.update!(post_event_params.merge(raw_invitees: []))
+          post_event.invitees.destroy_all
+        end
+
+        post_event.publish_update!
+        serializer = PostEventSerializer.new(post_event, scope: guardian)
+        render_json_dump(serializer)
+      end
+    end
+
+    def create
+      post_event = DiscourseCalendar::PostEvent.new(post_event_params)
+      guardian.ensure_can_edit!(post_event.post)
+      guardian.ensure_can_create_post_event!(post_event)
+      post_event.enforce_utc!(post_event_params)
+
+      case post_event_params[:status].to_i
+      when PostEvent.statuses[:private]
+        raw_invitees = Array(post_event_params[:raw_invitees])
+        post_event.update!(raw_invitees: raw_invitees)
+        post_event.fill_invitees!
+        post_event.notify_invitees!
+      when PostEvent.statuses[:public], PostEvent.statuses[:standalone]
+        post_event.update!(post_event_params.merge(raw_invitees: []))
+      end
+
+      post_event.publish_update!
+      serializer = PostEventSerializer.new(post_event, scope: guardian)
+      render_json_dump(serializer)
+    end
+
+    private
+
+    def post_event_params
+      params
+        .require(:post_event)
+        .permit(
+          :id,
+          :name,
+          :starts_at,
+          :ends_at,
+          :status,
+          :display_invitees,
+          raw_invitees: []
+        )
+    end
+  end
+end
diff --git a/app/controllers/discourse_calendar/upcoming_events_controller.rb b/app/controllers/discourse_calendar/upcoming_events_controller.rb
new file mode 100644
index 0000000..f2f1502
--- /dev/null
+++ b/app/controllers/discourse_calendar/upcoming_events_controller.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+module DiscourseCalendar
+  class UpcomingEventsController < ::ApplicationController
+    before_action :ensure_logged_in
+
+    def index
+    end
+  end
+end
diff --git a/app/models/discourse_calendar/invitee.rb b/app/models/discourse_calendar/invitee.rb
new file mode 100644
index 0000000..1f321ed
--- /dev/null
+++ b/app/models/discourse_calendar/invitee.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module DiscourseCalendar
+  class Invitee < ActiveRecord::Base
+    self.table_name = 'discourse_calendar_invitees'
+
+    belongs_to :post_event, foreign_key: :post_id
+    belongs_to :user
+
+    scope :with_status, ->(status) {
+      where(status: Invitee.statuses[status])
+    }
+
+    def self.statuses
+      @statuses ||= Enum.new(going: 0, interested: 1, not_going: 2)
+    end
+
+    def update_attendance(params)
+      self.update!(params)
+    end
+  end
+end
diff --git a/app/models/discourse_calendar/post_event.rb b/app/models/discourse_calendar/post_event.rb
new file mode 100644
index 0000000..9817352
--- /dev/null
+++ b/app/models/discourse_calendar/post_event.rb
@@ -0,0 +1,142 @@
+# frozen_string_literal: true
+
+module DiscourseCalendar
+  class PostEvent < ActiveRecord::Base
+    self.table_name = 'discourse_calendar_post_events'
+
+    def self.attributes_protected_by_default
+      super - ['id']
+    end
+
+    has_many :invitees, foreign_key: :post_id, dependent: :delete_all
+    belongs_to :post, foreign_key: :id
+
+    scope :visible, -> { where(deleted_at: nil) }
+
+    validates :name,
+      length: { in: 5..30 },
+      unless: -> (post_event) { post_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_calendar.post_event.errors.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_calendar.post_event.errors.ends_at_before_starts_at"))
+      end
+    end
+
+    def create_invitees(attrs)
+      timestamp = Time.now
+      attrs.map! do |attr|
+        {
+          post_id: self.id,
+          created_at: timestamp,
+          updated_at: timestamp
+        }.merge(attr)
+      end
+
+      self.invitees.insert_all!(attrs)
+    end
+
+    def notify_invitees!
+      self.invitees.where(notified: false).each do |invitee|
+        invitee.user.notifications.create!(

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

GitHub sha: 988b066a

This commit appears in #24 which was merged by jjaffeux.

This looks ripe for SQL injection - can we do some sanitizing on this?

2 Likes

One thing to be careful about here is the objects in ATTRIBUTES are not copied, they are returned by reference. For example name: {} will return that same object literal {} to all calls. Could be the cause of bugs in the future.

1 Like

Can we import this?

1 Like

Does this need rel noopener?

1 Like

It seems peculiar to me that you are storing the raw invites - if the goal is to make it easy to reconstruct the list of who was invited without another query I understand that. But what if an invitee is renamed?

1 Like

Isn’t this redundant? I thought add_to_serializer only included attributes if plugin.enabled?

1 Like

I changed it already and I think new version is safe:

Unless I’m misunderstanding how these placeholders conditions work ?

I already changed it, but yes you were right :+1:

I guess that would be better yes.

Done in: FIX: uses the safer rel noopener param · discourse/discourse-calendar@15ca343 · GitHub

I actually don’t need this, I refactored it so I don’t have to create this variable which is not used anywhere else:

Yes rename is an issue, very edge case though, given the amount of renames, people in events, and “short time” of events. I can live with this for V1. One solution could be to add a discourse event on user rename and handle this. We should also handle user bing merged.

The basic idea is that having raw_invitees let’s you be very precise about who should still be in the list. Imagine this scenario: you add group foo and bar. John is in foo and bar groups, Marc is only in bar group.

If you decide to remove the bar group, having this raw_invitees list makes it possible to have a very precise reconciliation of who should be in the invitees.

1 Like