FEATURE: inline bulk invite

FEATURE: inline bulk invite

diff --git a/app/controllers/discourse_post_event/events_controller.rb b/app/controllers/discourse_post_event/events_controller.rb
index ef2e90e..fe31606 100644
--- a/app/controllers/discourse_post_event/events_controller.rb
+++ b/app/controllers/discourse_post_event/events_controller.rb
@@ -71,7 +71,7 @@ module DiscoursePostEvent
       render_json_dump(serializer)
     end
 
-    def bulk_invite
+    def csv_bulk_invite
       require 'csv'
 
       event = Event.find(params[:id])
@@ -83,12 +83,10 @@ module DiscoursePostEvent
 
       hijack do
         begin
-          count = 0
           invitees = []
 
           CSV.foreach(file.tempfile) do |row|
             if row[0].present?
-              count += 1
               invitees << { identifier: row[0], attendance: row[1] || 'going' }
             end
           end
@@ -110,6 +108,27 @@ module DiscoursePostEvent
       end
     end
 
+    def bulk_invite
+      event = Event.find(params[:id])
+      guardian.ensure_can_edit!(event.post)
+      guardian.ensure_can_create_discourse_post_event!
+
+      invitees = Array(params[:invitees]).reject { |x| x.empty? }
+      raise Discourse::InvalidParameters.new(:invitees) if invitees.blank?
+
+      begin
+        Jobs.enqueue(
+          :discourse_post_event_bulk_invite,
+          event_id: event.id,
+          invitees: invitees.as_json,
+          current_user_id: current_user.id
+        )
+        render json: success_json
+      rescue
+        render json: failed_json.merge(errors: [I18n.t('discourse_post_event.errors.bulk_invite.error')]), status: 422
+      end
+    end
+
     private
 
     def event_params
diff --git a/app/models/discourse_post_event/event.rb b/app/models/discourse_post_event/event.rb
index 3c2aac7..4ae4b81 100644
--- a/app/models/discourse_post_event/event.rb
+++ b/app/models/discourse_post_event/event.rb
@@ -130,14 +130,18 @@ module DiscoursePostEvent
       self.invitees.insert_all!(attrs)
     end
 
-    def notify_invitees!
+    def notify_invitees!(auto: false)
       self.invitees.where(notified: false).each do |invitee|
-        create_notification!(invitee.user, self.post)
+        create_notification!(invitee.user, self.post, auto: auto)
         invitee.update!(notified: true)
       end
     end
 
-    def create_notification!(user, post)
+    def create_notification!(user, post, auto: false)
+      message = auto ?
+        'discourse_post_event.notifications.invite_user_auto_notification' :
+        'discourse_post_event.notifications.invite_user_notification'
+
       user.notifications.create!(
         notification_type: Notification.types[:custom],
         topic_id: post.topic_id,
@@ -145,7 +149,7 @@ module DiscoursePostEvent
         data: {
           topic_title: post.topic.title,
           display_username: post.user.username,
-          message: 'discourse_calendar.invite_user_notification'
+          message: message
         }.to_json
       )
     end
@@ -196,7 +200,7 @@ module DiscoursePostEvent
     def enforce_raw_invitees!
       self.destroy_extraneous_invitees!
       self.fill_invitees!
-      self.notify_invitees!
+      self.notify_invitees!(auto: false)
     end
 
     def can_user_update_attendance(user)
diff --git a/app/serializers/discourse_post_event/event_serializer.rb b/app/serializers/discourse_post_event/event_serializer.rb
index 531e42a..ee4e424 100644
--- a/app/serializers/discourse_post_event/event_serializer.rb
+++ b/app/serializers/discourse_post_event/event_serializer.rb
@@ -19,6 +19,9 @@ module DiscoursePostEvent
     attributes :should_display_invitees
     attributes :url
     attributes :custom_fields
+    attributes :is_public
+    attributes :is_private
+    attributes :is_standalone
 
     def can_act_on_discourse_post_event
       scope.can_act_on_discourse_post_event?(object)
@@ -28,6 +31,18 @@ module DiscoursePostEvent
       object.is_expired?
     end
 
+    def is_public
+      object.status === Event.statuses[:public]
+    end
+
+    def is_private
+      object.status === Event.statuses[:private]
+    end
+
+    def is_standalone
+      object.status === Event.statuses[:standalone]
+    end
+
     def status
       Event.statuses[object.status]
     end
diff --git a/assets/javascripts/discourse/controllers/discourse-post-event-bulk-invite.js.es6 b/assets/javascripts/discourse/controllers/discourse-post-event-bulk-invite.js.es6
index 5f64691..aaa06aa 100644
--- a/assets/javascripts/discourse/controllers/discourse-post-event-bulk-invite.js.es6
+++ b/assets/javascripts/discourse/controllers/discourse-post-event-bulk-invite.js.es6
@@ -1,8 +1,99 @@
+import { isPresent } from "@ember/utils";
+import { ajax } from "discourse/lib/ajax";
+import { extractError } from "discourse/lib/ajax-error";
+import EmberObject, { action } from "@ember/object";
+import { observes } from "discourse-common/utils/decorators";
 import ModalFunctionality from "discourse/mixins/modal-functionality";
 import Controller from "@ember/controller";
-import { action } from "@ember/object";
+import Group from "discourse/models/group";
+import I18n from "I18n";
 
 export default Controller.extend(ModalFunctionality, {
+  bulkInvites: null,
+  bulkInviteStatuses: null,
+  bulkInviteDisabled: true,
+
+  init() {
+    this._super(...arguments);
+
+    this.set("bulkInviteStatuses", [
+      {
+        label: I18n.t("discourse_post_event.models.invitee.status.unknown"),
+        name: "unknown"
+      },
+      {
+        label: I18n.t("discourse_post_event.models.invitee.status.going"),
+        name: "going"
+      },
+      {
+        label: I18n.t("discourse_post_event.models.invitee.status.not_going"),
+        name: "not_going"
+      },
+      {
+        label: I18n.t("discourse_post_event.models.invitee.status.interested"),
+        name: "interested"
+      }
+    ]);
+  },
+
+  onShow() {
+    this.set("bulkInvites", [
+      EmberObject.create({ identifier: null, attendance: "unknown" })
+    ]);
+  },
+
+  @action
+  groupFinder(term) {
+    return Group.findAll({ term, ignore_automatic: true });
+  },
+
+  // TODO: improves core to avoid having to rely on observer for group changes
+  // using onChangeCallback doesn't solve the issue as it doesn't provide the object
+  @observes("bulkInvites.@each.identifier")
+  setBulkInviteDisabled() {
+    this.set(
+      "bulkInviteDisabled",
+      this.bulkInvites.filter(x => isPresent(x.identifier)).length === 0
+    );
+  },
+
+  @action
+  sendBulkInvites() {
+    return ajax(
+      `/discourse-post-event/events/${this.model.eventModel.id}/bulk-invite.json`,
+      {
+        type: "POST",
+        dataType: "json",
+        contentType: "application/json",
+        data: JSON.stringify({
+          invitees: this.bulkInvites.filter(x => isPresent(x.identifier))
+        })
+      }
+    )
+      .then(data => {
+        if (data.success) {
+          this.send("closeModal");
+        }
+      })
+      .catch(e => this.flash(extractError(e), "error"));
+  },
+
+  @action
+  removeBulkInvite(bulkInvite) {
+    this.bulkInvites.removeObject(bulkInvite);
+
+    if (!this.bulkInvites.length) {
+      this.set("bulkInvites", [
+        EmberObject.create({ identifier: null, attendance: "unknown" })
+      ]);
+    }
+  },
+
+  @action
+  addBulkInvite() {
+    this.bulkInvites.pushObject({ identifier: null, attendance: "unknown" });
+  },
+
   @action
   uploadDone() {
     bootbox.alert(
diff --git a/assets/javascripts/discourse/templates/modal/discourse-post-event-bulk-invite.hbs b/assets/javascripts/discourse/templates/modal/discourse-post-event-bulk-invite.hbs
index 46b4d32..27169dd 100644
--- a/assets/javascripts/discourse/templates/modal/discourse-post-event-bulk-invite.hbs
+++ b/assets/javascripts/discourse/templates/modal/discourse-post-event-bulk-invite.hbs
@@ -2,16 +2,66 @@
   title=(concat "discourse_post_event.bulk_invite_modal.title")
   class="discourse-post-event-bulk-invite"
 }}

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

GitHub sha: 4e5fad6b

auto is a little unclear to me. What would be an automatic notification?

yes agreed :+1: I should come up with something more explicit, this is to express the fact that the invite is coming from bulk invite and more importantly the attendance has been set by the bulk invite.