FEATURE: allows to set reminder
Note that reminders; as custom fields, are only editable on created event and not when creating it.
This commit is contained in:
parent
b359e9a23f
commit
da91f7417d
|
@ -36,7 +36,7 @@ module DiscoursePostEvent
|
||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
event = Event.find(params[:id])
|
event = Event.includes(:reminders).find(params[:id])
|
||||||
guardian.ensure_can_see!(event.post)
|
guardian.ensure_can_see!(event.post)
|
||||||
serializer = EventSerializer.new(event, scope: guardian)
|
serializer = EventSerializer.new(event, scope: guardian)
|
||||||
render_json_dump(serializer)
|
render_json_dump(serializer)
|
||||||
|
@ -56,6 +56,7 @@ module DiscoursePostEvent
|
||||||
guardian.ensure_can_edit!(event.post)
|
guardian.ensure_can_edit!(event.post)
|
||||||
guardian.ensure_can_act_on_discourse_post_event!(event)
|
guardian.ensure_can_act_on_discourse_post_event!(event)
|
||||||
event.update_with_params!(event_params)
|
event.update_with_params!(event_params)
|
||||||
|
event.update_reminders!(event_reminders_params)
|
||||||
serializer = EventSerializer.new(event, scope: guardian)
|
serializer = EventSerializer.new(event, scope: guardian)
|
||||||
render_json_dump(serializer)
|
render_json_dump(serializer)
|
||||||
end
|
end
|
||||||
|
@ -131,6 +132,10 @@ module DiscoursePostEvent
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def event_reminders_params
|
||||||
|
Array(params.require(:event).permit(reminders: [:id, :value, :unit])[:reminders])
|
||||||
|
end
|
||||||
|
|
||||||
def event_params
|
def event_params
|
||||||
allowed_custom_fields = SiteSetting.discourse_post_event_allowed_custom_fields.split('|')
|
allowed_custom_fields = SiteSetting.discourse_post_event_allowed_custom_fields.split('|')
|
||||||
|
|
||||||
|
@ -144,7 +149,7 @@ module DiscoursePostEvent
|
||||||
:status,
|
:status,
|
||||||
:url,
|
:url,
|
||||||
custom_fields: allowed_custom_fields,
|
custom_fields: allowed_custom_fields,
|
||||||
raw_invitees: []
|
raw_invitees: [],
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
@ -34,34 +34,41 @@ module DiscoursePostEvent
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
after_commit :setup_starts_at_handler, on: [:create, :update]
|
after_commit :setup_handlers, on: [:create, :update]
|
||||||
def setup_starts_at_handler
|
def setup_handlers
|
||||||
if !transaction_include_any_action?([:create])
|
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_started, event_id: self.id)
|
||||||
Jobs.cancel_scheduled_job(:discourse_post_event_event_will_start, event_id: self.id)
|
Jobs.cancel_scheduled_job(:discourse_post_event_event_will_start, event_id: self.id)
|
||||||
end
|
|
||||||
|
|
||||||
if self.starts_at > Time.now
|
if new_starts_at > Time.now
|
||||||
Jobs.enqueue_at(self.starts_at, :discourse_post_event_event_started, event_id: self.id)
|
Jobs.enqueue_at(new_starts_at, :discourse_post_event_event_started, event_id: self.id)
|
||||||
|
|
||||||
if self.starts_at - 1.hour > Time.now
|
will_start_at = new_starts_at - 1.hour
|
||||||
Jobs.enqueue_at(self.starts_at - 1.hour, :discourse_post_event_event_will_start, event_id: self.id)
|
if will_start_at > Time.now
|
||||||
end
|
Jobs.enqueue_at(will_start_at, :discourse_post_event_event_will_start, event_id: self.id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
after_commit :setup_ends_at_handler, on: [:create, :update]
|
self.refresh_reminders!
|
||||||
def setup_ends_at_handler
|
end
|
||||||
if !transaction_include_any_action?([:create])
|
|
||||||
|
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)
|
Jobs.cancel_scheduled_job(:discourse_post_event_event_ended, event_id: self.id)
|
||||||
end
|
|
||||||
|
|
||||||
if self.ends_at && self.ends_at > Time.now
|
if new_ends_at && new_ends_at > Time.now
|
||||||
Jobs.enqueue_at(self.ends_at, :discourse_post_event_event_ended, event_id: self.id)
|
Jobs.enqueue_at(new_ends_at, :discourse_post_event_event_ended, event_id: self.id)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
has_many :invitees, foreign_key: :post_id, dependent: :delete_all
|
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
|
belongs_to :post, foreign_key: :id
|
||||||
|
|
||||||
scope :visible, -> { where(deleted_at: nil) }
|
scope :visible, -> { where(deleted_at: nil) }
|
||||||
|
@ -219,14 +226,12 @@ module DiscoursePostEvent
|
||||||
|
|
||||||
if events.present?
|
if events.present?
|
||||||
event_params = events.first
|
event_params = events.first
|
||||||
|
|
||||||
event = post.event || DiscoursePostEvent::Event.new(id: post.id)
|
event = post.event || DiscoursePostEvent::Event.new(id: post.id)
|
||||||
|
|
||||||
params = {
|
params = {
|
||||||
name: event_params[:name],
|
name: event_params[:name],
|
||||||
starts_at: event_params[:start] || event.starts_at,
|
starts_at: event_params[:start] || event.starts_at,
|
||||||
ends_at: event_params[:end],
|
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,
|
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
|
raw_invitees: event_params[:"allowed-groups"] ? event_params[:"allowed-groups"].split(',') : nil
|
||||||
}
|
}
|
||||||
|
@ -254,5 +259,20 @@ module DiscoursePostEvent
|
||||||
|
|
||||||
self.publish_update!
|
self.publish_update!
|
||||||
end
|
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -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
|
|
@ -22,6 +22,7 @@ module DiscoursePostEvent
|
||||||
attributes :is_public
|
attributes :is_public
|
||||||
attributes :is_private
|
attributes :is_private
|
||||||
attributes :is_standalone
|
attributes :is_standalone
|
||||||
|
attributes :reminders
|
||||||
|
|
||||||
def can_act_on_discourse_post_event
|
def can_act_on_discourse_post_event
|
||||||
scope.can_act_on_discourse_post_event?(object)
|
scope.can_act_on_discourse_post_event?(object)
|
||||||
|
|
|
@ -1,68 +1,6 @@
|
||||||
import { Result } from "discourse/adapters/rest";
|
import DiscoursePostEventNestedAdapter from "./discourse-post-event-nested-adapter";
|
||||||
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);
|
|
||||||
},
|
|
||||||
|
|
||||||
|
export default DiscoursePostEventNestedAdapter.extend({
|
||||||
apiNameFor() {
|
apiNameFor() {
|
||||||
return "invitee";
|
return "invitee";
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,7 @@
|
||||||
|
import DiscoursePostEventNestedAdapter from "./discourse-post-event-nested-adapter";
|
||||||
|
|
||||||
|
export default DiscoursePostEventNestedAdapter.extend({
|
||||||
|
apiNameFor() {
|
||||||
|
return "reminder";
|
||||||
|
}
|
||||||
|
});
|
|
@ -1,19 +1,24 @@
|
||||||
import { set } from "@ember/object";
|
|
||||||
import TextLib from "discourse/lib/text";
|
import TextLib from "discourse/lib/text";
|
||||||
import Group from "discourse/models/group";
|
import Group from "discourse/models/group";
|
||||||
import ModalFunctionality from "discourse/mixins/modal-functionality";
|
import ModalFunctionality from "discourse/mixins/modal-functionality";
|
||||||
import Controller from "@ember/controller";
|
import Controller from "@ember/controller";
|
||||||
import { action, computed } from "@ember/object";
|
import { set, action, computed } from "@ember/object";
|
||||||
import { equal } from "@ember/object/computed";
|
import { equal, gte } from "@ember/object/computed";
|
||||||
import { extractError } from "discourse/lib/ajax-error";
|
import { extractError } from "discourse/lib/ajax-error";
|
||||||
import { Promise } from "rsvp";
|
import { Promise } from "rsvp";
|
||||||
|
|
||||||
import { buildParams, replaceRaw } from "../../lib/raw-event-helper";
|
import { buildParams, replaceRaw } from "../../lib/raw-event-helper";
|
||||||
|
|
||||||
|
const DEFAULT_REMINDER = { value: 15, unit: "minutes", type: "notification" };
|
||||||
|
|
||||||
export default Controller.extend(ModalFunctionality, {
|
export default Controller.extend(ModalFunctionality, {
|
||||||
|
reminders: null,
|
||||||
|
isLoadingReminders: false,
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
this._super(...arguments);
|
this._super(...arguments);
|
||||||
this._dirtyCustomFields = false;
|
|
||||||
|
this.set("reminderUnits", ["minutes", "hours", "days", "weeks"]);
|
||||||
},
|
},
|
||||||
|
|
||||||
modalTitle: computed("model.eventModel.isNew", {
|
modalTitle: computed("model.eventModel.isNew", {
|
||||||
|
@ -39,9 +44,10 @@ export default Controller.extend(ModalFunctionality, {
|
||||||
|
|
||||||
allowsInvitees: equal("model.eventModel.status", "private"),
|
allowsInvitees: equal("model.eventModel.status", "private"),
|
||||||
|
|
||||||
|
addReminderDisabled: gte("reminders.length", 5),
|
||||||
|
|
||||||
@action
|
@action
|
||||||
onChangeCustomField(field, event) {
|
onChangeCustomField(field, event) {
|
||||||
this._dirtyCustomFields = true;
|
|
||||||
const value = event.target.value;
|
const value = event.target.value;
|
||||||
set(this.model.eventModel.custom_fields, field, 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);
|
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", {
|
startsAt: computed("model.eventModel.starts_at", {
|
||||||
get() {
|
get() {
|
||||||
return this.model.eventModel.starts_at
|
return this.model.eventModel.starts_at
|
||||||
|
@ -137,17 +167,13 @@ export default Controller.extend(ModalFunctionality, {
|
||||||
updateEvent() {
|
updateEvent() {
|
||||||
return this.store.find("post", this.model.eventModel.id).then(post => {
|
return this.store.find("post", this.model.eventModel.id).then(post => {
|
||||||
const promises = [];
|
const promises = [];
|
||||||
if (this._dirtyCustomFields) {
|
|
||||||
// custom_fields are not stored on the raw and are updated separately
|
// custom_fields are not stored on the raw and are updated separately
|
||||||
const customFields = this.model.eventModel.getProperties(
|
const data = this.model.eventModel.getProperties(
|
||||||
"custom_fields"
|
"custom_fields",
|
||||||
|
"reminders"
|
||||||
);
|
);
|
||||||
promises.push(
|
promises.push(this.model.eventModel.update(data));
|
||||||
this.model.eventModel
|
|
||||||
.update(customFields)
|
|
||||||
.finally(() => (this._dirtyCustomFields = false))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateRawPromise = new Promise(resolve => {
|
const updateRawPromise = new Promise(resolve => {
|
||||||
const raw = post.raw;
|
const raw = post.raw;
|
||||||
|
|
|
@ -19,9 +19,7 @@ export default Controller.extend(ModalFunctionality, {
|
||||||
|
|
||||||
@action
|
@action
|
||||||
removeInvitee(invitee) {
|
removeInvitee(invitee) {
|
||||||
invitee
|
invitee.destroyRecord().then(() => this._fetchInvitees());
|
||||||
.destroyRecord({ parent: this.model })
|
|
||||||
.then(() => this._fetchInvitees());
|
|
||||||
},
|
},
|
||||||
|
|
||||||
_fetchInvitees(filter) {
|
_fetchInvitees(filter) {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import RestModel from "discourse/models/rest";
|
import RestModel from "discourse/models/rest";
|
||||||
|
import { ajax } from "discourse/lib/ajax";
|
||||||
|
|
||||||
const ATTRIBUTES = {
|
const ATTRIBUTES = {
|
||||||
id: {},
|
id: {},
|
||||||
|
@ -27,6 +28,15 @@ const Event = RestModel.extend({
|
||||||
this.__type = "discourse-post-event-event";
|
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() {
|
updateProperties() {
|
||||||
const attributesKeys = Object.keys(ATTRIBUTES);
|
const attributesKeys = Object.keys(ATTRIBUTES);
|
||||||
return this.getProperties(attributesKeys);
|
return this.getProperties(attributesKeys);
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
import RestModel from "discourse/models/rest";
|
||||||
|
|
||||||
|
export default RestModel.extend({
|
||||||
|
init() {
|
||||||
|
this._super(...arguments);
|
||||||
|
|
||||||
|
this.__type = "discourse-post-event-reminder";
|
||||||
|
}
|
||||||
|
});
|
|
@ -77,6 +77,48 @@
|
||||||
}}
|
}}
|
||||||
{{/event-field}}
|
{{/event-field}}
|
||||||
|
|
||||||
|
{{#if model.eventModel.reminders}}
|
||||||
|
{{#event-field class="reminders" label="discourse_post_event.builder_modal.reminders.label"}}
|
||||||
|
<div class="reminders-list">
|
||||||
|
{{#each model.eventModel.reminders as |reminder|}}
|
||||||
|
<div class="reminder-item">
|
||||||
|
{{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
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{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}}
|
{{#if allowedCustomFields.length}}
|
||||||
{{#event-field label="discourse_post_event.builder_modal.custom_fields.label"}}
|
{{#event-field label="discourse_post_event.builder_modal.custom_fields.label"}}
|
||||||
<p>{{i18n "discourse_post_event.builder_modal.custom_fields.description"}}</p>
|
<p>{{i18n "discourse_post_event.builder_modal.custom_fields.description"}}</p>
|
||||||
|
@ -91,6 +133,7 @@
|
||||||
{{/each}}
|
{{/each}}
|
||||||
{{/event-field}}
|
{{/event-field}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
{{/if}}
|
||||||
</form>
|
</form>
|
||||||
{{/conditional-loading-section}}
|
{{/conditional-loading-section}}
|
||||||
{{/d-modal-body}}
|
{{/d-modal-body}}
|
||||||
|
|
|
@ -218,6 +218,16 @@ function initializeDiscoursePostEventDecorator(api) {
|
||||||
"calendar-day"
|
"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", {
|
api.modifyClass("controller:topic", {
|
||||||
subscribe() {
|
subscribe() {
|
||||||
this._super(...arguments);
|
this._super(...arguments);
|
||||||
|
|
|
@ -12,6 +12,10 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
font-size: $font-down-1;
|
||||||
|
}
|
||||||
|
|
||||||
&.from {
|
&.from {
|
||||||
margin-right: 2.65em;
|
margin-right: 2.65em;
|
||||||
}
|
}
|
||||||
|
@ -43,6 +47,16 @@
|
||||||
min-height: 200px;
|
min-height: 200px;
|
||||||
|
|
||||||
.d-date-time-input-range {
|
.d-date-time-input-range {
|
||||||
|
.d-time-input {
|
||||||
|
.selected-name {
|
||||||
|
border: 0;
|
||||||
|
|
||||||
|
.name {
|
||||||
|
font-size: $font-down-1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.to.d-date-time-input {
|
.to.d-date-time-input {
|
||||||
.pika-single.is-bound {
|
.pika-single.is-bound {
|
||||||
left: -2px !important;
|
left: -2px !important;
|
||||||
|
@ -90,7 +104,7 @@
|
||||||
|
|
||||||
.custom-field-label {
|
.custom-field-label {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
margin-bottom: 0.5em;
|
margin: 0.5em 0 0.25em 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-field-input {
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -274,6 +274,8 @@ en:
|
||||||
notifications:
|
notifications:
|
||||||
invite_user_notification: "%{username} has invited you to %{description}"
|
invite_user_notification: "%{username} has invited you to %{description}"
|
||||||
invite_user_auto_notification: "%{username} has automatically set your attendance and 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:
|
preview:
|
||||||
more_than_one_event: You can’t have more than one event.
|
more_than_one_event: You can’t have more than one event.
|
||||||
edit_reason: Event updated
|
edit_reason: Event updated
|
||||||
|
@ -338,6 +340,10 @@ en:
|
||||||
create: Create
|
create: Create
|
||||||
update: Save
|
update: Save
|
||||||
attach: Create event
|
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:
|
url:
|
||||||
label: URL
|
label: URL
|
||||||
placeholder: Optional
|
placeholder: Optional
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -83,8 +83,10 @@ after_initialize do
|
||||||
"../app/controllers/discourse_post_event_controller.rb",
|
"../app/controllers/discourse_post_event_controller.rb",
|
||||||
"../app/controllers/discourse_post_event/invitees_controller.rb",
|
"../app/controllers/discourse_post_event/invitees_controller.rb",
|
||||||
"../app/controllers/discourse_post_event/events_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/controllers/discourse_post_event/upcoming_events_controller.rb",
|
||||||
"../app/models/discourse_post_event/event.rb",
|
"../app/models/discourse_post_event/event.rb",
|
||||||
|
"../app/models/discourse_post_event/reminder.rb",
|
||||||
"../app/models/discourse_post_event/invitee.rb",
|
"../app/models/discourse_post_event/invitee.rb",
|
||||||
"../lib/discourse_post_event/event_parser.rb",
|
"../lib/discourse_post_event/event_parser.rb",
|
||||||
"../lib/discourse_post_event/event_validator.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_will_start.rb",
|
||||||
"../jobs/regular/discourse_post_event/event_started.rb",
|
"../jobs/regular/discourse_post_event/event_started.rb",
|
||||||
"../jobs/regular/discourse_post_event/event_ended.rb",
|
"../jobs/regular/discourse_post_event/event_ended.rb",
|
||||||
|
"../jobs/regular/discourse_post_event/send_reminder.rb",
|
||||||
"../lib/discourse_post_event/event_finder.rb",
|
"../lib/discourse_post_event/event_finder.rb",
|
||||||
"../app/serializers/discourse_post_event/invitee_serializer.rb",
|
"../app/serializers/discourse_post_event/invitee_serializer.rb",
|
||||||
"../app/serializers/discourse_post_event/event_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'
|
get '/discourse-post-event/events/:id' => 'events#show'
|
||||||
delete '/discourse-post-event/events/:id' => 'events#destroy'
|
delete '/discourse-post-event/events/:id' => 'events#destroy'
|
||||||
post '/discourse-post-event/events' => 'events#create'
|
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/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/bulk-invite' => 'events#bulk_invite', format: :json
|
||||||
post '/discourse-post-event/events/:id/invite' => 'events#invite'
|
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'
|
post '/discourse-post-event/events/:post_id/invitees' => 'invitees#create'
|
||||||
get '/discourse-post-event/events/:post_id/invitees' => 'invitees#index'
|
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/invitees/:id' => 'invitees#destroy'
|
||||||
|
delete '/discourse-post-event/events/:post_id/reminders/:id' => 'reminders#destroy'
|
||||||
get '/upcoming-events' => 'upcoming_events#index'
|
get '/upcoming-events' => 'upcoming_events#index'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -4,16 +4,6 @@ require 'rails_helper'
|
||||||
require 'securerandom'
|
require 'securerandom'
|
||||||
require_relative '../fabricators/event_fabricator'
|
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
|
describe Post do
|
||||||
Event ||= DiscoursePostEvent::Event
|
Event ||= DiscoursePostEvent::Event
|
||||||
Invitee ||= DiscoursePostEvent::Invitee
|
Invitee ||= DiscoursePostEvent::Invitee
|
||||||
|
|
|
@ -13,3 +13,13 @@ Fabricator(:event, from: 'DiscoursePostEvent::Event') do
|
||||||
starts_at { |attrs| attrs[:starts_at] || 1.day.from_now.iso8601 }
|
starts_at { |attrs| attrs[:starts_at] || 1.day.from_now.iso8601 }
|
||||||
ends_at { |attrs| attrs[:ends_at] }
|
ends_at { |attrs| attrs[:ends_at] }
|
||||||
end
|
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
|
||||||
|
|
|
@ -80,17 +80,17 @@ describe DiscoursePostEvent::Event do
|
||||||
Jobs
|
Jobs
|
||||||
.expects(:cancel_scheduled_job)
|
.expects(:cancel_scheduled_job)
|
||||||
.with(:discourse_post_event_event_ended, event_id: first_post.id)
|
.with(:discourse_post_event_event_ended, event_id: first_post.id)
|
||||||
.once
|
.never
|
||||||
|
|
||||||
Jobs
|
Jobs
|
||||||
.expects(:cancel_scheduled_job)
|
.expects(:cancel_scheduled_job)
|
||||||
.with(:discourse_post_event_event_started, event_id: first_post.id)
|
.with(:discourse_post_event_event_started, event_id: first_post.id)
|
||||||
.once
|
.at_least_once
|
||||||
|
|
||||||
Jobs
|
Jobs
|
||||||
.expects(:cancel_scheduled_job)
|
.expects(:cancel_scheduled_job)
|
||||||
.with(:discourse_post_event_event_will_start, event_id: first_post.id)
|
.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)
|
Event.create!(id: first_post.id, starts_at: starts_at)
|
||||||
|
|
||||||
|
@ -131,17 +131,17 @@ describe DiscoursePostEvent::Event do
|
||||||
Jobs
|
Jobs
|
||||||
.expects(:cancel_scheduled_job)
|
.expects(:cancel_scheduled_job)
|
||||||
.with(:discourse_post_event_event_ended, event_id: first_post.id)
|
.with(:discourse_post_event_event_ended, event_id: first_post.id)
|
||||||
.once
|
.at_least_once
|
||||||
|
|
||||||
Jobs
|
Jobs
|
||||||
.expects(:cancel_scheduled_job)
|
.expects(:cancel_scheduled_job)
|
||||||
.with(:discourse_post_event_event_started, event_id: first_post.id)
|
.with(:discourse_post_event_event_started, event_id: first_post.id)
|
||||||
.once
|
.at_least_once
|
||||||
|
|
||||||
Jobs
|
Jobs
|
||||||
.expects(:cancel_scheduled_job)
|
.expects(:cancel_scheduled_job)
|
||||||
.with(:discourse_post_event_event_will_start, event_id: first_post.id)
|
.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)
|
Event.create!(id: first_post.id, starts_at: Time.now - 1.day, ends_at: Time.now + 12.hours)
|
||||||
|
|
||||||
|
|
|
@ -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
|
Loading…
Reference in New Issue