FEATURE: adds support for custom fields on event

Custom fields are create in the site settings of the event plugin. Once at least one custom field is created, a new form will appear in each event UI. These custom fields are passed when DIscourseEvent triggers of the plugin are called, allowing you to pass custom data of the even to other plugins.
This commit is contained in:
jjaffeux 2020-08-07 17:50:15 +02:00
parent bf31bb359c
commit 6754bdfc8b
15 changed files with 218 additions and 18 deletions

View File

@ -8,5 +8,13 @@ Topic discussing the plugin itself can be found here: [https://meta.discourse.or
#### Plugins
##### Events
- `discourse_post_event_event_will_start` this DiscourseEvent will be triggered one hour before an event starts
- `discourse_post_event_event_started` this DiscourseEvent will be triggered when an event starts
- `discourse_post_event_event_ended` this DiscourseEvent will be triggered when an event ends
#### Custom Fields
Custom fields can be set in plugin settings. Once added a new form will appear on event UI.
These custom fields are available when a plugin event is triggered.

View File

@ -113,6 +113,8 @@ module DiscoursePostEvent
private
def event_params
allowed_custom_fields = SiteSetting.discourse_post_event_allowed_custom_fields.split('|')
params
.require(:event)
.permit(
@ -122,6 +124,7 @@ module DiscoursePostEvent
:ends_at,
:status,
:url,
custom_fields: allowed_custom_fields,
raw_invitees: []
)
end

View File

@ -38,10 +38,15 @@ module DiscoursePostEvent
def setup_starts_at_handler
if !transaction_include_any_action?([:create])
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)
end
if self.starts_at > Time.now
Jobs.enqueue_at(self.starts_at, :discourse_post_event_event_started, event_id: self.id)
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)
end
end
end
@ -102,6 +107,16 @@ module DiscoursePostEvent
end
end
validate :allowed_custom_fields
def allowed_custom_fields
allowed_custom_fields = SiteSetting.discourse_post_event_allowed_custom_fields.split('|')
self.custom_fields.each do |key, value|
if !allowed_custom_fields.include?(key)
errors.add(:base, I18n.t("discourse_post_event.errors.models.event.custom_field_is_invalid", field: key))
end
end
end
def create_invitees(attrs)
timestamp = Time.now
attrs.map! do |attr|
@ -219,6 +234,8 @@ module DiscoursePostEvent
end
def update_with_params!(params)
params[:custom_fields] = (params[:custom_fields] || {}).reject { |_, value| value.blank? }
case params[:status].to_i
when Event.statuses[:private]
raw_invitees = Array(params[:raw_invitees])

View File

@ -18,6 +18,7 @@ module DiscoursePostEvent
attributes :is_expired
attributes :should_display_invitees
attributes :url
attributes :custom_fields
def can_act_on_discourse_post_event
scope.can_act_on_discourse_post_event?(object)

View File

@ -1,3 +1,4 @@
import { set } from "@ember/object";
import TextLib from "discourse/lib/text";
import Group from "discourse/models/group";
import ModalFunctionality from "discourse/mixins/modal-functionality";
@ -5,8 +6,14 @@ import Controller from "@ember/controller";
import { action, computed } from "@ember/object";
import { equal } from "@ember/object/computed";
import { extractError } from "discourse/lib/ajax-error";
import { Promise } from "rsvp";
export default Controller.extend(ModalFunctionality, {
init() {
this._super(...arguments);
this._dirtyCustomFields = false;
},
modalTitle: computed("model.eventModel.isNew", {
get() {
return this.model.eventModel.isNew
@ -15,12 +22,28 @@ export default Controller.extend(ModalFunctionality, {
}
}),
allowedCustomFields: computed(
"siteSettings.discourse_post_event_allowed_custom_fields",
function() {
return this.siteSettings.discourse_post_event_allowed_custom_fields
.split("|")
.filter(Boolean);
}
),
groupFinder(term) {
return Group.findAll({ term, ignore_automatic: true });
},
allowsInvitees: equal("model.eventModel.status", "private"),
@action
onChangeCustomField(field, event) {
this._dirtyCustomFields = true;
const value = event.target.value;
set(this.model.eventModel.custom_fields, field, value);
},
@action
setRawInvitees(_, newInvitees) {
this.set("model.eventModel.raw_invitees", newInvitees);
@ -106,9 +129,23 @@ export default Controller.extend(ModalFunctionality, {
@action
updateEvent() {
const eventParams = this._buildEventParams();
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))
);
}
const updateRawPromise = new Promise(resolve => {
const raw = post.raw;
const eventParams = this._buildEventParams();
const newRaw = this._replaceRawEvent(eventParams, raw);
if (newRaw) {
@ -122,10 +159,16 @@ export default Controller.extend(ModalFunctionality, {
return post
.save(props)
.catch(e => this.flash(extractError(e), "error"))
.then(result => result && this.send("closeModal"));
.then(result => result && this.send("closeModal"))
.finally(resolve);
});
} else {
resolve();
}
});
return Promise.all(promises.concat(updateRawPromise));
});
},
_buildEventParams() {

View File

@ -76,6 +76,21 @@
placeholderKey="topic.invite_private.group_name"
}}
{{/event-field}}
{{#if allowedCustomFields.length}}
{{#event-field label="discourse_post_event.builder_modal.custom_fields.label"}}
<p>{{i18n "discourse_post_event.builder_modal.custom_fields.description"}}</p>
{{#each allowedCustomFields as |allowedCustomField|}}
<span class="label custom-field-label">{{allowedCustomField}}</span>
{{input
class="custom-field-input"
value=(readonly (get model.eventModel.custom_fields allowedCustomField))
placeholderKey="discourse_post_event.builder_modal.name.placeholder"
input=(action "onChangeCustomField" allowedCustomField)
}}
{{/each}}
{{/event-field}}
{{/if}}
</form>
{{/conditional-loading-section}}
{{/d-modal-body}}

View File

@ -88,6 +88,15 @@
flex: 1;
flex-direction: column;
.custom-field-label {
font-weight: 500;
margin-bottom: 0.5em;
}
.custom-field-input {
width: 100%;
}
.radio-label {
display: flex;
align-items: center;

View File

@ -320,6 +320,9 @@ en:
<blockquote>username or group name,desired attendance (going, not_going, interested, unknown)</blockquote>
download_sample_csv: Download a sample CSV file
builder_modal:
custom_fields:
label: Custom Fields
description: Allowed custom fields are defined in site settings. Custom fields are used to transmit data to other plugins.
create_event_title: Create Event
update_event_title: Edit Event
confirm_delete: Are you sure you want to delete this event?

View File

@ -22,6 +22,7 @@ en:
displayed_invitees_limit: "Limits the numbers of invitees displayed on an event."
display_post_event_date_on_topic_title: "Displays the date of the event after the topic title."
discourse_post_event_allowed_on_groups: "Groups that are allowed to create events."
discourse_post_event_allowed_custom_fields: "Allows to let each each event to set the value of custom fields."
holiday_calendar_topic_id: "Topic ID of staffs holiday / absence calendar."
delete_expired_event_posts_after: "Posts with expired events will be automatically deleted after (n) hours. Set to -1 to disable deletion."
all_day_event_start_time: "Events that do not have a start time specified will start at this time. Format is HH:mm. For 6:00 am, enter 06:00"
@ -54,5 +55,6 @@ en:
end_must_be_a_valid_date: "End date must be a valid date."
acting_user_not_allowed_to_create_event: "Current user is not allowed to create events."
acting_user_not_allowed_to_act_on_this_event: "Current user is not allowed to act on this event."
custom_field_is_invalid: "The custom field `%{field}` is not allowed."
name:
length: "Event name length must be between %{minimum} and %{maximum} characters."

View File

@ -62,3 +62,7 @@ discourse_post_event:
discourse_post_event_max_bulk_invitees:
default: 500
hidden: true
discourse_post_event_allowed_custom_fields:
type: list
client: true
default: ""

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
class AddCustomFieldsToEvent < ActiveRecord::Migration[6.0]
def up
add_column :discourse_post_event_events, :custom_fields, :jsonb, null: false, default: {}
end
def down
remove_column :discourse_post_event_events, :custom_fields
end
end

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
module Jobs
class DiscoursePostEventEventWillStart < ::Jobs::Base
sidekiq_options retry: false
def execute(args)
raise Discourse::InvalidParameters.new(:event_id) if args[:event_id].blank?
event = DiscoursePostEvent::Event.find(args[:event_id])
DiscourseEvent.trigger(:discourse_post_event_event_will_start, event)
end
end
end

View File

@ -88,6 +88,7 @@ after_initialize do
"../lib/discourse_post_event/event_parser.rb",
"../lib/discourse_post_event/event_validator.rb",
"../jobs/regular/discourse_post_event/bulk_invite.rb",
"../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",
"../lib/discourse_post_event/event_finder.rb",
@ -517,5 +518,22 @@ after_initialize do
ids
end
end
on(:site_setting_changed) do |name, old_val, new_val|
next if name != :discourse_post_event_allowed_custom_fields
previous_fields = old_val.split('|')
new_fields = new_val.split('|')
removed_fields = previous_fields - new_fields
next if removed_fields.empty?
DiscoursePostEvent::Event.all.find_each do |event|
removed_fields.each do |field|
event.custom_fields.delete(field)
end
event.save
end
end
end
end

View File

@ -0,0 +1,40 @@
# frozen_string_literal: true
require 'rails_helper'
require_relative '../fabricators/event_fabricator'
describe 'discourse_post_event_allowed_custom_fields' do
let(:user_1) { Fabricate(:user, admin: true) }
let(:topic_1) { Fabricate(:topic, user: user_1) }
let(:post_1) { Fabricate(:post, topic: topic_1) }
let(:post_event_1) { Fabricate(:event, post: post_1) }
before do
SiteSetting.discourse_post_event_allowed_custom_fields = 'foo|bar'
post_event_1.update!(custom_fields: {})
SiteSetting.calendar_enabled = true
SiteSetting.discourse_post_event_enabled = true
end
it 'removes the key on the custom fields when removing a key from site setting' do
post_event_1.update!(custom_fields: { foo: 1, bar: 2 })
expect(post_event_1.custom_fields['foo']).to eq(1)
expect(post_event_1.custom_fields['bar']).to eq(2)
DiscourseEvent.trigger(:site_setting_changed, :discourse_post_event_allowed_custom_fields, 'foo|bar', 'foo')
post_event_1.reload
expect(post_event_1.custom_fields['foo']).to eq(1)
expect(post_event_1.custom_fields['bar']).to eq(nil)
end
it 'doesnt set a not allowed key from site setting' do
expect {
post_event_1.update!(custom_fields: { baz: 3 })
}.to raise_error(ActiveRecord::RecordInvalid)
end
end

View File

@ -69,7 +69,8 @@ describe DiscoursePostEvent::Event do
expect {
Event.create!(id: first_post.id, starts_at: Time.now - 1.day)
}.to change {
Jobs::DiscoursePostEventEventStarted.jobs.count
Jobs::DiscoursePostEventEventStarted.jobs.count +
Jobs::DiscoursePostEventEventWillStart.jobs.count
}.by(0)
end
end
@ -86,13 +87,20 @@ describe DiscoursePostEvent::Event do
.with(:discourse_post_event_event_started, event_id: first_post.id)
.once
Jobs
.expects(:cancel_scheduled_job)
.with(:discourse_post_event_event_will_start, event_id: first_post.id)
.once
Event.create!(id: first_post.id, starts_at: starts_at)
expect(Jobs::DiscoursePostEventEventStarted.jobs.count).to eq(1)
expect(Jobs::DiscoursePostEventEventWillStart.jobs.count).to eq(0)
Event.find(first_post.id).update!(starts_at: Time.now + 2.hours)
expect(Jobs::DiscoursePostEventEventStarted.jobs.count).to eq(2)
expect(Jobs::DiscoursePostEventEventWillStart.jobs.count).to eq(1)
end
end
end
@ -130,6 +138,11 @@ describe DiscoursePostEvent::Event do
.with(:discourse_post_event_event_started, event_id: first_post.id)
.once
Jobs
.expects(:cancel_scheduled_job)
.with(:discourse_post_event_event_will_start, event_id: first_post.id)
.once
Event.create!(id: first_post.id, starts_at: Time.now - 1.day, ends_at: Time.now + 12.hours)
expect(Jobs::DiscoursePostEventEventEnded.jobs.count).to eq(1)