From da91f7417d8a3b2095cd61251a0725a69c061c9d Mon Sep 17 00:00:00 2001 From: jjaffeux Date: Mon, 10 Aug 2020 16:02:12 +0200 Subject: [PATCH] FEATURE: allows to set reminder Note that reminders; as custom fields, are only editable on created event and not when creating it. --- .../discourse_post_event/events_controller.rb | 9 ++- .../reminders_controller.rb | 14 ++++ app/models/discourse_post_event/event.rb | 62 +++++++++++------ app/models/discourse_post_event/reminder.rb | 25 +++++++ .../discourse_post_event/event_serializer.rb | 1 + .../discourse-post-event-invitee.js.es6 | 66 +------------------ ...discourse-post-event-nested-adapter.js.es6 | 65 ++++++++++++++++++ .../discourse-post-event-reminder.js.es6 | 7 ++ .../discourse-post-event-builder.js.es6 | 58 +++++++++++----- .../discourse-post-event-invitees.js.es6 | 4 +- .../models/discourse-post-event-event.js.es6 | 10 +++ .../discourse-post-event-reminder.js.es6 | 9 +++ .../modal/discourse-post-event-builder.hbs | 43 ++++++++++++ .../discourse-post-event-decorator.js.es6 | 10 +++ .../common/discourse-post-event-builder.scss | 45 ++++++++++++- config/locales/client.en.yml | 6 ++ .../20200809154642_create_reminders_table.rb | 12 ++++ .../discourse_post_event/send_reminder.rb | 38 +++++++++++ plugin.rb | 6 +- spec/acceptance/post_spec.rb | 10 --- spec/fabricators/event_fabricator.rb | 10 +++ .../models/discourse_post_event/event_spec.rb | 12 ++-- spec/requests/reminders_controller_spec.rb | 58 ++++++++++++++++ 23 files changed, 456 insertions(+), 124 deletions(-) create mode 100644 app/controllers/discourse_post_event/reminders_controller.rb create mode 100644 app/models/discourse_post_event/reminder.rb create mode 100644 assets/javascripts/discourse/adapters/discourse-post-event-nested-adapter.js.es6 create mode 100644 assets/javascripts/discourse/adapters/discourse-post-event-reminder.js.es6 create mode 100644 assets/javascripts/discourse/models/discourse-post-event-reminder.js.es6 create mode 100644 db/migrate/20200809154642_create_reminders_table.rb create mode 100644 jobs/regular/discourse_post_event/send_reminder.rb create mode 100644 spec/requests/reminders_controller_spec.rb diff --git a/app/controllers/discourse_post_event/events_controller.rb b/app/controllers/discourse_post_event/events_controller.rb index fe316062..a55727a1 100644 --- a/app/controllers/discourse_post_event/events_controller.rb +++ b/app/controllers/discourse_post_event/events_controller.rb @@ -36,7 +36,7 @@ module DiscoursePostEvent end def show - event = Event.find(params[:id]) + event = Event.includes(:reminders).find(params[:id]) guardian.ensure_can_see!(event.post) serializer = EventSerializer.new(event, scope: guardian) render_json_dump(serializer) @@ -56,6 +56,7 @@ module DiscoursePostEvent guardian.ensure_can_edit!(event.post) guardian.ensure_can_act_on_discourse_post_event!(event) event.update_with_params!(event_params) + event.update_reminders!(event_reminders_params) serializer = EventSerializer.new(event, scope: guardian) render_json_dump(serializer) end @@ -131,6 +132,10 @@ module DiscoursePostEvent private + def event_reminders_params + Array(params.require(:event).permit(reminders: [:id, :value, :unit])[:reminders]) + end + def event_params allowed_custom_fields = SiteSetting.discourse_post_event_allowed_custom_fields.split('|') @@ -144,7 +149,7 @@ module DiscoursePostEvent :status, :url, custom_fields: allowed_custom_fields, - raw_invitees: [] + raw_invitees: [], ) end diff --git a/app/controllers/discourse_post_event/reminders_controller.rb b/app/controllers/discourse_post_event/reminders_controller.rb new file mode 100644 index 00000000..2577c0de --- /dev/null +++ b/app/controllers/discourse_post_event/reminders_controller.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module DiscoursePostEvent + class RemindersController < DiscoursePostEventController + def destroy + event = Event.find_by(id: params[:post_id]) + reminder = event.reminders.find_by(id: params[:id]) + guardian.ensure_can_act_on_discourse_post_event!(event) + Jobs.cancel_scheduled_job(:discourse_post_event_send_reminder, reminder_id: reminder.id) + reminder.destroy! + render json: success_json + end + end +end diff --git a/app/models/discourse_post_event/event.rb b/app/models/discourse_post_event/event.rb index 4ae4b81f..b9b95189 100644 --- a/app/models/discourse_post_event/event.rb +++ b/app/models/discourse_post_event/event.rb @@ -34,34 +34,41 @@ module DiscoursePostEvent end end - after_commit :setup_starts_at_handler, on: [:create, :update] - def setup_starts_at_handler - if !transaction_include_any_action?([:create]) + after_commit :setup_handlers, on: [: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 + + self.refresh_reminders! end - if self.starts_at > Time.now - Jobs.enqueue_at(self.starts_at, :discourse_post_event_event_started, event_id: self.id) + ends_at_changes = saved_change_to_ends_at + if ends_at_changes + new_ends_at = ends_at_changes[1] - if self.starts_at - 1.hour > Time.now - Jobs.enqueue_at(self.starts_at - 1.hour, :discourse_post_event_event_will_start, event_id: self.id) + 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 end - after_commit :setup_ends_at_handler, on: [:create, :update] - def setup_ends_at_handler - if !transaction_include_any_action?([:create]) - Jobs.cancel_scheduled_job(:discourse_post_event_event_ended, event_id: self.id) - end - - if self.ends_at && self.ends_at > Time.now - Jobs.enqueue_at(self.ends_at, :discourse_post_event_event_ended, event_id: self.id) - end - end - has_many :invitees, foreign_key: :post_id, dependent: :delete_all + has_many :reminders, foreign_key: :post_id, dependent: :delete_all belongs_to :post, foreign_key: :id scope :visible, -> { where(deleted_at: nil) } @@ -219,14 +226,12 @@ module DiscoursePostEvent if events.present? event_params = events.first - event = post.event || DiscoursePostEvent::Event.new(id: post.id) - params = { name: event_params[:name], starts_at: event_params[:start] || event.starts_at, ends_at: event_params[:end], - url: event_params[:"url"], + url: event_params[:url], status: event_params[:status].present? ? Event.statuses[event_params[:status].to_sym] : event.status, raw_invitees: event_params[:"allowed-groups"] ? event_params[:"allowed-groups"].split(',') : nil } @@ -254,5 +259,20 @@ module DiscoursePostEvent self.publish_update! end + + def refresh_reminders! + self.reminders.each(&:refresh!) + end + + def update_reminders!(reminders) + reminders.each do |reminder| + if reminder[:id] + model = Reminder.find(reminder[:id]) + model.update!(value: reminder[:value], unit: reminder[:unit]) + else + model = Reminder.create!(value: reminder[:value], unit: reminder[:unit], post_id: self.id) + end + end + end end end diff --git a/app/models/discourse_post_event/reminder.rb b/app/models/discourse_post_event/reminder.rb new file mode 100644 index 00000000..f36c775c --- /dev/null +++ b/app/models/discourse_post_event/reminder.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module DiscoursePostEvent + class Reminder < ActiveRecord::Base + self.table_name = 'discourse_post_event_reminders' + + belongs_to :event, foreign_key: :post_id + + def self.means + @means ||= Enum.new(notification: 0) + end + + after_commit :refresh!, on: [:create, :update] + def refresh! + if transaction_include_any_action?([:update]) + Jobs.cancel_scheduled_job(:discourse_post_event_send_reminder, reminder_id: self.id) + end + + enqueue_at = self.event.starts_at - self.value.send(self.unit) + if enqueue_at > Time.now + Jobs.enqueue_at(enqueue_at, :discourse_post_event_send_reminder, reminder_id: self.id) + end + end + end +end diff --git a/app/serializers/discourse_post_event/event_serializer.rb b/app/serializers/discourse_post_event/event_serializer.rb index ee4e4248..b055a5b0 100644 --- a/app/serializers/discourse_post_event/event_serializer.rb +++ b/app/serializers/discourse_post_event/event_serializer.rb @@ -22,6 +22,7 @@ module DiscoursePostEvent attributes :is_public attributes :is_private attributes :is_standalone + attributes :reminders def can_act_on_discourse_post_event scope.can_act_on_discourse_post_event?(object) diff --git a/assets/javascripts/discourse/adapters/discourse-post-event-invitee.js.es6 b/assets/javascripts/discourse/adapters/discourse-post-event-invitee.js.es6 index c9258050..1ba03169 100644 --- a/assets/javascripts/discourse/adapters/discourse-post-event-invitee.js.es6 +++ b/assets/javascripts/discourse/adapters/discourse-post-event-invitee.js.es6 @@ -1,68 +1,6 @@ -import { Result } from "discourse/adapters/rest"; -import { ajax } from "discourse/lib/ajax"; -import DiscoursePostEventAdapter from "./discourse-post-event-adapter"; -import { underscore } from "@ember/string"; - -export default DiscoursePostEventAdapter.extend({ - // TODO: destroy/update/create should be improved in core to allow for nested models - destroyRecord(store, type, record) { - return ajax( - this.pathFor(store, type, { - post_id: record.post_id, - invitee_id: record.id - }), - { - type: "DELETE" - } - ); - }, - - update(store, type, id, attrs) { - const data = {}; - const typeField = underscore(this.apiNameFor(type)); - data[typeField] = attrs; - - return ajax( - this.pathFor(store, type, { invitee_id: id, post_id: attrs.post_id }), - this.getPayload("PUT", data) - ).then(function(json) { - return new Result(json[typeField], json); - }); - }, - - createRecord(store, type, attrs) { - const data = {}; - const typeField = underscore(this.apiNameFor(type)); - data[typeField] = attrs; - return ajax( - this.pathFor(store, type, attrs), - this.getPayload("POST", data) - ).then(function(json) { - return new Result(json[typeField], json); - }); - }, - - pathFor(store, type, findArgs) { - const post_id = findArgs["post_id"]; - delete findArgs["post_id"]; - - const invitee_id = findArgs["invitee_id"]; - delete findArgs["invitee_id"]; - - let path = - this.basePath(store, type, {}) + - "events/" + - post_id + - "/" + - underscore(store.pluralize(this.apiNameFor())); - - if (invitee_id) { - path += `/${invitee_id}`; - } - - return this.appendQueryParams(path, findArgs); - }, +import DiscoursePostEventNestedAdapter from "./discourse-post-event-nested-adapter"; +export default DiscoursePostEventNestedAdapter.extend({ apiNameFor() { return "invitee"; } diff --git a/assets/javascripts/discourse/adapters/discourse-post-event-nested-adapter.js.es6 b/assets/javascripts/discourse/adapters/discourse-post-event-nested-adapter.js.es6 new file mode 100644 index 00000000..9328701a --- /dev/null +++ b/assets/javascripts/discourse/adapters/discourse-post-event-nested-adapter.js.es6 @@ -0,0 +1,65 @@ +import DiscoursePostEventAdapter from "./discourse-post-event-adapter"; +import { underscore } from "@ember/string"; +import { Result } from "discourse/adapters/rest"; +import { ajax } from "discourse/lib/ajax"; + +export default DiscoursePostEventAdapter.extend({ + // TODO: destroy/update/create should be improved in core to allow for nested models + destroyRecord(store, type, record) { + return ajax( + this.pathFor(store, type, { + post_id: record.post_id, + id: record.id + }), + { + type: "DELETE" + } + ); + }, + + update(store, type, id, attrs) { + const data = {}; + const typeField = underscore(this.apiNameFor(type)); + data[typeField] = attrs; + + return ajax( + this.pathFor(store, type, { id, post_id: attrs.post_id }), + this.getPayload("PUT", data) + ).then(function(json) { + return new Result(json[typeField], json); + }); + }, + + createRecord(store, type, attrs) { + const data = {}; + const typeField = underscore(this.apiNameFor(type)); + data[typeField] = attrs; + return ajax( + this.pathFor(store, type, attrs), + this.getPayload("POST", data) + ).then(function(json) { + return new Result(json[typeField], json); + }); + }, + + pathFor(store, type, findArgs) { + const post_id = findArgs["post_id"]; + delete findArgs["post_id"]; + + const id = findArgs["id"]; + delete findArgs["id"]; + + let path = + this.basePath(store, type, {}) + + "events/" + + post_id + + "/" + + underscore(store.pluralize(this.apiNameFor())); + + if (id) { + path += `/${id}`; + } + + return this.appendQueryParams(path, findArgs); + } +}); diff --git a/assets/javascripts/discourse/adapters/discourse-post-event-reminder.js.es6 b/assets/javascripts/discourse/adapters/discourse-post-event-reminder.js.es6 new file mode 100644 index 00000000..c630a75f --- /dev/null +++ b/assets/javascripts/discourse/adapters/discourse-post-event-reminder.js.es6 @@ -0,0 +1,7 @@ +import DiscoursePostEventNestedAdapter from "./discourse-post-event-nested-adapter"; + +export default DiscoursePostEventNestedAdapter.extend({ + apiNameFor() { + return "reminder"; + } +}); 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 fd225e1c..9b656366 100644 --- a/assets/javascripts/discourse/controllers/discourse-post-event-builder.js.es6 +++ b/assets/javascripts/discourse/controllers/discourse-post-event-builder.js.es6 @@ -1,19 +1,24 @@ -import { set } from "@ember/object"; import TextLib from "discourse/lib/text"; import Group from "discourse/models/group"; import ModalFunctionality from "discourse/mixins/modal-functionality"; import Controller from "@ember/controller"; -import { action, computed } from "@ember/object"; -import { equal } from "@ember/object/computed"; +import { set, action, computed } from "@ember/object"; +import { equal, gte } from "@ember/object/computed"; import { extractError } from "discourse/lib/ajax-error"; import { Promise } from "rsvp"; import { buildParams, replaceRaw } from "../../lib/raw-event-helper"; +const DEFAULT_REMINDER = { value: 15, unit: "minutes", type: "notification" }; + export default Controller.extend(ModalFunctionality, { + reminders: null, + isLoadingReminders: false, + init() { this._super(...arguments); - this._dirtyCustomFields = false; + + this.set("reminderUnits", ["minutes", "hours", "days", "weeks"]); }, modalTitle: computed("model.eventModel.isNew", { @@ -39,9 +44,10 @@ export default Controller.extend(ModalFunctionality, { allowsInvitees: equal("model.eventModel.status", "private"), + addReminderDisabled: gte("reminders.length", 5), + @action onChangeCustomField(field, event) { - this._dirtyCustomFields = true; const value = event.target.value; set(this.model.eventModel.custom_fields, field, value); }, @@ -51,6 +57,30 @@ export default Controller.extend(ModalFunctionality, { this.set("model.eventModel.raw_invitees", newInvitees); }, + @action + removeReminder(reminder) { + this.model.eventModel.reminders.removeObject(reminder); + + if (reminder.id) { + this.set("isLoadingReminders", true); + + this.store + .createRecord("discourse-post-event-reminder", { + id: reminder.id, + post_id: this.model.eventModel.id + }) + .destroyRecord() + .finally(() => this.set("isLoadingReminders", false)); + } + }, + + @action + addReminder() { + this.model.eventModel.reminders.pushObject( + Object.assign({}, DEFAULT_REMINDER) + ); + }, + startsAt: computed("model.eventModel.starts_at", { get() { return this.model.eventModel.starts_at @@ -137,17 +167,13 @@ export default Controller.extend(ModalFunctionality, { updateEvent() { return this.store.find("post", this.model.eventModel.id).then(post => { const promises = []; - if (this._dirtyCustomFields) { - // custom_fields are not stored on the raw and are updated separately - const customFields = this.model.eventModel.getProperties( - "custom_fields" - ); - promises.push( - this.model.eventModel - .update(customFields) - .finally(() => (this._dirtyCustomFields = false)) - ); - } + + // custom_fields are not stored on the raw and are updated separately + const data = this.model.eventModel.getProperties( + "custom_fields", + "reminders" + ); + promises.push(this.model.eventModel.update(data)); const updateRawPromise = new Promise(resolve => { const raw = post.raw; diff --git a/assets/javascripts/discourse/controllers/discourse-post-event-invitees.js.es6 b/assets/javascripts/discourse/controllers/discourse-post-event-invitees.js.es6 index a0468704..87b54070 100644 --- a/assets/javascripts/discourse/controllers/discourse-post-event-invitees.js.es6 +++ b/assets/javascripts/discourse/controllers/discourse-post-event-invitees.js.es6 @@ -19,9 +19,7 @@ export default Controller.extend(ModalFunctionality, { @action removeInvitee(invitee) { - invitee - .destroyRecord({ parent: this.model }) - .then(() => this._fetchInvitees()); + invitee.destroyRecord().then(() => this._fetchInvitees()); }, _fetchInvitees(filter) { diff --git a/assets/javascripts/discourse/models/discourse-post-event-event.js.es6 b/assets/javascripts/discourse/models/discourse-post-event-event.js.es6 index ac14bfe2..fc704146 100644 --- a/assets/javascripts/discourse/models/discourse-post-event-event.js.es6 +++ b/assets/javascripts/discourse/models/discourse-post-event-event.js.es6 @@ -1,4 +1,5 @@ import RestModel from "discourse/models/rest"; +import { ajax } from "discourse/lib/ajax"; const ATTRIBUTES = { id: {}, @@ -27,6 +28,15 @@ const Event = RestModel.extend({ this.__type = "discourse-post-event-event"; }, + update(data) { + return ajax(`/discourse-post-event/events/${this.id}.json`, { + type: "PUT", + dataType: "json", + contentType: "application/json", + data: JSON.stringify({ event: data }) + }); + }, + updateProperties() { const attributesKeys = Object.keys(ATTRIBUTES); return this.getProperties(attributesKeys); diff --git a/assets/javascripts/discourse/models/discourse-post-event-reminder.js.es6 b/assets/javascripts/discourse/models/discourse-post-event-reminder.js.es6 new file mode 100644 index 00000000..c476cb1b --- /dev/null +++ b/assets/javascripts/discourse/models/discourse-post-event-reminder.js.es6 @@ -0,0 +1,9 @@ +import RestModel from "discourse/models/rest"; + +export default RestModel.extend({ + init() { + this._super(...arguments); + + this.__type = "discourse-post-event-reminder"; + } +}); 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 cfb8cd7e..176d6f60 100644 --- a/assets/javascripts/discourse/templates/modal/discourse-post-event-builder.hbs +++ b/assets/javascripts/discourse/templates/modal/discourse-post-event-builder.hbs @@ -77,6 +77,48 @@ }} {{/event-field}} + {{#if model.eventModel.reminders}} + {{#event-field class="reminders" label="discourse_post_event.builder_modal.reminders.label"}} +
+ {{#each model.eventModel.reminders as |reminder|}} +
+ {{input + class="reminder-value" + value=(readonly reminder.value) + placeholderKey="discourse_post_event.builder_modal.name.placeholder" + input=(action (mut reminder.value) value="target.value") + }} + + {{combo-box + class="reminder-unit" + value=reminder.unit + nameProperty=null + valueProperty=null + content=reminderUnits + onChange=(action (mut reminder.unit)) + }} + + {{d-button + class="remove-reminder" + icon="times" + action=(action "removeReminder" reminder) + disabled=isLoadingReminders + }} +
+ {{/each}} +
+ + {{d-button + class="add-reminder" + disabled=addReminderDisabled + icon="plus" + label="discourse_post_event.builder_modal.add_reminder" + action=(action "addReminder") + }} + {{/event-field}} + {{/if}} + + {{#if model.eventModel.custom_fields}} {{#if allowedCustomFields.length}} {{#event-field label="discourse_post_event.builder_modal.custom_fields.label"}}

{{i18n "discourse_post_event.builder_modal.custom_fields.description"}}

@@ -91,6 +133,7 @@ {{/each}} {{/event-field}} {{/if}} + {{/if}} {{/conditional-loading-section}} {{/d-modal-body}} diff --git a/assets/javascripts/initializers/discourse-post-event-decorator.js.es6 b/assets/javascripts/initializers/discourse-post-event-decorator.js.es6 index 05716401..a13336a8 100644 --- a/assets/javascripts/initializers/discourse-post-event-decorator.js.es6 +++ b/assets/javascripts/initializers/discourse-post-event-decorator.js.es6 @@ -218,6 +218,16 @@ function initializeDiscoursePostEventDecorator(api) { "calendar-day" ); + api.replaceIcon( + "notification.discourse_post_event.notifications.before_event_reminder", + "calendar-day" + ); + + api.replaceIcon( + "notification.discourse_post_event.notifications.after_event_reminder", + "calendar-day" + ); + api.modifyClass("controller:topic", { subscribe() { this._super(...arguments); diff --git a/assets/stylesheets/common/discourse-post-event-builder.scss b/assets/stylesheets/common/discourse-post-event-builder.scss index 8f27adf9..7303eff2 100644 --- a/assets/stylesheets/common/discourse-post-event-builder.scss +++ b/assets/stylesheets/common/discourse-post-event-builder.scss @@ -12,6 +12,10 @@ width: 100%; } + .name { + font-size: $font-down-1; + } + &.from { margin-right: 2.65em; } @@ -43,6 +47,16 @@ min-height: 200px; .d-date-time-input-range { + .d-time-input { + .selected-name { + border: 0; + + .name { + font-size: $font-down-1; + } + } + } + .to.d-date-time-input { .pika-single.is-bound { left: -2px !important; @@ -90,7 +104,7 @@ .custom-field-label { font-weight: 500; - margin-bottom: 0.5em; + margin: 0.5em 0 0.25em 0; } .custom-field-input { @@ -132,4 +146,33 @@ } } } + + .event-field.reminders { + display: flex; + + .reminders-list { + display: flex; + flex-direction: column; + margin-bottom: 1em; + + .reminder-item { + display: flex; + flex: 1 0 auto; + padding: 0.25em 0; + + .reminder-value { + width: 80px; + margin-right: 0.5em; + } + + .remove-reminder { + margin-left: auto; + } + } + } + + .add-reminder { + align-self: flex-start; + } + } } diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index d924fc9f..a846436e 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -274,6 +274,8 @@ en: notifications: invite_user_notification: "%{username} has invited you to %{description}" invite_user_auto_notification: "%{username} has automatically set your attendance and invited you to %{description}" + before_event_reminder: "An event is about to start %{description}" + after_event_reminder: "An event has started %{description}" preview: more_than_one_event: You can’t have more than one event. edit_reason: Event updated @@ -338,6 +340,10 @@ en: create: Create update: Save attach: Create event + add_reminder: Add reminder + reminders: + label: Reminders + description: A negative value will be sent after event started to `going` users who didn't visit the event since it started. url: label: URL placeholder: Optional diff --git a/db/migrate/20200809154642_create_reminders_table.rb b/db/migrate/20200809154642_create_reminders_table.rb new file mode 100644 index 00000000..e8dec17f --- /dev/null +++ b/db/migrate/20200809154642_create_reminders_table.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class CreateRemindersTable < ActiveRecord::Migration[6.0] + def change + create_table :discourse_post_event_reminders do |t| + t.integer :post_id, null: false + t.integer :value, null: false, default: 0 + t.integer :mean, null: false, default: 0 + t.string :unit, null: false, default: 'minutes' + end + end +end diff --git a/jobs/regular/discourse_post_event/send_reminder.rb b/jobs/regular/discourse_post_event/send_reminder.rb new file mode 100644 index 00000000..38b54a92 --- /dev/null +++ b/jobs/regular/discourse_post_event/send_reminder.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module Jobs + class DiscoursePostEventSendReminder < ::Jobs::Base + sidekiq_options retry: false + + def execute(args) + raise Discourse::InvalidParameters.new(:reminder_id) if args[:reminder_id].blank? + + reminder = DiscoursePostEvent::Reminder.includes(event: [post: [:topic], invitees: [:user]]).find(args[:reminder_id]) + event = reminder.event + invitees = event.invitees + + unread_notified_user_ids = Notification.where( + read: false, + notification_type: Notification.types[:custom], + topic_id: event.post.topic_id, + post_number: 1 + ).pluck(:user_id) + + invitees + .where(status: DiscoursePostEvent::Invitee.statuses[:going]) + .where.not(user_id: unread_notified_user_ids) + .find_each do |invitee| + invitee.user.notifications.create!( + notification_type: Notification.types[:custom], + topic_id: event.post.topic_id, + post_number: event.post.post_number, + data: { + topic_title: event.post.topic.title, + display_username: invitee.user.username, + message: 'discourse_post_event.notifications.before_event_reminder' + }.to_json + ) + end + end + end +end diff --git a/plugin.rb b/plugin.rb index fb91690e..82fcccbb 100644 --- a/plugin.rb +++ b/plugin.rb @@ -83,8 +83,10 @@ after_initialize do "../app/controllers/discourse_post_event_controller.rb", "../app/controllers/discourse_post_event/invitees_controller.rb", "../app/controllers/discourse_post_event/events_controller.rb", + "../app/controllers/discourse_post_event/reminders_controller.rb", "../app/controllers/discourse_post_event/upcoming_events_controller.rb", "../app/models/discourse_post_event/event.rb", + "../app/models/discourse_post_event/reminder.rb", "../app/models/discourse_post_event/invitee.rb", "../lib/discourse_post_event/event_parser.rb", "../lib/discourse_post_event/event_validator.rb", @@ -92,6 +94,7 @@ after_initialize do "../jobs/regular/discourse_post_event/event_will_start.rb", "../jobs/regular/discourse_post_event/event_started.rb", "../jobs/regular/discourse_post_event/event_ended.rb", + "../jobs/regular/discourse_post_event/send_reminder.rb", "../lib/discourse_post_event/event_finder.rb", "../app/serializers/discourse_post_event/invitee_serializer.rb", "../app/serializers/discourse_post_event/event_serializer.rb" @@ -108,7 +111,7 @@ after_initialize do get '/discourse-post-event/events/:id' => 'events#show' delete '/discourse-post-event/events/:id' => 'events#destroy' post '/discourse-post-event/events' => 'events#create' - put '/discourse-post-event/events/:id' => 'events#update' + put '/discourse-post-event/events/:id' => 'events#update', format: :json post '/discourse-post-event/events/:id/csv-bulk-invite' => 'events#csv_bulk_invite' post '/discourse-post-event/events/:id/bulk-invite' => 'events#bulk_invite', format: :json post '/discourse-post-event/events/:id/invite' => 'events#invite' @@ -116,6 +119,7 @@ after_initialize do post '/discourse-post-event/events/:post_id/invitees' => 'invitees#create' get '/discourse-post-event/events/:post_id/invitees' => 'invitees#index' delete '/discourse-post-event/events/:post_id/invitees/:id' => 'invitees#destroy' + delete '/discourse-post-event/events/:post_id/reminders/:id' => 'reminders#destroy' get '/upcoming-events' => 'upcoming_events#index' end diff --git a/spec/acceptance/post_spec.rb b/spec/acceptance/post_spec.rb index a4acd344..12f9f1dd 100644 --- a/spec/acceptance/post_spec.rb +++ b/spec/acceptance/post_spec.rb @@ -4,16 +4,6 @@ require 'rails_helper' require 'securerandom' require_relative '../fabricators/event_fabricator' -def create_post_with_event(user, extra_raw = '') - start = (Time.now - 10.seconds).utc.iso8601(3) - - PostCreator.create!( - user, - title: "Sell a boat party ##{SecureRandom.alphanumeric}", - raw: "[event start=\"#{start}\" #{extra_raw}]\n[/event]", - ) -end - describe Post do Event ||= DiscoursePostEvent::Event Invitee ||= DiscoursePostEvent::Invitee diff --git a/spec/fabricators/event_fabricator.rb b/spec/fabricators/event_fabricator.rb index 32dbf4cf..6fb5cf56 100644 --- a/spec/fabricators/event_fabricator.rb +++ b/spec/fabricators/event_fabricator.rb @@ -13,3 +13,13 @@ Fabricator(:event, from: 'DiscoursePostEvent::Event') do starts_at { |attrs| attrs[:starts_at] || 1.day.from_now.iso8601 } ends_at { |attrs| attrs[:ends_at] } end + +def create_post_with_event(user, extra_raw = '') + start = (Time.now - 10.seconds).utc.iso8601(3) + + PostCreator.create!( + user, + title: "Sell a boat party ##{SecureRandom.alphanumeric}", + raw: "[event start=\"#{start}\" #{extra_raw}]\n[/event]", + ) +end diff --git a/spec/models/discourse_post_event/event_spec.rb b/spec/models/discourse_post_event/event_spec.rb index 4cb09a47..e9e00f63 100644 --- a/spec/models/discourse_post_event/event_spec.rb +++ b/spec/models/discourse_post_event/event_spec.rb @@ -80,17 +80,17 @@ describe DiscoursePostEvent::Event do Jobs .expects(:cancel_scheduled_job) .with(:discourse_post_event_event_ended, event_id: first_post.id) - .once + .never Jobs .expects(:cancel_scheduled_job) .with(:discourse_post_event_event_started, event_id: first_post.id) - .once + .at_least_once Jobs .expects(:cancel_scheduled_job) .with(:discourse_post_event_event_will_start, event_id: first_post.id) - .once + .at_least_once Event.create!(id: first_post.id, starts_at: starts_at) @@ -131,17 +131,17 @@ describe DiscoursePostEvent::Event do Jobs .expects(:cancel_scheduled_job) .with(:discourse_post_event_event_ended, event_id: first_post.id) - .once + .at_least_once Jobs .expects(:cancel_scheduled_job) .with(:discourse_post_event_event_started, event_id: first_post.id) - .once + .at_least_once Jobs .expects(:cancel_scheduled_job) .with(:discourse_post_event_event_will_start, event_id: first_post.id) - .once + .at_least_once Event.create!(id: first_post.id, starts_at: Time.now - 1.day, ends_at: Time.now + 12.hours) diff --git a/spec/requests/reminders_controller_spec.rb b/spec/requests/reminders_controller_spec.rb new file mode 100644 index 00000000..6f7d4a4f --- /dev/null +++ b/spec/requests/reminders_controller_spec.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +require "rails_helper" +require_relative '../fabricators/event_fabricator' + +module DiscoursePostEvent + describe RemindersController do + let(:admin_1) { Fabricate(:user, admin: true) } + + before do + freeze_time + Jobs.run_immediately! + SiteSetting.calendar_enabled = true + SiteSetting.discourse_post_event_enabled = true + sign_in(admin_1) + end + + context 'destroying a reminder' do + let!(:post_1) { create_post_with_event(admin_1) } + + before do + post_1.reload + post_1.event.reminders.create!(value: 15, unit: 'minutes') + end + + context 'current user is allowed to destroy it' do + it 'detroys the reminder' do + reminders = post_1.event.reminders + reminder = reminders.first + + expect(reminders.count).to eq(1) + Jobs.expects(:cancel_scheduled_job).with(:discourse_post_event_send_reminder, reminder_id: reminder.id).once + + delete "/discourse-post-event/events/#{post_1.id}/reminders/#{reminder.id}.json" + + expect(response.status).to eq(200) + expect(reminders.count).to eq(0) + end + end + + context 'current user is not allowed to destroy it' do + let(:lurker) { Fabricate(:user) } + + before do + sign_in(lurker) + end + + it 'it doesn’t destroy the reminder' do + reminders = post_1.event.reminders + expect(reminders.count).to eq(1) + delete "/discourse-post-event/events/#{post_1.id}/reminders/#{reminders.first.id}.json" + expect(response.status).to eq(403) + expect(reminders.count).to eq(1) + end + end + end + end +end