FEATURE: allows to export event invitees in a .csv file (#40)

FEATURE: allows to export event invitees in a .csv file (#40)

diff --git a/assets/javascripts/discourse/widgets/discourse-post-event.js.es6 b/assets/javascripts/discourse/widgets/discourse-post-event.js.es6
index 177331f..0fec61c 100644
--- a/assets/javascripts/discourse/widgets/discourse-post-event.js.es6
+++ b/assets/javascripts/discourse/widgets/discourse-post-event.js.es6
@@ -1,3 +1,4 @@
+import { exportEntity } from "discourse/lib/export-csv";
 import { emojiUnescape } from "discourse/lib/text";
 import cleanTitle from "discourse/plugins/discourse-calendar/lib/clean-title";
 import { dasherize } from "@ember/string";
@@ -74,6 +75,13 @@ export default createWidget("discourse-post-event", {
     };
   },
 
+  exportPostEvent(postId) {
+    exportEntity("post_event", {
+      name: "post_event",
+      id: postId
+    });
+  },
+
   sendPMToCreator() {
     const router = this.register.lookup("service:router")._router;
     routeAction(
diff --git a/assets/javascripts/discourse/widgets/more-dropdown.js.es6 b/assets/javascripts/discourse/widgets/more-dropdown.js.es6
index 9553d7c..a3d4cd9 100644
--- a/assets/javascripts/discourse/widgets/more-dropdown.js.es6
+++ b/assets/javascripts/discourse/widgets/more-dropdown.js.es6
@@ -63,6 +63,13 @@ export default createWidget("more-dropdown", {
       content.push("separator");
 
       content.push({
+        icon: "file-csv",
+        id: "exportPostEvent",
+        label: "discourse_post_event.event_ui.export_event",
+        param: attrs.postEventId
+      });
+
+      content.push({
         icon: "pencil-alt",
         id: "editPostEvent",
         label: "discourse_post_event.event_ui.edit_event",
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index e38d391..0af8c81 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -48,6 +48,7 @@ en:
         add_to_calendar: Add to calendar
         send_pm_to_creator: Send PM to %{username}
         edit_event: Edit event
+        export_event: Export event
         created_by: Created by
       invitees_modal:
         title_invited: "List of invited users"
diff --git a/plugin.rb b/plugin.rb
index 37b0058..b328f24 100644
--- a/plugin.rb
+++ b/plugin.rb
@@ -26,7 +26,7 @@ register_asset "stylesheets/mobile/discourse-post-event.scss", :mobile
 register_asset "stylesheets/desktop/discourse-calendar.scss", :desktop
 register_svg_icon "fas fa-calendar-day"
 register_svg_icon "fas fa-clock"
-register_svg_icon "fas fa-clock"
+register_svg_icon "fas fa-file-csv"
 register_svg_icon "fas fa-star"
 
 after_initialize do
@@ -152,7 +152,7 @@ after_initialize do
     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
+      can_create_discourse_post_event? || event.post.user_id == id
     rescue
       false
     end
@@ -374,4 +374,80 @@ after_initialize do
   add_to_serializer(:site, :include_users_on_holiday?) do
     scope.is_staff?
   end
+
+  reloadable_patch do
+    module ExportPostEventCsvParamsExtension
+      private
+
+      def export_params
+        if post_event_export?
+          @_export_params ||= begin
+            params.require(:entity)
+            params.permit(:entity, args: [:id]).to_h
+          end
+        else
+          super
+        end
+      end
+
+      def post_event_export?
+        params[:entity] === 'post_event'
+      end
+
+      def ensure_can_export_post_event
+        return if !post_event_export?
+        return if !SiteSetting.discourse_post_event_enabled
+
+        post_event = DiscoursePostEvent::Event.find(export_params[:args][:id])
+        post_event && guardian.can_act_on_discourse_post_event?(post_event)
+      end
+    end
+
+    require_dependency 'export_csv_controller'
+    class ::ExportCsvController
+      before_action :ensure_can_export_post_event
+      prepend ExportPostEventCsvParamsExtension
+    end
+
+    module ExportPostEventCsvReportExtension
+      def post_event_export(&block)
+        return enum_for(:post_event_export) unless block_given?
+
+        guardian = Guardian.new(current_user)
+
+        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
+      end
+
+      def get_header(entity)
+        if SiteSetting.discourse_post_event_enabled && entity === 'post_event'
+          [
+            'username',
+            'status',
+            'first_answered_at',
+            'last_updated_at',
+          ]
+        else
+          super
+        end
+      end
+    end
+
+    class Jobs::ExportCsvFile
+      prepend ExportPostEventCsvReportExtension
+    end
+  end
 end
diff --git a/spec/jobs/export_post_event_report_csv_spec.rb b/spec/jobs/export_post_event_report_csv_spec.rb
new file mode 100644
index 0000000..0e6fdac
--- /dev/null
+++ b/spec/jobs/export_post_event_report_csv_spec.rb
@@ -0,0 +1,130 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+require_relative '../fabricators/event_fabricator'
+
+describe Jobs::ExportCsvFile do
+  before do
+    freeze_time
+    Jobs.run_immediately!
+    SiteSetting.calendar_enabled = true
+    SiteSetting.discourse_post_event_enabled = true
+  end
+
+  context '#execute' do
+    context 'the requesting user is admin' do
+      let(:user) { Fabricate(:user, admin: true) }
+      let(:user_1) { Fabricate(:user) }
+      let(:user_2) { Fabricate(:user) }
+      let(:topic) { Fabricate(:topic, user: user) }
+      let(:post1) { Fabricate(:post, topic: topic) }
+      let(:post_event) { Fabricate(:event, post: post1) }
+
+      context 'the event exists' do
+        context 'the event has invitees' do
+          before do
+            post_event.create_invitees([
+              { user_id: user_1.id, status: nil },
+              { user_id: user_2.id, status: 2 }
+            ])
+          end
+
+          context 'the user requesting the upload is admin' do
+            it 'generates the upload and notify the user' do
+              begin
+                expect do
+                  Jobs::ExportCsvFile.new.execute(
+                    user_id: user.id,
+                    entity: 'post_event',
+                    args: { id: post_event.id }
+                  )
+                end.to change { Upload.count }.by(1)
+
+                system_message = user.topics_allowed.last
+
+                expect(system_message.title).to eq(I18n.t(
+                  'system_messages.csv_export_succeeded.subject_template',
+                  export_title: 'Post Event'
+                ))
+
+                upload = system_message.first_post.uploads.first
+
+                expect(system_message.first_post.raw).to eq(I18n.t(
+                  'system_messages.csv_export_succeeded.text_body_template',
+                  download_link: "[#{upload.original_filename}|attachment](#{upload.short_url}) (#{upload.filesize} Bytes)"
+                ).chomp)
+
+                expect(system_message.id).to eq(UserExport.last.topic_id)
+                expect(system_message.closed).to eq(true)
+
+                files = []
+                Zip::File.open(Discourse.store.path_for(upload)) do |zip_file|
+
+                  zip_file.each do |entry|
+                    files << entry.name
+
+                    input_stream = entry.get_input_stream
+                    parsed_csv = CSV.parse(input_stream.read)
+
+                    expect(parsed_csv[0]).to eq(['username', 'status', 'first_answered_at', 'last_updated_at'])
+                    invitee_1 = post_event.invitees.find_by(user_id: user_1.id)

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

GitHub sha: 94868315

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