UX: events are now managed through md markup
This commit is contained in:
parent
1213b09932
commit
d12d45d247
|
@ -55,21 +55,7 @@ module DiscoursePostEvent
|
||||||
event = Event.find(params[:id])
|
event = Event.find(params[:id])
|
||||||
guardian.ensure_can_edit!(event.post)
|
guardian.ensure_can_edit!(event.post)
|
||||||
guardian.ensure_can_act_on_event!(event)
|
guardian.ensure_can_act_on_event!(event)
|
||||||
event.enforce_utc!(event_params)
|
event.update_with_params!(event_params)
|
||||||
|
|
||||||
case event_params[:status].to_i
|
|
||||||
when Event.statuses[:private]
|
|
||||||
raw_invitees = Array(event_params[:raw_invitees])
|
|
||||||
event.update!(event_params.merge(raw_invitees: raw_invitees))
|
|
||||||
event.enforce_raw_invitees!
|
|
||||||
when Event.statuses[:public]
|
|
||||||
event.update!(event_params.merge(raw_invitees: []))
|
|
||||||
when Event.statuses[:standalone]
|
|
||||||
event.update!(event_params.merge(raw_invitees: []))
|
|
||||||
event.invitees.destroy_all
|
|
||||||
end
|
|
||||||
|
|
||||||
event.publish_update!
|
|
||||||
serializer = EventSerializer.new(event, scope: guardian)
|
serializer = EventSerializer.new(event, scope: guardian)
|
||||||
render_json_dump(serializer)
|
render_json_dump(serializer)
|
||||||
end
|
end
|
||||||
|
|
|
@ -42,8 +42,10 @@ module DiscoursePostEvent
|
||||||
|
|
||||||
validates :starts_at, presence: true
|
validates :starts_at, presence: true
|
||||||
|
|
||||||
|
MIN_NAME_LENGTH = 5
|
||||||
|
MAX_NAME_LENGTH = 30
|
||||||
validates :name,
|
validates :name,
|
||||||
length: { in: 5..30 },
|
length: { in: MIN_NAME_LENGTH..MAX_NAME_LENGTH },
|
||||||
unless: -> (event) { event.name.blank? }
|
unless: -> (event) { event.name.blank? }
|
||||||
|
|
||||||
validate :raw_invitees_length
|
validate :raw_invitees_length
|
||||||
|
@ -148,11 +150,11 @@ module DiscoursePostEvent
|
||||||
end
|
end
|
||||||
|
|
||||||
def enforce_utc!(params)
|
def enforce_utc!(params)
|
||||||
if params['starts_at'].present?
|
if params[:starts_at].present?
|
||||||
params['starts_at'] = Time.parse(params['starts_at']).utc
|
params[:starts_at] = Time.parse(params[:starts_at]).utc
|
||||||
end
|
end
|
||||||
if params['ends_at'].present?
|
if params[:ends_at].present?
|
||||||
params['ends_at'] = Time.parse(params['ends_at']).utc
|
params[:ends_at] = Time.parse(params[:ends_at]).utc
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -171,5 +173,40 @@ module DiscoursePostEvent
|
||||||
def is_expired?
|
def is_expired?
|
||||||
Time.now > (self.ends_at || self.starts_at || Time.now)
|
Time.now > (self.ends_at || self.starts_at || Time.now)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.update_from_raw(post)
|
||||||
|
events = DiscoursePostEvent::EventParser.extract_events(post.raw)
|
||||||
|
if events.present?
|
||||||
|
event_params = events.first
|
||||||
|
event = post.event || Event.new(id: post.id)
|
||||||
|
params = {
|
||||||
|
name: event_params[:name] || event.name,
|
||||||
|
starts_at: event_params[:start] || event.starts_at,
|
||||||
|
ends_at: event_params[:end] || event.ends_at,
|
||||||
|
status: event_params[:status].present? ? Event.statuses[event_params[:status].to_sym] : event.status,
|
||||||
|
raw_invitees: event_params[:allowedGroups] ? event_params[:allowedGroups].split(',') : nil
|
||||||
|
}
|
||||||
|
event.enforce_utc!(params)
|
||||||
|
event.update_with_params!(params)
|
||||||
|
elsif post.event
|
||||||
|
post.event.destroy
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_with_params!(params)
|
||||||
|
case params[:status].to_i
|
||||||
|
when Event.statuses[:private]
|
||||||
|
raw_invitees = Array(params[:raw_invitees])
|
||||||
|
self.update!(params.merge(raw_invitees: raw_invitees))
|
||||||
|
self.enforce_raw_invitees!
|
||||||
|
when Event.statuses[:public]
|
||||||
|
self.update!(params.merge(raw_invitees: []))
|
||||||
|
when Event.statuses[:standalone]
|
||||||
|
self.update!(params.merge(raw_invitees: []))
|
||||||
|
self.invitees.destroy_all
|
||||||
|
end
|
||||||
|
|
||||||
|
self.publish_update!
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
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";
|
||||||
|
@ -58,14 +59,32 @@ export default Controller.extend(ModalFunctionality, {
|
||||||
@action
|
@action
|
||||||
destroyPostEvent() {
|
destroyPostEvent() {
|
||||||
bootbox.confirm(
|
bootbox.confirm(
|
||||||
I18n.t("event.ui_builder.confirm_delete"),
|
I18n.t("discourse_post_event.builder_modal.confirm_delete"),
|
||||||
I18n.t("no_value"),
|
I18n.t("no_value"),
|
||||||
I18n.t("yes_value"),
|
I18n.t("yes_value"),
|
||||||
confirmed => {
|
confirmed => {
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
this.model.eventModel
|
return this.store
|
||||||
.destroyRecord()
|
.find("post", this.model.eventModel.id)
|
||||||
.then(() => this.send("closeModal"));
|
.then(post => {
|
||||||
|
const raw = post.raw;
|
||||||
|
const newRaw = this._removeRawEvent(raw);
|
||||||
|
|
||||||
|
if (newRaw) {
|
||||||
|
const props = {
|
||||||
|
raw: newRaw,
|
||||||
|
edit_reason: I18n.t("discourse_post_event.destroy_event")
|
||||||
|
};
|
||||||
|
|
||||||
|
return TextLib.cookAsync(newRaw).then(cooked => {
|
||||||
|
props.cooked = cooked.string;
|
||||||
|
return post
|
||||||
|
.save(props)
|
||||||
|
.catch(e => this.flash(extractError(e), "error"))
|
||||||
|
.then(result => result && this.send("closeModal"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -73,17 +92,93 @@ export default Controller.extend(ModalFunctionality, {
|
||||||
|
|
||||||
@action
|
@action
|
||||||
createEvent() {
|
createEvent() {
|
||||||
this.model.eventModel
|
if (!this.startsAt) {
|
||||||
.save()
|
this.send("closeModal");
|
||||||
.then(() => this.send("closeModal"))
|
return;
|
||||||
.catch(e => this.flash(extractError(e), "error"));
|
}
|
||||||
|
|
||||||
|
const eventParams = this._buildEventParams();
|
||||||
|
const markdownParams = [];
|
||||||
|
Object.keys(eventParams).forEach(key => {
|
||||||
|
markdownParams.push(`${key}="${eventParams[key]}"`);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.toolbarEvent.addText(
|
||||||
|
`[wrap=event ${markdownParams.join(" ")}]\n[/wrap]`
|
||||||
|
);
|
||||||
|
this.send("closeModal");
|
||||||
},
|
},
|
||||||
|
|
||||||
@action
|
@action
|
||||||
updateEvent() {
|
updateEvent() {
|
||||||
this.model.eventModel
|
const eventParams = this._buildEventParams();
|
||||||
.save()
|
return this.store.find("post", this.model.eventModel.id).then(post => {
|
||||||
.then(() => this.send("closeModal"))
|
const raw = post.raw;
|
||||||
.catch(e => this.flash(extractError(e), "error"));
|
const newRaw = this._replaceRawEvent(eventParams, raw);
|
||||||
|
|
||||||
|
if (newRaw) {
|
||||||
|
const props = {
|
||||||
|
raw: newRaw,
|
||||||
|
edit_reason: I18n.t("discourse_post_event.update_event")
|
||||||
|
};
|
||||||
|
|
||||||
|
return TextLib.cookAsync(newRaw).then(cooked => {
|
||||||
|
props.cooked = cooked.string;
|
||||||
|
return post
|
||||||
|
.save(props)
|
||||||
|
.catch(e => this.flash(extractError(e), "error"))
|
||||||
|
.then(result => result && this.send("closeModal"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_buildEventParams() {
|
||||||
|
const eventParams = {
|
||||||
|
start: this.startsAt,
|
||||||
|
status: this.model.eventModel.status,
|
||||||
|
name: this.model.eventModel.name
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.endsAt) {
|
||||||
|
eventParams.end = this.endsAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.model.eventModel.status === "private") {
|
||||||
|
eventParams.allowedGroups = (
|
||||||
|
this.model.eventModel.raw_invitees || []
|
||||||
|
).join(",");
|
||||||
|
}
|
||||||
|
|
||||||
|
return eventParams;
|
||||||
|
},
|
||||||
|
|
||||||
|
_removeRawEvent(raw) {
|
||||||
|
const eventRegex = new RegExp(
|
||||||
|
`\\[wrap=event\\s(.*?)\\]\\n\\[\\/wrap\\]`,
|
||||||
|
"m"
|
||||||
|
);
|
||||||
|
|
||||||
|
return raw.replace(eventRegex, "");
|
||||||
|
},
|
||||||
|
|
||||||
|
_replaceRawEvent(eventparams, raw) {
|
||||||
|
const eventRegex = new RegExp(`\\[wrap=event\\s(.*?)\\]`, "m");
|
||||||
|
const eventMatches = raw.match(eventRegex);
|
||||||
|
|
||||||
|
if (eventMatches && eventMatches[1]) {
|
||||||
|
const markdownParams = [];
|
||||||
|
const eventParams = this._buildEventParams();
|
||||||
|
Object.keys(eventParams).forEach(eventParam =>
|
||||||
|
markdownParams.push(`${eventParam}="${eventParams[eventParam]}"`)
|
||||||
|
);
|
||||||
|
|
||||||
|
return raw.replace(
|
||||||
|
eventRegex,
|
||||||
|
`[wrap=event ${markdownParams.join(" ")}]`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -19,19 +19,6 @@
|
||||||
}}
|
}}
|
||||||
|
|
||||||
{{#event-field label="discourse_post_event.builder_modal.status.label"}}
|
{{#event-field label="discourse_post_event.builder_modal.status.label"}}
|
||||||
<label class="radio-label">
|
|
||||||
{{radio-button
|
|
||||||
name="status"
|
|
||||||
value="standalone"
|
|
||||||
selection=model.eventModel.status
|
|
||||||
onChange=(action (mut model.eventModel.status))
|
|
||||||
}}
|
|
||||||
<span class="message">
|
|
||||||
<span class="title">{{i18n "discourse_post_event.models.event.status.standalone.title"}}</span>
|
|
||||||
<span class="description">{{i18n "discourse_post_event.models.event.status.standalone.description"}}</span>
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="radio-label">
|
<label class="radio-label">
|
||||||
{{radio-button
|
{{radio-button
|
||||||
name="status"
|
name="status"
|
||||||
|
@ -56,6 +43,18 @@
|
||||||
<span class="description">{{i18n "discourse_post_event.models.event.status.private.description"}}</span>
|
<span class="description">{{i18n "discourse_post_event.models.event.status.private.description"}}</span>
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
|
<label class="radio-label">
|
||||||
|
{{radio-button
|
||||||
|
name="status"
|
||||||
|
value="standalone"
|
||||||
|
selection=model.eventModel.status
|
||||||
|
onChange=(action (mut model.eventModel.status))
|
||||||
|
}}
|
||||||
|
<span class="message">
|
||||||
|
<span class="title">{{i18n "discourse_post_event.models.event.status.standalone.title"}}</span>
|
||||||
|
<span class="description">{{i18n "discourse_post_event.models.event.status.standalone.description"}}</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
{{/event-field}}
|
{{/event-field}}
|
||||||
|
|
||||||
{{#event-field enabled=allowsInvitees label="discourse_post_event.builder_modal.invitees.label"}}
|
{{#event-field enabled=allowsInvitees label="discourse_post_event.builder_modal.invitees.label"}}
|
||||||
|
|
|
@ -25,7 +25,7 @@ export default createWidget("discourse-post-event-dates", {
|
||||||
attrs.eventModel.status !== "standalone"
|
attrs.eventModel.status !== "standalone"
|
||||||
) {
|
) {
|
||||||
let participants;
|
let participants;
|
||||||
const label = I18n.t("event.post_ui.participants", {
|
const label = I18n.t("discourse_post_event.event_ui.participants", {
|
||||||
count: attrs.eventModel.stats.going
|
count: attrs.eventModel.stats.going
|
||||||
});
|
});
|
||||||
if (attrs.eventModel.stats.going > 0) {
|
if (attrs.eventModel.stats.going > 0) {
|
||||||
|
|
|
@ -136,7 +136,7 @@ export default createWidget("discourse-post-event", {
|
||||||
{{#unless transformed.isStandaloneEvent}}
|
{{#unless transformed.isStandaloneEvent}}
|
||||||
{{#if state.eventModel.is_expired}}
|
{{#if state.eventModel.is_expired}}
|
||||||
<span class="status expired">
|
<span class="status expired">
|
||||||
{{i18n "event.expired"}}
|
{{i18n "discourse_post_event.models.event.expired"}}
|
||||||
</span>
|
</span>
|
||||||
{{else}}
|
{{else}}
|
||||||
<span class={{transformed.statusClass}} title={{transformed.eventStatusDescription}}>
|
<span class={{transformed.statusClass}} title={{transformed.eventStatusDescription}}>
|
||||||
|
|
|
@ -1,45 +1,43 @@
|
||||||
import { withPluginApi } from "discourse/lib/plugin-api";
|
import { withPluginApi } from "discourse/lib/plugin-api";
|
||||||
import showModal from "discourse/lib/show-modal";
|
import showModal from "discourse/lib/show-modal";
|
||||||
import { Promise } from "rsvp";
|
|
||||||
|
|
||||||
function initializeEventBuilder(api) {
|
function initializeEventBuilder(api) {
|
||||||
api.attachWidgetAction("post", "showEventBuilder", function({
|
const currentUser = api.getCurrentUser();
|
||||||
postId,
|
const siteSettings = api.container.lookup("site-settings:main");
|
||||||
topicId
|
|
||||||
}) {
|
|
||||||
return new Promise(resolve => {
|
|
||||||
if (postId) {
|
|
||||||
this.store
|
|
||||||
.find("discourse-post-event-event", postId)
|
|
||||||
.then(resolve)
|
|
||||||
.catch(() => {
|
|
||||||
const eventModel = this.store.createRecord(
|
|
||||||
"discourse-post-event-event"
|
|
||||||
);
|
|
||||||
eventModel.setProperties({
|
|
||||||
id: postId,
|
|
||||||
status: "public"
|
|
||||||
});
|
|
||||||
resolve(eventModel);
|
|
||||||
});
|
|
||||||
} else if (this.model) {
|
|
||||||
resolve(this.model);
|
|
||||||
}
|
|
||||||
}).then(eventModel => {
|
|
||||||
showModal("discourse-post-event-builder", {
|
|
||||||
model: { eventModel, topicId },
|
|
||||||
modalClass: "discourse-post-event-builder"
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
api.decorateWidget("post-admin-menu:after", dec => {
|
api.onToolbarCreate(toolbar => {
|
||||||
return dec.attach("post-admin-menu-button", {
|
if (!currentUser.staff) {
|
||||||
icon: "calendar-day",
|
return;
|
||||||
label: "discourse_event.builder.attach",
|
}
|
||||||
action: "showEventBuilder",
|
|
||||||
actionParam: { postId: dec.attrs.id, topicId: dec.attrs.topicId }
|
const composer = toolbar.context.outletArgs.composer;
|
||||||
});
|
if (
|
||||||
|
!composer.replyingToTopic &&
|
||||||
|
(composer.topicFirstPost ||
|
||||||
|
(composer.editingPost &&
|
||||||
|
composer.post &&
|
||||||
|
composer.post.post_number === 1))
|
||||||
|
) {
|
||||||
|
toolbar.addButton({
|
||||||
|
title: "discourse_post_event.builder_modal.attach",
|
||||||
|
id: "insertEvent",
|
||||||
|
group: "insertions",
|
||||||
|
icon: "calendar-day",
|
||||||
|
perform: toolbarEvent => {
|
||||||
|
const eventModel = toolbar.context.store.createRecord(
|
||||||
|
"discourse-post-event-event"
|
||||||
|
);
|
||||||
|
eventModel.setProperties({
|
||||||
|
status: "public"
|
||||||
|
});
|
||||||
|
|
||||||
|
showModal("discourse-post-event-builder").setProperties({
|
||||||
|
toolbarEvent,
|
||||||
|
model: { eventModel }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,8 +18,9 @@ function cleanUp() {
|
||||||
|
|
||||||
function _attachWidget(api, cooked, eventModel) {
|
function _attachWidget(api, cooked, eventModel) {
|
||||||
const existing = cooked.querySelector(".discourse-post-event");
|
const existing = cooked.querySelector(".discourse-post-event");
|
||||||
|
const wrap = cooked.querySelector("[data-wrap=event]");
|
||||||
|
|
||||||
if (eventModel) {
|
if (eventModel && wrap) {
|
||||||
let widgetHeight = 300;
|
let widgetHeight = 300;
|
||||||
|
|
||||||
if (eventModel.can_update_attendance) {
|
if (eventModel.can_update_attendance) {
|
||||||
|
@ -31,7 +32,7 @@ function _attachWidget(api, cooked, eventModel) {
|
||||||
eventContainer.classList.add("is-loading");
|
eventContainer.classList.add("is-loading");
|
||||||
eventContainer.style.height = `${widgetHeight}px`;
|
eventContainer.style.height = `${widgetHeight}px`;
|
||||||
eventContainer.innerHTML = '<div class="spinner medium"></div>';
|
eventContainer.innerHTML = '<div class="spinner medium"></div>';
|
||||||
cooked.prepend(eventContainer);
|
wrap.prepend(eventContainer);
|
||||||
|
|
||||||
const dates = [];
|
const dates = [];
|
||||||
const startsAt = moment(eventModel.starts_at);
|
const startsAt = moment(eventModel.starts_at);
|
||||||
|
@ -79,7 +80,7 @@ function _attachWidget(api, cooked, eventModel) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function initializeEventDecorator(api) {
|
function initializeDiscoursePostEventDecorator(api) {
|
||||||
api.cleanupStream(cleanUp);
|
api.cleanupStream(cleanUp);
|
||||||
|
|
||||||
api.decorateCooked(($cooked, helper) => {
|
api.decorateCooked(($cooked, helper) => {
|
||||||
|
@ -128,7 +129,7 @@ export default {
|
||||||
initialize(container) {
|
initialize(container) {
|
||||||
const siteSettings = container.lookup("site-settings:main");
|
const siteSettings = container.lookup("site-settings:main");
|
||||||
if (siteSettings.discourse_post_event_enabled) {
|
if (siteSettings.discourse_post_event_enabled) {
|
||||||
withPluginApi("0.8.7", initializeEventDecorator);
|
withPluginApi("0.8.7", initializeDiscoursePostEventDecorator);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
|
@ -24,5 +24,11 @@ en:
|
||||||
errors:
|
errors:
|
||||||
models:
|
models:
|
||||||
event:
|
event:
|
||||||
raw_invitees_length: "An event is limited to %{count} users/groups"
|
only_one_event: A post can only have one event.
|
||||||
ends_at_before_starts_at: "An event can't end before it starts"
|
must_be_in_first_post: An event can only be in the first post of a topic.
|
||||||
|
raw_invitees_length: "An event is limited to %{count} users/groups."
|
||||||
|
ends_at_before_starts_at: "An event can't end before it starts."
|
||||||
|
start_must_be_present_and_a_valid_date: "An event requires a valid start date."
|
||||||
|
end_must_be_a_valid_date: "End date must be a valid date."
|
||||||
|
name:
|
||||||
|
length: "Event name length must be between %{minimum} and %{maximum} characters."
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
EVENT_REGEX = /\[wrap=event\s(.*?)\](?:\n|\\n)?\[\/wrap\]/m
|
||||||
|
|
||||||
|
VALID_OPTIONS = [
|
||||||
|
:start,
|
||||||
|
:end,
|
||||||
|
:status,
|
||||||
|
:allowedGroups,
|
||||||
|
:name
|
||||||
|
]
|
||||||
|
|
||||||
|
module DiscoursePostEvent
|
||||||
|
class EventParser
|
||||||
|
def self.extract_events(str)
|
||||||
|
str.scan(EVENT_REGEX).map do |scan|
|
||||||
|
extract_options(scan[0].gsub(/\\/, ''))
|
||||||
|
end.compact
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.extract_options(str)
|
||||||
|
options = nil
|
||||||
|
str.split(" ").each do |option|
|
||||||
|
key, value = option.split("=")
|
||||||
|
if VALID_OPTIONS.include?(key.to_sym) && value
|
||||||
|
options ||= {}
|
||||||
|
options[key.to_sym] = value.delete('\\"')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
options
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,55 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module DiscoursePostEvent
|
||||||
|
class EventValidator
|
||||||
|
def initialize(post)
|
||||||
|
@post = post
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_event
|
||||||
|
extracted_events = DiscoursePostEvent::EventParser::extract_events(@post.raw)
|
||||||
|
|
||||||
|
if extracted_events.count == 0
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
if extracted_events.count > 1
|
||||||
|
@post.errors.add(:base, I18n.t("discourse_post_event.errors.models.event.only_one_event"))
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
if !@post.is_first_post?
|
||||||
|
@post.errors.add(:base, I18n.t("discourse_post_event.errors.models.event.must_be_in_first_post"))
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
extracted_event = extracted_events.first
|
||||||
|
|
||||||
|
if extracted_event[:start].blank? || (DateTime.parse(extracted_event[:start]) rescue nil).nil?
|
||||||
|
@post.errors.add(:base, I18n.t("discourse_post_event.errors.models.event.start_must_be_present_and_a_valid_date"))
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
if extracted_event[:end].present? && (DateTime.parse(extracted_event[:end]) rescue nil).nil?
|
||||||
|
@post.errors.add(:base, I18n.t("discourse_post_event.errors.models.event.end_must_be_a_valid_date"))
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
if extracted_event[:start].present? && extracted_event[:end].present?
|
||||||
|
if Time.parse(extracted_event[:start]) > Time.parse(extracted_event[:end])
|
||||||
|
@post.errors.add(:base, I18n.t("discourse_post_event.errors.models.event.ends_at_before_starts_at"))
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if extracted_event[:name].present? && extracted_event[:name]
|
||||||
|
if !(Event::MIN_NAME_LENGTH..Event::MAX_NAME_LENGTH).cover?(extracted_event[:name].length)
|
||||||
|
@post.errors.add(:base, I18n.t('discourse_post_event.errors.models.event.name.length', minimum: Event::MIN_NAME_LENGTH, maximum: Event::MAX_NAME_LENGTH))
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
51
plugin.rb
51
plugin.rb
|
@ -81,6 +81,8 @@ after_initialize do
|
||||||
"../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/invitee.rb",
|
"../app/models/discourse_post_event/invitee.rb",
|
||||||
|
"../lib/discourse_post_event/event_parser.rb",
|
||||||
|
"../lib/discourse_post_event/event_validator.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"
|
||||||
|
@ -88,25 +90,6 @@ after_initialize do
|
||||||
|
|
||||||
::ActionController::Base.prepend_view_path File.expand_path("../app/views", __FILE__)
|
::ActionController::Base.prepend_view_path File.expand_path("../app/views", __FILE__)
|
||||||
|
|
||||||
reloadable_patch do
|
|
||||||
require 'post'
|
|
||||||
|
|
||||||
class ::Post
|
|
||||||
has_one :event,
|
|
||||||
dependent: :destroy,
|
|
||||||
class_name: 'DiscoursePostEvent::Event',
|
|
||||||
foreign_key: :id
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
add_to_serializer(:post, :event) do
|
|
||||||
DiscoursePostEvent::EventSerializer.new(object.event, scope: scope, root: false)
|
|
||||||
end
|
|
||||||
|
|
||||||
add_to_serializer(:post, :include_event?) do
|
|
||||||
SiteSetting.discourse_post_event_enabled
|
|
||||||
end
|
|
||||||
|
|
||||||
Discourse::Application.routes.append do
|
Discourse::Application.routes.append do
|
||||||
mount ::DiscoursePostEvent::Engine, at: '/'
|
mount ::DiscoursePostEvent::Engine, at: '/'
|
||||||
end
|
end
|
||||||
|
@ -124,6 +107,36 @@ after_initialize do
|
||||||
get '/upcoming-events' => 'upcoming_events#index'
|
get '/upcoming-events' => 'upcoming_events#index'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
reloadable_patch do
|
||||||
|
require 'post'
|
||||||
|
|
||||||
|
class ::Post
|
||||||
|
has_one :event,
|
||||||
|
dependent: :destroy,
|
||||||
|
class_name: 'DiscoursePostEvent::Event',
|
||||||
|
foreign_key: :id
|
||||||
|
|
||||||
|
validate :valid_event
|
||||||
|
def valid_event
|
||||||
|
return unless self.raw_changed?
|
||||||
|
validator = DiscoursePostEvent::EventValidator.new(self)
|
||||||
|
validator.validate_event
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
add_to_serializer(:post, :event) do
|
||||||
|
DiscoursePostEvent::EventSerializer.new(object.event, scope: scope, root: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
add_to_serializer(:post, :include_event?) do
|
||||||
|
SiteSetting.discourse_post_event_enabled
|
||||||
|
end
|
||||||
|
|
||||||
|
DiscourseEvent.on(:post_process_cooked) do |doc, post|
|
||||||
|
DiscoursePostEvent::Event.update_from_raw(post)
|
||||||
|
end
|
||||||
|
|
||||||
DiscourseEvent.on(:post_destroyed) do |post|
|
DiscourseEvent.on(:post_destroyed) do |post|
|
||||||
if SiteSetting.discourse_post_event_enabled && post.event
|
if SiteSetting.discourse_post_event_enabled && post.event
|
||||||
post.event.update!(deleted_at: Time.now)
|
post.event.update!(deleted_at: Time.now)
|
||||||
|
|
|
@ -17,6 +17,105 @@ describe Post do
|
||||||
SiteSetting.discourse_post_event_enabled = true
|
SiteSetting.discourse_post_event_enabled = true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when a post is updated' do
|
||||||
|
context 'when the post has a valid event' do
|
||||||
|
context 'context the event markup is removed' do
|
||||||
|
it 'destroys the associated event' do
|
||||||
|
start = Time.now.utc.iso8601(3)
|
||||||
|
|
||||||
|
post = PostCreator.create!(
|
||||||
|
user,
|
||||||
|
title: 'Sell a boat party',
|
||||||
|
raw: "[wrap=event start=#{start}]\n[/wrap]",
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(post.event.persisted?).to eq(true)
|
||||||
|
|
||||||
|
revisor = PostRevisor.new(post, post.topic)
|
||||||
|
revisor.revise!(user, raw: 'The event is over. Come back another day.')
|
||||||
|
|
||||||
|
expect(post.reload.event).to be(nil)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when a post is created' do
|
||||||
|
context 'when the post contains one valid event' do
|
||||||
|
it 'creates the post event' do
|
||||||
|
start = Time.now.utc.iso8601(3)
|
||||||
|
|
||||||
|
post = PostCreator.create!(
|
||||||
|
user,
|
||||||
|
title: 'Sell a boat party',
|
||||||
|
raw: "[wrap=event start=#{start}]\n[/wrap]",
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(post.persisted?).to eq(true)
|
||||||
|
expect(post.event.persisted?).to eq(true)
|
||||||
|
expect(post.event.starts_at).to eq_time(Time.parse(start))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the post contains one invalid event' do
|
||||||
|
context 'when start is not provided or is invalid' do
|
||||||
|
it 'raises an error' do
|
||||||
|
expect {
|
||||||
|
PostCreator.create!(
|
||||||
|
user,
|
||||||
|
title: 'Sell a boat party',
|
||||||
|
raw: "[wrap=event end=\"1\"]\n[/wrap]",
|
||||||
|
)
|
||||||
|
}.to(
|
||||||
|
raise_error(ActiveRecord::RecordNotSaved)
|
||||||
|
.with_message(I18n.t("discourse_post_event.errors.models.event.start_must_be_present_and_a_valid_date"))
|
||||||
|
)
|
||||||
|
|
||||||
|
expect {
|
||||||
|
PostCreator.create!(
|
||||||
|
user,
|
||||||
|
title: 'Sell a boat party',
|
||||||
|
raw: "[wrap=event start=\"x\"]\n[/wrap]",
|
||||||
|
)
|
||||||
|
}.to(
|
||||||
|
raise_error(ActiveRecord::RecordNotSaved)
|
||||||
|
.with_message(I18n.t("discourse_post_event.errors.models.event.start_must_be_present_and_a_valid_date"))
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when end is provided and is invalid' do
|
||||||
|
it 'raises an error' do
|
||||||
|
expect {
|
||||||
|
PostCreator.create!(
|
||||||
|
user,
|
||||||
|
title: 'Sell a boat party',
|
||||||
|
raw: "[wrap=event start=\"#{Time.now.utc.iso8601(3)}\" end=\"d\"]\n[/wrap]",
|
||||||
|
)
|
||||||
|
}.to(
|
||||||
|
raise_error(ActiveRecord::RecordNotSaved)
|
||||||
|
.with_message(I18n.t("discourse_post_event.errors.models.event.end_must_be_a_valid_date"))
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the post contains multiple events' do
|
||||||
|
it 'raises an error' do
|
||||||
|
expect {
|
||||||
|
PostCreator.create!(
|
||||||
|
user,
|
||||||
|
title: 'Sell a boat party',
|
||||||
|
raw: "[wrap=event start=\"#{Time.now.utc.iso8601(3)}\"]\n[/wrap] foo [wrap=event start=\"#{Time.now.utc.iso8601(3)}\"]\n[/wrap]",
|
||||||
|
)
|
||||||
|
}.to(
|
||||||
|
raise_error(ActiveRecord::RecordNotSaved)
|
||||||
|
.with_message(I18n.t("discourse_post_event.errors.models.event.only_one_event"))
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'when a post with an event is destroyed' do
|
context 'when a post with an event is destroyed' do
|
||||||
it 'sets deleted_at on the post_event' do
|
it 'sets deleted_at on the post_event' do
|
||||||
expect(post_event.deleted_at).to be_nil
|
expect(post_event.deleted_at).to be_nil
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require "rails_helper"
|
||||||
|
|
||||||
|
describe DiscoursePostEvent::EventParser do
|
||||||
|
subject { DiscoursePostEvent::EventParser }
|
||||||
|
|
||||||
|
it 'works with no event' do
|
||||||
|
events = subject.extract_events('this could be a nice event')
|
||||||
|
expect(events.length).to eq(0)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'finds one event' do
|
||||||
|
events = subject.extract_events('[wrap=event start="foo" end="bar"]\n[/wrap]')
|
||||||
|
expect(events.length).to eq(1)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'finds multiple events' do
|
||||||
|
events = subject.extract_events('[wrap=event start="foo" end="bar"]\n[/wrap] baz [wrap=event start="foo" end="bar"]\n[/wrap]')
|
||||||
|
expect(events.length).to eq(2)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'parses options' do
|
||||||
|
events = subject.extract_events('[wrap=event start="foo" end="bar"]\n[/wrap]')
|
||||||
|
expect(events[0][:start]).to eq("foo")
|
||||||
|
expect(events[0][:end]).to eq("bar")
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'works with escaped string' do
|
||||||
|
events = subject.extract_events("I am going to get that fixed.\n\n[wrap=event start=\"bar\"]\n[/wrap]\n\n[wrap=event start=\"foo\"]\n[/wrap]")
|
||||||
|
expect(events[0][:start]).to eq("bar")
|
||||||
|
expect(events[1][:start]).to eq("foo")
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'doesn’t parse invalid options' do
|
||||||
|
events = subject.extract_events("I am going to get that fixed.\n\n[wrap=event start=\"foo\" something=\"bar\"]\n[/wrap]")
|
||||||
|
expect(events[0][:something]).to be(nil)
|
||||||
|
|
||||||
|
events = subject.extract_events("I am going to get that fixed.\n\n[wrap=event something=\"bar\"]\n[/wrap]")
|
||||||
|
expect(events).to eq([])
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in New Issue