From a925474ed0b1fc3ad4df4eeb7158921de3580db9 Mon Sep 17 00:00:00 2001 From: Jarek Radosz Date: Tue, 15 Jul 2025 14:14:01 +0200 Subject: [PATCH] DEV: Move discourse-calendar to core (#33570) https://meta.discourse.org/t/373574 Internal `/t/-/156778` --- .github/labeler.yml | 4 + .gitignore | 1 + .streerc | 1 + plugins/discourse-calendar/.prettierignore | 2 + plugins/discourse-calendar/README.md | 42 + .../admin_holidays_controller.rb | 48 + .../discourse_post_event_controller.rb | 14 + .../discourse_post_event/events_controller.rb | 130 ++ .../invitees_controller.rb | 99 ++ .../upcoming_events_controller.rb | 8 + .../app/models/calendar_event.rb | 131 ++ .../discourse_calendar/disabled_holiday.rb | 24 + .../app/models/discourse_post_event/event.rb | 437 +++++ .../models/discourse_post_event/event_date.rb | 73 + .../models/discourse_post_event/invitee.rb | 90 + .../discourse_post_event/event_serializer.rb | 156 ++ .../event_stats_serializer.rb | 36 + .../event_summary_serializer.rb | 62 + .../invitee_list_serializer.rb | 32 + .../invitee_serializer.rb | 28 + .../serializers/user_timezone_serializer.rb | 9 + .../services/discourse_calendar/holiday.rb | 30 + .../discourse_post_event/chat_channel_sync.rb | 69 + .../adapters/discourse-post-event-adapter.js | 7 + .../adapters/discourse-post-event-event.js | 15 + .../adapters/discourse-post-event-invitee.js | 7 + .../discourse-post-event-nested-adapter.js | 65 + .../adapters/discourse-post-event-reminder.js | 7 + .../discourse/admin-calendar-route-map.js | 7 + .../discourse-group-timezones.gjs | 35 + .../components/admin-holidays-list-item.gjs | 69 + .../components/admin-holidays-list.gjs | 25 + .../bulk-invite-sample-csv-file.gjs | 36 + .../discourse/components/csv-uploader.gjs | 64 + .../discourse-post-event/chat-channel.gjs | 20 + .../discourse-post-event/creator.gjs | 23 + .../components/discourse-post-event/dates.gjs | 154 ++ .../discourse-post-event/description.gjs | 11 + .../discourse-post-event/event-status.gjs | 36 + .../components/discourse-post-event/index.gjs | 157 ++ .../discourse-post-event/invitee.gjs | 53 + .../discourse-post-event/invitees.gjs | 55 + .../discourse-post-event/location.gjs | 14 + .../discourse-post-event/more-menu.gjs | 382 +++++ .../discourse-post-event/status.gjs | 183 ++ .../components/discourse-post-event/url.gjs | 26 + .../discourse/components/event-date.gjs | 112 ++ .../discourse/components/event-field.gjs | 20 + .../components/group-timezones/index.gjs | 184 ++ .../components/group-timezones/new-day.gjs | 16 + .../group-timezones/time-traveller.gjs | 58 + .../components/group-timezones/timezone.gjs | 44 + .../components/modal/post-event-builder.gjs | 690 ++++++++ .../modal/post-event-bulk-invite.gjs | 227 +++ .../modal/post-event-invite-user-or-group.gjs | 64 + .../modal/post-event-invitees/index.gjs | 157 ++ .../modal/post-event-invitees/user.gjs | 16 + .../discourse/components/region-input.js | 44 + .../discourse/components/toggle-invitees.gjs | 37 + .../components/upcoming-events-calendar.gjs | 212 +++ .../components/upcoming-events-list.gjs | 222 +++ .../category-calendar.gjs | 13 + .../show-event-category-sorting-settings.gjs | 48 + .../category-events-calendar.gjs | 14 + .../event-date-container.gjs | 7 + .../topic-list-after-title/event-badge.gjs | 15 + .../user-custom-preferences/region.gjs | 50 + .../controllers/admin-plugins-calendar.js | 28 + ...course-post-event-upcoming-events-index.js | 5 + ...scourse-post-event-upcoming-events-mine.js | 5 + ...scourse-event-upcoming-events-route-map.js | 10 + .../discourse/helpers/format-event-name.js | 28 + .../discourse/helpers/format-future-date.js | 8 + .../initializers/add-event-ui-builder.js | 51 + .../add-upcoming-events-to-sidebar.js | 26 + .../discourse/initializers/disable-sort.js | 24 + .../initializers/discourse-calendar.js | 1043 ++++++++++++ .../discourse-post-event-decorator.gjs | 159 ++ .../initializers/event-relative-date.js | 38 + .../discourse/lib/add-recurrent-events.js | 28 + .../discourse/lib/calendar-locale.js | 15 + .../javascripts/discourse/lib/colors.js | 28 + .../discourse-markdown/discourse-calendar.js | 127 ++ .../discourse-post-event-block.js | 41 + .../discourse/lib/event-relative-date.js | 58 + .../lib/full-calendar-default-options.js | 25 + .../discourse/lib/guess-best-date-format.js | 18 + .../javascripts/discourse/lib/popover.js | 39 + .../discourse/lib/raw-event-helper.js | 136 ++ .../javascripts/discourse/lib/regions.js | 481 ++++++ .../javascripts/discourse/lib/round-time.js | 84 + .../discourse-post-event-event-stats.js | 19 + .../models/discourse-post-event-event.js | 204 +++ .../models/discourse-post-event-invitee.js | 25 + .../models/discourse-post-event-invitees.js | 50 + .../models/discourse-post-event-reminder.js | 9 + .../pre-initializers/rich-editor-extension.js | 105 ++ .../pre-initializers/transformers.js | 12 + ...course-post-event-upcoming-events-index.js | 19 + ...course-post-event-upcoming-events-mine.gjs | 22 + .../services/discourse-post-event-api.js | 124 ++ .../services/discourse-post-event-service.js | 14 + .../templates/admin-plugins-calendar.gjs | 33 + ...ourse-post-event-upcoming-events-index.gjs | 10 + ...course-post-event-upcoming-events-mine.gjs | 10 + .../discourse-post-event-upcoming-events.gjs | 3 + .../assets/stylesheets/colors.scss | 15 + .../common/discourse-calendar-holidays.scss | 9 + .../common/discourse-calendar.scss | 433 +++++ ...iscourse-post-event-bulk-invite-modal.scss | 52 + .../common/discourse-post-event-core-ext.scss | 45 + .../common/discourse-post-event-invitees.scss | 90 + .../common/discourse-post-event-preview.scss | 25 + .../discourse-post-event-upcoming-events.scss | 19 + .../common/discourse-post-event.scss | 448 +++++ .../common/post-event-builder.scss | 246 +++ .../common/upcoming-events-calendar.scss | 133 ++ .../common/upcoming-events-list.scss | 79 + .../stylesheets/common/user-preferences.scss | 5 + .../desktop/discourse-calendar.scss | 14 + .../discourse-post-event-invitees.scss | 5 + .../mobile/discourse-calendar.scss | 46 + .../mobile/discourse-post-event-core-ext.scss | 16 + .../mobile/discourse-post-event-invitees.scss | 5 + .../mobile/discourse-post-event.scss | 34 + .../stylesheets/vendor/fullcalendar.min.css | 5 + .../config/locales/client.ar.yml | 502 ++++++ .../config/locales/client.be.yml | 52 + .../config/locales/client.bg.yml | 70 + .../config/locales/client.bs_BA.yml | 73 + .../config/locales/client.ca.yml | 74 + .../config/locales/client.cs.yml | 501 ++++++ .../config/locales/client.da.yml | 411 +++++ .../config/locales/client.de.yml | 496 ++++++ .../config/locales/client.el.yml | 216 +++ .../config/locales/client.en.yml | 493 ++++++ .../config/locales/client.en_GB.yml | 12 + .../config/locales/client.es.yml | 482 ++++++ .../config/locales/client.et.yml | 72 + .../config/locales/client.fa_IR.yml | 448 +++++ .../config/locales/client.fi.yml | 482 ++++++ .../config/locales/client.fr.yml | 482 ++++++ .../config/locales/client.gl.yml | 79 + .../config/locales/client.he.yml | 498 ++++++ .../config/locales/client.hr.yml | 80 + .../config/locales/client.hu.yml | 418 +++++ .../config/locales/client.hy.yml | 71 + .../config/locales/client.id.yml | 102 ++ .../config/locales/client.it.yml | 482 ++++++ .../config/locales/client.ja.yml | 477 ++++++ .../config/locales/client.ko.yml | 164 ++ .../config/locales/client.lt.yml | 81 + .../config/locales/client.lv.yml | 71 + .../config/locales/client.nb_NO.yml | 77 + .../config/locales/client.nl.yml | 482 ++++++ .../config/locales/client.pl_PL.yml | 459 +++++ .../config/locales/client.pt.yml | 80 + .../config/locales/client.pt_BR.yml | 482 ++++++ .../config/locales/client.ro.yml | 100 ++ .../config/locales/client.ru.yml | 492 ++++++ .../config/locales/client.sk.yml | 81 + .../config/locales/client.sl.yml | 410 +++++ .../config/locales/client.sq.yml | 39 + .../config/locales/client.sr.yml | 37 + .../config/locales/client.sv.yml | 428 +++++ .../config/locales/client.sw.yml | 62 + .../config/locales/client.te.yml | 73 + .../config/locales/client.th.yml | 70 + .../config/locales/client.tr_TR.yml | 482 ++++++ .../config/locales/client.ug.yml | 78 + .../config/locales/client.uk.yml | 505 ++++++ .../config/locales/client.ur.yml | 80 + .../config/locales/client.vi.yml | 80 + .../config/locales/client.zh_CN.yml | 477 ++++++ .../config/locales/client.zh_TW.yml | 75 + .../config/locales/server.ar.yml | 104 ++ .../config/locales/server.be.yml | 15 + .../config/locales/server.bg.yml | 11 + .../config/locales/server.bs_BA.yml | 11 + .../config/locales/server.ca.yml | 15 + .../config/locales/server.cs.yml | 104 ++ .../config/locales/server.da.yml | 75 + .../config/locales/server.de.yml | 104 ++ .../config/locales/server.el.yml | 15 + .../config/locales/server.en.yml | 100 ++ .../config/locales/server.en_GB.yml | 7 + .../config/locales/server.es.yml | 104 ++ .../config/locales/server.et.yml | 15 + .../config/locales/server.fa_IR.yml | 31 + .../config/locales/server.fi.yml | 104 ++ .../config/locales/server.fr.yml | 104 ++ .../config/locales/server.gl.yml | 15 + .../config/locales/server.he.yml | 104 ++ .../config/locales/server.hr.yml | 15 + .../config/locales/server.hu.yml | 65 + .../config/locales/server.hy.yml | 15 + .../config/locales/server.id.yml | 17 + .../config/locales/server.it.yml | 97 ++ .../config/locales/server.ja.yml | 104 ++ .../config/locales/server.ko.yml | 21 + .../config/locales/server.lt.yml | 40 + .../config/locales/server.lv.yml | 11 + .../config/locales/server.nb_NO.yml | 15 + .../config/locales/server.nl.yml | 104 ++ .../config/locales/server.pl_PL.yml | 80 + .../config/locales/server.pt.yml | 19 + .../config/locales/server.pt_BR.yml | 104 ++ .../config/locales/server.ro.yml | 19 + .../config/locales/server.ru.yml | 104 ++ .../config/locales/server.sk.yml | 15 + .../config/locales/server.sl.yml | 74 + .../config/locales/server.sq.yml | 11 + .../config/locales/server.sr.yml | 11 + .../config/locales/server.sv.yml | 84 + .../config/locales/server.sw.yml | 15 + .../config/locales/server.te.yml | 11 + .../config/locales/server.th.yml | 11 + .../config/locales/server.tr_TR.yml | 104 ++ .../config/locales/server.ug.yml | 15 + .../config/locales/server.uk.yml | 104 ++ .../config/locales/server.ur.yml | 15 + .../config/locales/server.vi.yml | 15 + .../config/locales/server.zh_CN.yml | 104 ++ .../config/locales/server.zh_TW.yml | 15 + plugins/discourse-calendar/config/routes.rb | 32 + .../discourse-calendar/config/settings.yml | 129 ++ .../20190724181542_add_on_holiday_index.rb | 25 + .../20200226183018_create_calendar_events.rb | 46 + ...0310200000_remove_timezone_custom_field.rb | 11 + ...opic_custom_field_post_event_date_index.rb | 11 + ...drop_incorrect_future_schema_migrations.rb | 16 + ...20200409102640_create_post_events_table.rb | 22 + .../20200409102641_create_invitees_table.rb | 21 + ..._rename_setting_to_discourse_post_event.rb | 11 + ...3_rename_tables_to_discourse_post_event.rb | 61 + ..._field_topic_post_event_starts_at_index.rb | 17 + .../20200409181607_remove_display_invitees.rb | 11 + .../20200729094848_add_url_column_to_event.rb | 11 + ...3343_drop_old_discourse_calendar_tables.rb | 19 + ...200805133257_add_custom_fields_to_event.rb | 11 + .../20200809154642_create_reminders_table.rb | 12 + .../20200810185432_refactor_reminders.rb | 11 + .../20200810190429_drop_reminders_table.rb | 11 + ...20200812193122_add_recurrence_to_events.rb | 11 + ...dex_to_topic_event_ends_at_custom_field.rb | 15 + ...110225115_create_post_event_dates_table.rb | 22 + ...20201111005205_move_data_to_event_dates.rb | 137 ++ ...6124303_add_timezone_to_calendar_events.rb | 7 + ..._timezone_to_discourse_post_event_event.rb | 7 + ...20220604200919_create_disabled_holidays.rb | 17 + .../20220613073844_unescape_event_name.rb | 82 + ...20220724130519_fix_post_event_timezones.rb | 37 + ...5352_add_type_field_to_events_reminders.rb | 32 + ...25_add_minimal_option_to_calendar_event.rb | 7 + ...231123233308_delete_duplicated_holidays.rb | 19 + .../20231124021939_delete_similar_holidays.rb | 25 + ...0542_add_closed_to_discourse_post_event.rb | 7 + ...0250520042223_add_chat_fields_to_events.rb | 7 + .../20250526154632_add_recurrence_until.rb | 7 + .../20250602114410_add_show_local_time.rb | 7 + .../20250616101944_add_location_to_event.rb | 7 + ...20250616101945_add_description_to_event.rb | 7 + .../discourse_post_event/bulk_invite.rb | 135 ++ .../discourse_post_event/bump_topic.rb | 17 + .../discourse_post_event/event_started.rb | 14 + .../discourse_post_event/send_reminder.rb | 90 + .../jobs/scheduled/create_holiday_events.rb | 122 ++ .../scheduled/delete_expired_event_posts.rb | 56 + .../jobs/scheduled/monitor_event_dates.rb | 80 + .../scheduled/update_holiday_usernames.rb | 53 + plugins/discourse-calendar/lib/calendar.rb | 49 + .../lib/calendar_settings_validator.rb | 27 + .../lib/calendar_validator.rb | 27 + .../lib/discourse_calendar/engine.rb | 8 + .../map_event_tag_colors_json_schema.rb | 36 + .../map_events_title_json_schema.rb | 31 + .../lib/discourse_post_event/engine.rb | 8 + .../lib/discourse_post_event/event_finder.rb | 95 ++ .../lib/discourse_post_event/event_parser.rb | 69 + .../discourse_post_event/event_validator.rb | 178 ++ .../export_csv_controller_extension.rb | 45 + .../export_csv_file_extension.rb | 37 + .../discourse_post_event/post_extension.rb | 20 + .../rrule_configurator.rb | 55 + .../discourse_post_event/rrule_generator.rb | 44 + .../discourse-calendar/lib/event_validator.rb | 31 + .../discourse-calendar/lib/group_timezones.rb | 24 + .../discourse-calendar/lib/holiday_status.rb | 33 + .../lib/tasks/javascript.rake | 48 + .../discourse-calendar/lib/time_sniffer.rb | 322 ++++ .../lib/users_on_holiday.rb | 70 + plugins/discourse-calendar/plugin.rb | 608 +++++++ .../fullcalendar-with-moment-timezone.min.js | 21 + .../fabricators/calendar_event_fabricator.rb | 11 + .../spec/fabricators/event_fabricator.rb | 34 + .../allowed_custom_fields_setting_spec.rb | 56 + .../integration/curently_away_report_spec.rb | 29 + .../spec/integration/invitee_spec.rb | 32 + .../spec/integration/post_spec.rb | 805 +++++++++ .../spec/integration/recurrence_spec.rb | 121 ++ .../spec/integration/topic_spec.rb | 29 + .../jobs/export_post_event_report_csv_spec.rb | 154 ++ .../discourse_post_event/bulk_invite_spec.rb | 204 +++ .../discourse_post_event/bump_topic_spec.rb | 41 + .../send_reminder_spec.rb | 285 ++++ .../scheduled/create_holiday_events_spec.rb | 262 +++ .../delete_expired_event_posts_spec.rb | 166 ++ .../scheduled/monitor_event_dates_spec.rb | 202 +++ .../update_holiday_usernames_spec.rb | 189 +++ .../lib/calendar_settings_validator_spec.rb | 28 + .../spec/lib/calendar_spec.rb | 126 ++ .../discourse_post_event/event_finder_spec.rb | 254 +++ .../discourse_post_event/event_parser_spec.rb | 151 ++ .../discourse_post_event/pretty_text_spec.rb | 90 + .../rrule_configurator_spec.rb | 131 ++ .../rrule_generator_spec.rb | 53 + .../discourse_post_event/time_sniffer_spec.rb | 180 ++ .../spec/lib/group_timezones_spec.rb | 26 + .../spec/lib/users_on_holiday_spec.rb | 169 ++ .../spec/models/calendar_event_spec.rb | 181 ++ .../discourse_post_event/event_date_spec.rb | 38 + .../models/discourse_post_event/event_spec.rb | 504 ++++++ .../models/discourse_post_event/user_spec.rb | 98 ++ .../spec/models/topic_query_spec.rb | 53 + .../admin/admin_holidays_controller_spec.rb | 200 +++ .../spec/requests/events_controller_spec.rb | 442 +++++ .../spec/requests/invitees_controller_spec.rb | 307 ++++ .../spec/requests/site_spec.rb | 27 + .../spec/requests/sort_event_topics_spec.rb | 53 + .../event_serializer_spec.rb | 51 + .../event_summary_serializer_spec.rb | 183 ++ .../post_serializer_spec.rb | 30 + .../spec/serializers/post_serializer_spec.rb | 79 + .../spec/serializers/user_serializer_spec.rb | 36 + .../chat_channel_sync_spec.rb | 30 + .../discouse-calendar/holiday_spec.rb | 72 + .../spec/system/category_calendar_spec.rb | 31 + .../spec/system/core_features_spec.rb | 7 + .../spec/system/disable_sort_spec.rb | 25 + .../spec/system/group_timezones_spec.rb | 31 + .../discourse_calendar/bulk_invite_modal.rb | 47 + .../discourse_calendar/post_event.rb | 59 + .../discourse_calendar/post_event_form.rb | 35 + .../discourse_calendar/upcoming_events.rb | 17 + .../spec/system/post_event_spec.rb | 228 +++ .../spec/system/upcoming_events_spec.rb | 119 ++ .../acceptance/admin-holidays-test.js | 77 + .../category-events-calendar-outlet-test.js | 117 ++ .../category-events-calendar-test.js | 196 +++ .../acceptance/notifications-test.js | 196 +++ .../acceptance/post-event-builder-test.js | 118 ++ .../sidebar-upcoming-events-item-test.js | 79 + .../acceptance/sort-event-topics-test.js | 27 + .../acceptance/timezone-offset-test.js | 138 ++ .../acceptance/topic-calendar-events-test.js | 32 + .../topic-calendar-holidays-test.js | 52 + .../acceptance/topic-title-decorator-test.js | 55 + .../upcoming-events-calendar-test.js | 133 ++ .../helpers/event-topic-fixture.js | 262 +++ .../javascripts/helpers/get-event-by-text.js | 14 + .../admin-holidays-list-item-test.gjs | 58 + .../components/admin-holidays-list-test.gjs | 38 + .../integration/components/dates-test.gjs | 202 +++ .../integration/components/more-menu-test.gjs | 59 + .../components/region-input-test.gjs | 45 + .../components/rich-editor-extension-test.js | 34 + .../components/upcoming-events-list-test.gjs | 431 +++++ .../javascripts/lib/raw-event-helper-test.js | 35 + .../vendor/holidays/CHANGELOG.md | 416 +++++ .../vendor/holidays/CODE_OF_CONDUCT.md | 74 + .../vendor/holidays/Gemfile | 3 + .../vendor/holidays/Gemfile.lock | 42 + .../vendor/holidays/LICENSE | 21 + .../vendor/holidays/Makefile | 45 + .../vendor/holidays/README.md | 322 ++++ .../vendor/holidays/Rakefile | 109 ++ .../vendor/holidays/bin/console | 7 + .../vendor/holidays/bin/setup | 6 + .../vendor/holidays/definitions/CHANGELOG.md | 272 +++ .../vendor/holidays/definitions/Gemfile | 6 + .../vendor/holidays/definitions/LICENSE | 21 + .../vendor/holidays/definitions/METHODS.yml | 96 ++ .../vendor/holidays/definitions/Makefile | 9 + .../vendor/holidays/definitions/README.md | 22 + .../vendor/holidays/definitions/ae.yaml | 73 + .../vendor/holidays/definitions/ar.yaml | 202 +++ .../vendor/holidays/definitions/at.yaml | 116 ++ .../vendor/holidays/definitions/au.yaml | 760 +++++++++ .../vendor/holidays/definitions/be_fr.yaml | 153 ++ .../vendor/holidays/definitions/be_nl.yaml | 153 ++ .../vendor/holidays/definitions/bg.yaml | 186 +++ .../vendor/holidays/definitions/br.yaml | 174 ++ .../vendor/holidays/definitions/ca.yaml | 685 ++++++++ .../vendor/holidays/definitions/ch.yaml | 277 +++ .../vendor/holidays/definitions/cl.yaml | 294 ++++ .../vendor/holidays/definitions/co.yaml | 437 +++++ .../vendor/holidays/definitions/cr.yaml | 100 ++ .../vendor/holidays/definitions/cz.yaml | 137 ++ .../vendor/holidays/definitions/de.yaml | 367 ++++ .../vendor/holidays/definitions/dk.yaml | 208 +++ .../holidays/definitions/doc/CONTRIBUTING.md | 34 + .../holidays/definitions/doc/MAINTAINERS.md | 39 + .../vendor/holidays/definitions/doc/SYNTAX.md | 435 +++++ .../definitions/doc/architecture/README.md | 15 + .../definitions/doc/architecture/adr-001.md | 86 + .../definitions/doc/architecture/adr-002.md | 64 + .../holidays/definitions/ecbtarget.yaml | 74 + .../vendor/holidays/definitions/ee.yaml | 123 ++ .../vendor/holidays/definitions/el.yaml | 158 ++ .../vendor/holidays/definitions/es.yaml | 495 ++++++ .../holidays/definitions/federalreserve.yaml | 364 ++++ .../definitions/federalreservebanks.yaml | 802 +++++++++ .../vendor/holidays/definitions/fedex.yaml | 102 ++ .../vendor/holidays/definitions/fi.yaml | 234 +++ .../vendor/holidays/definitions/fr.yaml | 157 ++ .../vendor/holidays/definitions/gb.yaml | 513 ++++++ .../vendor/holidays/definitions/ge.yaml | 158 ++ .../vendor/holidays/definitions/gh.yaml | 207 +++ .../vendor/holidays/definitions/hk.yaml | 287 ++++ .../vendor/holidays/definitions/hr.yaml | 171 ++ .../vendor/holidays/definitions/hu.yaml | 156 ++ .../vendor/holidays/definitions/id.yaml | 164 ++ .../vendor/holidays/definitions/ie.yaml | 172 ++ .../vendor/holidays/definitions/in.yaml | 146 ++ .../vendor/holidays/definitions/index.yaml | 86 + .../vendor/holidays/definitions/is.yaml | 247 +++ .../vendor/holidays/definitions/it.yaml | 246 +++ .../vendor/holidays/definitions/jp.yaml | 761 +++++++++ .../vendor/holidays/definitions/ke.yaml | 107 ++ .../vendor/holidays/definitions/kr.yaml | 166 ++ .../vendor/holidays/definitions/kz.yaml | 128 ++ .../vendor/holidays/definitions/li.yaml | 154 ++ .../lib/validation/custom_method_validator.rb | 38 + .../lib/validation/definition_validator.rb | 35 + .../definitions/lib/validation/error.rb | 11 + .../lib/validation/month_validator.rb | 58 + .../definitions/lib/validation/run.rb | 66 + .../lib/validation/test_validator.rb | 83 + .../vendor/holidays/definitions/lt.yaml | 190 +++ .../vendor/holidays/definitions/lu.yaml | 123 ++ .../vendor/holidays/definitions/lv.yaml | 216 +++ .../vendor/holidays/definitions/ma.yaml | 96 ++ .../vendor/holidays/definitions/mt_en.yaml | 131 ++ .../vendor/holidays/definitions/mt_mt.yaml | 131 ++ .../vendor/holidays/definitions/mx.yaml | 153 ++ .../vendor/holidays/definitions/my.yaml | 171 ++ .../vendor/holidays/definitions/nerc.yaml | 94 ++ .../vendor/holidays/definitions/ng.yaml | 97 ++ .../vendor/holidays/definitions/nl.yaml | 127 ++ .../vendor/holidays/definitions/no.yaml | 169 ++ .../definitions/northamericainformal.yaml | 105 ++ .../vendor/holidays/definitions/nyse.yaml | 116 ++ .../vendor/holidays/definitions/nz.yaml | 324 ++++ .../vendor/holidays/definitions/pe.yaml | 208 +++ .../vendor/holidays/definitions/ph.yaml | 130 ++ .../vendor/holidays/definitions/pl.yaml | 784 +++++++++ .../vendor/holidays/definitions/pt.yaml | 187 +++ .../vendor/holidays/definitions/ro.yaml | 240 +++ .../vendor/holidays/definitions/rs_cyrl.yaml | 127 ++ .../vendor/holidays/definitions/rs_la.yaml | 127 ++ .../vendor/holidays/definitions/ru.yaml | 108 ++ .../vendor/holidays/definitions/sa.yaml | 100 ++ .../vendor/holidays/definitions/se.yaml | 238 +++ .../vendor/holidays/definitions/sg.yaml | 81 + .../vendor/holidays/definitions/si.yaml | 159 ++ .../vendor/holidays/definitions/sk.yaml | 154 ++ .../definitions/spec/coverage_report.rb | 9 + .../data/invalid/months/malformed/bad.yaml | 15 + .../invalid/months/missing/no_months.yaml | 9 + .../definitions/spec/data/valid/simple.yaml | 15 + .../holidays/definitions/spec/spec_helper.rb | 7 + .../custom_method_validator_spec.rb | 60 + .../validation/definition_validator_spec.rb | 43 + .../spec/validation/month_validator_spec.rb | 175 ++ .../spec/validation/test_validator_spec.rb | 169 ++ .../vendor/holidays/definitions/th.yaml | 111 ++ .../vendor/holidays/definitions/tn.yaml | 83 + .../vendor/holidays/definitions/tr.yaml | 174 ++ .../vendor/holidays/definitions/ua.yaml | 161 ++ .../holidays/definitions/unitednations.yaml | 189 +++ .../vendor/holidays/definitions/ups.yaml | 102 ++ .../vendor/holidays/definitions/us.yaml | 933 +++++++++++ .../vendor/holidays/definitions/ve.yaml | 134 ++ .../vendor/holidays/definitions/vi.yaml | 54 + .../vendor/holidays/definitions/za.yaml | 139 ++ .../vendor/holidays/definitions/zw.yaml | 150 ++ .../vendor/holidays/doc/CONTRIBUTING.md | 72 + .../vendor/holidays/doc/MAINTAINERS.md | 81 + .../vendor/holidays/doc/REFERENCES | 19 + .../vendor/holidays/holidays.gemspec | 26 + .../lib/generated_definitions/MANIFEST | 86 + .../lib/generated_definitions/REGIONS.rb | 6 + .../holidays/lib/generated_definitions/ae.rb | 31 + .../holidays/lib/generated_definitions/ar.rb | 41 + .../holidays/lib/generated_definitions/at.rb | 37 + .../holidays/lib/generated_definitions/au.rb | 140 ++ .../holidays/lib/generated_definitions/be.rb | 42 + .../lib/generated_definitions/be_fr.rb | 36 + .../lib/generated_definitions/be_nl.rb | 36 + .../holidays/lib/generated_definitions/bg.rb | 53 + .../holidays/lib/generated_definitions/br.rb | 40 + .../holidays/lib/generated_definitions/ca.rb | 75 + .../holidays/lib/generated_definitions/ch.rb | 95 ++ .../holidays/lib/generated_definitions/cl.rb | 71 + .../holidays/lib/generated_definitions/co.rb | 121 ++ .../holidays/lib/generated_definitions/cr.rb | 35 + .../holidays/lib/generated_definitions/cz.rb | 37 + .../holidays/lib/generated_definitions/de.rb | 62 + .../holidays/lib/generated_definitions/dk.rb | 48 + .../lib/generated_definitions/ecbtarget.rb | 30 + .../holidays/lib/generated_definitions/ee.rb | 36 + .../holidays/lib/generated_definitions/el.rb | 38 + .../holidays/lib/generated_definitions/es.rb | 56 + .../lib/generated_definitions/europe.rb | 626 +++++++ .../generated_definitions/federalreserve.rb | 34 + .../federalreservebanks.rb | 34 + .../lib/generated_definitions/fedex.rb | 36 + .../holidays/lib/generated_definitions/fi.rb | 61 + .../holidays/lib/generated_definitions/fr.rb | 39 + .../holidays/lib/generated_definitions/gb.rb | 49 + .../holidays/lib/generated_definitions/ge.rb | 41 + .../holidays/lib/generated_definitions/gh.rb | 58 + .../holidays/lib/generated_definitions/hk.rb | 106 ++ .../holidays/lib/generated_definitions/hr.rb | 40 + .../holidays/lib/generated_definitions/hu.rb | 35 + .../holidays/lib/generated_definitions/id.rb | 108 ++ .../holidays/lib/generated_definitions/ie.rb | 33 + .../holidays/lib/generated_definitions/in.rb | 54 + .../holidays/lib/generated_definitions/is.rb | 60 + .../holidays/lib/generated_definitions/it.rb | 45 + .../holidays/lib/generated_definitions/jp.rb | 166 ++ .../holidays/lib/generated_definitions/ke.rb | 34 + .../holidays/lib/generated_definitions/kr.rb | 40 + .../holidays/lib/generated_definitions/kz.rb | 38 + .../holidays/lib/generated_definitions/li.rb | 44 + .../holidays/lib/generated_definitions/lt.rb | 37 + .../holidays/lib/generated_definitions/lu.rb | 35 + .../holidays/lib/generated_definitions/lv.rb | 52 + .../holidays/lib/generated_definitions/ma.rb | 33 + .../lib/generated_definitions/mt_en.rb | 38 + .../lib/generated_definitions/mt_mt.rb | 38 + .../holidays/lib/generated_definitions/mx.rb | 54 + .../holidays/lib/generated_definitions/my.rb | 38 + .../lib/generated_definitions/nerc.rb | 30 + .../holidays/lib/generated_definitions/ng.rb | 33 + .../holidays/lib/generated_definitions/nl.rb | 37 + .../holidays/lib/generated_definitions/no.rb | 40 + .../lib/generated_definitions/northamerica.rb | 207 +++ .../lib/generated_definitions/nyse.rb | 33 + .../holidays/lib/generated_definitions/nz.rb | 104 ++ .../holidays/lib/generated_definitions/pe.rb | 43 + .../holidays/lib/generated_definitions/ph.rb | 50 + .../holidays/lib/generated_definitions/pl.rb | 72 + .../holidays/lib/generated_definitions/pt.rb | 40 + .../holidays/lib/generated_definitions/ro.rb | 39 + .../lib/generated_definitions/rs_cyrl.rb | 39 + .../lib/generated_definitions/rs_la.rb | 39 + .../holidays/lib/generated_definitions/ru.rb | 37 + .../holidays/lib/generated_definitions/sa.rb | 43 + .../lib/generated_definitions/scandinavia.rb | 166 ++ .../holidays/lib/generated_definitions/se.rb | 53 + .../holidays/lib/generated_definitions/sg.rb | 36 + .../holidays/lib/generated_definitions/si.rb | 38 + .../holidays/lib/generated_definitions/sk.rb | 39 + .../lib/generated_definitions/southamerica.rb | 234 +++ .../holidays/lib/generated_definitions/th.rb | 36 + .../holidays/lib/generated_definitions/tn.rb | 32 + .../holidays/lib/generated_definitions/tr.rb | 64 + .../holidays/lib/generated_definitions/ua.rb | 37 + .../generated_definitions/unitednations.rb | 81 + .../holidays/lib/generated_definitions/ups.rb | 36 + .../holidays/lib/generated_definitions/us.rb | 144 ++ .../holidays/lib/generated_definitions/ve.rb | 38 + .../holidays/lib/generated_definitions/vi.rb | 29 + .../holidays/lib/generated_definitions/za.rb | 36 + .../holidays/lib/generated_definitions/zw.rb | 36 + .../vendor/holidays/lib/holidays.rb | 130 ++ .../lib/holidays/core_extensions/date.rb | 57 + .../lib/holidays/core_extensions/time.rb | 23 + .../holidays/date_calculator/day_of_month.rb | 68 + .../lib/holidays/date_calculator/easter.rb | 91 + .../holidays/date_calculator/lunar_date.rb | 375 +++++ .../date_calculator/weekend_modifier.rb | 80 + .../definition/context/function_processor.rb | 91 + .../holidays/definition/context/generator.rb | 209 +++ .../lib/holidays/definition/context/load.rb | 29 + .../lib/holidays/definition/context/merger.rb | 22 + .../decorator/custom_method_proc.rb | 28 + .../decorator/custom_method_source.rb | 30 + .../lib/holidays/definition/decorator/test.rb | 37 + .../definition/entity/custom_method.rb | 11 + .../lib/holidays/definition/entity/test.rb | 11 + .../holidays/definition/generator/module.rb | 54 + .../holidays/definition/generator/regions.rb | 55 + .../lib/holidays/definition/generator/test.rb | 51 + .../definition/parser/custom_method.rb | 67 + .../lib/holidays/definition/parser/test.rb | 86 + .../holidays/definition/repository/cache.rb | 47 + .../definition/repository/custom_methods.rb | 27 + .../repository/holidays_by_month.rb | 57 + .../repository/proc_result_cache.rb | 51 + .../holidays/definition/repository/regions.rb | 46 + .../definition/validator/custom_method.rb | 31 + .../holidays/definition/validator/region.rb | 36 + .../lib/holidays/definition/validator/test.rb | 71 + .../vendor/holidays/lib/holidays/errors.rb | 11 + .../lib/holidays/factory/date_calculator.rb | 42 + .../lib/holidays/factory/definition.rb | 143 ++ .../holidays/lib/holidays/factory/finder.rb | 70 + .../lib/holidays/finder/context/between.rb | 45 + .../finder/context/dates_driver_builder.rb | 64 + .../holidays/finder/context/next_holiday.rb | 57 + .../holidays/finder/context/parse_options.rb | 104 ++ .../lib/holidays/finder/context/search.rb | 110 ++ .../holidays/finder/context/year_holiday.rb | 57 + .../lib/holidays/finder/rules/in_region.rb | 31 + .../lib/holidays/finder/rules/year_range.rb | 58 + .../lib/holidays/load_all_definitions.rb | 56 + .../vendor/holidays/lib/holidays/version.rb | 3 + .../vendor/holidays/test/coverage_report.rb | 26 + .../data/test_custom_govt_holiday_defs.yaml | 5 + .../test_custom_informal_holidays_defs.yaml | 11 + .../test_custom_year_range_holiday_defs.yaml | 31 + .../holidays/test/data/test_invalid_region.rb | 15 + .../test_multiple_custom_holiday_defs.yaml | 12 + ...tiple_regions_with_conflicts_region_1.yaml | 38 + ...tiple_regions_with_conflicts_region_2.yaml | 38 + .../vendor/holidays/test/data/test_region.rb | 15 + .../data/test_single_custom_holiday_defs.yaml | 12 + ...ngle_custom_holiday_with_custom_procs.yaml | 28 + .../vendor/holidays/test/defs/test_defs_ae.rb | 19 + .../vendor/holidays/test/defs/test_defs_ar.rb | 53 + .../vendor/holidays/test/defs/test_defs_at.rb | 31 + .../vendor/holidays/test/defs/test_defs_au.rb | 200 +++ .../holidays/test/defs/test_defs_be_fr.rb | 45 + .../holidays/test/defs/test_defs_be_nl.rb | 45 + .../vendor/holidays/test/defs/test_defs_bg.rb | 41 + .../vendor/holidays/test/defs/test_defs_br.rb | 45 + .../vendor/holidays/test/defs/test_defs_ca.rb | 214 +++ .../vendor/holidays/test/defs/test_defs_ch.rb | 51 + .../vendor/holidays/test/defs/test_defs_cl.rb | 69 + .../vendor/holidays/test/defs/test_defs_co.rb | 113 ++ .../vendor/holidays/test/defs/test_defs_cr.rb | 29 + .../vendor/holidays/test/defs/test_defs_cz.rb | 37 + .../vendor/holidays/test/defs/test_defs_de.rb | 85 + .../vendor/holidays/test/defs/test_defs_dk.rb | 43 + .../holidays/test/defs/test_defs_ecbtarget.rb | 27 + .../vendor/holidays/test/defs/test_defs_ee.rb | 41 + .../vendor/holidays/test/defs/test_defs_el.rb | 41 + .../vendor/holidays/test/defs/test_defs_es.rb | 137 ++ .../holidays/test/defs/test_defs_europe.rb | 1488 +++++++++++++++++ .../holidays/test/defs/test_defs_fed_ex.rb | 24 + .../test/defs/test_defs_federalreserve.rb | 113 ++ .../defs/test_defs_federalreservebanks.rb | 247 +++ .../holidays/test/defs/test_defs_fedex.rb | 31 + .../vendor/holidays/test/defs/test_defs_fi.rb | 59 + .../vendor/holidays/test/defs/test_defs_fr.rb | 43 + .../vendor/holidays/test/defs/test_defs_gb.rb | 145 ++ .../vendor/holidays/test/defs/test_defs_ge.rb | 53 + .../vendor/holidays/test/defs/test_defs_gh.rb | 48 + .../vendor/holidays/test/defs/test_defs_hk.rb | 59 + .../vendor/holidays/test/defs/test_defs_hr.rb | 45 + .../vendor/holidays/test/defs/test_defs_hu.rb | 47 + .../vendor/holidays/test/defs/test_defs_id.rb | 17 + .../vendor/holidays/test/defs/test_defs_ie.rb | 53 + .../vendor/holidays/test/defs/test_defs_in.rb | 11 + .../vendor/holidays/test/defs/test_defs_is.rb | 51 + .../vendor/holidays/test/defs/test_defs_it.rb | 55 + .../vendor/holidays/test/defs/test_defs_jp.rb | 159 ++ .../vendor/holidays/test/defs/test_defs_ke.rb | 31 + .../vendor/holidays/test/defs/test_defs_kr.rb | 37 + .../vendor/holidays/test/defs/test_defs_kz.rb | 39 + .../vendor/holidays/test/defs/test_defs_li.rb | 35 + .../vendor/holidays/test/defs/test_defs_lt.rb | 63 + .../vendor/holidays/test/defs/test_defs_lu.rb | 35 + .../vendor/holidays/test/defs/test_defs_lv.rb | 90 + .../vendor/holidays/test/defs/test_defs_ma.rb | 29 + .../holidays/test/defs/test_defs_mt_en.rb | 41 + .../holidays/test/defs/test_defs_mt_mt.rb | 41 + .../vendor/holidays/test/defs/test_defs_mx.rb | 47 + .../vendor/holidays/test/defs/test_defs_my.rb | 39 + .../holidays/test/defs/test_defs_nerc.rb | 29 + .../vendor/holidays/test/defs/test_defs_ng.rb | 29 + .../vendor/holidays/test/defs/test_defs_nl.rb | 33 + .../vendor/holidays/test/defs/test_defs_no.rb | 43 + .../test/defs/test_defs_northamerica.rb | 582 +++++++ .../holidays/test/defs/test_defs_nyse.rb | 39 + .../vendor/holidays/test/defs/test_defs_nz.rb | 63 + .../vendor/holidays/test/defs/test_defs_pe.rb | 47 + .../vendor/holidays/test/defs/test_defs_ph.rb | 29 + .../vendor/holidays/test/defs/test_defs_pl.rb | 227 +++ .../vendor/holidays/test/defs/test_defs_pt.rb | 47 + .../vendor/holidays/test/defs/test_defs_ro.rb | 65 + .../holidays/test/defs/test_defs_rs_cyrl.rb | 46 + .../holidays/test/defs/test_defs_rs_la.rb | 46 + .../vendor/holidays/test/defs/test_defs_ru.rb | 34 + .../vendor/holidays/test/defs/test_defs_sa.rb | 11 + .../test/defs/test_defs_scandinavia.rb | 211 +++ .../vendor/holidays/test/defs/test_defs_se.rb | 59 + .../vendor/holidays/test/defs/test_defs_sg.rb | 11 + .../vendor/holidays/test/defs/test_defs_si.rb | 105 ++ .../vendor/holidays/test/defs/test_defs_sk.rb | 41 + .../test/defs/test_defs_southamerica.rb | 311 ++++ .../vendor/holidays/test/defs/test_defs_th.rb | 33 + .../vendor/holidays/test/defs/test_defs_tn.rb | 27 + .../vendor/holidays/test/defs/test_defs_tr.rb | 60 + .../vendor/holidays/test/defs/test_defs_ua.rb | 41 + .../test/defs/test_defs_unitednations.rb | 11 + .../holidays/test/defs/test_defs_ups.rb | 31 + .../vendor/holidays/test/defs/test_defs_us.rb | 379 +++++ .../vendor/holidays/test/defs/test_defs_ve.rb | 39 + .../vendor/holidays/test/defs/test_defs_vi.rb | 22 + .../vendor/holidays/test/defs/test_defs_za.rb | 35 + .../vendor/holidays/test/defs/test_defs_zw.rb | 35 + .../holidays/core_extensions/test_date.rb | 122 ++ .../core_extensions/test_date_time.rb | 60 + .../date_calculator/test_day_of_month.rb | 27 + .../date_calculator/test_easter_gregorian.rb | 30 + .../date_calculator/test_easter_julian.rb | 36 + .../date_calculator/test_lunar_date.rb | 89 + .../date_calculator/test_weekend_modifier.rb | 54 + .../context/test_function_processor.rb | 199 +++ .../definition/context/test_generator.rb | 226 +++ .../holidays/definition/context/test_load.rb | 37 + .../definition/context/test_merger.rb | 25 + .../decorator/test_custom_method_proc.rb | 113 ++ .../decorator/test_custom_method_source.rb | 96 ++ .../definition/decorator/test_test.rb | 123 ++ .../definition/generator/test_module.rb | 268 +++ .../definition/generator/test_regions.rb | 97 ++ .../definition/generator/test_test.rb | 113 ++ .../definition/parser/test_custom_method.rb | 79 + .../holidays/definition/parser/test_test.rb | 142 ++ .../definition/repository/test_cache.rb | 123 ++ .../repository/test_custom_methods.rb | 43 + .../repository/test_holidays_by_month.rb | 275 +++ .../repository/test_proc_result_cache.rb | 91 + .../definition/repository/test_regions.rb | 104 ++ .../validator/test_custom_method.rb | 94 ++ .../definition/validator/test_region.rb | 54 + .../definition/validator/test_test.rb | 60 + .../holidays/finder/context/test_between.rb | 172 ++ .../context/test_dates_driver_builder.rb | 91 + .../finder/context/test_next_holiday.rb | 156 ++ .../finder/context/test_parse_options.rb | 141 ++ .../holidays/finder/context/test_search.rb | 232 +++ .../finder/context/test_year_holiday.rb | 202 +++ .../holidays/finder/rules/test_in_region.rb | 42 + .../holidays/finder/rules/test_year_range.rb | 166 ++ .../holidays/test/integration/README.md | 9 + .../test/integration/test_all_regions.rb | 49 + .../test_any_holidays_during_work_week.rb | 90 + .../integration/test_available_regions.rb | 23 + .../test/integration/test_custom_holidays.rb | 41 + .../test_custom_informal_holidays.rb | 15 + .../test_custom_year_range_holidays.rb | 35 + .../test/integration/test_holidays.rb | 249 +++ .../test/integration/test_holidays_between.rb | 87 + .../test/integration/test_multiple_regions.rb | 71 + .../test_multiple_regions_with_conflict.rb | 29 + .../integration/test_nonstandard_regions.rb | 25 + .../vendor/holidays/test/test_helper.rb | 37 + .../modal/recalculate-scores-form.gjs | 2 + translator.yml | 7 + 764 files changed, 79331 insertions(+) create mode 100644 plugins/discourse-calendar/.prettierignore create mode 100644 plugins/discourse-calendar/README.md create mode 100644 plugins/discourse-calendar/app/controllers/admin/discourse_calendar/admin_holidays_controller.rb create mode 100644 plugins/discourse-calendar/app/controllers/discourse_post_event/discourse_post_event_controller.rb create mode 100644 plugins/discourse-calendar/app/controllers/discourse_post_event/events_controller.rb create mode 100644 plugins/discourse-calendar/app/controllers/discourse_post_event/invitees_controller.rb create mode 100644 plugins/discourse-calendar/app/controllers/discourse_post_event/upcoming_events_controller.rb create mode 100644 plugins/discourse-calendar/app/models/calendar_event.rb create mode 100644 plugins/discourse-calendar/app/models/discourse_calendar/disabled_holiday.rb create mode 100644 plugins/discourse-calendar/app/models/discourse_post_event/event.rb create mode 100644 plugins/discourse-calendar/app/models/discourse_post_event/event_date.rb create mode 100644 plugins/discourse-calendar/app/models/discourse_post_event/invitee.rb create mode 100644 plugins/discourse-calendar/app/serializers/discourse_post_event/event_serializer.rb create mode 100644 plugins/discourse-calendar/app/serializers/discourse_post_event/event_stats_serializer.rb create mode 100644 plugins/discourse-calendar/app/serializers/discourse_post_event/event_summary_serializer.rb create mode 100644 plugins/discourse-calendar/app/serializers/discourse_post_event/invitee_list_serializer.rb create mode 100644 plugins/discourse-calendar/app/serializers/discourse_post_event/invitee_serializer.rb create mode 100644 plugins/discourse-calendar/app/serializers/user_timezone_serializer.rb create mode 100644 plugins/discourse-calendar/app/services/discourse_calendar/holiday.rb create mode 100644 plugins/discourse-calendar/app/services/discourse_post_event/chat_channel_sync.rb create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/adapters/discourse-post-event-adapter.js create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/adapters/discourse-post-event-event.js create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/adapters/discourse-post-event-invitee.js create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/adapters/discourse-post-event-nested-adapter.js create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/adapters/discourse-post-event-reminder.js create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/admin-calendar-route-map.js create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/api-initializers/discourse-group-timezones.gjs create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/components/admin-holidays-list-item.gjs create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/components/admin-holidays-list.gjs create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/components/bulk-invite-sample-csv-file.gjs create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/components/csv-uploader.gjs create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/components/discourse-post-event/chat-channel.gjs create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/components/discourse-post-event/creator.gjs create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/components/discourse-post-event/dates.gjs create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/components/discourse-post-event/description.gjs create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/components/discourse-post-event/event-status.gjs create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/components/discourse-post-event/index.gjs create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/components/discourse-post-event/invitee.gjs create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/components/discourse-post-event/invitees.gjs create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/components/discourse-post-event/location.gjs create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/components/discourse-post-event/more-menu.gjs create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/components/discourse-post-event/status.gjs create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/components/discourse-post-event/url.gjs create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/components/event-date.gjs create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/components/event-field.gjs create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/components/group-timezones/index.gjs create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/components/group-timezones/new-day.gjs create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/components/group-timezones/time-traveller.gjs create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/components/group-timezones/timezone.gjs create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/components/modal/post-event-builder.gjs create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/components/modal/post-event-bulk-invite.gjs create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/components/modal/post-event-invite-user-or-group.gjs create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/components/modal/post-event-invitees/index.gjs create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/components/modal/post-event-invitees/user.gjs create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/components/region-input.js create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/components/toggle-invitees.gjs create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/components/upcoming-events-calendar.gjs create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/components/upcoming-events-list.gjs create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/connectors/before-topic-list-body/category-calendar.gjs create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/connectors/category-custom-settings/show-event-category-sorting-settings.gjs create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/connectors/discovery-list-container-top/category-events-calendar.gjs create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/connectors/header-topic-title-suffix/event-date-container.gjs create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/connectors/topic-list-after-title/event-badge.gjs create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/connectors/user-custom-preferences/region.gjs create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/controllers/admin-plugins-calendar.js create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/controllers/discourse-post-event-upcoming-events-index.js create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/controllers/discourse-post-event-upcoming-events-mine.js create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/discourse-event-upcoming-events-route-map.js create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/helpers/format-event-name.js create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/helpers/format-future-date.js create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/initializers/add-event-ui-builder.js create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/initializers/add-upcoming-events-to-sidebar.js create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/initializers/disable-sort.js create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/initializers/discourse-calendar.js create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/initializers/discourse-post-event-decorator.gjs create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/initializers/event-relative-date.js create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/lib/add-recurrent-events.js create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/lib/calendar-locale.js create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/lib/colors.js create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/lib/discourse-markdown/discourse-calendar.js create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/lib/discourse-markdown/discourse-post-event-block.js create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/lib/event-relative-date.js create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/lib/full-calendar-default-options.js create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/lib/guess-best-date-format.js create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/lib/popover.js create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/lib/raw-event-helper.js create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/lib/regions.js create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/lib/round-time.js create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/models/discourse-post-event-event-stats.js create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/models/discourse-post-event-event.js create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/models/discourse-post-event-invitee.js create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/models/discourse-post-event-invitees.js create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/models/discourse-post-event-reminder.js create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/pre-initializers/rich-editor-extension.js create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/pre-initializers/transformers.js create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/routes/discourse-post-event-upcoming-events-index.js create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/routes/discourse-post-event-upcoming-events-mine.gjs create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/services/discourse-post-event-api.js create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/services/discourse-post-event-service.js create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/templates/admin-plugins-calendar.gjs create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/templates/discourse-post-event-upcoming-events-index.gjs create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/templates/discourse-post-event-upcoming-events-mine.gjs create mode 100644 plugins/discourse-calendar/assets/javascripts/discourse/templates/discourse-post-event-upcoming-events.gjs create mode 100644 plugins/discourse-calendar/assets/stylesheets/colors.scss create mode 100644 plugins/discourse-calendar/assets/stylesheets/common/discourse-calendar-holidays.scss create mode 100644 plugins/discourse-calendar/assets/stylesheets/common/discourse-calendar.scss create mode 100644 plugins/discourse-calendar/assets/stylesheets/common/discourse-post-event-bulk-invite-modal.scss create mode 100644 plugins/discourse-calendar/assets/stylesheets/common/discourse-post-event-core-ext.scss create mode 100644 plugins/discourse-calendar/assets/stylesheets/common/discourse-post-event-invitees.scss create mode 100644 plugins/discourse-calendar/assets/stylesheets/common/discourse-post-event-preview.scss create mode 100644 plugins/discourse-calendar/assets/stylesheets/common/discourse-post-event-upcoming-events.scss create mode 100644 plugins/discourse-calendar/assets/stylesheets/common/discourse-post-event.scss create mode 100644 plugins/discourse-calendar/assets/stylesheets/common/post-event-builder.scss create mode 100644 plugins/discourse-calendar/assets/stylesheets/common/upcoming-events-calendar.scss create mode 100644 plugins/discourse-calendar/assets/stylesheets/common/upcoming-events-list.scss create mode 100644 plugins/discourse-calendar/assets/stylesheets/common/user-preferences.scss create mode 100644 plugins/discourse-calendar/assets/stylesheets/desktop/discourse-calendar.scss create mode 100644 plugins/discourse-calendar/assets/stylesheets/desktop/discourse-post-event-invitees.scss create mode 100644 plugins/discourse-calendar/assets/stylesheets/mobile/discourse-calendar.scss create mode 100644 plugins/discourse-calendar/assets/stylesheets/mobile/discourse-post-event-core-ext.scss create mode 100644 plugins/discourse-calendar/assets/stylesheets/mobile/discourse-post-event-invitees.scss create mode 100644 plugins/discourse-calendar/assets/stylesheets/mobile/discourse-post-event.scss create mode 100644 plugins/discourse-calendar/assets/stylesheets/vendor/fullcalendar.min.css create mode 100644 plugins/discourse-calendar/config/locales/client.ar.yml create mode 100644 plugins/discourse-calendar/config/locales/client.be.yml create mode 100644 plugins/discourse-calendar/config/locales/client.bg.yml create mode 100644 plugins/discourse-calendar/config/locales/client.bs_BA.yml create mode 100644 plugins/discourse-calendar/config/locales/client.ca.yml create mode 100644 plugins/discourse-calendar/config/locales/client.cs.yml create mode 100644 plugins/discourse-calendar/config/locales/client.da.yml create mode 100644 plugins/discourse-calendar/config/locales/client.de.yml create mode 100644 plugins/discourse-calendar/config/locales/client.el.yml create mode 100644 plugins/discourse-calendar/config/locales/client.en.yml create mode 100644 plugins/discourse-calendar/config/locales/client.en_GB.yml create mode 100644 plugins/discourse-calendar/config/locales/client.es.yml create mode 100644 plugins/discourse-calendar/config/locales/client.et.yml create mode 100644 plugins/discourse-calendar/config/locales/client.fa_IR.yml create mode 100644 plugins/discourse-calendar/config/locales/client.fi.yml create mode 100644 plugins/discourse-calendar/config/locales/client.fr.yml create mode 100644 plugins/discourse-calendar/config/locales/client.gl.yml create mode 100644 plugins/discourse-calendar/config/locales/client.he.yml create mode 100644 plugins/discourse-calendar/config/locales/client.hr.yml create mode 100644 plugins/discourse-calendar/config/locales/client.hu.yml create mode 100644 plugins/discourse-calendar/config/locales/client.hy.yml create mode 100644 plugins/discourse-calendar/config/locales/client.id.yml create mode 100644 plugins/discourse-calendar/config/locales/client.it.yml create mode 100644 plugins/discourse-calendar/config/locales/client.ja.yml create mode 100644 plugins/discourse-calendar/config/locales/client.ko.yml create mode 100644 plugins/discourse-calendar/config/locales/client.lt.yml create mode 100644 plugins/discourse-calendar/config/locales/client.lv.yml create mode 100644 plugins/discourse-calendar/config/locales/client.nb_NO.yml create mode 100644 plugins/discourse-calendar/config/locales/client.nl.yml create mode 100644 plugins/discourse-calendar/config/locales/client.pl_PL.yml create mode 100644 plugins/discourse-calendar/config/locales/client.pt.yml create mode 100644 plugins/discourse-calendar/config/locales/client.pt_BR.yml create mode 100644 plugins/discourse-calendar/config/locales/client.ro.yml create mode 100644 plugins/discourse-calendar/config/locales/client.ru.yml create mode 100644 plugins/discourse-calendar/config/locales/client.sk.yml create mode 100644 plugins/discourse-calendar/config/locales/client.sl.yml create mode 100644 plugins/discourse-calendar/config/locales/client.sq.yml create mode 100644 plugins/discourse-calendar/config/locales/client.sr.yml create mode 100644 plugins/discourse-calendar/config/locales/client.sv.yml create mode 100644 plugins/discourse-calendar/config/locales/client.sw.yml create mode 100644 plugins/discourse-calendar/config/locales/client.te.yml create mode 100644 plugins/discourse-calendar/config/locales/client.th.yml create mode 100644 plugins/discourse-calendar/config/locales/client.tr_TR.yml create mode 100644 plugins/discourse-calendar/config/locales/client.ug.yml create mode 100644 plugins/discourse-calendar/config/locales/client.uk.yml create mode 100644 plugins/discourse-calendar/config/locales/client.ur.yml create mode 100644 plugins/discourse-calendar/config/locales/client.vi.yml create mode 100644 plugins/discourse-calendar/config/locales/client.zh_CN.yml create mode 100644 plugins/discourse-calendar/config/locales/client.zh_TW.yml create mode 100644 plugins/discourse-calendar/config/locales/server.ar.yml create mode 100644 plugins/discourse-calendar/config/locales/server.be.yml create mode 100644 plugins/discourse-calendar/config/locales/server.bg.yml create mode 100644 plugins/discourse-calendar/config/locales/server.bs_BA.yml create mode 100644 plugins/discourse-calendar/config/locales/server.ca.yml create mode 100644 plugins/discourse-calendar/config/locales/server.cs.yml create mode 100644 plugins/discourse-calendar/config/locales/server.da.yml create mode 100644 plugins/discourse-calendar/config/locales/server.de.yml create mode 100644 plugins/discourse-calendar/config/locales/server.el.yml create mode 100644 plugins/discourse-calendar/config/locales/server.en.yml create mode 100644 plugins/discourse-calendar/config/locales/server.en_GB.yml create mode 100644 plugins/discourse-calendar/config/locales/server.es.yml create mode 100644 plugins/discourse-calendar/config/locales/server.et.yml create mode 100644 plugins/discourse-calendar/config/locales/server.fa_IR.yml create mode 100644 plugins/discourse-calendar/config/locales/server.fi.yml create mode 100644 plugins/discourse-calendar/config/locales/server.fr.yml create mode 100644 plugins/discourse-calendar/config/locales/server.gl.yml create mode 100644 plugins/discourse-calendar/config/locales/server.he.yml create mode 100644 plugins/discourse-calendar/config/locales/server.hr.yml create mode 100644 plugins/discourse-calendar/config/locales/server.hu.yml create mode 100644 plugins/discourse-calendar/config/locales/server.hy.yml create mode 100644 plugins/discourse-calendar/config/locales/server.id.yml create mode 100644 plugins/discourse-calendar/config/locales/server.it.yml create mode 100644 plugins/discourse-calendar/config/locales/server.ja.yml create mode 100644 plugins/discourse-calendar/config/locales/server.ko.yml create mode 100644 plugins/discourse-calendar/config/locales/server.lt.yml create mode 100644 plugins/discourse-calendar/config/locales/server.lv.yml create mode 100644 plugins/discourse-calendar/config/locales/server.nb_NO.yml create mode 100644 plugins/discourse-calendar/config/locales/server.nl.yml create mode 100644 plugins/discourse-calendar/config/locales/server.pl_PL.yml create mode 100644 plugins/discourse-calendar/config/locales/server.pt.yml create mode 100644 plugins/discourse-calendar/config/locales/server.pt_BR.yml create mode 100644 plugins/discourse-calendar/config/locales/server.ro.yml create mode 100644 plugins/discourse-calendar/config/locales/server.ru.yml create mode 100644 plugins/discourse-calendar/config/locales/server.sk.yml create mode 100644 plugins/discourse-calendar/config/locales/server.sl.yml create mode 100644 plugins/discourse-calendar/config/locales/server.sq.yml create mode 100644 plugins/discourse-calendar/config/locales/server.sr.yml create mode 100644 plugins/discourse-calendar/config/locales/server.sv.yml create mode 100644 plugins/discourse-calendar/config/locales/server.sw.yml create mode 100644 plugins/discourse-calendar/config/locales/server.te.yml create mode 100644 plugins/discourse-calendar/config/locales/server.th.yml create mode 100644 plugins/discourse-calendar/config/locales/server.tr_TR.yml create mode 100644 plugins/discourse-calendar/config/locales/server.ug.yml create mode 100644 plugins/discourse-calendar/config/locales/server.uk.yml create mode 100644 plugins/discourse-calendar/config/locales/server.ur.yml create mode 100644 plugins/discourse-calendar/config/locales/server.vi.yml create mode 100644 plugins/discourse-calendar/config/locales/server.zh_CN.yml create mode 100644 plugins/discourse-calendar/config/locales/server.zh_TW.yml create mode 100644 plugins/discourse-calendar/config/routes.rb create mode 100644 plugins/discourse-calendar/config/settings.yml create mode 100644 plugins/discourse-calendar/db/migrate/20190724181542_add_on_holiday_index.rb create mode 100644 plugins/discourse-calendar/db/migrate/20200226183018_create_calendar_events.rb create mode 100644 plugins/discourse-calendar/db/migrate/20200310200000_remove_timezone_custom_field.rb create mode 100644 plugins/discourse-calendar/db/migrate/20200327195549_add_topic_custom_field_post_event_date_index.rb create mode 100644 plugins/discourse-calendar/db/migrate/20200409102639_drop_incorrect_future_schema_migrations.rb create mode 100644 plugins/discourse-calendar/db/migrate/20200409102640_create_post_events_table.rb create mode 100644 plugins/discourse-calendar/db/migrate/20200409102641_create_invitees_table.rb create mode 100644 plugins/discourse-calendar/db/migrate/20200409102642_rename_setting_to_discourse_post_event.rb create mode 100644 plugins/discourse-calendar/db/migrate/20200409102643_rename_tables_to_discourse_post_event.rb create mode 100644 plugins/discourse-calendar/db/migrate/20200409120815_rename_topic_custom_field_topic_post_event_starts_at_index.rb create mode 100644 plugins/discourse-calendar/db/migrate/20200409181607_remove_display_invitees.rb create mode 100644 plugins/discourse-calendar/db/migrate/20200729094848_add_url_column_to_event.rb create mode 100644 plugins/discourse-calendar/db/migrate/20200805073343_drop_old_discourse_calendar_tables.rb create mode 100644 plugins/discourse-calendar/db/migrate/20200805133257_add_custom_fields_to_event.rb create mode 100644 plugins/discourse-calendar/db/migrate/20200809154642_create_reminders_table.rb create mode 100644 plugins/discourse-calendar/db/migrate/20200810185432_refactor_reminders.rb create mode 100644 plugins/discourse-calendar/db/migrate/20200810190429_drop_reminders_table.rb create mode 100644 plugins/discourse-calendar/db/migrate/20200812193122_add_recurrence_to_events.rb create mode 100644 plugins/discourse-calendar/db/migrate/20200926144256_add_unique_index_to_topic_event_ends_at_custom_field.rb create mode 100644 plugins/discourse-calendar/db/migrate/20201110225115_create_post_event_dates_table.rb create mode 100644 plugins/discourse-calendar/db/migrate/20201111005205_move_data_to_event_dates.rb create mode 100644 plugins/discourse-calendar/db/migrate/20211216124303_add_timezone_to_calendar_events.rb create mode 100644 plugins/discourse-calendar/db/migrate/20220228163400_adds_timezone_to_discourse_post_event_event.rb create mode 100644 plugins/discourse-calendar/db/migrate/20220604200919_create_disabled_holidays.rb create mode 100644 plugins/discourse-calendar/db/migrate/20220613073844_unescape_event_name.rb create mode 100644 plugins/discourse-calendar/db/migrate/20220724130519_fix_post_event_timezones.rb create mode 100644 plugins/discourse-calendar/db/migrate/20221121165352_add_type_field_to_events_reminders.rb create mode 100644 plugins/discourse-calendar/db/migrate/20221223210225_add_minimal_option_to_calendar_event.rb create mode 100644 plugins/discourse-calendar/db/migrate/20231123233308_delete_duplicated_holidays.rb create mode 100644 plugins/discourse-calendar/db/migrate/20231124021939_delete_similar_holidays.rb create mode 100644 plugins/discourse-calendar/db/migrate/20240513140542_add_closed_to_discourse_post_event.rb create mode 100644 plugins/discourse-calendar/db/migrate/20250520042223_add_chat_fields_to_events.rb create mode 100644 plugins/discourse-calendar/db/migrate/20250526154632_add_recurrence_until.rb create mode 100644 plugins/discourse-calendar/db/migrate/20250602114410_add_show_local_time.rb create mode 100644 plugins/discourse-calendar/db/migrate/20250616101944_add_location_to_event.rb create mode 100644 plugins/discourse-calendar/db/migrate/20250616101945_add_description_to_event.rb create mode 100644 plugins/discourse-calendar/jobs/regular/discourse_post_event/bulk_invite.rb create mode 100644 plugins/discourse-calendar/jobs/regular/discourse_post_event/bump_topic.rb create mode 100644 plugins/discourse-calendar/jobs/regular/discourse_post_event/event_started.rb create mode 100644 plugins/discourse-calendar/jobs/regular/discourse_post_event/send_reminder.rb create mode 100644 plugins/discourse-calendar/jobs/scheduled/create_holiday_events.rb create mode 100644 plugins/discourse-calendar/jobs/scheduled/delete_expired_event_posts.rb create mode 100644 plugins/discourse-calendar/jobs/scheduled/monitor_event_dates.rb create mode 100644 plugins/discourse-calendar/jobs/scheduled/update_holiday_usernames.rb create mode 100644 plugins/discourse-calendar/lib/calendar.rb create mode 100644 plugins/discourse-calendar/lib/calendar_settings_validator.rb create mode 100644 plugins/discourse-calendar/lib/calendar_validator.rb create mode 100644 plugins/discourse-calendar/lib/discourse_calendar/engine.rb create mode 100644 plugins/discourse-calendar/lib/discourse_calendar/site_settings/map_event_tag_colors_json_schema.rb create mode 100644 plugins/discourse-calendar/lib/discourse_calendar/site_settings/map_events_title_json_schema.rb create mode 100644 plugins/discourse-calendar/lib/discourse_post_event/engine.rb create mode 100644 plugins/discourse-calendar/lib/discourse_post_event/event_finder.rb create mode 100644 plugins/discourse-calendar/lib/discourse_post_event/event_parser.rb create mode 100644 plugins/discourse-calendar/lib/discourse_post_event/event_validator.rb create mode 100644 plugins/discourse-calendar/lib/discourse_post_event/export_csv_controller_extension.rb create mode 100644 plugins/discourse-calendar/lib/discourse_post_event/export_csv_file_extension.rb create mode 100644 plugins/discourse-calendar/lib/discourse_post_event/post_extension.rb create mode 100644 plugins/discourse-calendar/lib/discourse_post_event/rrule_configurator.rb create mode 100644 plugins/discourse-calendar/lib/discourse_post_event/rrule_generator.rb create mode 100644 plugins/discourse-calendar/lib/event_validator.rb create mode 100644 plugins/discourse-calendar/lib/group_timezones.rb create mode 100644 plugins/discourse-calendar/lib/holiday_status.rb create mode 100644 plugins/discourse-calendar/lib/tasks/javascript.rake create mode 100644 plugins/discourse-calendar/lib/time_sniffer.rb create mode 100644 plugins/discourse-calendar/lib/users_on_holiday.rb create mode 100644 plugins/discourse-calendar/plugin.rb create mode 100644 plugins/discourse-calendar/public/javascripts/fullcalendar-with-moment-timezone.min.js create mode 100644 plugins/discourse-calendar/spec/fabricators/calendar_event_fabricator.rb create mode 100644 plugins/discourse-calendar/spec/fabricators/event_fabricator.rb create mode 100644 plugins/discourse-calendar/spec/integration/allowed_custom_fields_setting_spec.rb create mode 100644 plugins/discourse-calendar/spec/integration/curently_away_report_spec.rb create mode 100644 plugins/discourse-calendar/spec/integration/invitee_spec.rb create mode 100644 plugins/discourse-calendar/spec/integration/post_spec.rb create mode 100644 plugins/discourse-calendar/spec/integration/recurrence_spec.rb create mode 100644 plugins/discourse-calendar/spec/integration/topic_spec.rb create mode 100644 plugins/discourse-calendar/spec/jobs/export_post_event_report_csv_spec.rb create mode 100644 plugins/discourse-calendar/spec/jobs/regular/discourse_post_event/bulk_invite_spec.rb create mode 100644 plugins/discourse-calendar/spec/jobs/regular/discourse_post_event/bump_topic_spec.rb create mode 100644 plugins/discourse-calendar/spec/jobs/regular/discourse_post_event/send_reminder_spec.rb create mode 100644 plugins/discourse-calendar/spec/jobs/scheduled/create_holiday_events_spec.rb create mode 100644 plugins/discourse-calendar/spec/jobs/scheduled/delete_expired_event_posts_spec.rb create mode 100644 plugins/discourse-calendar/spec/jobs/scheduled/monitor_event_dates_spec.rb create mode 100644 plugins/discourse-calendar/spec/jobs/scheduled/update_holiday_usernames_spec.rb create mode 100644 plugins/discourse-calendar/spec/lib/calendar_settings_validator_spec.rb create mode 100644 plugins/discourse-calendar/spec/lib/calendar_spec.rb create mode 100644 plugins/discourse-calendar/spec/lib/discourse_post_event/event_finder_spec.rb create mode 100644 plugins/discourse-calendar/spec/lib/discourse_post_event/event_parser_spec.rb create mode 100644 plugins/discourse-calendar/spec/lib/discourse_post_event/pretty_text_spec.rb create mode 100644 plugins/discourse-calendar/spec/lib/discourse_post_event/rrule_configurator_spec.rb create mode 100644 plugins/discourse-calendar/spec/lib/discourse_post_event/rrule_generator_spec.rb create mode 100644 plugins/discourse-calendar/spec/lib/discourse_post_event/time_sniffer_spec.rb create mode 100644 plugins/discourse-calendar/spec/lib/group_timezones_spec.rb create mode 100644 plugins/discourse-calendar/spec/lib/users_on_holiday_spec.rb create mode 100644 plugins/discourse-calendar/spec/models/calendar_event_spec.rb create mode 100644 plugins/discourse-calendar/spec/models/discourse_post_event/event_date_spec.rb create mode 100644 plugins/discourse-calendar/spec/models/discourse_post_event/event_spec.rb create mode 100644 plugins/discourse-calendar/spec/models/discourse_post_event/user_spec.rb create mode 100644 plugins/discourse-calendar/spec/models/topic_query_spec.rb create mode 100644 plugins/discourse-calendar/spec/requests/admin/admin_holidays_controller_spec.rb create mode 100644 plugins/discourse-calendar/spec/requests/events_controller_spec.rb create mode 100644 plugins/discourse-calendar/spec/requests/invitees_controller_spec.rb create mode 100644 plugins/discourse-calendar/spec/requests/site_spec.rb create mode 100644 plugins/discourse-calendar/spec/requests/sort_event_topics_spec.rb create mode 100644 plugins/discourse-calendar/spec/serializers/discourse_post_event/event_serializer_spec.rb create mode 100644 plugins/discourse-calendar/spec/serializers/discourse_post_event/event_summary_serializer_spec.rb create mode 100644 plugins/discourse-calendar/spec/serializers/discourse_post_event/post_serializer_spec.rb create mode 100644 plugins/discourse-calendar/spec/serializers/post_serializer_spec.rb create mode 100644 plugins/discourse-calendar/spec/serializers/user_serializer_spec.rb create mode 100644 plugins/discourse-calendar/spec/services/discourse_post_event/chat_channel_sync_spec.rb create mode 100644 plugins/discourse-calendar/spec/services/discouse-calendar/holiday_spec.rb create mode 100644 plugins/discourse-calendar/spec/system/category_calendar_spec.rb create mode 100644 plugins/discourse-calendar/spec/system/core_features_spec.rb create mode 100644 plugins/discourse-calendar/spec/system/disable_sort_spec.rb create mode 100644 plugins/discourse-calendar/spec/system/group_timezones_spec.rb create mode 100644 plugins/discourse-calendar/spec/system/page_objects/discourse_calendar/bulk_invite_modal.rb create mode 100644 plugins/discourse-calendar/spec/system/page_objects/discourse_calendar/post_event.rb create mode 100644 plugins/discourse-calendar/spec/system/page_objects/discourse_calendar/post_event_form.rb create mode 100644 plugins/discourse-calendar/spec/system/page_objects/discourse_calendar/upcoming_events.rb create mode 100644 plugins/discourse-calendar/spec/system/post_event_spec.rb create mode 100644 plugins/discourse-calendar/spec/system/upcoming_events_spec.rb create mode 100644 plugins/discourse-calendar/test/javascripts/acceptance/admin-holidays-test.js create mode 100644 plugins/discourse-calendar/test/javascripts/acceptance/category-events-calendar-outlet-test.js create mode 100644 plugins/discourse-calendar/test/javascripts/acceptance/category-events-calendar-test.js create mode 100644 plugins/discourse-calendar/test/javascripts/acceptance/notifications-test.js create mode 100644 plugins/discourse-calendar/test/javascripts/acceptance/post-event-builder-test.js create mode 100644 plugins/discourse-calendar/test/javascripts/acceptance/sidebar-upcoming-events-item-test.js create mode 100644 plugins/discourse-calendar/test/javascripts/acceptance/sort-event-topics-test.js create mode 100644 plugins/discourse-calendar/test/javascripts/acceptance/timezone-offset-test.js create mode 100644 plugins/discourse-calendar/test/javascripts/acceptance/topic-calendar-events-test.js create mode 100644 plugins/discourse-calendar/test/javascripts/acceptance/topic-calendar-holidays-test.js create mode 100644 plugins/discourse-calendar/test/javascripts/acceptance/topic-title-decorator-test.js create mode 100644 plugins/discourse-calendar/test/javascripts/acceptance/upcoming-events-calendar-test.js create mode 100644 plugins/discourse-calendar/test/javascripts/helpers/event-topic-fixture.js create mode 100644 plugins/discourse-calendar/test/javascripts/helpers/get-event-by-text.js create mode 100644 plugins/discourse-calendar/test/javascripts/integration/components/admin-holidays-list-item-test.gjs create mode 100644 plugins/discourse-calendar/test/javascripts/integration/components/admin-holidays-list-test.gjs create mode 100644 plugins/discourse-calendar/test/javascripts/integration/components/dates-test.gjs create mode 100644 plugins/discourse-calendar/test/javascripts/integration/components/more-menu-test.gjs create mode 100644 plugins/discourse-calendar/test/javascripts/integration/components/region-input-test.gjs create mode 100644 plugins/discourse-calendar/test/javascripts/integration/components/rich-editor-extension-test.js create mode 100644 plugins/discourse-calendar/test/javascripts/integration/components/upcoming-events-list-test.gjs create mode 100644 plugins/discourse-calendar/test/javascripts/lib/raw-event-helper-test.js create mode 100644 plugins/discourse-calendar/vendor/holidays/CHANGELOG.md create mode 100644 plugins/discourse-calendar/vendor/holidays/CODE_OF_CONDUCT.md create mode 100644 plugins/discourse-calendar/vendor/holidays/Gemfile create mode 100644 plugins/discourse-calendar/vendor/holidays/Gemfile.lock create mode 100644 plugins/discourse-calendar/vendor/holidays/LICENSE create mode 100644 plugins/discourse-calendar/vendor/holidays/Makefile create mode 100644 plugins/discourse-calendar/vendor/holidays/README.md create mode 100644 plugins/discourse-calendar/vendor/holidays/Rakefile create mode 100755 plugins/discourse-calendar/vendor/holidays/bin/console create mode 100755 plugins/discourse-calendar/vendor/holidays/bin/setup create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/CHANGELOG.md create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/Gemfile create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/LICENSE create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/METHODS.yml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/Makefile create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/README.md create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/ae.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/ar.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/at.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/au.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/be_fr.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/be_nl.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/bg.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/br.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/ca.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/ch.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/cl.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/co.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/cr.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/cz.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/de.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/dk.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/doc/CONTRIBUTING.md create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/doc/MAINTAINERS.md create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/doc/SYNTAX.md create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/doc/architecture/README.md create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/doc/architecture/adr-001.md create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/doc/architecture/adr-002.md create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/ecbtarget.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/ee.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/el.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/es.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/federalreserve.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/federalreservebanks.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/fedex.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/fi.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/fr.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/gb.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/ge.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/gh.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/hk.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/hr.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/hu.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/id.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/ie.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/in.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/index.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/is.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/it.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/jp.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/ke.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/kr.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/kz.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/li.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/lib/validation/custom_method_validator.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/lib/validation/definition_validator.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/lib/validation/error.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/lib/validation/month_validator.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/lib/validation/run.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/lib/validation/test_validator.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/lt.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/lu.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/lv.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/ma.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/mt_en.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/mt_mt.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/mx.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/my.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/nerc.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/ng.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/nl.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/no.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/northamericainformal.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/nyse.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/nz.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/pe.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/ph.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/pl.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/pt.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/ro.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/rs_cyrl.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/rs_la.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/ru.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/sa.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/se.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/sg.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/si.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/sk.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/spec/coverage_report.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/spec/data/invalid/months/malformed/bad.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/spec/data/invalid/months/missing/no_months.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/spec/data/valid/simple.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/spec/spec_helper.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/spec/validation/custom_method_validator_spec.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/spec/validation/definition_validator_spec.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/spec/validation/month_validator_spec.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/spec/validation/test_validator_spec.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/th.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/tn.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/tr.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/ua.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/unitednations.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/ups.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/us.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/ve.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/vi.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/za.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/definitions/zw.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/doc/CONTRIBUTING.md create mode 100644 plugins/discourse-calendar/vendor/holidays/doc/MAINTAINERS.md create mode 100644 plugins/discourse-calendar/vendor/holidays/doc/REFERENCES create mode 100644 plugins/discourse-calendar/vendor/holidays/holidays.gemspec create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/MANIFEST create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/REGIONS.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/ae.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/ar.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/at.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/au.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/be.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/be_fr.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/be_nl.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/bg.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/br.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/ca.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/ch.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/cl.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/co.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/cr.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/cz.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/de.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/dk.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/ecbtarget.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/ee.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/el.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/es.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/europe.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/federalreserve.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/federalreservebanks.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/fedex.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/fi.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/fr.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/gb.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/ge.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/gh.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/hk.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/hr.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/hu.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/id.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/ie.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/in.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/is.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/it.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/jp.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/ke.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/kr.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/kz.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/li.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/lt.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/lu.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/lv.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/ma.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/mt_en.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/mt_mt.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/mx.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/my.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/nerc.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/ng.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/nl.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/no.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/northamerica.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/nyse.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/nz.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/pe.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/ph.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/pl.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/pt.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/ro.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/rs_cyrl.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/rs_la.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/ru.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/sa.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/scandinavia.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/se.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/sg.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/si.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/sk.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/southamerica.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/th.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/tn.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/tr.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/ua.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/unitednations.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/ups.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/us.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/ve.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/vi.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/za.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/generated_definitions/zw.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/holidays.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/holidays/core_extensions/date.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/holidays/core_extensions/time.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/holidays/date_calculator/day_of_month.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/holidays/date_calculator/easter.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/holidays/date_calculator/lunar_date.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/holidays/date_calculator/weekend_modifier.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/holidays/definition/context/function_processor.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/holidays/definition/context/generator.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/holidays/definition/context/load.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/holidays/definition/context/merger.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/holidays/definition/decorator/custom_method_proc.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/holidays/definition/decorator/custom_method_source.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/holidays/definition/decorator/test.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/holidays/definition/entity/custom_method.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/holidays/definition/entity/test.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/holidays/definition/generator/module.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/holidays/definition/generator/regions.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/holidays/definition/generator/test.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/holidays/definition/parser/custom_method.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/holidays/definition/parser/test.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/holidays/definition/repository/cache.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/holidays/definition/repository/custom_methods.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/holidays/definition/repository/holidays_by_month.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/holidays/definition/repository/proc_result_cache.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/holidays/definition/repository/regions.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/holidays/definition/validator/custom_method.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/holidays/definition/validator/region.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/holidays/definition/validator/test.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/holidays/errors.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/holidays/factory/date_calculator.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/holidays/factory/definition.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/holidays/factory/finder.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/holidays/finder/context/between.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/holidays/finder/context/dates_driver_builder.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/holidays/finder/context/next_holiday.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/holidays/finder/context/parse_options.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/holidays/finder/context/search.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/holidays/finder/context/year_holiday.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/holidays/finder/rules/in_region.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/holidays/finder/rules/year_range.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/holidays/load_all_definitions.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/lib/holidays/version.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/coverage_report.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/data/test_custom_govt_holiday_defs.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/test/data/test_custom_informal_holidays_defs.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/test/data/test_custom_year_range_holiday_defs.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/test/data/test_invalid_region.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/data/test_multiple_custom_holiday_defs.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/test/data/test_multiple_regions_with_conflicts_region_1.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/test/data/test_multiple_regions_with_conflicts_region_2.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/test/data/test_region.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/data/test_single_custom_holiday_defs.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/test/data/test_single_custom_holiday_with_custom_procs.yaml create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_ae.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_ar.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_at.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_au.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_be_fr.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_be_nl.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_bg.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_br.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_ca.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_ch.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_cl.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_co.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_cr.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_cz.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_de.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_dk.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_ecbtarget.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_ee.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_el.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_es.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_europe.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_fed_ex.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_federalreserve.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_federalreservebanks.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_fedex.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_fi.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_fr.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_gb.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_ge.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_gh.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_hk.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_hr.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_hu.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_id.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_ie.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_in.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_is.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_it.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_jp.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_ke.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_kr.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_kz.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_li.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_lt.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_lu.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_lv.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_ma.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_mt_en.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_mt_mt.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_mx.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_my.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_nerc.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_ng.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_nl.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_no.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_northamerica.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_nyse.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_nz.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_pe.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_ph.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_pl.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_pt.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_ro.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_rs_cyrl.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_rs_la.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_ru.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_sa.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_scandinavia.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_se.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_sg.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_si.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_sk.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_southamerica.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_th.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_tn.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_tr.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_ua.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_unitednations.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_ups.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_us.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_ve.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_vi.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_za.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/defs/test_defs_zw.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/holidays/core_extensions/test_date.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/holidays/core_extensions/test_date_time.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/holidays/date_calculator/test_day_of_month.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/holidays/date_calculator/test_easter_gregorian.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/holidays/date_calculator/test_easter_julian.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/holidays/date_calculator/test_lunar_date.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/holidays/date_calculator/test_weekend_modifier.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/holidays/definition/context/test_function_processor.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/holidays/definition/context/test_generator.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/holidays/definition/context/test_load.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/holidays/definition/context/test_merger.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/holidays/definition/decorator/test_custom_method_proc.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/holidays/definition/decorator/test_custom_method_source.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/holidays/definition/decorator/test_test.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/holidays/definition/generator/test_module.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/holidays/definition/generator/test_regions.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/holidays/definition/generator/test_test.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/holidays/definition/parser/test_custom_method.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/holidays/definition/parser/test_test.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/holidays/definition/repository/test_cache.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/holidays/definition/repository/test_custom_methods.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/holidays/definition/repository/test_holidays_by_month.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/holidays/definition/repository/test_proc_result_cache.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/holidays/definition/repository/test_regions.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/holidays/definition/validator/test_custom_method.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/holidays/definition/validator/test_region.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/holidays/definition/validator/test_test.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/holidays/finder/context/test_between.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/holidays/finder/context/test_dates_driver_builder.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/holidays/finder/context/test_next_holiday.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/holidays/finder/context/test_parse_options.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/holidays/finder/context/test_search.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/holidays/finder/context/test_year_holiday.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/holidays/finder/rules/test_in_region.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/holidays/finder/rules/test_year_range.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/integration/README.md create mode 100644 plugins/discourse-calendar/vendor/holidays/test/integration/test_all_regions.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/integration/test_any_holidays_during_work_week.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/integration/test_available_regions.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/integration/test_custom_holidays.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/integration/test_custom_informal_holidays.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/integration/test_custom_year_range_holidays.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/integration/test_holidays.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/integration/test_holidays_between.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/integration/test_multiple_regions.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/integration/test_multiple_regions_with_conflict.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/integration/test_nonstandard_regions.rb create mode 100644 plugins/discourse-calendar/vendor/holidays/test/test_helper.rb diff --git a/.github/labeler.yml b/.github/labeler.yml index a37a4a806d4..bd86fe18f50 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -106,6 +106,10 @@ discourse-gamification: - changed-files: - any-glob-to-any-file: plugins/discourse-gamification/**/* +discourse-calendar: + - changed-files: + - any-glob-to-any-file: plugins/discourse-calendar/**/* + footnote: - changed-files: - any-glob-to-any-file: plugins/footnote/**/* diff --git a/.gitignore b/.gitignore index fda4321d2c8..57770038dd3 100644 --- a/.gitignore +++ b/.gitignore @@ -69,6 +69,7 @@ !/plugins/discourse-subscriptions !/plugins/discourse-hcaptcha !/plugins/discourse-gamification +!/plugins/discourse-calendar /plugins/*/auto_generated /spec/fixtures/plugins/my_plugin/auto_generated diff --git a/.streerc b/.streerc index 5c477379e17..a286bcf70a6 100644 --- a/.streerc +++ b/.streerc @@ -1,2 +1,3 @@ --print-width=100 --plugins=plugin/trailing_comma,plugin/disable_auto_ternary +--ignore-files=plugins/discourse-calendar/vendor/* diff --git a/plugins/discourse-calendar/.prettierignore b/plugins/discourse-calendar/.prettierignore new file mode 100644 index 00000000000..f8f98305742 --- /dev/null +++ b/plugins/discourse-calendar/.prettierignore @@ -0,0 +1,2 @@ +assets/stylesheets/vendor/*.scss +public/ diff --git a/plugins/discourse-calendar/README.md b/plugins/discourse-calendar/README.md new file mode 100644 index 00000000000..e5e5819a166 --- /dev/null +++ b/plugins/discourse-calendar/README.md @@ -0,0 +1,42 @@ +# Discourse Calendar + +Adds the ability to create a dynamic calendar in the first post of a topic. + +Topic discussing the plugin itself can be found here: [https://meta.discourse.org/t/discourse-calendar/97376](https://meta.discourse.org/t/discourse-calendar/97376) + +## Customization + +### 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. + +### Holidays + +See an incorrect or missing holiday? Familiarize yourself with the [holiday definition Syntax](vendor/holidays/definitions/doc/SYNTAX.md). Then make your updates in the `vendor/holiday/definitions` directory. + +Generate updated holidays as follows. + +```sh +cd vendor/holidays + +# Generate holiday definitions +rake generate:definitions +``` + +Install the plugin and switch to the discourse root(not the plugin directory). + +```sh +# Collect all holiday regions into assets/javascripts/lib/regions.js +bin/rails javascript:update_constants +``` + +### Interactions with Other Plugins + +You can use an element of this plugin with the [Right Sidebar Blocks](https://github.com/discourse/discourse-right-sidebar-blocks) component. You'll want to ensure the desired route is enabled via the `events calendar categories` setting. In Right Sidebar Block's settings, the block name will be `upcoming-events-list`, and the params use this [syntax](https://momentjs.com/docs/#/displaying/format/), for example `MMMM D, YYYY`. diff --git a/plugins/discourse-calendar/app/controllers/admin/discourse_calendar/admin_holidays_controller.rb b/plugins/discourse-calendar/app/controllers/admin/discourse_calendar/admin_holidays_controller.rb new file mode 100644 index 00000000000..c153c123aa1 --- /dev/null +++ b/plugins/discourse-calendar/app/controllers/admin/discourse_calendar/admin_holidays_controller.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +module Admin::DiscourseCalendar + class AdminHolidaysController < Admin::AdminController + requires_plugin DiscourseCalendar::PLUGIN_NAME + + def index + region_code = params[:region_code] + + begin + holidays = DiscourseCalendar::Holiday.find_holidays_for(region_code: region_code) + rescue Holidays::InvalidRegion + return( + render_json_error( + I18n.t("system_messages.discourse_calendar_holiday_region_invalid"), + 422, + ) + ) + end + + render json: { region_code: region_code, holidays: holidays } + end + + def disable + DiscourseCalendar::DisabledHoliday.create!(disabled_holiday_params) + CalendarEvent.destroy_by( + description: disabled_holiday_params[:holiday_name], + region: disabled_holiday_params[:region_code], + ) + + render json: success_json + end + + def enable + if DiscourseCalendar::DisabledHoliday.destroy_by(disabled_holiday_params).present? + render json: success_json + else + render_json_error(I18n.t("system_messages.discourse_calendar_enable_holiday_failed"), 422) + end + end + + private + + def disabled_holiday_params + params.require(:disabled_holiday).permit(:holiday_name, :region_code) + end + end +end diff --git a/plugins/discourse-calendar/app/controllers/discourse_post_event/discourse_post_event_controller.rb b/plugins/discourse-calendar/app/controllers/discourse_post_event/discourse_post_event_controller.rb new file mode 100644 index 00000000000..ab6efc05e73 --- /dev/null +++ b/plugins/discourse-calendar/app/controllers/discourse_post_event/discourse_post_event_controller.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module DiscoursePostEvent + class DiscoursePostEventController < ::ApplicationController + requires_plugin DiscourseCalendar::PLUGIN_NAME + before_action :ensure_discourse_post_event_enabled + + private + + def ensure_discourse_post_event_enabled + raise Discourse::NotFound if !SiteSetting.discourse_post_event_enabled + end + end +end diff --git a/plugins/discourse-calendar/app/controllers/discourse_post_event/events_controller.rb b/plugins/discourse-calendar/app/controllers/discourse_post_event/events_controller.rb new file mode 100644 index 00000000000..a54b0dd3cd0 --- /dev/null +++ b/plugins/discourse-calendar/app/controllers/discourse_post_event/events_controller.rb @@ -0,0 +1,130 @@ +# frozen_string_literal: true + +module DiscoursePostEvent + class EventsController < DiscoursePostEventController + def index + @events = + DiscoursePostEvent::EventFinder.search(current_user, filtered_events_params).includes( + post: :topic, + ) + + # The detailed serializer is currently not used anywhere in the frontend, but available via API + serializer = params[:include_details] == "true" ? EventSerializer : EventSummarySerializer + + render json: + ActiveModel::ArraySerializer.new( + @events, + each_serializer: serializer, + scope: guardian, + ).as_json + end + + def invite + event = Event.find(params[:id]) + guardian.ensure_can_act_on_discourse_post_event!(event) + invites = Array(params.permit(invites: [])[:invites]) + users = User.real.where(username: invites) + + users.each { |user| event.create_notification!(user, event.post) } + + render json: success_json + end + + def show + event = Event.find(params[:id]) + guardian.ensure_can_see!(event.post) + serializer = EventSerializer.new(event, scope: guardian) + render_json_dump(serializer) + end + + def destroy + event = Event.find(params[:id]) + guardian.ensure_can_act_on_discourse_post_event!(event) + event.publish_update! + event.destroy + render json: success_json + end + + def csv_bulk_invite + require "csv" + + event = Event.find(params[:id]) + guardian.ensure_can_edit!(event.post) + guardian.ensure_can_create_discourse_post_event! + + file = params[:file] || (params[:files] || []).first + raise Discourse::InvalidParameters.new(:file) if file.blank? + + hijack do + begin + invitees = [] + + CSV.foreach(file.tempfile) do |row| + invitees << { identifier: row[0], attendance: row[1] || "going" } if row[0].present? + end + + if invitees.present? + Jobs.enqueue( + :discourse_post_event_bulk_invite, + event_id: event.id, + invitees: invitees, + current_user_id: current_user.id, + ) + render json: success_json + else + render json: + failed_json.merge( + errors: [I18n.t("discourse_post_event.errors.bulk_invite.error")], + ), + status: 422 + end + rescue StandardError + render json: + failed_json.merge( + errors: [I18n.t("discourse_post_event.errors.bulk_invite.error")], + ), + status: 422 + end + end + end + + def bulk_invite + event = Event.find(params[:id]) + guardian.ensure_can_edit!(event.post) + guardian.ensure_can_create_discourse_post_event! + + invitees = Array(params[:invitees]).reject { |x| x.empty? } + raise Discourse::InvalidParameters.new(:invitees) if invitees.blank? + + begin + Jobs.enqueue( + :discourse_post_event_bulk_invite, + event_id: event.id, + invitees: invitees.as_json, + current_user_id: current_user.id, + ) + render json: success_json + rescue StandardError + render json: + failed_json.merge( + errors: [I18n.t("discourse_post_event.errors.bulk_invite.error")], + ), + status: 422 + end + end + + private + + def filtered_events_params + params.permit( + :post_id, + :category_id, + :include_subcategories, + :include_expired, + :limit, + :before, + :attending_user, + ) + end + end +end diff --git a/plugins/discourse-calendar/app/controllers/discourse_post_event/invitees_controller.rb b/plugins/discourse-calendar/app/controllers/discourse_post_event/invitees_controller.rb new file mode 100644 index 00000000000..96bb04da5ce --- /dev/null +++ b/plugins/discourse-calendar/app/controllers/discourse_post_event/invitees_controller.rb @@ -0,0 +1,99 @@ +# frozen_string_literal: true + +module DiscoursePostEvent + class InviteesController < DiscoursePostEventController + def index + event = Event.find(params[:post_id]) + guardian.ensure_can_see!(event.post) + + filter = params[:filter].downcase if params[:filter] + + event_invitees = event.invitees + event_invitees = event_invitees.with_status(params[:type].to_sym) if params[:type] + + suggested_users = [] + if filter.present? && guardian.can_act_on_discourse_post_event?(event) + missing_users = event.missing_users(event_invitees.select(:user_id)) + + if filter + missing_users = missing_users.where("LOWER(username) LIKE :filter", filter: "%#{filter}%") + + custom_order = <<~SQL + CASE + WHEN LOWER(username) = ? THEN 0 + ELSE 1 + END ASC, + LOWER(username) ASC + SQL + + custom_order = ActiveRecord::Base.sanitize_sql_array([custom_order, filter]) + missing_users = missing_users.order(custom_order).limit(10) + else + missing_users = missing_users.order(:username_lower).limit(10) + end + + suggested_users = missing_users + end + + if filter + event_invitees = + event_invitees.joins(:user).where( + "LOWER(users.username) LIKE :filter", + filter: "%#{filter}%", + ) + end + + event_invitees = event_invitees.order(%i[status username_lower]).limit(200) + + render json: + InviteeListSerializer.new(invitees: event_invitees, suggested_users: suggested_users) + end + + def update + invitee = Invitee.find_by(id: params[:invitee_id], post_id: params[:event_id]) + guardian.ensure_can_act_on_invitee!(invitee) + invitee.update_attendance!(invitee_params[:status]) + render json: InviteeSerializer.new(invitee) + end + + def create + event = Event.find(params[:event_id]) + guardian.ensure_can_see!(event.post) + + invitee_params = invitee_params(event) + + user = current_user + if user_id = invitee_params[:user_id] + user = User.find(user_id.to_i) + end + + raise Discourse::InvalidAccess if !event.can_user_update_attendance(user) + + if current_user.id != user.id + raise Discourse::InvalidAccess if !guardian.can_act_on_discourse_post_event?(event) + end + + invitee = Invitee.create_attendance!(user.id, params[:event_id], invitee_params[:status]) + render json: InviteeSerializer.new(invitee) + end + + def destroy + event = Event.find_by(id: params[:post_id]) + invitee = event.invitees.find_by(id: params[:id]) + guardian.ensure_can_act_on_invitee!(invitee) + invitee.destroy! + event.publish_update! + render json: success_json + end + + private + + def invitee_params(event = nil) + if event && guardian.can_act_on_discourse_post_event?(event) + params.require(:invitee).permit(:status, :user_id) + else + params.require(:invitee).permit(:status) + end + end + end +end diff --git a/plugins/discourse-calendar/app/controllers/discourse_post_event/upcoming_events_controller.rb b/plugins/discourse-calendar/app/controllers/discourse_post_event/upcoming_events_controller.rb new file mode 100644 index 00000000000..9c8e972b02d --- /dev/null +++ b/plugins/discourse-calendar/app/controllers/discourse_post_event/upcoming_events_controller.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module DiscoursePostEvent + class UpcomingEventsController < DiscoursePostEventController + def index + end + end +end diff --git a/plugins/discourse-calendar/app/models/calendar_event.rb b/plugins/discourse-calendar/app/models/calendar_event.rb new file mode 100644 index 00000000000..e97abc5a47f --- /dev/null +++ b/plugins/discourse-calendar/app/models/calendar_event.rb @@ -0,0 +1,131 @@ +# frozen_string_literal: true + +class CalendarEvent < ActiveRecord::Base + belongs_to :topic + belongs_to :post + belongs_to :user + + after_save do + if SiteSetting.enable_user_status && is_holiday? && underway? + DiscourseCalendar::HolidayStatus.set!(user, ends_at) + end + end + + after_destroy { DiscourseCalendar::HolidayStatus.clear!(user) if SiteSetting.enable_user_status } + + def ends_at + end_date || (start_date + 24.hours) + end + + def underway? + now = Time.zone.now + start_date <= now && now < ends_at + end + + def is_holiday? + SiteSetting.holiday_calendar_topic_id.to_i == topic_id + end + + def in_future? + start_date > Time.zone.now + end + + def self.update(post) + CalendarEvent.where(post_id: post.id).destroy_all + + dates = post.local_dates + return if !dates || dates.size < 1 || dates.size > 2 + + first_post = post.topic&.first_post + return if !first_post || !first_post.custom_fields[DiscourseCalendar::CALENDAR_CUSTOM_FIELD] + + from = self.convert_to_date_time(dates[0]) + to = self.convert_to_date_time(dates[1]) if dates.size == 2 + + adjust_to = !to || !dates[1]["time"] + if !to && dates[0]["time"] + to = from + 1.hour + artificial_to = true + end + + if SiteSetting.all_day_event_start_time.present? && SiteSetting.all_day_event_end_time.present? + from = from.change(hour_adjustment(SiteSetting.all_day_event_start_time)) if !dates[0]["time"] + to = (to || from).change(hour_adjustment(SiteSetting.all_day_event_end_time)) if adjust_to && + !artificial_to + end + + doc = Nokogiri::HTML5.fragment(post.cooked) + doc.css(".discourse-local-date").each(&:remove) + html = doc.to_html.sub(/\s*→\s*/, "") + + description = + PrettyText.excerpt( + html, + 1000, + strip_links: true, + text_entities: true, + keep_emoji_images: true, + ) + recurrence = dates[0]["recurring"].presence + timezone = dates[0]["timezone"].presence + + CalendarEvent.create!( + topic_id: post.topic_id, + post_id: post.id, + post_number: post.post_number, + user_id: post.user_id, + username: post.user.username, + description: description, + start_date: from, + end_date: to, + recurrence: recurrence, + timezone: timezone, + ) + + post.publish_change_to_clients!(:calendar_change) + end + + private + + def self.convert_to_date_time(value) + return if value.blank? + + datetime = value["date"].to_s + datetime << " #{value["time"]}" if value["time"] + timezone = value["timezone"] || "UTC" + + ActiveSupport::TimeZone[timezone].parse(datetime) + end + + def self.hour_adjustment(setting) + setting = setting.split(":") + + { hour: setting.first, min: setting.last } + end +end + +# == Schema Information +# +# Table name: calendar_events +# +# id :bigint not null, primary key +# topic_id :integer not null +# post_id :integer +# post_number :integer +# user_id :integer +# username :string +# description :string +# start_date :datetime not null +# end_date :datetime +# recurrence :string +# region :string +# created_at :datetime not null +# updated_at :datetime not null +# timezone :string +# +# Indexes +# +# index_calendar_events_on_post_id (post_id) +# index_calendar_events_on_topic_id (topic_id) +# index_calendar_events_on_user_id (user_id) +# diff --git a/plugins/discourse-calendar/app/models/discourse_calendar/disabled_holiday.rb b/plugins/discourse-calendar/app/models/discourse_calendar/disabled_holiday.rb new file mode 100644 index 00000000000..e8407883ed8 --- /dev/null +++ b/plugins/discourse-calendar/app/models/discourse_calendar/disabled_holiday.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module DiscourseCalendar + class DisabledHoliday < ActiveRecord::Base + validates :holiday_name, presence: true + validates :region_code, presence: true + end +end + +# == Schema Information +# +# Table name: discourse_calendar_disabled_holidays +# +# id :bigint not null, primary key +# holiday_name :string not null +# region_code :string not null +# disabled :boolean default(TRUE), not null +# created_at :datetime not null +# updated_at :datetime not null +# +# Indexes +# +# index_disabled_holidays_on_holiday_name_and_region_code (holiday_name,region_code) +# diff --git a/plugins/discourse-calendar/app/models/discourse_post_event/event.rb b/plugins/discourse-calendar/app/models/discourse_post_event/event.rb new file mode 100644 index 00000000000..7b690cc0925 --- /dev/null +++ b/plugins/discourse-calendar/app/models/discourse_post_event/event.rb @@ -0,0 +1,437 @@ +# frozen_string_literal: true + +module DiscoursePostEvent + class Event < ActiveRecord::Base + PUBLIC_GROUP = "trust_level_0" + MIN_NAME_LENGTH = 5 + MAX_NAME_LENGTH = 255 + self.table_name = "discourse_post_event_events" + self.ignored_columns = %w[starts_at ends_at] + + has_many :event_dates, dependent: :destroy + # this is a cross plugin dependency, only called if chat is enabled + belongs_to :chat_channel, class_name: "Chat::Channel" + has_many :invitees, foreign_key: :post_id, dependent: :delete_all + belongs_to :post, foreign_key: :id + + scope :visible, -> { where(deleted_at: nil) } + + after_commit :destroy_topic_custom_field, on: %i[destroy] + after_commit :create_or_update_event_date, on: %i[create update] + before_save :chat_channel_sync + + validate :raw_invitees_are_groups + validates :original_starts_at, presence: true + validates :name, + length: { + in: MIN_NAME_LENGTH..MAX_NAME_LENGTH, + }, + unless: ->(event) { event.name.blank? } + + validate :raw_invitees_length + validate :ends_before_start + validate :allowed_custom_fields + + def self.attributes_protected_by_default + super - %w[id] + end + + def destroy_topic_custom_field + if self.post && self.post.is_first_post? + TopicCustomField.where( + topic_id: self.post.topic_id, + name: TOPIC_POST_EVENT_STARTS_AT, + ).delete_all + + TopicCustomField.where( + topic_id: self.post.topic_id, + name: TOPIC_POST_EVENT_ENDS_AT, + ).delete_all + end + end + + def create_or_update_event_date + starts_at_changed = saved_change_to_original_starts_at + ends_at_changed = saved_change_to_original_ends_at + + return if !starts_at_changed && !ends_at_changed + + event_dates.update_all(finished_at: Time.current) + set_next_date + end + + def set_next_date + next_dates = calculate_next_date + return if !next_dates + + event_dates.create!( + starts_at: next_dates[:starts_at], + ends_at: next_dates[:ends_at], + ) do |event_date| + if next_dates[:ends_at] && next_dates[:ends_at] < Time.current + event_date.finished_at = next_dates[:ends_at] + end + end + + invitees.where.not(status: Invitee.statuses[:going]).update_all(status: nil, notified: false) + + if !next_dates[:rescheduled] + notify_invitees! + notify_missing_invitees! + end + + publish_update! + end + + def set_topic_bump + date = nil + + return if reminders.blank? + reminders + .split(",") + .each do |reminder| + type, value, unit = reminder.split(".") + next if type != "bumpTopic" || !validate_reminder_unit(unit) + date = starts_at - value.to_i.public_send(unit) + break + end + + return if date.blank? + Jobs.enqueue(:discourse_post_event_bump_topic, topic_id: self.post.topic_id, date: date) + end + + def validate_reminder_unit(input) + ActiveSupport::Duration::PARTS.any? { |part| part.to_s == input } + end + + def expired? + (ends_at || starts_at.end_of_day) <= Time.now + end + + def starts_at + event_dates.pending.order(:starts_at).last&.starts_at || + event_dates.order(:updated_at, :id).last&.starts_at + end + + def ends_at + event_dates.pending.order(:starts_at).last&.ends_at || + event_dates.order(:updated_at, :id).last&.ends_at + end + + def on_going_event_invitees + return [] if !self.ends_at && self.starts_at < Time.now + + if self.ends_at + extended_ends_at = + self.ends_at + SiteSetting.discourse_post_event_edit_notifications_time_extension.minutes + return [] if !(self.starts_at..extended_ends_at).cover?(Time.now) + end + + invitees.where(status: DiscoursePostEvent::Invitee.statuses[:going]) + end + + def raw_invitees_length + if self.raw_invitees && self.raw_invitees.length > 10 + errors.add( + :base, + I18n.t("discourse_post_event.errors.models.event.raw_invitees_length", count: 10), + ) + end + end + + def raw_invitees_are_groups + if self.raw_invitees && User.select(:id).where(username: self.raw_invitees).limit(1).count > 0 + errors.add( + :base, + I18n.t("discourse_post_event.errors.models.event.raw_invitees.only_group"), + ) + end + end + + def ends_before_start + if self.original_starts_at && self.original_ends_at && + self.original_starts_at >= self.original_ends_at + errors.add( + :base, + I18n.t("discourse_post_event.errors.models.event.ends_at_before_starts_at"), + ) + end + end + + 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| + { post_id: self.id, created_at: timestamp, updated_at: timestamp }.merge(attr) + end + result = self.invitees.insert_all!(attrs) + + # batch event does not call calleback + ChatChannelSync.sync(self) if chat_enabled? + + result + end + + def notify_invitees!(predefined_attendance: false) + self + .invitees + .where(notified: false) + .find_each do |invitee| + create_notification!( + invitee.user, + self.post, + predefined_attendance: predefined_attendance, + ) + invitee.update!(notified: true) + end + end + + def notify_missing_invitees! + self.missing_users.each { |user| create_notification!(user, self.post) } if self.private? + end + + def create_notification!(user, post, predefined_attendance: false) + return if post.event.starts_at < Time.current + + message = + if predefined_attendance + "discourse_post_event.notifications.invite_user_predefined_attendance_notification" + else + "discourse_post_event.notifications.invite_user_notification" + end + + attrs = { + notification_type: Notification.types[:event_invitation] || Notification.types[:custom], + topic_id: post.topic_id, + post_number: post.post_number, + data: { + user_id: user.id, + topic_title: self.name || post.topic.title, + display_username: post.user.username, + message: message, + }.to_json, + } + + user.notifications.consolidate_or_create!(attrs) + end + + def ongoing? + 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 + @statuses ||= Enum.new(standalone: 0, public: 1, private: 2) + end + + def public? + status == Event.statuses[:public] + end + + def standalone? + status == Event.statuses[:standalone] + end + + def private? + status == Event.statuses[:private] + end + + def recurring? + recurrence.present? + end + + def most_likely_going(limit = SiteSetting.displayed_invitees_limit) + going = self.invitees.order(%i[status user_id]).limit(limit) + + if self.private? && going.count < limit + # invitees are only created when an attendance is set + # so we create a dummy invitee object with only what's needed for serializer + going = + going + + missing_users(going.pluck(:user_id)) + .limit(limit - going.count) + .map { |user| Invitee.new(user: user, post_id: self.id) } + end + + going + end + + def publish_update! + self.post.publish_message!("/discourse-post-event/#{self.post.topic_id}", id: self.id) + end + + def fetch_users + @fetched_users ||= Invitee.extract_uniq_usernames(self.raw_invitees) + end + + def enforce_private_invitees! + self.invitees.where.not(user_id: fetch_users.select(:id)).delete_all + end + + def can_user_update_attendance(user) + return false if self.closed || self.expired? + return true if self.public? + + self.private? && + ( + self.invitees.exists?(user_id: user.id) || + (user.groups.pluck(:name) & self.raw_invitees).any? + ) + end + + def self.update_from_raw(post) + events = DiscoursePostEvent::EventParser.extract_events(post) + + if events.present? + event_params = events.first + event = post.event || DiscoursePostEvent::Event.new(id: post.id) + + tz = ActiveSupport::TimeZone[event_params[:timezone] || "UTC"] + parsed_starts_at = tz.parse(event_params[:start]) + parsed_ends_at = event_params[:end] ? tz.parse(event_params[:end]) : nil + parsed_recurrence_until = + event_params[:"recurrence-until"] ? tz.parse(event_params[:"recurrence-until"]) : nil + + params = { + name: event_params[:name], + original_starts_at: parsed_starts_at, + original_ends_at: parsed_ends_at, + url: event_params[:url], + description: event_params[:description], + location: event_params[:location], + recurrence: event_params[:recurrence], + recurrence_until: parsed_recurrence_until, + timezone: event_params[:timezone], + show_local_time: event_params[:"show-local-time"] == "true", + status: Event.statuses[event_params[:status]&.to_sym] || event.status, + reminders: event_params[:reminders], + raw_invitees: event_params[:"allowed-groups"]&.split(","), + minimal: event_params[:minimal], + closed: event_params[:closed] || false, + chat_enabled: event_params[:"chat-enabled"]&.downcase == "true", + } + + params[:custom_fields] = {} + SiteSetting + .discourse_post_event_allowed_custom_fields + .split("|") + .each do |setting| + if event_params[setting.to_sym].present? + params[:custom_fields][setting] = event_params[setting.to_sym] + end + end + + event.update_with_params!(params) + event.set_topic_bump + elsif post.event + post.event.destroy! + end + end + + def missing_users(excluded_ids = self.invitees.select(:user_id)) + users = User.real.activated.not_silenced.not_suspended.not_staged + + if self.raw_invitees.present? + user_ids = + users + .joins(:groups) + .where("groups.name" => self.raw_invitees) + .where.not(id: excluded_ids) + .select(:id) + User.where(id: user_ids) + else + users.where.not(id: excluded_ids) + end + end + + def update_with_params!(params) + case params[:status] ? params[:status].to_i : self.status + when Event.statuses[:private] + if params.key?(:raw_invitees) + params = params.merge(raw_invitees: Array(params[:raw_invitees]) - [PUBLIC_GROUP]) + else + params = params.merge(raw_invitees: Array(self.raw_invitees) - [PUBLIC_GROUP]) + end + self.update!(params) + self.enforce_private_invitees! + when Event.statuses[:public] + self.update!(params.merge(raw_invitees: [PUBLIC_GROUP])) + when Event.statuses[:standalone] + self.update!(params.merge(raw_invitees: [])) + self.invitees.destroy_all + end + + self.publish_update! + end + + def chat_channel_sync + if self.chat_enabled && self.chat_channel_id.blank? && post.last_editor_id.present? + DiscoursePostEvent::ChatChannelSync.sync( + self, + guardian: Guardian.new(User.find_by(id: post.last_editor_id)), + ) + end + end + + def calculate_next_date + if self.recurrence.blank? || original_starts_at > Time.current + return { starts_at: original_starts_at, ends_at: original_ends_at, rescheduled: false } + end + + next_starts_at = + RRuleGenerator.generate( + starts_at: original_starts_at.in_time_zone(timezone), + timezone:, + recurrence:, + recurrence_until:, + ).first + + if original_ends_at + difference = original_ends_at - original_starts_at + next_ends_at = next_starts_at + difference.seconds + else + next_ends_at = nil + end + + { starts_at: next_starts_at, ends_at: next_ends_at, rescheduled: true } + end + end +end + +# == Schema Information +# +# Table name: discourse_post_event_events +# +# id :bigint not null, primary key +# status :integer default(0), not null +# original_starts_at :datetime not null +# original_ends_at :datetime +# deleted_at :datetime +# raw_invitees :string is an Array +# name :string +# url :string(1000) +# description :string(1000) +# location :string(1000) +# custom_fields :jsonb not null +# reminders :string +# recurrence :string +# timezone :string +# minimal :boolean +# closed :boolean default(FALSE), not null +# chat_enabled :boolean default(FALSE), not null +# chat_channel_id :bigint +# recurrence_until :datetime +# show_local_time :boolean default(FALSE), not null +# diff --git a/plugins/discourse-calendar/app/models/discourse_post_event/event_date.rb b/plugins/discourse-calendar/app/models/discourse_post_event/event_date.rb new file mode 100644 index 00000000000..8f820914a9d --- /dev/null +++ b/plugins/discourse-calendar/app/models/discourse_post_event/event_date.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +module DiscoursePostEvent + class EventDate < ActiveRecord::Base + self.table_name = "discourse_calendar_post_event_dates" + belongs_to :event + + scope :pending, + -> do + where(finished_at: nil).joins(:event).where( + "discourse_post_event_events.deleted_at is NULL", + ) + end + scope :expired, -> { where("ends_at IS NOT NULL AND ends_at < ?", Time.now) } + scope :not_expired, -> { where("ends_at IS NULL OR ends_at > ?", Time.now) } + + after_commit :upsert_topic_custom_field, on: %i[create] + def upsert_topic_custom_field + if self.event.post && self.event.post.is_first_post? + TopicCustomField.upsert( + { + topic_id: self.event.post.topic_id, + name: TOPIC_POST_EVENT_STARTS_AT, + value: self.starts_at, + created_at: Time.now, + updated_at: Time.now, + }, + unique_by: "idx_topic_custom_fields_topic_post_event_starts_at", + ) + + TopicCustomField.upsert( + { + topic_id: self.event.post.topic_id, + name: TOPIC_POST_EVENT_ENDS_AT, + value: self.ends_at, + created_at: Time.now, + updated_at: Time.now, + }, + unique_by: "idx_topic_custom_fields_topic_post_event_ends_at", + ) + end + end + + def started? + starts_at <= Time.current + end + + def ended? + (ends_at || starts_at.end_of_day) <= Time.current + end + end +end + +# == Schema Information +# +# Table name: discourse_calendar_post_event_dates +# +# id :bigint not null, primary key +# event_id :integer +# starts_at :datetime +# ends_at :datetime +# reminder_counter :integer default(0) +# event_will_start_sent_at :datetime +# event_started_sent_at :datetime +# finished_at :datetime +# created_at :datetime not null +# updated_at :datetime not null +# +# Indexes +# +# index_discourse_calendar_post_event_dates_on_event_id (event_id) +# index_discourse_calendar_post_event_dates_on_finished_at (finished_at) +# diff --git a/plugins/discourse-calendar/app/models/discourse_post_event/invitee.rb b/plugins/discourse-calendar/app/models/discourse_post_event/invitee.rb new file mode 100644 index 00000000000..570d38657ee --- /dev/null +++ b/plugins/discourse-calendar/app/models/discourse_post_event/invitee.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +module DiscoursePostEvent + class Invitee < ActiveRecord::Base + UNKNOWN_ATTENDANCE = "unknown" + + self.table_name = "discourse_post_event_invitees" + + belongs_to :event, foreign_key: :post_id + belongs_to :user + + default_scope { joins(:user).includes(:user).where("users.id IS NOT NULL") } + scope :with_status, ->(status) { where(status: Invitee.statuses[status]) } + + after_commit :sync_chat_channel_members + + def self.statuses + @statuses ||= Enum.new(going: 0, interested: 1, not_going: 2) + end + + def self.create_attendance!(user_id, post_id, status) + invitee = + Invitee.create!(status: Invitee.statuses[status.to_sym], post_id: post_id, user_id: user_id) + invitee.event.publish_update! + invitee.update_topic_tracking! + DiscourseEvent.trigger(:discourse_calendar_post_event_invitee_status_changed, invitee) + invitee + rescue ActiveRecord::RecordNotUnique + # do nothing in case multiple new attendances would be created very fast + Invitee.find_by(post_id: post_id, user_id: user_id) + end + + def update_attendance!(status) + new_status = Invitee.statuses[status.to_sym] + status_changed = self.status != new_status + self.update(status: new_status) + self.event.publish_update! + self.update_topic_tracking! if status_changed + DiscourseEvent.trigger(:discourse_calendar_post_event_invitee_status_changed, self) + self + end + + def self.extract_uniq_usernames(groups) + User.real.where( + id: GroupUser.where(group_id: Group.where(name: groups).select(:id)).select(:user_id), + ) + end + + def sync_chat_channel_members + return if !self.event.chat_enabled? + ChatChannelSync.sync(self.event) + end + + def update_topic_tracking! + topic_id = self.event.post.topic.id + user_id = self.user.id + tracking = :regular + + case self.status + when Invitee.statuses[:going] + tracking = :watching + when Invitee.statuses[:interested] + tracking = :tracking + end + + TopicUser.change( + user_id, + topic_id, + notification_level: TopicUser.notification_levels[tracking], + ) + end + end +end + +# == Schema Information +# +# Table name: discourse_post_event_invitees +# +# id :bigint not null, primary key +# post_id :integer not null +# user_id :integer not null +# status :integer +# created_at :datetime not null +# updated_at :datetime not null +# notified :boolean default(FALSE), not null +# +# Indexes +# +# discourse_post_event_invitees_post_id_user_id_idx (post_id,user_id) UNIQUE +# diff --git a/plugins/discourse-calendar/app/serializers/discourse_post_event/event_serializer.rb b/plugins/discourse-calendar/app/serializers/discourse_post_event/event_serializer.rb new file mode 100644 index 00000000000..ac48921d607 --- /dev/null +++ b/plugins/discourse-calendar/app/serializers/discourse_post_event/event_serializer.rb @@ -0,0 +1,156 @@ +# frozen_string_literal: true + +module DiscoursePostEvent + class EventSerializer < ApplicationSerializer + 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 :is_private + attributes :is_public + attributes :is_standalone + attributes :minimal + attributes :name + attributes :post + attributes :raw_invitees + attributes :recurrence + attributes :recurrence_rule + attributes :recurrence_until + attributes :reminders + attributes :sample_invitees + attributes :should_display_invitees + attributes :starts_at + attributes :stats + attributes :status + attributes :timezone + attributes :show_local_time + attributes :url + attributes :description + attributes :location + attributes :watching_invitee + attributes :chat_enabled + attributes :channel + + def channel + ::Chat::ChannelSerializer.new(object.chat_channel, root: false, scope:) + end + + def include_channel? + object.chat_enabled && defined?(::Chat::ChannelSerializer) && object.chat_channel.present? + end + + def can_act_on_discourse_post_event + scope.can_act_on_discourse_post_event?(object) + end + + def reminders + (object.reminders || "") + .split(",") + .map do |reminder| + unit, value, type = reminder.split(".").reverse + type ||= "notification" + + value = value.to_i + { value: value.to_i.abs, unit: unit, period: value > 0 ? "before" : "after", type: type } + end + end + + def is_expired + object.expired? + end + + def is_ongoing + object.ongoing? + end + + def is_public + object.public? + end + + def is_private + object.private? + end + + def is_standalone + object.standalone? + end + + def is_closed + object.closed + end + + def status + Event.statuses[object.status] + end + + # lightweight post object containing + # only needed info for client + def post + { + id: object.post.id, + post_number: object.post.post_number, + url: object.post.url, + topic: { + id: object.post.topic.id, + title: object.post.topic.title, + }, + } + end + + def can_update_attendance + scope.current_user && object.can_user_update_attendance(scope.current_user) + end + + def creator + BasicUserSerializer.new(object.post.user, embed: :objects, root: false) + end + + def stats + EventStatsSerializer.new(object, root: false).as_json + end + + def watching_invitee + if scope.current_user + watching_invitee = Invitee.find_by(user_id: scope.current_user.id, post_id: object.id) + end + + InviteeSerializer.new(watching_invitee, root: false) if watching_invitee + end + + def sample_invitees + invitees = object.most_likely_going + ActiveModel::ArraySerializer.new(invitees, each_serializer: InviteeSerializer) + end + + def should_display_invitees + (object.public? && object.invitees.count > 0) || + (object.private? && object.raw_invitees.count > 0) + end + + def category_id + object.post.topic.category_id + end + + def include_url? + object.url.present? + end + + def include_recurrence_rule? + object.recurring? + end + + def recurrence_rule + RRuleConfigurator.rule( + recurrence: object.recurrence, + starts_at: object.starts_at.in_time_zone(object.timezone), + recurrence_until: object.recurrence_until&.in_time_zone(object.timezone), + ) + end + end +end diff --git a/plugins/discourse-calendar/app/serializers/discourse_post_event/event_stats_serializer.rb b/plugins/discourse-calendar/app/serializers/discourse_post_event/event_stats_serializer.rb new file mode 100644 index 00000000000..4e3674df602 --- /dev/null +++ b/plugins/discourse-calendar/app/serializers/discourse_post_event/event_stats_serializer.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module DiscoursePostEvent + class EventStatsSerializer < ApplicationSerializer + attributes :going + attributes :interested + attributes :not_going + attributes :invited + + def invited + unanswered = counts[nil] || 0 + + # when a group is private we know the list of possible users + # even if an invitee has not been created yet + unanswered += object.missing_users.count if object.private? + + going + interested + not_going + unanswered + end + + def going + @going ||= counts[Invitee.statuses[:going]] || 0 + end + + def interested + @interested ||= counts[Invitee.statuses[:interested]] || 0 + end + + def not_going + @not_going ||= counts[Invitee.statuses[:not_going]] || 0 + end + + def counts + @counts ||= object.invitees.group(:status).count + end + end +end diff --git a/plugins/discourse-calendar/app/serializers/discourse_post_event/event_summary_serializer.rb b/plugins/discourse-calendar/app/serializers/discourse_post_event/event_summary_serializer.rb new file mode 100644 index 00000000000..04940c27280 --- /dev/null +++ b/plugins/discourse-calendar/app/serializers/discourse_post_event/event_summary_serializer.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +module DiscoursePostEvent + class EventSummarySerializer < ApplicationSerializer + attributes :id + attributes :starts_at + attributes :ends_at + attributes :show_local_time + attributes :timezone + attributes :post + attributes :name + attributes :category_id + attributes :upcoming_dates + + # lightweight post object containing + # only needed info for client + def post + post_hash = { + id: object.post.id, + post_number: object.post.post_number, + url: object.post.url, + topic: { + id: object.post.topic.id, + title: object.post.topic.title, + }, + } + + if post_hash[:topic][:title].match?(/:[\w\-+]+:/) + post_hash[:topic][:title] = Emoji.gsub_emoji_to_unicode(post_hash[:topic][:title]) + end + + if JSON.parse(SiteSetting.map_events_to_color).size > 0 + post_hash[:topic][:category_slug] = object.post.topic&.category&.slug + post_hash[:topic][:tags] = object.post.topic.tags&.map(&:name) + end + + post_hash + end + + def category_id + object.post.topic.category_id + end + + def include_upcoming_dates? + object.recurring? + end + + def upcoming_dates + difference = object.original_ends_at ? object.original_ends_at - object.original_starts_at : 0 + + RRuleGenerator + .generate( + starts_at: object.original_starts_at.in_time_zone(object.timezone), + timezone: object.timezone, + max_years: 1, + recurrence: object.recurrence, + recurrence_until: object.recurrence_until, + ) + .map { |date| { starts_at: date, ends_at: date + difference.seconds } } + end + end +end diff --git a/plugins/discourse-calendar/app/serializers/discourse_post_event/invitee_list_serializer.rb b/plugins/discourse-calendar/app/serializers/discourse_post_event/invitee_list_serializer.rb new file mode 100644 index 00000000000..ac4c949d9f8 --- /dev/null +++ b/plugins/discourse-calendar/app/serializers/discourse_post_event/invitee_list_serializer.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module DiscoursePostEvent + class InviteeListSerializer < ApplicationSerializer + root false + attributes :meta + has_many :invitees, serializer: InviteeSerializer, embed: :objects + + def invitees + object[:invitees] + end + + def meta + { + suggested_users: + ActiveModel::ArraySerializer.new( + suggested_users, + each_serializer: BasicUserSerializer, + scope: scope, + ), + } + end + + def include_meta? + suggested_users.present? + end + + def suggested_users + object[:suggested_users] + end + end +end diff --git a/plugins/discourse-calendar/app/serializers/discourse_post_event/invitee_serializer.rb b/plugins/discourse-calendar/app/serializers/discourse_post_event/invitee_serializer.rb new file mode 100644 index 00000000000..c2c6b8cc447 --- /dev/null +++ b/plugins/discourse-calendar/app/serializers/discourse_post_event/invitee_serializer.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module DiscoursePostEvent + class InviteeSerializer < ApplicationSerializer + attributes :id, :status, :user, :post_id, :meta + + def status + object.status ? Invitee.statuses[object.status] : nil + end + + def include_id? + object.id + end + + def user + BasicUserSerializer.new(object.user, embed: :objects, root: false) + end + + def meta + { + event_should_display_invitees: + (object.event.public? && object.event.invitees.count > 0) || + (object.event.private? && object.event.raw_invitees.count > 0), + event_stats: EventStatsSerializer.new(object.event, root: false), + } + end + end +end diff --git a/plugins/discourse-calendar/app/serializers/user_timezone_serializer.rb b/plugins/discourse-calendar/app/serializers/user_timezone_serializer.rb new file mode 100644 index 00000000000..f1625229132 --- /dev/null +++ b/plugins/discourse-calendar/app/serializers/user_timezone_serializer.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class UserTimezoneSerializer < BasicUserSerializer + attributes :timezone, :on_holiday + + def on_holiday + @options[:on_holiday] || false + end +end diff --git a/plugins/discourse-calendar/app/services/discourse_calendar/holiday.rb b/plugins/discourse-calendar/app/services/discourse_calendar/holiday.rb new file mode 100644 index 00000000000..acb54346ea1 --- /dev/null +++ b/plugins/discourse-calendar/app/services/discourse_calendar/holiday.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require "holidays" + +module DiscourseCalendar + class Holiday + def self.find_holidays_for( + region_code:, + start_date: Date.current.beginning_of_year, + end_date: Date.current.end_of_year, + show_holiday_observed_on_dates: false + ) + holidays = + Holidays.between( + start_date, + end_date, + [region_code], + show_holiday_observed_on_dates ? :observed : [], + ) + + holidays.map do |holiday| + holiday[:disabled] = DiscourseCalendar::DisabledHoliday.where( + region_code: region_code, + ).exists?(holiday_name: holiday[:name]) + end + + holidays + end + end +end diff --git a/plugins/discourse-calendar/app/services/discourse_post_event/chat_channel_sync.rb b/plugins/discourse-calendar/app/services/discourse_post_event/chat_channel_sync.rb new file mode 100644 index 00000000000..37f9320a66f --- /dev/null +++ b/plugins/discourse-calendar/app/services/discourse_post_event/chat_channel_sync.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true +# +module DiscoursePostEvent + class ChatChannelSync + def self.sync(event, guardian: nil) + return if !event.chat_enabled? + if !event.chat_channel_id && guardian&.can_create_chat_channel? + ensure_chat_channel!(event, guardian:) + end + sync_chat_channel_members!(event) if event.chat_channel_id + end + + def self.sync_chat_channel_members!(event) + missing_members_sql = <<~SQL + SELECT user_id + FROM discourse_post_event_invitees + WHERE post_id = :post_id + AND status in (:statuses) + AND user_id NOT IN ( + SELECT user_id + FROM user_chat_channel_memberships + WHERE chat_channel_id = :chat_channel_id + ) + SQL + + missing_user_ids = + DB.query_single( + missing_members_sql, + post_id: event.post.id, + statuses: [ + DiscoursePostEvent::Invitee.statuses[:going], + DiscoursePostEvent::Invitee.statuses[:interested], + ], + chat_channel_id: event.chat_channel_id, + ) + + if missing_user_ids.present? + ActiveRecord::Base.transaction do + missing_user_ids.each do |user_id| + event.chat_channel.user_chat_channel_memberships.create!( + user_id:, + chat_channel_id: event.chat_channel_id, + following: true, + ) + end + end + end + end + + def self.ensure_chat_channel!(event, guardian:) + name = event.name + + channel = nil + Chat::CreateCategoryChannel.call( + guardian:, + params: { + name:, + category_id: event.post.topic.category_id, + }, + ) do |result| + on_success { channel = result.channel } + on_failure { raise StandardError, result.inspect_steps } + end + + # event creator will be a member of the channel + event.chat_channel_id = channel.id + end + end +end diff --git a/plugins/discourse-calendar/assets/javascripts/discourse/adapters/discourse-post-event-adapter.js b/plugins/discourse-calendar/assets/javascripts/discourse/adapters/discourse-post-event-adapter.js new file mode 100644 index 00000000000..455506cf72f --- /dev/null +++ b/plugins/discourse-calendar/assets/javascripts/discourse/adapters/discourse-post-event-adapter.js @@ -0,0 +1,7 @@ +import RestAdapter from "discourse/adapters/rest"; + +export default class DiscoursePostEventAdapter extends RestAdapter { + basePath() { + return "/discourse-post-event/"; + } +} diff --git a/plugins/discourse-calendar/assets/javascripts/discourse/adapters/discourse-post-event-event.js b/plugins/discourse-calendar/assets/javascripts/discourse/adapters/discourse-post-event-event.js new file mode 100644 index 00000000000..6c80730092b --- /dev/null +++ b/plugins/discourse-calendar/assets/javascripts/discourse/adapters/discourse-post-event-event.js @@ -0,0 +1,15 @@ +import { underscore } from "@ember/string"; +import DiscoursePostEventAdapter from "./discourse-post-event-adapter"; + +export default class DiscoursePostEventEvent extends DiscoursePostEventAdapter { + pathFor(store, type, findArgs) { + const path = + this.basePath(store, type, findArgs) + + underscore(store.pluralize(this.apiNameFor(type))); + return this.appendQueryParams(path, findArgs); + } + + apiNameFor() { + return "event"; + } +} diff --git a/plugins/discourse-calendar/assets/javascripts/discourse/adapters/discourse-post-event-invitee.js b/plugins/discourse-calendar/assets/javascripts/discourse/adapters/discourse-post-event-invitee.js new file mode 100644 index 00000000000..b4ca774f4ab --- /dev/null +++ b/plugins/discourse-calendar/assets/javascripts/discourse/adapters/discourse-post-event-invitee.js @@ -0,0 +1,7 @@ +import DiscoursePostEventNestedAdapter from "./discourse-post-event-nested-adapter"; + +export default class DiscoursePostEventInvitee extends DiscoursePostEventNestedAdapter { + apiNameFor() { + return "invitee"; + } +} diff --git a/plugins/discourse-calendar/assets/javascripts/discourse/adapters/discourse-post-event-nested-adapter.js b/plugins/discourse-calendar/assets/javascripts/discourse/adapters/discourse-post-event-nested-adapter.js new file mode 100644 index 00000000000..891cd5c2fac --- /dev/null +++ b/plugins/discourse-calendar/assets/javascripts/discourse/adapters/discourse-post-event-nested-adapter.js @@ -0,0 +1,65 @@ +import { underscore } from "@ember/string"; +import { Result } from "discourse/adapters/rest"; +import { ajax } from "discourse/lib/ajax"; +import DiscoursePostEventAdapter from "./discourse-post-event-adapter"; + +export default class DiscoursePostEventNestedAdapter extends DiscoursePostEventAdapter { + // TODO: destroy/update/create should be improved in core to allow for nested models + destroyRecord(store, type, record) { + return ajax( + this.pathFor(store, type, { + post_id: record.post_id, + id: record.id, + }), + { + type: "DELETE", + } + ); + } + + update(store, type, id, attrs) { + const data = {}; + const typeField = underscore(this.apiNameFor(type)); + data[typeField] = attrs; + + return ajax( + this.pathFor(store, type, { id, post_id: attrs.post_id }), + this.getPayload("PUT", data) + ).then(function (json) { + return new Result(json[typeField], json); + }); + } + + createRecord(store, type, attrs) { + const data = {}; + const typeField = underscore(this.apiNameFor(type)); + data[typeField] = attrs; + return ajax( + this.pathFor(store, type, attrs), + this.getPayload("POST", data) + ).then(function (json) { + return new Result(json[typeField], json); + }); + } + + pathFor(store, type, findArgs) { + const post_id = findArgs["post_id"]; + delete findArgs["post_id"]; + + const id = findArgs["id"]; + delete findArgs["id"]; + + let path = + this.basePath(store, type, {}) + + "events/" + + post_id + + "/" + + underscore(store.pluralize(this.apiNameFor())); + + if (id) { + path += `/${id}`; + } + + return this.appendQueryParams(path, findArgs); + } +} diff --git a/plugins/discourse-calendar/assets/javascripts/discourse/adapters/discourse-post-event-reminder.js b/plugins/discourse-calendar/assets/javascripts/discourse/adapters/discourse-post-event-reminder.js new file mode 100644 index 00000000000..a0a71c543e7 --- /dev/null +++ b/plugins/discourse-calendar/assets/javascripts/discourse/adapters/discourse-post-event-reminder.js @@ -0,0 +1,7 @@ +import DiscoursePostEventNestedAdapter from "./discourse-post-event-nested-adapter"; + +export default class DiscoursePostEventReminder extends DiscoursePostEventNestedAdapter { + apiNameFor() { + return "reminder"; + } +} diff --git a/plugins/discourse-calendar/assets/javascripts/discourse/admin-calendar-route-map.js b/plugins/discourse-calendar/assets/javascripts/discourse/admin-calendar-route-map.js new file mode 100644 index 00000000000..5a49b3a5082 --- /dev/null +++ b/plugins/discourse-calendar/assets/javascripts/discourse/admin-calendar-route-map.js @@ -0,0 +1,7 @@ +export default { + resource: "admin.adminPlugins", + path: "/plugins", + map() { + this.route("calendar"); + }, +}; diff --git a/plugins/discourse-calendar/assets/javascripts/discourse/api-initializers/discourse-group-timezones.gjs b/plugins/discourse-calendar/assets/javascripts/discourse/api-initializers/discourse-group-timezones.gjs new file mode 100644 index 00000000000..e07a8320428 --- /dev/null +++ b/plugins/discourse-calendar/assets/javascripts/discourse/api-initializers/discourse-group-timezones.gjs @@ -0,0 +1,35 @@ +import { apiInitializer } from "discourse/lib/api"; +import GroupTimezones from "../components/group-timezones"; + +const GroupTimezonesShim = ; + +export default apiInitializer((api) => { + api.decorateCookedElement((element, helper) => { + element.querySelectorAll(".group-timezones").forEach((el) => { + const post = helper.getModel(); + + if (!post) { + return; + } + + const group = el.dataset.group; + if (!group) { + throw new Error( + "Group timezone element is missing 'data-group' attribute" + ); + } + + helper.renderGlimmer(el, GroupTimezonesShim, { + group, + members: (post.group_timezones || {})[group] || [], + size: el.dataset.size || "medium", + }); + }); + }); +}); diff --git a/plugins/discourse-calendar/assets/javascripts/discourse/components/admin-holidays-list-item.gjs b/plugins/discourse-calendar/assets/javascripts/discourse/components/admin-holidays-list-item.gjs new file mode 100644 index 00000000000..407ad74fcb4 --- /dev/null +++ b/plugins/discourse-calendar/assets/javascripts/discourse/components/admin-holidays-list-item.gjs @@ -0,0 +1,69 @@ +import Component from "@ember/component"; +import { action } from "@ember/object"; +import { classNameBindings, tagName } from "@ember-decorators/component"; +import DButton from "discourse/components/d-button"; +import { ajax } from "discourse/lib/ajax"; +import { popupAjaxError } from "discourse/lib/ajax-error"; + +@tagName("tr") +@classNameBindings("isHolidayDisabled:disabled") +export default class AdminHolidaysListItem extends Component { + loading = false; + isHolidayDisabled = false; + + @action + disableHoliday(holiday, region_code) { + if (this.loading) { + return; + } + + this.set("loading", true); + + return ajax({ + url: `/admin/discourse-calendar/holidays/disable`, + type: "POST", + data: { disabled_holiday: { holiday_name: holiday.name, region_code } }, + }) + .then(() => this.set("isHolidayDisabled", true)) + .catch(popupAjaxError) + .finally(() => this.set("loading", false)); + } + + @action + enableHoliday(holiday, region_code) { + if (this.loading) { + return; + } + + this.set("loading", true); + + return ajax({ + url: `/admin/discourse-calendar/holidays/enable`, + type: "DELETE", + data: { disabled_holiday: { holiday_name: holiday.name, region_code } }, + }) + .then(() => this.set("isHolidayDisabled", false)) + .catch(popupAjaxError) + .finally(() => this.set("loading", false)); + } + + +} diff --git a/plugins/discourse-calendar/assets/javascripts/discourse/components/admin-holidays-list.gjs b/plugins/discourse-calendar/assets/javascripts/discourse/components/admin-holidays-list.gjs new file mode 100644 index 00000000000..058beed6848 --- /dev/null +++ b/plugins/discourse-calendar/assets/javascripts/discourse/components/admin-holidays-list.gjs @@ -0,0 +1,25 @@ +import { i18n } from "discourse-i18n"; +import AdminHolidaysListItem from "./admin-holidays-list-item"; + +const AdminHolidaysList = ; + +export default AdminHolidaysList; diff --git a/plugins/discourse-calendar/assets/javascripts/discourse/components/bulk-invite-sample-csv-file.gjs b/plugins/discourse-calendar/assets/javascripts/discourse/components/bulk-invite-sample-csv-file.gjs new file mode 100644 index 00000000000..b18ddd14211 --- /dev/null +++ b/plugins/discourse-calendar/assets/javascripts/discourse/components/bulk-invite-sample-csv-file.gjs @@ -0,0 +1,36 @@ +import Component from "@ember/component"; +import { action } from "@ember/object"; +import DButton from "discourse/components/d-button"; + +export default class BulkInviteSampleCsvFile extends Component { + @action + downloadSampleCsv() { + const sampleData = [ + ["my_awesome_group", "going"], + ["lucy", "interested"], + ["mark", "not_going"], + ["sam", "unknown"], + ]; + + let csv = ""; + sampleData.forEach((row) => { + csv += row.join(","); + csv += "\n"; + }); + + const btn = document.createElement("a"); + btn.href = `data:text/csv;charset=utf-8,${encodeURI(csv)}`; + btn.target = "_blank"; + btn.rel = "noopener noreferrer"; + btn.download = "bulk-invite-sample.csv"; + btn.click(); + } + + +} diff --git a/plugins/discourse-calendar/assets/javascripts/discourse/components/csv-uploader.gjs b/plugins/discourse-calendar/assets/javascripts/discourse/components/csv-uploader.gjs new file mode 100644 index 00000000000..d828f61879d --- /dev/null +++ b/plugins/discourse-calendar/assets/javascripts/discourse/components/csv-uploader.gjs @@ -0,0 +1,64 @@ +import Component from "@glimmer/component"; +import { getOwner } from "@ember/owner"; +import didInsert from "@ember/render-modifiers/modifiers/did-insert"; +import { service } from "@ember/service"; +import { or } from "truth-helpers"; +import icon from "discourse/helpers/d-icon"; +import UppyUpload from "discourse/lib/uppy/uppy-upload"; +import { i18n } from "discourse-i18n"; + +export default class CsvUploader extends Component { + @service dialog; + + uppyUpload = new UppyUpload(getOwner(this), { + type: "csv", + id: "discourse-post-event-csv-uploader", + autoStartUploads: false, + uploadUrl: this.args.uploadUrl, + uppyReady: () => { + this.uppyUpload.uppyWrapper.uppyInstance.on("file-added", () => { + this.dialog.confirm({ + message: i18n(`${this.args.i18nPrefix}.confirmation_message`), + didConfirm: () => this.uppyUpload.startUpload(), + didCancel: () => this.uppyUpload.reset(), + }); + }); + }, + uploadDone: () => { + this.dialog.alert(i18n(`${this.args.i18nPrefix}.success`)); + }, + validateUploadedFilesOptions: { + csvOnly: true, + }, + }); + + get uploadButtonText() { + return this.uppyUpload.uploading + ? i18n("uploading") + : i18n(`${this.args.i18nPrefix}.text`); + } + + get uploadButtonDisabled() { + // https://github.com/emberjs/ember.js/issues/10976#issuecomment-132417731 + return this.uppyUpload.uploading || this.uppyUpload.processing || null; + } + + +} diff --git a/plugins/discourse-calendar/assets/javascripts/discourse/components/discourse-post-event/chat-channel.gjs b/plugins/discourse-calendar/assets/javascripts/discourse/components/discourse-post-event/chat-channel.gjs new file mode 100644 index 00000000000..c1740da8e10 --- /dev/null +++ b/plugins/discourse-calendar/assets/javascripts/discourse/components/discourse-post-event/chat-channel.gjs @@ -0,0 +1,20 @@ +import { LinkTo } from "@ember/routing"; +import { and } from "truth-helpers"; +import { optionalRequire } from "discourse/lib/utilities"; + +const ChannelTitle = optionalRequire( + "discourse/plugins/chat/discourse/components/channel-title" +); + +const DiscoursePostEventChatChannel = ; + +export default DiscoursePostEventChatChannel; diff --git a/plugins/discourse-calendar/assets/javascripts/discourse/components/discourse-post-event/creator.gjs b/plugins/discourse-calendar/assets/javascripts/discourse/components/discourse-post-event/creator.gjs new file mode 100644 index 00000000000..44dc031fd66 --- /dev/null +++ b/plugins/discourse-calendar/assets/javascripts/discourse/components/discourse-post-event/creator.gjs @@ -0,0 +1,23 @@ +import Component from "@glimmer/component"; +import avatar from "discourse/helpers/avatar"; +import { formatUsername } from "discourse/lib/utilities"; +import { i18n } from "discourse-i18n"; + +export default class DiscoursePostEventCreator extends Component { + get username() { + return this.args.user.name || formatUsername(this.args.user.username); + } + + +} diff --git a/plugins/discourse-calendar/assets/javascripts/discourse/components/discourse-post-event/dates.gjs b/plugins/discourse-calendar/assets/javascripts/discourse/components/discourse-post-event/dates.gjs new file mode 100644 index 00000000000..48f2e911030 --- /dev/null +++ b/plugins/discourse-calendar/assets/javascripts/discourse/components/discourse-post-event/dates.gjs @@ -0,0 +1,154 @@ +import Component from "@glimmer/component"; +import { tracked } from "@glimmer/tracking"; +import { action } from "@ember/object"; +import didInsert from "@ember/render-modifiers/modifiers/did-insert"; +import { next } from "@ember/runloop"; +import { service } from "@ember/service"; +import { htmlSafe } from "@ember/template"; +import icon from "discourse/helpers/d-icon"; +import { applyLocalDates } from "discourse/lib/local-dates"; +import { cook } from "discourse/lib/text"; + +export default class DiscoursePostEventDates extends Component { + @service siteSettings; + + @tracked htmlDates = ""; + + get startsAt() { + return moment(this.args.event.startsAt).tz(this.timezone); + } + + get endsAt() { + return ( + this.args.event.endsAt && moment(this.args.event.endsAt).tz(this.timezone) + ); + } + + get timezone() { + return this.args.event.timezone || "UTC"; + } + + get startsAtFormat() { + return this._buildFormat(this.startsAt, { + includeYear: !this.isSameYear(this.startsAt), + includeTime: this.hasTime(this.startsAt) || this.isSingleDayEvent, + }); + } + + get endsAtFormat() { + if (this.isSingleDayEvent) { + return "LT"; + } + + return this._buildFormat(this.endsAt, { + includeYear: + !this.isSameYear(this.endsAt) || + !this.isSameYear(this.endsAt, this.startsAt), + includeTime: this.hasTime(this.endsAt), + }); + } + + _buildFormat(date, { includeYear, includeTime }) { + const formatParts = ["ddd, MMM D"]; + if (includeYear) { + formatParts.push("YYYY"); + } + + const dateString = formatParts.join(", "); + const timeString = includeTime ? " LT" : ""; + + return `\u0022${dateString}${timeString}\u0022`; + } + + get isSingleDayEvent() { + return this.startsAt.isSame(this.endsAt, "day"); + } + + get datesBBCode() { + const dates = []; + + dates.push( + this.buildDateBBCode({ + date: this.startsAt, + format: this.startsAtFormat, + range: !!this.endsAt && "from", + }) + ); + + if (this.endsAt) { + dates.push( + this.buildDateBBCode({ + date: this.endsAt, + format: this.endsAtFormat, + range: "to", + }) + ); + } + + return dates; + } + + isSameYear(date1, date2) { + return date1.isSame(date2 || moment(), "year"); + } + + hasTime(date) { + return date.hour() || date.minute(); + } + + buildDateBBCode({ date, format, range }) { + const bbcode = { + date: date.format("YYYY-MM-DD"), + time: date.format("HH:mm"), + format, + timezone: this.timezone, + hideTimezone: this.args.event.showLocalTime, + }; + + if (this.args.event.showLocalTime) { + bbcode.displayedTimezone = this.args.event.timezone; + } + + if (range) { + bbcode.range = range; + } + + const content = Object.entries(bbcode) + .map(([key, value]) => `${key}=${value}`) + .join(" "); + + return `[${content}]`; + } + + @action + async computeDates(element) { + if (this.siteSettings.discourse_local_dates_enabled) { + const result = await cook(this.datesBBCode.join("")); + this.htmlDates = htmlSafe(result.toString()); + + next(() => { + if (this.isDestroying || this.isDestroyed) { + return; + } + + applyLocalDates( + element.querySelectorAll( + `[data-post-id="${this.args.event.id}"] .discourse-local-date` + ), + this.siteSettings + ); + }); + } else { + let dates = `${this.startsAt.format(this.startsAtFormat)}`; + if (this.endsAt) { + dates += ` → ${moment(this.endsAt).format(this.endsAtFormat)}`; + } + this.htmlDates = htmlSafe(dates); + } + } + + +} diff --git a/plugins/discourse-calendar/assets/javascripts/discourse/components/discourse-post-event/description.gjs b/plugins/discourse-calendar/assets/javascripts/discourse/components/discourse-post-event/description.gjs new file mode 100644 index 00000000000..c83c1a55dbe --- /dev/null +++ b/plugins/discourse-calendar/assets/javascripts/discourse/components/discourse-post-event/description.gjs @@ -0,0 +1,11 @@ +import CookText from "discourse/components/cook-text"; + +const DiscoursePostEventDescription = ; + +export default DiscoursePostEventDescription; diff --git a/plugins/discourse-calendar/assets/javascripts/discourse/components/discourse-post-event/event-status.gjs b/plugins/discourse-calendar/assets/javascripts/discourse/components/discourse-post-event/event-status.gjs new file mode 100644 index 00000000000..c773732f0e9 --- /dev/null +++ b/plugins/discourse-calendar/assets/javascripts/discourse/components/discourse-post-event/event-status.gjs @@ -0,0 +1,36 @@ +import Component from "@glimmer/component"; +import { i18n } from "discourse-i18n"; + +export default class EventStatus extends Component { + get eventStatusLabel() { + return i18n( + `discourse_post_event.models.event.status.${this.args.event.status}.title` + ); + } + + get eventStatusDescription() { + return i18n( + `discourse_post_event.models.event.status.${this.args.event.status}.description` + ); + } + + get statusClass() { + return `status ${this.args.event.status}`; + } + + +} diff --git a/plugins/discourse-calendar/assets/javascripts/discourse/components/discourse-post-event/index.gjs b/plugins/discourse-calendar/assets/javascripts/discourse/components/discourse-post-event/index.gjs new file mode 100644 index 00000000000..df47d81fd04 --- /dev/null +++ b/plugins/discourse-calendar/assets/javascripts/discourse/components/discourse-post-event/index.gjs @@ -0,0 +1,157 @@ +import Component from "@glimmer/component"; +import { service } from "@ember/service"; +import { modifier } from "ember-modifier"; +import PluginOutlet from "discourse/components/plugin-outlet"; +import concatClass from "discourse/helpers/concat-class"; +import icon from "discourse/helpers/d-icon"; +import lazyHash from "discourse/helpers/lazy-hash"; +import replaceEmoji from "discourse/helpers/replace-emoji"; +import routeAction from "discourse/helpers/route-action"; +import ChatChannel from "./chat-channel"; +import Creator from "./creator"; +import Dates from "./dates"; +import Description from "./description"; +import EventStatus from "./event-status"; +import Invitees from "./invitees"; +import Location from "./location"; +import MoreMenu from "./more-menu"; +import Status from "./status"; +import Url from "./url"; + +const StatusSeparator = ; + +const InfoSection = ; + +export default class DiscoursePostEvent extends Component { + @service currentUser; + @service discoursePostEventApi; + @service messageBus; + + setupMessageBus = modifier(() => { + const { event } = this.args; + const path = `/discourse-post-event/${event.post.topic.id}`; + this.messageBus.subscribe(path, async (msg) => { + const eventData = await this.discoursePostEventApi.event(msg.id); + event.updateFromEvent(eventData); + }); + + return () => this.messageBus.unsubscribe(path); + }); + + get localStartsAtTime() { + let time = moment(this.args.event.startsAt); + if (this.args.event.showLocalTime && this.args.event.timezone) { + time = time.tz(this.args.event.timezone); + } + return time; + } + + get startsAtMonth() { + return this.localStartsAtTime.format("MMM"); + } + + get startsAtDay() { + return this.localStartsAtTime.format("D"); + } + + get eventName() { + return this.args.event.name || this.args.event.post.topic.title; + } + + get isPublicEvent() { + return this.args.event.status === "public"; + } + + get isStandaloneEvent() { + return this.args.event.status === "standalone"; + } + + get canActOnEvent() { + return this.currentUser && this.args.event.can_act_on_discourse_post_event; + } + + get watchingInviteeStatus() { + return this.args.event.watchingInvitee?.status; + } + + +} diff --git a/plugins/discourse-calendar/assets/javascripts/discourse/components/discourse-post-event/invitee.gjs b/plugins/discourse-calendar/assets/javascripts/discourse/components/discourse-post-event/invitee.gjs new file mode 100644 index 00000000000..76b08b0bb40 --- /dev/null +++ b/plugins/discourse-calendar/assets/javascripts/discourse/components/discourse-post-event/invitee.gjs @@ -0,0 +1,53 @@ +import Component from "@glimmer/component"; +import { concat } from "@ember/helper"; +import { service } from "@ember/service"; +import { eq } from "truth-helpers"; +import AvatarFlair from "discourse/components/avatar-flair"; +import avatar from "discourse/helpers/avatar"; +import concatClass from "discourse/helpers/concat-class"; +import { i18n } from "discourse-i18n"; + +export default class DiscoursePostEventInvitee extends Component { + @service site; + @service currentUser; + + get statusIcon() { + switch (this.args.invitee.status) { + case "going": + return "check"; + case "interested": + return "star"; + case "not_going": + return "xmark"; + } + } + + get flairName() { + const string = `discourse_post_event.models.invitee.status.${this.args.invitee.status}`; + + return i18n(string); + } + + +} diff --git a/plugins/discourse-calendar/assets/javascripts/discourse/components/discourse-post-event/invitees.gjs b/plugins/discourse-calendar/assets/javascripts/discourse/components/discourse-post-event/invitees.gjs new file mode 100644 index 00000000000..4e6d8377484 --- /dev/null +++ b/plugins/discourse-calendar/assets/javascripts/discourse/components/discourse-post-event/invitees.gjs @@ -0,0 +1,55 @@ +import Component from "@glimmer/component"; +import { service } from "@ember/service"; +import icon from "discourse/helpers/d-icon"; +import { i18n } from "discourse-i18n"; +import Invitee from "./invitee"; + +export default class DiscoursePostEventInvitees extends Component { + @service modal; + @service siteSettings; + + get hasAttendees() { + return this.args.event.stats.going > 0; + } + + get statsInfo() { + return this.args.event.stats.going; + } + + get inviteesTitle() { + return i18n("discourse_post_event.models.invitee.status.going_count", { + count: this.args.event.stats.going, + }); + } + + +} diff --git a/plugins/discourse-calendar/assets/javascripts/discourse/components/discourse-post-event/location.gjs b/plugins/discourse-calendar/assets/javascripts/discourse/components/discourse-post-event/location.gjs new file mode 100644 index 00000000000..a0c48ad00e1 --- /dev/null +++ b/plugins/discourse-calendar/assets/javascripts/discourse/components/discourse-post-event/location.gjs @@ -0,0 +1,14 @@ +import CookText from "discourse/components/cook-text"; +import icon from "discourse/helpers/d-icon"; + +const DiscoursePostEventLocation = ; + +export default DiscoursePostEventLocation; diff --git a/plugins/discourse-calendar/assets/javascripts/discourse/components/discourse-post-event/more-menu.gjs b/plugins/discourse-calendar/assets/javascripts/discourse/components/discourse-post-event/more-menu.gjs new file mode 100644 index 00000000000..ea75d468205 --- /dev/null +++ b/plugins/discourse-calendar/assets/javascripts/discourse/components/discourse-post-event/more-menu.gjs @@ -0,0 +1,382 @@ +import Component from "@glimmer/component"; +import { tracked } from "@glimmer/tracking"; +import { hash } from "@ember/helper"; +import EmberObject, { action } from "@ember/object"; +import { service } from "@ember/service"; +import DButton from "discourse/components/d-button"; +import DropdownMenu from "discourse/components/dropdown-menu"; +import concatClass from "discourse/helpers/concat-class"; +import { popupAjaxError } from "discourse/lib/ajax-error"; +import { downloadCalendar } from "discourse/lib/download-calendar"; +import { exportEntity } from "discourse/lib/export-csv"; +import { getAbsoluteURL } from "discourse/lib/get-url"; +import { cook } from "discourse/lib/text"; +import { applyValueTransformer } from "discourse/lib/transformer"; +import { i18n } from "discourse-i18n"; +import DMenu from "float-kit/components/d-menu"; +import { buildParams, replaceRaw } from "../../lib/raw-event-helper"; +import PostEventBuilder from "../modal/post-event-builder"; +import PostEventBulkInvite from "../modal/post-event-bulk-invite"; +import PostEventInviteUserOrGroup from "../modal/post-event-invite-user-or-group"; +import PostEventInvitees from "../modal/post-event-invitees"; + +export default class DiscoursePostEventMoreMenu extends Component { + @service currentUser; + @service dialog; + @service discoursePostEventApi; + @service modal; + @service router; + @service siteSettings; + @service store; + + @tracked isSavingEvent = false; + + get expiredOrClosed() { + return this.args.event.isExpired || this.args.event.isClosed; + } + + get canActOnEvent() { + return this.currentUser && this.args.event.canActOnDiscoursePostEvent; + } + + get shouldShowParticipants() { + return applyValueTransformer( + "discourse-calendar-event-more-menu-should-show-participants", + this.canActOnEvent && !this.args.isStandaloneEvent, + { + event: this.args.event, + } + ); + } + + get canInvite() { + return ( + !this.expiredOrClosed && this.canActOnEvent && this.args.event.isPublic + ); + } + + get canSeeUpcomingEvents() { + return !this.args.event.isClosed && this.args.event.recurrence; + } + + get canBulkInvite() { + return !this.expiredOrClosed && !this.args.event.isStandalone; + } + + get canSendPmToCreator() { + return ( + this.currentUser && + this.currentUser.username !== this.args.event.creator.username + ); + } + + @action + addToCalendar() { + this.menuApi.close(); + + const event = this.args.event; + + downloadCalendar( + event.name || event.post.topic.title, + [ + { + startsAt: event.startsAt, + endsAt: event.endsAt, + }, + ], + { + recurrenceRule: event.recurrenceRule, + location: event.url, + details: getAbsoluteURL(event.post.url), + } + ); + } + + @action + sendPMToCreator() { + this.menuApi.close(); + + this.args.composePrivateMessage( + EmberObject.create(this.args.event.creator), + EmberObject.create(this.args.event.post) + ); + } + + @action + upcomingEvents() { + this.router.transitionTo("discourse-post-event-upcoming-events"); + } + + @action + registerMenuApi(api) { + this.menuApi = api; + } + + @action + async inviteUserOrGroup() { + this.menuApi.close(); + + try { + this.modal.show(PostEventInviteUserOrGroup, { + model: { event: this.args.event }, + }); + } catch (e) { + popupAjaxError(e); + } + } + + @action + exportPostEvent() { + this.menuApi.close(); + + exportEntity("post_event", { + name: "post_event", + id: this.args.event.id, + }); + } + + @action + bulkInvite() { + this.menuApi.close(); + + this.modal.show(PostEventBulkInvite, { + model: { event: this.args.event }, + }); + } + + @action + async openEvent() { + this.menuApi.close(); + + this.dialog.yesNoConfirm({ + message: i18n("discourse_post_event.builder_modal.confirm_open"), + didConfirm: async () => { + this.isSavingEvent = true; + + try { + const post = await this.store.find("post", this.args.event.id); + this.args.event.isClosed = false; + + const eventParams = buildParams( + this.args.event.startsAt, + this.args.event.endsAt, + this.args.event, + this.siteSettings + ); + + const newRaw = replaceRaw(eventParams, post.raw); + + if (newRaw) { + const props = { + raw: newRaw, + edit_reason: i18n("discourse_post_event.edit_reason_opened"), + }; + + const cooked = await cook(newRaw); + props.cooked = cooked.string; + await post.save(props); + } + } catch (e) { + popupAjaxError(e); + } finally { + this.isSavingEvent = false; + } + }, + }); + } + + @action + async editPostEvent() { + this.menuApi.close(); + + this.modal.show(PostEventBuilder, { + model: { + event: this.args.event, + }, + }); + } + + @action + showParticipants() { + this.menuApi.close(); + + this.modal.show(PostEventInvitees, { + model: { + event: this.args.event, + title: this.args.event.title, + extraClass: this.args.event.extraClass, + }, + }); + } + + @action + async closeEvent() { + this.menuApi.close(); + + this.dialog.yesNoConfirm({ + message: i18n("discourse_post_event.builder_modal.confirm_close"), + didConfirm: () => { + this.isSavingEvent = true; + return this.store.find("post", this.args.event.id).then((post) => { + this.args.event.isClosed = true; + + const eventParams = buildParams( + this.args.event.startsAt, + this.args.event.endsAt, + this.args.event, + this.siteSettings + ); + + const newRaw = replaceRaw(eventParams, post.raw); + + if (newRaw) { + const props = { + raw: newRaw, + edit_reason: i18n("discourse_post_event.edit_reason_closed"), + }; + + return cook(newRaw) + .then((cooked) => { + props.cooked = cooked.string; + return post.save(props); + }) + .finally(() => { + this.isSavingEvent = false; + }); + } + }); + }, + }); + } + + +} diff --git a/plugins/discourse-calendar/assets/javascripts/discourse/components/discourse-post-event/status.gjs b/plugins/discourse-calendar/assets/javascripts/discourse/components/discourse-post-event/status.gjs new file mode 100644 index 00000000000..191eceec464 --- /dev/null +++ b/plugins/discourse-calendar/assets/javascripts/discourse/components/discourse-post-event/status.gjs @@ -0,0 +1,183 @@ +import Component from "@glimmer/component"; +import { concat, fn } from "@ember/helper"; +import { action } from "@ember/object"; +import { service } from "@ember/service"; +import DButton from "discourse/components/d-button"; +import PluginOutlet from "discourse/components/plugin-outlet"; +import concatClass from "discourse/helpers/concat-class"; +import lazyHash from "discourse/helpers/lazy-hash"; +import { popupAjaxError } from "discourse/lib/ajax-error"; + +export default class DiscoursePostEventStatus extends Component { + @service appEvents; + @service discoursePostEventApi; + @service siteSettings; + + get eventButtons() { + return this.siteSettings.event_participation_buttons.split("|"); + } + + get showGoingButton() { + return !!this.eventButtons.find((button) => button === "going"); + } + + get showInterestedButton() { + return !!this.eventButtons.find((button) => button === "interested"); + } + + get showNotGoingButton() { + return !!this.eventButtons.find((button) => button === "not going"); + } + + get canLeave() { + return this.args.event.watchingInvitee && this.args.event.isPublic; + } + + get watchingInviteeStatus() { + return this.args.event.watchingInvitee?.status; + } + + @action + async leaveEvent() { + try { + const invitee = this.args.event.watchingInvitee; + + await this.discoursePostEventApi.leaveEvent(this.args.event, invitee); + + this.appEvents.trigger("calendar:invitee-left-event", { + invitee, + postId: this.args.event.id, + }); + } catch (e) { + popupAjaxError(e); + } + } + + @action + async updateEventAttendance(status) { + try { + await this.discoursePostEventApi.updateEventAttendance(this.args.event, { + status, + }); + + this.appEvents.trigger("calendar:update-invitee-status", { + status, + postId: this.args.event.id, + }); + } catch (e) { + popupAjaxError(e); + } + } + + @action + async joinEventWithStatus(status) { + try { + await this.discoursePostEventApi.joinEvent(this.args.event, { + status, + }); + + this.appEvents.trigger("calendar:create-invitee-status", { + status, + postId: this.args.event.id, + }); + } catch (e) { + popupAjaxError(e); + } + } + + @action + async changeWatchingInviteeStatus(status) { + if (this.args.event.watchingInvitee) { + const currentStatus = this.args.event.watchingInvitee.status; + if (this.canLeave) { + if (status === currentStatus) { + await this.leaveEvent(); + } else { + await this.updateEventAttendance(status); + } + } else { + if (status === currentStatus) { + status = null; + } + + await this.updateEventAttendance(status); + } + } else { + await this.joinEventWithStatus(status); + } + } + + +} diff --git a/plugins/discourse-calendar/assets/javascripts/discourse/components/discourse-post-event/url.gjs b/plugins/discourse-calendar/assets/javascripts/discourse/components/discourse-post-event/url.gjs new file mode 100644 index 00000000000..57c5f2dba38 --- /dev/null +++ b/plugins/discourse-calendar/assets/javascripts/discourse/components/discourse-post-event/url.gjs @@ -0,0 +1,26 @@ +import Component from "@glimmer/component"; +import icon from "discourse/helpers/d-icon"; + +export default class DiscoursePostEventUrl extends Component { + get url() { + return this.args.url.includes("://") || this.args.url.includes("mailto:") + ? this.args.url + : `https://${this.args.url}`; + } + + +} diff --git a/plugins/discourse-calendar/assets/javascripts/discourse/components/event-date.gjs b/plugins/discourse-calendar/assets/javascripts/discourse/components/event-date.gjs new file mode 100644 index 00000000000..16ed4958b1c --- /dev/null +++ b/plugins/discourse-calendar/assets/javascripts/discourse/components/event-date.gjs @@ -0,0 +1,112 @@ +import Component from "@glimmer/component"; +import { service } from "@ember/service"; +import { i18n } from "discourse-i18n"; +import guessDateFormat from "../lib/guess-best-date-format"; + +export default class EventDate extends Component { + @service siteSettings; + + + + get shouldRender() { + return ( + this.siteSettings.discourse_post_event_enabled && + this.args.topic.event_starts_at + ); + } + + get eventStartedAt() { + return this._parsedDate(this.args.topic.event_starts_at); + } + + get eventEndedAt() { + return this.args.topic.event_ends_at + ? this._parsedDate(this.args.topic.event_ends_at) + : this.eventStartedAt; + } + + get dateRange() { + return this.args.topic.event_ends_at + ? `${this._formattedDate(this.eventStartedAt)} → ${this._formattedDate( + this.eventEndedAt + )}` + : this._formattedDate(this.eventStartedAt); + } + + get localDateContent() { + return this._formattedDate(this.eventStartedAt); + } + + get relativeDateType() { + if (this.isWithinDateRange) { + return "current"; + } + if (this.eventStartedAt.isAfter(moment())) { + return "future"; + } + return "past"; + } + + get isWithinDateRange() { + return ( + this.eventStartedAt.isBefore(moment()) && + this.eventEndedAt.isAfter(moment()) + ); + } + + get relativeDateContent() { + // dateType "current" uses a different implementation + const relativeDates = { + future: this.eventStartedAt.from(moment()), + past: this.eventEndedAt.from(moment()), + }; + return relativeDates[this.relativeDateType]; + } + + get timeRemainingContent() { + return i18n("discourse_post_event.topic_title.ends_in_duration", { + duration: this.eventEndedAt.from(moment()), + }); + } + + _parsedDate(date) { + return moment.utc(date).tz(moment.tz.guess()); + } + + _guessedDateFormat() { + return guessDateFormat(this.eventStartedAt); + } + + _formattedDate(date) { + return date.format(this._guessedDateFormat()); + } +} diff --git a/plugins/discourse-calendar/assets/javascripts/discourse/components/event-field.gjs b/plugins/discourse-calendar/assets/javascripts/discourse/components/event-field.gjs new file mode 100644 index 00000000000..ef23a09e14c --- /dev/null +++ b/plugins/discourse-calendar/assets/javascripts/discourse/components/event-field.gjs @@ -0,0 +1,20 @@ +import { notEq } from "truth-helpers"; +import { i18n } from "discourse-i18n"; + +const EventField = ; + +export default EventField; diff --git a/plugins/discourse-calendar/assets/javascripts/discourse/components/group-timezones/index.gjs b/plugins/discourse-calendar/assets/javascripts/discourse/components/group-timezones/index.gjs new file mode 100644 index 00000000000..1a1910d030c --- /dev/null +++ b/plugins/discourse-calendar/assets/javascripts/discourse/components/group-timezones/index.gjs @@ -0,0 +1,184 @@ +import Component from "@glimmer/component"; +import { tracked } from "@glimmer/tracking"; +import { fn } from "@ember/helper"; +import { on } from "@ember/modifier"; +import { action } from "@ember/object"; +import { service } from "@ember/service"; +import { eq } from "truth-helpers"; +import { i18n } from "discourse-i18n"; +import roundTime from "../../lib/round-time"; +import NewDay from "./new-day"; +import TimeTraveller from "./time-traveller"; +import Timezone from "./timezone"; + +const nbsp = "\xa0"; + +export default class GroupTimezones extends Component { + @service siteSettings; + + @tracked filter = ""; + @tracked localTimeOffset = 0; + + get groupedTimezones() { + let groupedTimezones = []; + + this.args.members.filterBy("timezone").forEach((member) => { + if (this.#shouldAddMemberToGroup(this.filter, member)) { + const timezone = member.timezone; + const identifier = parseInt(moment.tz(timezone).format("YYYYMDHm"), 10); + let groupedTimezone = groupedTimezones.findBy("identifier", identifier); + + if (groupedTimezone) { + groupedTimezone.members.push(member); + } else { + const now = this.#roundMoment(moment.tz(timezone)); + const workingDays = this.#workingDays(); + const offset = moment.tz(moment.utc(), timezone).utcOffset(); + + groupedTimezone = { + identifier, + offset, + type: "discourse-group-timezone", + nowWithOffset: now.add(this.localTimeOffset, "minutes"), + closeToWorkingHours: this.#closeToWorkingHours(now, workingDays), + inWorkingHours: this.#inWorkingHours(now, workingDays), + utcOffset: this.#utcOffset(offset), + members: [member], + }; + groupedTimezones.push(groupedTimezone); + } + } + }); + + groupedTimezones = groupedTimezones + .sortBy("offset") + .filter((g) => g.members.length); + + let newDayIndex; + groupedTimezones.forEach((groupedTimezone, index) => { + if (index > 0) { + if ( + groupedTimezones[index - 1].nowWithOffset.format("dddd") !== + groupedTimezone.nowWithOffset.format("dddd") + ) { + newDayIndex = index; + } + } + }); + + if (newDayIndex) { + groupedTimezones.splice(newDayIndex, 0, { + type: "discourse-group-timezone-new-day", + beforeDate: + groupedTimezones[newDayIndex - 1].nowWithOffset.format("dddd"), + afterDate: groupedTimezones[newDayIndex].nowWithOffset.format("dddd"), + }); + } + + return groupedTimezones; + } + + #shouldAddMemberToGroup(filter, member) { + if (filter) { + filter = filter.toLowerCase(); + if ( + member.username.toLowerCase().indexOf(filter) > -1 || + (member.name && member.name.toLowerCase().indexOf(filter) > -1) + ) { + return true; + } + } else { + return true; + } + + return false; + } + + #roundMoment(date) { + if (this.localTimeOffset) { + date = roundTime(date); + } + + return date; + } + + #closeToWorkingHours(moment, workingDays) { + const hours = moment.hours(); + const startHour = this.siteSettings.working_day_start_hour; + const endHour = this.siteSettings.working_day_end_hour; + const extension = this.siteSettings.close_to_working_day_hours_extension; + + return ( + ((hours >= Math.max(startHour - extension, 0) && hours <= startHour) || + (hours <= Math.min(endHour + extension, 23) && hours >= endHour)) && + workingDays.includes(moment.isoWeekday()) + ); + } + + #inWorkingHours(moment, workingDays) { + const hours = moment.hours(); + return ( + hours > this.siteSettings.working_day_start_hour && + hours < this.siteSettings.working_day_end_hour && + workingDays.includes(moment.isoWeekday()) + ); + } + + #utcOffset(offset) { + const sign = Math.sign(offset) === 1 ? "+" : "-"; + offset = Math.abs(offset); + let hours = Math.floor(offset / 60).toString(); + hours = hours.length === 1 ? `0${hours}` : hours; + let minutes = (offset % 60).toString(); + minutes = minutes.length === 1 ? `:${minutes}0` : `:${minutes}`; + return `${sign}${hours.replace(/^0(\d)/, "$1")}${minutes.replace( + /:00$/, + "" + )}`.replace(/-0/, nbsp); + } + + #workingDays() { + const enMoment = moment().locale("en"); + const getIsoWeekday = (day) => + enMoment.localeData()._weekdays.indexOf(day) || 7; + return this.siteSettings.working_days + .split("|") + .filter(Boolean) + .map((x) => getIsoWeekday(x)); + } + + @action + handleFilterChange(event) { + this.filter = event.target.value; + } + + +} diff --git a/plugins/discourse-calendar/assets/javascripts/discourse/components/group-timezones/new-day.gjs b/plugins/discourse-calendar/assets/javascripts/discourse/components/group-timezones/new-day.gjs new file mode 100644 index 00000000000..121c7cb931d --- /dev/null +++ b/plugins/discourse-calendar/assets/javascripts/discourse/components/group-timezones/new-day.gjs @@ -0,0 +1,16 @@ +import icon from "discourse/helpers/d-icon"; + +const NewDay = ; + +export default NewDay; diff --git a/plugins/discourse-calendar/assets/javascripts/discourse/components/group-timezones/time-traveller.gjs b/plugins/discourse-calendar/assets/javascripts/discourse/components/group-timezones/time-traveller.gjs new file mode 100644 index 00000000000..c6f07176d9d --- /dev/null +++ b/plugins/discourse-calendar/assets/javascripts/discourse/components/group-timezones/time-traveller.gjs @@ -0,0 +1,58 @@ +import Component from "@glimmer/component"; +import { on } from "@ember/modifier"; +import { action } from "@ember/object"; +import { not } from "truth-helpers"; +import DButton from "discourse/components/d-button"; +import roundTime from "../../lib/round-time"; + +export default class TimeTraveller extends Component { + get localTimeWithOffset() { + let date = moment().add(this.args.localTimeOffset, "minutes"); + + if (this.args.localTimeOffset) { + date = roundTime(date); + } + + return date.format("HH:mm"); + } + + @action + reset() { + this.args.setOffset(0); + } + + @action + sliderMoved(event) { + const value = parseInt(event.target.value, 10); + const offset = value * 15; + this.args.setOffset(offset); + } + + +} diff --git a/plugins/discourse-calendar/assets/javascripts/discourse/components/group-timezones/timezone.gjs b/plugins/discourse-calendar/assets/javascripts/discourse/components/group-timezones/timezone.gjs new file mode 100644 index 00000000000..2e1f854a511 --- /dev/null +++ b/plugins/discourse-calendar/assets/javascripts/discourse/components/group-timezones/timezone.gjs @@ -0,0 +1,44 @@ +import Component from "@glimmer/component"; +import UserAvatar from "discourse/components/user-avatar"; +import concatClass from "discourse/helpers/concat-class"; + +export default class GroupTimezone extends Component { + get formattedTime() { + return this.args.groupedTimezone.nowWithOffset.format("LT"); + } + + +} diff --git a/plugins/discourse-calendar/assets/javascripts/discourse/components/modal/post-event-builder.gjs b/plugins/discourse-calendar/assets/javascripts/discourse/components/modal/post-event-builder.gjs new file mode 100644 index 00000000000..f2438c59ea5 --- /dev/null +++ b/plugins/discourse-calendar/assets/javascripts/discourse/components/modal/post-event-builder.gjs @@ -0,0 +1,690 @@ +import Component from "@glimmer/component"; +import { tracked } from "@glimmer/tracking"; +import { Input, Textarea } from "@ember/component"; +import { concat, fn, get } from "@ember/helper"; +import { on } from "@ember/modifier"; +import { action } from "@ember/object"; +import { service } from "@ember/service"; +import { eq } from "truth-helpers"; +import ConditionalLoadingSection from "discourse/components/conditional-loading-section"; +import DButton from "discourse/components/d-button"; +import DModal from "discourse/components/d-modal"; +import DateInput from "discourse/components/date-input"; +import DateTimeInputRange from "discourse/components/date-time-input-range"; +import GroupSelector from "discourse/components/group-selector"; +import PluginOutlet from "discourse/components/plugin-outlet"; +import RadioButton from "discourse/components/radio-button"; +import lazyHash from "discourse/helpers/lazy-hash"; +import { extractError } from "discourse/lib/ajax-error"; +import { cook } from "discourse/lib/text"; +import Group from "discourse/models/group"; +import { i18n } from "discourse-i18n"; +import ComboBox from "select-kit/components/combo-box"; +import TimezoneInput from "select-kit/components/timezone-input"; +import { buildParams, replaceRaw } from "../../lib/raw-event-helper"; +import EventField from "../event-field"; + +export default class PostEventBuilder extends Component { + @service dialog; + @service siteSettings; + @service store; + @service currentUser; + + @tracked flash = null; + @tracked isSaving = false; + + @tracked startsAt = moment(this.event.startsAt).tz( + this.event.timezone || "UTC" + ); + + @tracked + endsAt = + this.event.endsAt && + moment(this.event.endsAt).tz(this.event.timezone || "UTC"); + + get recurrenceUntil() { + return ( + this.event.recurrenceUntil && + moment(this.event.recurrenceUntil).tz(this.event.timezone || "UTC") + ); + } + + get event() { + return this.args.model.event; + } + + get reminderTypes() { + return [ + { + value: "notification", + name: i18n( + "discourse_post_event.builder_modal.reminders.types.notification" + ), + }, + { + value: "bumpTopic", + name: i18n( + "discourse_post_event.builder_modal.reminders.types.bump_topic" + ), + }, + ]; + } + + get reminderUnits() { + return [ + { + value: "minutes", + name: i18n( + "discourse_post_event.builder_modal.reminders.units.minutes" + ), + }, + { + value: "hours", + name: i18n("discourse_post_event.builder_modal.reminders.units.hours"), + }, + { + value: "days", + name: i18n("discourse_post_event.builder_modal.reminders.units.days"), + }, + { + value: "weeks", + name: i18n("discourse_post_event.builder_modal.reminders.units.weeks"), + }, + ]; + } + + get reminderPeriods() { + return [ + { + value: "before", + name: i18n( + "discourse_post_event.builder_modal.reminders.periods.before" + ), + }, + { + value: "after", + name: i18n( + "discourse_post_event.builder_modal.reminders.periods.after" + ), + }, + ]; + } + + get shouldRenderUrl() { + return this.args.model.event.url !== undefined; + } + + get availableRecurrences() { + return [ + { + id: "every_day", + name: i18n("discourse_post_event.builder_modal.recurrence.every_day"), + }, + { + id: "every_month", + name: i18n("discourse_post_event.builder_modal.recurrence.every_month"), + }, + { + id: "every_weekday", + name: i18n( + "discourse_post_event.builder_modal.recurrence.every_weekday" + ), + }, + { + id: "every_week", + name: i18n("discourse_post_event.builder_modal.recurrence.every_week"), + }, + { + id: "every_two_weeks", + name: i18n( + "discourse_post_event.builder_modal.recurrence.every_two_weeks" + ), + }, + { + id: "every_four_weeks", + name: i18n( + "discourse_post_event.builder_modal.recurrence.every_four_weeks" + ), + }, + ]; + } + + get allowedCustomFields() { + return this.siteSettings.discourse_post_event_allowed_custom_fields + .split("|") + .filter(Boolean); + } + + get addReminderDisabled() { + return this.event.reminders?.length >= 5; + } + + get showChat() { + // As of June 2025, chat channel creation is only available to admins and moderators + return ( + this.siteSettings.chat_enabled && + (this.currentUser.admin || this.currentUser.moderator) + ); + } + + @action + groupFinder(term) { + return Group.findAll({ term, ignore_automatic: true }); + } + + @action + setCustomField(field, e) { + this.event.customFields[field] = e.target.value; + } + + @action + onChangeDates(dates) { + this.event.startsAt = dates.from; + this.event.endsAt = dates.to; + this.startsAt = dates.from; + this.endsAt = dates.to; + } + + @action + onChangeStatus(newStatus) { + this.event.rawInvitees = []; + this.event.status = newStatus; + } + + @action + setRecurrence(newRecurrence) { + if (!newRecurrence) { + this.event.recurrence = null; + this.event.recurrenceUntil = null; + return; + } + + this.event.recurrence = newRecurrence; + } + + @action + setRecurrenceUntil(until) { + if (!until) { + this.event.recurrenceUntil = null; + } else { + this.event.recurrenceUntil = moment(until).endOf("day").toDate(); + } + } + + @action + setRawInvitees(_, newInvitees) { + this.event.rawInvitees = newInvitees; + } + + @action + setNewTimezone(newTz) { + this.event.timezone = newTz; + this.event.startsAt = moment.tz( + this.startsAt.format("YYYY-MM-DDTHH:mm"), + newTz + ); + this.event.endsAt = this.endsAt + ? moment.tz(this.endsAt.format("YYYY-MM-DDTHH:mm"), newTz) + : null; + this.startsAt = moment(this.event.startsAt).tz(newTz); + this.endsAt = this.event.endsAt + ? moment(this.event.endsAt).tz(newTz) + : null; + } + + @action + async destroyPostEvent() { + try { + const confirmResult = await this.dialog.yesNoConfirm({ + message: "Confirm delete", + }); + + if (confirmResult) { + const post = await this.store.find("post", this.event.id); + const raw = post.raw; + const newRaw = this._removeRawEvent(raw); + const props = { + raw: newRaw, + edit_reason: "Destroy event", + }; + + const cooked = await cook(newRaw); + props.cooked = cooked.string; + + const result = await post.save(props); + if (result) { + this.args.closeModal(); + } + } + } catch (e) { + this.flash = extractError(e); + } + } + + @action + createEvent() { + if (!this.startsAt) { + this.args.closeModal(); + return; + } + + const eventParams = buildParams( + this.startsAt, + this.endsAt, + this.event, + this.siteSettings + ); + const markdownParams = []; + Object.keys(eventParams).forEach((key) => { + let value = eventParams[key]; + markdownParams.push(`${key}="${value}"`); + }); + + this.args.model.toolbarEvent.addText( + `[event ${markdownParams.join(" ")}]\n[/event]` + ); + this.args.closeModal(); + } + + @action + async updateEvent() { + try { + this.isSaving = true; + + const post = await this.store.find("post", this.event.id); + const raw = post.raw; + const eventParams = buildParams( + this.startsAt, + this.endsAt, + this.event, + this.siteSettings + ); + const newRaw = replaceRaw(eventParams, raw); + if (newRaw) { + const props = { + raw: newRaw, + edit_reason: i18n("discourse_post_event.edit_reason"), + }; + + const cooked = await cook(newRaw); + props.cooked = cooked.string; + + const result = await post.save(props); + if (result) { + this.args.closeModal(); + } + } + } catch (e) { + this.flash = extractError(e); + } finally { + this.isSaving = false; + } + } + + _removeRawEvent(raw) { + const eventRegex = new RegExp(`\\[event\\s(.*?)\\]\\n\\[\\/event\\]`, "m"); + return raw.replace(eventRegex, ""); + } + +