diff --git a/app/models/discourse_post_event/event.rb b/app/models/discourse_post_event/event.rb
index 8d7461b9c..08d6a0ca6 100644
--- a/app/models/discourse_post_event/event.rb
+++ b/app/models/discourse_post_event/event.rb
@@ -230,9 +230,9 @@ def create_notification!(user, post, predefined_attendance: false)
end
def ongoing?
- (
- self.ends_at ? (self.starts_at..self.ends_at).cover?(Time.now) : self.starts_at >= Time.now
- ) && !self.expired?
+ return false if self.closed || self.expired?
+ finishes_at = self.ends_at || self.starts_at.end_of_day
+ (self.starts_at..finishes_at).cover?(Time.now)
end
def self.statuses
@@ -284,16 +284,13 @@ def enforce_private_invitees!
end
def can_user_update_attendance(user)
- !self.expired? &&
+ return false if self.closed || self.expired?
+ return true if self.public?
+
+ self.private? &&
(
- self.public? ||
- (
- self.private? &&
- (
- self.invitees.exists?(user_id: user.id) ||
- (user.groups.pluck(:name) & self.raw_invitees).any?
- )
- )
+ self.invitees.exists?(user_id: user.id) ||
+ (user.groups.pluck(:name) & self.raw_invitees).any?
)
end
@@ -315,18 +312,11 @@ def self.update_from_raw(post)
url: event_params[:url],
recurrence: event_params[:recurrence],
timezone: event_params[:timezone],
- status:
- (
- if event_params[:status].present?
- Event.statuses[event_params[:status].to_sym]
- else
- event.status
- end
- ),
+ status: Event.statuses[event_params[:status]&.to_sym] || event.status,
reminders: event_params[:reminders],
- raw_invitees:
- event_params[:"allowed-groups"] ? event_params[:"allowed-groups"].split(",") : nil,
+ raw_invitees: event_params[:"allowed-groups"]&.split(","),
minimal: event_params[:minimal],
+ closed: event_params[:closed] || false,
}
params[:custom_fields] = {}
@@ -417,4 +407,5 @@ def calculate_next_date(start_date: nil)
# recurrence :string
# timezone :string
# minimal :boolean default(FALSE), not null
+# closed :boolean default(FALSE), not null
#
diff --git a/app/serializers/discourse_post_event/event_serializer.rb b/app/serializers/discourse_post_event/event_serializer.rb
index 36c806428..f0d36bf81 100644
--- a/app/serializers/discourse_post_event/event_serializer.rb
+++ b/app/serializers/discourse_post_event/event_serializer.rb
@@ -2,33 +2,34 @@
module DiscoursePostEvent
class EventSerializer < ApplicationSerializer
- attributes :id
- attributes :creator
- attributes :sample_invitees
- attributes :watching_invitee
- attributes :starts_at
- attributes :ends_at
- attributes :timezone
- attributes :stats
- attributes :status
- attributes :raw_invitees
- attributes :post
- attributes :name
attributes :can_act_on_discourse_post_event
attributes :can_update_attendance
+ attributes :category_id
+ attributes :creator
+ attributes :custom_fields
+ attributes :ends_at
+ attributes :id
+ attributes :is_closed
attributes :is_expired
attributes :is_ongoing
- attributes :should_display_invitees
- attributes :url
- attributes :custom_fields
- attributes :is_public
attributes :is_private
+ attributes :is_public
attributes :is_standalone
- attributes :reminders
- attributes :recurrence
attributes :minimal
- attributes :category_id
+ attributes :name
+ attributes :post
+ attributes :raw_invitees
+ attributes :recurrence
attributes :recurrence_rule
+ attributes :reminders
+ attributes :sample_invitees
+ attributes :should_display_invitees
+ attributes :starts_at
+ attributes :stats
+ attributes :status
+ attributes :timezone
+ attributes :url
+ attributes :watching_invitee
def can_act_on_discourse_post_event
scope.can_act_on_discourse_post_event?(object)
@@ -55,15 +56,19 @@ def is_ongoing
end
def is_public
- object.status === Event.statuses[:public]
+ object.public?
end
def is_private
- object.status === Event.statuses[:private]
+ object.private?
end
def is_standalone
- object.status === Event.statuses[:standalone]
+ object.standalone?
+ end
+
+ def is_closed
+ object.closed
end
def status
diff --git a/assets/javascripts/discourse/lib/raw-event-helper.js b/assets/javascripts/discourse/lib/raw-event-helper.js
index aaa5bb083..29bbc82cc 100644
--- a/assets/javascripts/discourse/lib/raw-event-helper.js
+++ b/assets/javascripts/discourse/lib/raw-event-helper.js
@@ -5,6 +5,10 @@ export function buildParams(startsAt, endsAt, eventModel, siteSettings) {
params.start = moment(startsAt).tz(eventTz).format("YYYY-MM-DD HH:mm");
+ if (eventModel.closed) {
+ params.closed = "true";
+ }
+
if (eventModel.status) {
params.status = eventModel.status;
}
diff --git a/assets/javascripts/discourse/widgets/discourse-post-event-dates.js b/assets/javascripts/discourse/widgets/discourse-post-event-dates.js
index 5003bdeee..323ddb456 100644
--- a/assets/javascripts/discourse/widgets/discourse-post-event-dates.js
+++ b/assets/javascripts/discourse/widgets/discourse-post-event-dates.js
@@ -15,27 +15,28 @@ export default createWidget("discourse-post-event-dates", {
});
},
- html(attrs) {
+ html({ localDates, eventModel }) {
const content = [
iconNode("clock"),
- h("span.date", new RawHtml({ html: `${attrs.localDates}` })),
+ h("span.date", new RawHtml({ html: `${localDates}` })),
];
if (
- attrs.eventModel.is_expired &&
- attrs.eventModel.status !== "standalone"
+ eventModel.is_expired &&
+ !eventModel.is_closed &&
+ !eventModel.is_standalone
) {
let participants;
const label = I18n.t(
"discourse_calendar.discourse_post_event.event_ui.participants",
{
- count: attrs.eventModel.stats.going,
+ count: eventModel.stats.going,
}
);
- if (attrs.eventModel.stats.going > 0) {
+ if (eventModel.stats.going > 0) {
participants = this.attach("link", {
action: "showAllParticipatingInvitees",
- actionParam: attrs.eventModel.id,
+ actionParam: eventModel.id,
contents: () => label,
});
} else {
diff --git a/assets/javascripts/discourse/widgets/discourse-post-event.js b/assets/javascripts/discourse/widgets/discourse-post-event.js
index 9c5cbfb64..72e9debb0 100644
--- a/assets/javascripts/discourse/widgets/discourse-post-event.js
+++ b/assets/javascripts/discourse/widgets/discourse-post-event.js
@@ -82,23 +82,58 @@ export default createWidget("discourse-post-event", {
),
didConfirm: () => {
return this.store.find("post", eventModel.id).then((post) => {
- const raw = post.raw;
- const startsAt = eventModel.starts_at
- ? moment(eventModel.starts_at)
- : moment();
+ eventModel.closed = true;
+
const eventParams = buildParams(
- moment().isBefore(startsAt) ? moment() : startsAt,
- moment().isBefore(startsAt) ? moment().add(1, "minute") : moment(),
+ eventModel.starts_at,
+ eventModel.ends_at,
eventModel,
this.siteSettings
);
- const newRaw = replaceRaw(eventParams, raw);
+
+ const newRaw = replaceRaw(eventParams, post.raw);
+
+ if (newRaw) {
+ const props = {
+ raw: newRaw,
+ edit_reason: I18n.t(
+ "discourse_calendar.discourse_post_event.edit_reason_closed"
+ ),
+ };
+
+ return cook(newRaw).then((cooked) => {
+ props.cooked = cooked.string;
+ return post.save(props);
+ });
+ }
+ });
+ },
+ });
+ },
+
+ openEvent(eventModel) {
+ this.dialog.yesNoConfirm({
+ message: I18n.t(
+ "discourse_calendar.discourse_post_event.builder_modal.confirm_open"
+ ),
+ didConfirm: () => {
+ return this.store.find("post", eventModel.id).then((post) => {
+ eventModel.closed = false;
+
+ const eventParams = buildParams(
+ eventModel.starts_at,
+ eventModel.ends_at,
+ eventModel,
+ this.siteSettings
+ );
+
+ const newRaw = replaceRaw(eventParams, post.raw);
if (newRaw) {
const props = {
raw: newRaw,
edit_reason: I18n.t(
- "discourse_calendar.discourse_post_event.edit_reason"
+ "discourse_calendar.discourse_post_event.edit_reason_opened"
),
};
@@ -249,18 +284,20 @@ export default createWidget("discourse-post-event", {
{{{transformed.eventName}}}
- {{#unless transformed.isStandaloneEvent}}
- {{#if state.eventModel.is_expired}}
-
- {{i18n "discourse_calendar.discourse_post_event.models.event.expired"}}
-
- {{else}}
-
- {{transformed.eventStatusLabel}}
-
- {{/if}}
- ·
- {{/unless}}
+ {{#if state.eventModel.is_expired}}
+
+ {{i18n "discourse_calendar.discourse_post_event.models.event.expired"}}
+
+ {{else if state.eventModel.is_closed}}
+
+ {{i18n "discourse_calendar.discourse_post_event.models.event.closed"}}
+
+ {{else}}
+
+ {{transformed.eventStatusLabel}}
+
+ {{/if}}
+ ·
{{i18n "discourse_calendar.discourse_post_event.event_ui.created_by"}}
{{attach widget="discourse-post-event-creator" attrs=(hash user=state.eventModel.creator)}}
diff --git a/assets/javascripts/discourse/widgets/more-dropdown.js b/assets/javascripts/discourse/widgets/more-dropdown.js
index cb80c94bd..bf183e53a 100644
--- a/assets/javascripts/discourse/widgets/more-dropdown.js
+++ b/assets/javascripts/discourse/widgets/more-dropdown.js
@@ -36,10 +36,11 @@ export default createWidget("more-dropdown", {
}
},
- _buildContent(attrs) {
+ _buildContent({ canActOnEvent, isPublicEvent, eventModel }) {
const content = [];
+ const expiredOrClosed = eventModel.is_expired || eventModel.is_closed;
- if (!attrs.eventModel.is_expired) {
+ if (!expiredOrClosed) {
content.push({
id: "addToCalendar",
icon: "file",
@@ -54,30 +55,30 @@ export default createWidget("more-dropdown", {
icon: "envelope",
translatedLabel: I18n.t(
"discourse_calendar.discourse_post_event.event_ui.send_pm_to_creator",
- { username: attrs.eventModel.creator.username }
+ { username: eventModel.creator.username }
),
});
}
- if (!attrs.is_expired && attrs.canActOnEvent && attrs.isPublicEvent) {
+ if (!expiredOrClosed && canActOnEvent && isPublicEvent) {
content.push({
id: "inviteUserOrGroup",
icon: "user-plus",
label: "discourse_calendar.discourse_post_event.event_ui.invite",
- param: attrs.eventModel.id,
+ param: eventModel.id,
});
}
- if (attrs.eventModel.watching_invitee && attrs.isPublicEvent) {
+ if (eventModel.watching_invitee && isPublicEvent) {
content.push({
id: "leaveEvent",
icon: "times",
label: "discourse_calendar.discourse_post_event.event_ui.leave",
- param: attrs.eventModel.id,
+ param: eventModel.id,
});
}
- if (attrs.eventModel.recurrence) {
+ if (!eventModel.is_closed && eventModel.recurrence) {
content.push({
id: "upcomingEvents",
icon: "far-calendar-plus",
@@ -85,40 +86,50 @@ export default createWidget("more-dropdown", {
});
}
- if (attrs.canActOnEvent) {
+ if (canActOnEvent) {
content.push("separator");
content.push({
icon: "file-csv",
id: "exportPostEvent",
label: "discourse_calendar.discourse_post_event.event_ui.export_event",
- param: attrs.eventModel.id,
+ param: eventModel.id,
});
- if (!attrs.eventModel.is_expired && !attrs.eventModel.is_standalone) {
+ if (!expiredOrClosed && !eventModel.is_standalone) {
content.push({
icon: "file-upload",
id: "bulkInvite",
label: "discourse_calendar.discourse_post_event.event_ui.bulk_invite",
- param: attrs.eventModel,
+ param: eventModel,
});
}
- content.push({
- icon: "pencil-alt",
- id: "editPostEvent",
- label: "discourse_calendar.discourse_post_event.event_ui.edit_event",
- param: attrs.eventModel.id,
- });
-
- if (!attrs.eventModel.is_expired) {
+ if (eventModel.is_closed) {
content.push({
- icon: "times",
- id: "closeEvent",
- label: "discourse_calendar.discourse_post_event.event_ui.close_event",
- class: "danger",
- param: attrs.eventModel,
+ icon: "unlock",
+ id: "openEvent",
+ label: "discourse_calendar.discourse_post_event.event_ui.open_event",
+ param: eventModel,
});
+ } else {
+ content.push({
+ icon: "pencil-alt",
+ id: "editPostEvent",
+ label: "discourse_calendar.discourse_post_event.event_ui.edit_event",
+ param: eventModel.id,
+ });
+
+ if (!eventModel.is_expired) {
+ content.push({
+ icon: "times",
+ id: "closeEvent",
+ label:
+ "discourse_calendar.discourse_post_event.event_ui.close_event",
+ class: "danger",
+ param: eventModel,
+ });
+ }
}
}
diff --git a/assets/stylesheets/common/discourse-post-event.scss b/assets/stylesheets/common/discourse-post-event.scss
index 893aabbcd..aed0181dd 100644
--- a/assets/stylesheets/common/discourse-post-event.scss
+++ b/assets/stylesheets/common/discourse-post-event.scss
@@ -133,7 +133,8 @@ $interested: #fb985d;
}
.status {
- &.expired {
+ &.expired,
+ &.closed {
color: var(--danger-medium);
}
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index ba8f6cd55..58aa2673e 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -337,6 +337,7 @@ en:
interested: "Interested"
event:
expired: "Expired"
+ closed: "Closed"
status:
standalone:
title: "Standalone"
@@ -361,6 +362,7 @@ en:
created_by: "Created by"
bulk_invite: "Bulk Invite"
close_event: "Close event"
+ open_event: "Open event"
invitees_modal:
title_invited: "List of RSVPed users"
title_participated: "List of users who participated"
@@ -389,6 +391,7 @@ en:
update_event_title: "Edit Event"
confirm_delete: "Are you sure you want to delete this event?"
confirm_close: "Are you sure you want to close this event?"
+ confirm_open: "Are you sure you want to open this event?"
create: "Create"
update: "Save"
attach: "Create event"
@@ -466,6 +469,8 @@ en:
preview:
more_than_one_event: "You can’t have more than one event."
edit_reason: "Event updated"
+ edit_reason_closed: "Event closed"
+ edit_reason_opened: "Event opened"
topic_title:
starts_at: "Event will start: %{date}"
ended_at: "Event ended: %{date}"
diff --git a/db/migrate/20240513140542_add_closed_to_discourse_post_event.rb b/db/migrate/20240513140542_add_closed_to_discourse_post_event.rb
new file mode 100644
index 000000000..9abf97337
--- /dev/null
+++ b/db/migrate/20240513140542_add_closed_to_discourse_post_event.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class AddClosedToDiscoursePostEvent < ActiveRecord::Migration[7.0]
+ def change
+ add_column :discourse_post_event_events, :closed, :boolean, default: false, null: false
+ end
+end
diff --git a/lib/discourse_post_event/event_parser.rb b/lib/discourse_post_event/event_parser.rb
index ea723ff48..85c1a66dd 100644
--- a/lib/discourse_post_event/event_parser.rb
+++ b/lib/discourse_post_event/event_parser.rb
@@ -13,6 +13,7 @@ class EventParser
:recurrence,
:timezone,
:minimal,
+ :closed,
]
def self.extract_events(post)
diff --git a/spec/models/discourse_post_event/event_spec.rb b/spec/models/discourse_post_event/event_spec.rb
index 9d2d87453..4620deedb 100644
--- a/spec/models/discourse_post_event/event_spec.rb
+++ b/spec/models/discourse_post_event/event_spec.rb
@@ -272,10 +272,10 @@
context "without ends_at date" do
context "when starts_at < current date" do
- it "is not ongoing" do
+ it "is ongoing" do
post_event = Event.create!(original_starts_at: 2.hours.ago, post: first_post)
- expect(post_event.ongoing?).to be(false)
+ expect(post_event.ongoing?).to be(true)
end
end
@@ -288,10 +288,10 @@
end
context "when starts_at > current date" do
- it "is ongoing" do
+ it "is not ongoing" do
post_event = Event.create!(original_starts_at: 1.hours.from_now, post: first_post)
- expect(post_event.ongoing?).to be(true)
+ expect(post_event.ongoing?).to be(false)
end
end
end
diff --git a/spec/system/post_event_spec.rb b/spec/system/post_event_spec.rb
new file mode 100644
index 000000000..f40b15550
--- /dev/null
+++ b/spec/system/post_event_spec.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+describe "Post event", type: :system do
+ fab!(:admin)
+ let(:composer) { PageObjects::Components::Composer.new }
+
+ before do
+ SiteSetting.calendar_enabled = true
+ SiteSetting.discourse_post_event_enabled = true
+ sign_in(admin)
+ visit "/new-topic"
+ end
+
+ it "can create, close, and open an event" do
+ title = "My upcoming l33t event"
+ tomorrow = (Time.zone.now + 1.day).strftime("%Y-%m-%d")
+
+ composer.fill_title(title)
+
+ composer.fill_content <<~MD
+ [event start="#{tomorrow} 13:37" status="public"]
+ [/event]
+ MD
+
+ composer.submit
+
+ expect(page).to have_content(title)
+
+ page.find("#more-dropdown").click
+ page.find(".item-closeEvent").click
+ page.find("#dialog-holder .btn-primary").click
+
+ expect(page).to have_css(".discourse-post-event .status-and-creators .status.closed")
+
+ page.find("#more-dropdown").click
+ page.find(".item-openEvent").click
+ page.find("#dialog-holder .btn-primary").click
+
+ expect(page).to have_css(".discourse-post-event .status-and-creators .status.public")
+ end
+end