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