diff --git a/app/controllers/discourse_post_event/events_controller.rb b/app/controllers/discourse_post_event/events_controller.rb
index 6671e69c..fd6ef0c5 100644
--- a/app/controllers/discourse_post_event/events_controller.rb
+++ b/app/controllers/discourse_post_event/events_controller.rb
@@ -55,21 +55,7 @@ module DiscoursePostEvent
event = Event.find(params[:id])
guardian.ensure_can_edit!(event.post)
guardian.ensure_can_act_on_event!(event)
- event.enforce_utc!(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!
+ event.update_with_params!(event_params)
serializer = EventSerializer.new(event, scope: guardian)
render_json_dump(serializer)
end
diff --git a/app/models/discourse_post_event/event.rb b/app/models/discourse_post_event/event.rb
index 0d217530..eee307c9 100644
--- a/app/models/discourse_post_event/event.rb
+++ b/app/models/discourse_post_event/event.rb
@@ -42,8 +42,10 @@ module DiscoursePostEvent
validates :starts_at, presence: true
+ MIN_NAME_LENGTH = 5
+ MAX_NAME_LENGTH = 30
validates :name,
- length: { in: 5..30 },
+ length: { in: MIN_NAME_LENGTH..MAX_NAME_LENGTH },
unless: -> (event) { event.name.blank? }
validate :raw_invitees_length
@@ -148,11 +150,11 @@ module DiscoursePostEvent
end
def enforce_utc!(params)
- if params['starts_at'].present?
- params['starts_at'] = Time.parse(params['starts_at']).utc
+ if params[:starts_at].present?
+ params[:starts_at] = Time.parse(params[:starts_at]).utc
end
- if params['ends_at'].present?
- params['ends_at'] = Time.parse(params['ends_at']).utc
+ if params[:ends_at].present?
+ params[:ends_at] = Time.parse(params[:ends_at]).utc
end
end
@@ -171,5 +173,40 @@ module DiscoursePostEvent
def is_expired?
Time.now > (self.ends_at || self.starts_at || Time.now)
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
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 0cc6c17e..43643bf3 100644
--- a/assets/javascripts/discourse/controllers/discourse-post-event-builder.js.es6
+++ b/assets/javascripts/discourse/controllers/discourse-post-event-builder.js.es6
@@ -1,3 +1,4 @@
+import TextLib from "discourse/lib/text";
import Group from "discourse/models/group";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import Controller from "@ember/controller";
@@ -58,14 +59,32 @@ export default Controller.extend(ModalFunctionality, {
@action
destroyPostEvent() {
bootbox.confirm(
- I18n.t("event.ui_builder.confirm_delete"),
+ I18n.t("discourse_post_event.builder_modal.confirm_delete"),
I18n.t("no_value"),
I18n.t("yes_value"),
confirmed => {
if (confirmed) {
- this.model.eventModel
- .destroyRecord()
- .then(() => this.send("closeModal"));
+ return this.store
+ .find("post", this.model.eventModel.id)
+ .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
createEvent() {
- this.model.eventModel
- .save()
- .then(() => this.send("closeModal"))
- .catch(e => this.flash(extractError(e), "error"));
+ if (!this.startsAt) {
+ this.send("closeModal");
+ return;
+ }
+
+ 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
updateEvent() {
- this.model.eventModel
- .save()
- .then(() => this.send("closeModal"))
- .catch(e => this.flash(extractError(e), "error"));
+ const eventParams = this._buildEventParams();
+ return this.store.find("post", this.model.eventModel.id).then(post => {
+ const raw = post.raw;
+ 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;
}
});
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 76eb1896..2c34192d 100644
--- a/assets/javascripts/discourse/templates/modal/discourse-post-event-builder.hbs
+++ b/assets/javascripts/discourse/templates/modal/discourse-post-event-builder.hbs
@@ -19,19 +19,6 @@
}}
{{#event-field label="discourse_post_event.builder_modal.status.label"}}
-
-
+
{{/event-field}}
{{#event-field enabled=allowsInvitees label="discourse_post_event.builder_modal.invitees.label"}}
diff --git a/assets/javascripts/discourse/widgets/discourse-post-event-dates.js.es6 b/assets/javascripts/discourse/widgets/discourse-post-event-dates.js.es6
index a42854f8..ad5ce636 100644
--- a/assets/javascripts/discourse/widgets/discourse-post-event-dates.js.es6
+++ b/assets/javascripts/discourse/widgets/discourse-post-event-dates.js.es6
@@ -25,7 +25,7 @@ export default createWidget("discourse-post-event-dates", {
attrs.eventModel.status !== "standalone"
) {
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
});
if (attrs.eventModel.stats.going > 0) {
diff --git a/assets/javascripts/discourse/widgets/discourse-post-event.js.es6 b/assets/javascripts/discourse/widgets/discourse-post-event.js.es6
index 1dc2fe16..35c8971c 100644
--- a/assets/javascripts/discourse/widgets/discourse-post-event.js.es6
+++ b/assets/javascripts/discourse/widgets/discourse-post-event.js.es6
@@ -136,7 +136,7 @@ export default createWidget("discourse-post-event", {
{{#unless transformed.isStandaloneEvent}}
{{#if state.eventModel.is_expired}}
- {{i18n "event.expired"}}
+ {{i18n "discourse_post_event.models.event.expired"}}
{{else}}
diff --git a/assets/javascripts/initializers/add-event-ui-builder.js.es6 b/assets/javascripts/initializers/add-event-ui-builder.js.es6
index cf24d9c4..8c28c909 100644
--- a/assets/javascripts/initializers/add-event-ui-builder.js.es6
+++ b/assets/javascripts/initializers/add-event-ui-builder.js.es6
@@ -1,45 +1,43 @@
import { withPluginApi } from "discourse/lib/plugin-api";
import showModal from "discourse/lib/show-modal";
-import { Promise } from "rsvp";
function initializeEventBuilder(api) {
- api.attachWidgetAction("post", "showEventBuilder", function({
- postId,
- 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"
- });
- });
- });
+ const currentUser = api.getCurrentUser();
+ const siteSettings = api.container.lookup("site-settings:main");
- api.decorateWidget("post-admin-menu:after", dec => {
- return dec.attach("post-admin-menu-button", {
- icon: "calendar-day",
- label: "discourse_event.builder.attach",
- action: "showEventBuilder",
- actionParam: { postId: dec.attrs.id, topicId: dec.attrs.topicId }
- });
+ api.onToolbarCreate(toolbar => {
+ if (!currentUser.staff) {
+ return;
+ }
+
+ 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 }
+ });
+ }
+ });
+ }
});
}
diff --git a/assets/javascripts/initializers/discourse-event-decorator.js.es6 b/assets/javascripts/initializers/discourse-post-event-decorator.js.es6
similarity index 93%
rename from assets/javascripts/initializers/discourse-event-decorator.js.es6
rename to assets/javascripts/initializers/discourse-post-event-decorator.js.es6
index 7e5645a3..c3624cb7 100644
--- a/assets/javascripts/initializers/discourse-event-decorator.js.es6
+++ b/assets/javascripts/initializers/discourse-post-event-decorator.js.es6
@@ -18,8 +18,9 @@ function cleanUp() {
function _attachWidget(api, cooked, eventModel) {
const existing = cooked.querySelector(".discourse-post-event");
+ const wrap = cooked.querySelector("[data-wrap=event]");
- if (eventModel) {
+ if (eventModel && wrap) {
let widgetHeight = 300;
if (eventModel.can_update_attendance) {
@@ -31,7 +32,7 @@ function _attachWidget(api, cooked, eventModel) {
eventContainer.classList.add("is-loading");
eventContainer.style.height = `${widgetHeight}px`;
eventContainer.innerHTML = '';
- cooked.prepend(eventContainer);
+ wrap.prepend(eventContainer);
const dates = [];
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.decorateCooked(($cooked, helper) => {
@@ -128,7 +129,7 @@ export default {
initialize(container) {
const siteSettings = container.lookup("site-settings:main");
if (siteSettings.discourse_post_event_enabled) {
- withPluginApi("0.8.7", initializeEventDecorator);
+ withPluginApi("0.8.7", initializeDiscoursePostEventDecorator);
}
}
};
diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml
index 8daed8cb..648eea40 100644
--- a/config/locales/server.en.yml
+++ b/config/locales/server.en.yml
@@ -24,5 +24,11 @@ en:
errors:
models:
event:
- raw_invitees_length: "An event is limited to %{count} users/groups"
- ends_at_before_starts_at: "An event can't end before it starts"
+ only_one_event: A post can only have one event.
+ 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."
diff --git a/lib/discourse_post_event/event_parser.rb b/lib/discourse_post_event/event_parser.rb
new file mode 100644
index 00000000..354cec7a
--- /dev/null
+++ b/lib/discourse_post_event/event_parser.rb
@@ -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
diff --git a/lib/discourse_post_event/event_validator.rb b/lib/discourse_post_event/event_validator.rb
new file mode 100644
index 00000000..192da945
--- /dev/null
+++ b/lib/discourse_post_event/event_validator.rb
@@ -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
diff --git a/plugin.rb b/plugin.rb
index 4c734504..8a94bb15 100644
--- a/plugin.rb
+++ b/plugin.rb
@@ -81,6 +81,8 @@ after_initialize do
"../app/controllers/discourse_post_event/upcoming_events_controller.rb",
"../app/models/discourse_post_event/event.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",
"../app/serializers/discourse_post_event/invitee_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__)
- 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
mount ::DiscoursePostEvent::Engine, at: '/'
end
@@ -124,6 +107,36 @@ after_initialize do
get '/upcoming-events' => 'upcoming_events#index'
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|
if SiteSetting.discourse_post_event_enabled && post.event
post.event.update!(deleted_at: Time.now)
diff --git a/spec/acceptance/post_spec.rb b/spec/acceptance/post_spec.rb
index da7a1d61..0248c4e2 100644
--- a/spec/acceptance/post_spec.rb
+++ b/spec/acceptance/post_spec.rb
@@ -17,6 +17,105 @@ describe Post do
SiteSetting.discourse_post_event_enabled = true
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
it 'sets deleted_at on the post_event' do
expect(post_event.deleted_at).to be_nil
diff --git a/spec/lib/event_parser_spec.rb b/spec/lib/event_parser_spec.rb
new file mode 100644
index 00000000..e7368a8e
--- /dev/null
+++ b/spec/lib/event_parser_spec.rb
@@ -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