From d92edaa3b56bc175f1ffa71733053b5714f43ae0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Thu, 5 Dec 2024 11:30:45 +0100 Subject: [PATCH 1/3] Add ICS action --- .../show_page_header_component.html.erb | 6 + .../recurring_meetings_controller.rb | 12 +- .../meeting/app/models/recurring_meeting.rb | 2 +- .../meeting/app/models/scheduled_meeting.rb | 3 + .../app/services/meetings/ical_helpers.rb | 76 +++++++++ .../app/services/meetings/ical_service.rb | 48 +----- .../recurring_meetings/ical_service.rb | 137 ++++++++++++++++ modules/meeting/config/routes.rb | 1 + .../lib/open_project/meeting/engine.rb | 2 +- .../factories/recurring_meeting_factory.rb | 5 +- .../factories/scheduled_meeting_factory.rb | 11 +- .../recurring_meetings/ical_service_spec.rb | 148 ++++++++++++++++++ 12 files changed, 402 insertions(+), 49 deletions(-) create mode 100644 modules/meeting/app/services/meetings/ical_helpers.rb create mode 100644 modules/meeting/app/services/recurring_meetings/ical_service.rb create mode 100644 modules/meeting/spec/services/recurring_meetings/ical_service_spec.rb diff --git a/modules/meeting/app/components/recurring_meetings/show_page_header_component.html.erb b/modules/meeting/app/components/recurring_meetings/show_page_header_component.html.erb index 93121f0e7d07..998c20bc9bc9 100644 --- a/modules/meeting/app/components/recurring_meetings/show_page_header_component.html.erb +++ b/modules/meeting/app/components/recurring_meetings/show_page_header_component.html.erb @@ -31,6 +31,12 @@ 'aria-label': t(:label_recurring_meeting_series_edit), test_selector: "edit-meeting-details-button", ) + + menu.with_item( + label: t(:label_icalendar_download), + href: download_ics_recurring_meeting_path(@meeting) + ) + menu.with_item( label: I18n.t(:label_recurring_meeting_series_delete), href: polymorphic_path([@project, @meeting]), diff --git a/modules/meeting/app/controllers/recurring_meetings_controller.rb b/modules/meeting/app/controllers/recurring_meetings_controller.rb index 87e44a178686..eab8d64e4788 100644 --- a/modules/meeting/app/controllers/recurring_meetings_controller.rb +++ b/modules/meeting/app/controllers/recurring_meetings_controller.rb @@ -5,7 +5,7 @@ class RecurringMeetingsController < ApplicationController include OpTurbo::FlashStreamHelper include OpTurbo::DialogStreamHelper - before_action :find_meeting, only: %i[show update details_dialog destroy edit init delete_scheduled template_completed] + before_action :find_meeting, only: %i[show update details_dialog destroy edit init delete_scheduled template_completed download_ics] before_action :find_optional_project, only: %i[index show new create update details_dialog destroy edit delete_scheduled] before_action :authorize_global, only: %i[index new create] before_action :authorize, except: %i[index new create] @@ -167,6 +167,16 @@ def delete_scheduled redirect_to polymorphic_path([@project, @recurring_meeting]), status: :see_other end + def download_ics + ::RecurringMeetings::ICalService + .new(user: current_user, series: @recurring_meeting) + .call + .on_failure { |call| render_500(message: call.message) } + .on_success do |call| + send_data call.result, filename: filename_for_content_disposition("#{@recurring_meeting.title}.ics") + end + end + private def upcoming_meetings diff --git a/modules/meeting/app/models/recurring_meeting.rb b/modules/meeting/app/models/recurring_meeting.rb index a93e1bf1fc9a..094a5d1a0060 100644 --- a/modules/meeting/app/models/recurring_meeting.rb +++ b/modules/meeting/app/models/recurring_meeting.rb @@ -180,7 +180,7 @@ def count_rule(rule) if end_after_iterations? rule.count(iterations) else - rule.until(end_date) + rule.until(end_date.to_time(:utc)) end end end diff --git a/modules/meeting/app/models/scheduled_meeting.rb b/modules/meeting/app/models/scheduled_meeting.rb index 1b11cce49b07..39cd83603be5 100644 --- a/modules/meeting/app/models/scheduled_meeting.rb +++ b/modules/meeting/app/models/scheduled_meeting.rb @@ -33,6 +33,9 @@ class ScheduledMeeting < ApplicationRecord scope :upcoming, -> { where(start_time: Time.current..) } scope :past, -> { where(start_time: ...Time.current) } + scope :instantiated, -> { where.not(meeting_id: nil) } + scope :not_instantiated, -> { where(meeting_id: nil) } + scope :cancelled, -> { where(cancelled: true) } scope :not_cancelled, -> { where(cancelled: false) } diff --git a/modules/meeting/app/services/meetings/ical_helpers.rb b/modules/meeting/app/services/meetings/ical_helpers.rb new file mode 100644 index 000000000000..feebf05e86b2 --- /dev/null +++ b/modules/meeting/app/services/meetings/ical_helpers.rb @@ -0,0 +1,76 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ +require "icalendar" +require "icalendar/tzinfo" + +module Meetings + module ICalHelpers + def ical_event(start_time, &) + calendar = build_icalendar(start_time) + calendar.event(&) + calendar.publish + calendar.to_ical + end + + def build_icalendar(start_time) + ::Icalendar::Calendar.new.tap do |calendar| + ical_timezone = timezone.tzinfo.ical_timezone start_time + calendar.add_timezone ical_timezone + end + end + + def add_attendees(event, meeting) + meeting.participants.includes(:user).find_each do |participant| + user = participant.user + next unless user + + address = Icalendar::Values::CalAddress.new( + "mailto:#{user.mail}", + { + "CN" => user.name, + "EMAIL" => user.mail, + "PARTSTAT" => "NEEDS-ACTION", + "RSVP" => "TRUE", + "CUTYPE" => "INDIVIDUAL", + "ROLE" => "REQ-PARTICIPANT" + } + ) + + event.append_attendee(address) + end + end + + def ical_datetime(time, timezone_id) + Icalendar::Values::DateTime.new time.in_time_zone(timezone_id), "tzid" => timezone_id + end + + def ical_organizer(meeting) + Icalendar::Values::CalAddress.new("mailto:#{meeting.author.mail}", cn: meeting.author.name) + end + end +end diff --git a/modules/meeting/app/services/meetings/ical_service.rb b/modules/meeting/app/services/meetings/ical_service.rb index 565b00599662..dbdaa99958ec 100644 --- a/modules/meeting/app/services/meetings/ical_service.rb +++ b/modules/meeting/app/services/meetings/ical_service.rb @@ -30,6 +30,7 @@ module Meetings class ICalService + include ICalHelpers attr_reader :user, :meeting, :timezone, :url_helpers def initialize(meeting:, user:) @@ -52,7 +53,7 @@ def call # rubocop:disable Metrics/AbcSize def generate_ical - ical_event do |e| + ical_event(meeting.start_time) do |e| tzinfo = timezone.tzinfo tzid = tzinfo.canonical_identifier @@ -62,57 +63,16 @@ def generate_ical e.summary = "[#{meeting.project.name}] #{meeting.title}" e.description = ical_subject e.uid = "#{meeting.id}@#{meeting.project.identifier}" - e.organizer = ical_organizer + e.organizer = ical_organizer(meeting) e.location = meeting.location.presence - add_attendees(e) + add_attendees(e, meeting) end end # rubocop:enable Metrics/AbcSize - def ical_event(&) - calendar = ::Icalendar::Calendar.new - - ical_timezone = @timezone.tzinfo.ical_timezone meeting.start_time - calendar.add_timezone ical_timezone - - calendar.event(&) - - calendar.publish - - calendar.to_ical - end - - def add_attendees(event) - meeting.participants.includes(:user).find_each do |participant| - user = participant.user - next unless user - - address = Icalendar::Values::CalAddress.new( - "mailto:#{user.mail}", - { - "CN" => user.name, - "PARTSTAT" => "NEEDS-ACTION", - "RSVP" => "TRUE", - "CUTYPE" => "INDIVIDUAL", - "ROLE" => "REQ-PARTICIPANT" - } - ) - - event.append_attendee(address) - end - end - def ical_subject "[#{meeting.project.name}] #{I18n.t(:label_meeting)}: #{meeting.title}" end - - def ical_datetime(time, timezone_id) - Icalendar::Values::DateTime.new time.in_time_zone(timezone_id), "tzid" => timezone_id - end - - def ical_organizer - Icalendar::Values::CalAddress.new("mailto:#{meeting.author.mail}", cn: meeting.author.name) - end end end diff --git a/modules/meeting/app/services/recurring_meetings/ical_service.rb b/modules/meeting/app/services/recurring_meetings/ical_service.rb new file mode 100644 index 000000000000..3c6583858706 --- /dev/null +++ b/modules/meeting/app/services/recurring_meetings/ical_service.rb @@ -0,0 +1,137 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ +require "icalendar" +require "icalendar/tzinfo" + +module RecurringMeetings + class ICalService + include ::Meetings::ICalHelpers + attr_reader :user, + :series, + :schedule, + :timezone, + :calendar, + :url_helpers + + delegate :template, to: :series + + def initialize(series:, user:) + @user = user + @series = series + @url_helpers = OpenProject::StaticRouting::StaticUrlHelpers.new + end + + def call # rubocop:disable Metrics/AbcSize + User.execute_as(user) do + @timezone = Time.zone || Time.zone_default + @calendar = build_icalendar(series.start_time) + @schedule = series.schedule + ServiceResult.success(result: generate_ical) + end + rescue StandardError => e + Rails.logger.error("Failed to generate ICS for meeting series #{@series.id}: #{e.message}") + ServiceResult.failure(message: e.message) + end + + private + + def tzinfo + timezone.tzinfo + end + + def tzid + tzinfo.canonical_identifier + end + + def generate_ical + series_event + occurrences_events + + calendar.publish + calendar.to_ical + end + + def series_event # rubocop:disable Metrics/AbcSize + calendar.event do |e| + base_series_attributes(e) + + e.dtstart = ical_datetime template.start_time, tzid + e.dtend = ical_datetime template.end_time, tzid + e.url = url_helpers.project_recurring_meeting_url(series.project, series) + e.location = template.location.presence + + add_attendees(e, template) + e.exdate = cancelled_schedules + end + end + + def occurrences_events + upcoming_instantiated_schedules.find_each do |schedule| + occurrence_event(schedule.start_time, schedule.meeting) + end + end + + def occurrence_event(schedule_start_time, meeting) # rubocop:disable Metrics/AbcSize + calendar.event do |e| + base_series_attributes(e) + + e.recurrence_id = ical_datetime schedule_start_time, tzid + e.dtstart = ical_datetime meeting.start_time, tzid + e.dtend = ical_datetime meeting.end_time, tzid + e.url = url_helpers.project_meeting_url(meeting.project, meeting) + e.location = meeting.location.presence + + add_attendees(e, meeting) + end + end + + def upcoming_instantiated_schedules + series + .scheduled_meetings + .not_cancelled + .instantiated + .includes(:meeting) + end + + def base_series_attributes(event) # rubocop:disable Metrics/AbcSize + event.uid = "#{Setting.host_name}-meeting-series-#{series.id}" + event.rrule = schedule.rrules.first.to_ical # We currently only have one recurrence rule + event.summary = "[#{series.project.name}] #{series.title}" + event.description = "[#{series.project.name}] #{I18n.t(:label_meeting_series)}: #{series.title}" + event.organizer = ical_organizer(series) + end + + def cancelled_schedules + series + .scheduled_meetings + .cancelled + .pluck(:start_time) + .map { |time| Icalendar::Values::DateTime.new time.in_time_zone(tzid), "tzid" => tzid } + end + end +end diff --git a/modules/meeting/config/routes.rb b/modules/meeting/config/routes.rb index cde421953dc2..70dda4a74169 100644 --- a/modules/meeting/config/routes.rb +++ b/modules/meeting/config/routes.rb @@ -59,6 +59,7 @@ resources :recurring_meetings do member do get :details_dialog + get :download_ics post :init post :delete_scheduled post :template_completed diff --git a/modules/meeting/lib/open_project/meeting/engine.rb b/modules/meeting/lib/open_project/meeting/engine.rb index a822c4e11b5f..eb36687dfc38 100644 --- a/modules/meeting/lib/open_project/meeting/engine.rb +++ b/modules/meeting/lib/open_project/meeting/engine.rb @@ -45,7 +45,7 @@ class Engine < ::Rails::Engine meeting_minutes: %i[history show diff], "meetings/menus": %i[show], work_package_meetings_tab: %i[index count], - recurring_meetings: %i[index show new create] }, + recurring_meetings: %i[index show new create download_ics] }, permissible_on: :project permission :create_meetings, { diff --git a/modules/meeting/spec/factories/recurring_meeting_factory.rb b/modules/meeting/spec/factories/recurring_meeting_factory.rb index 770f69bb6515..3cc41f03f3a0 100644 --- a/modules/meeting/spec/factories/recurring_meeting_factory.rb +++ b/modules/meeting/spec/factories/recurring_meeting_factory.rb @@ -46,7 +46,10 @@ recurring_meeting.project = project # create template - template = create(:structured_meeting_template, recurring_meeting:, project:) + template = create(:structured_meeting_template, + author: recurring_meeting.author, + recurring_meeting:, + project:) # create agenda item create(:meeting_agenda_item, meeting: template, title: "My template item") diff --git a/modules/meeting/spec/factories/scheduled_meeting_factory.rb b/modules/meeting/spec/factories/scheduled_meeting_factory.rb index b963ce8333cd..d5c20b0be9df 100644 --- a/modules/meeting/spec/factories/scheduled_meeting_factory.rb +++ b/modules/meeting/spec/factories/scheduled_meeting_factory.rb @@ -40,7 +40,16 @@ end trait :persisted do - meeting factory: :structured_meeting + transient do + meeting_start_time { nil } + end + + after(:build) do |schedule, evaluator| + schedule.meeting = build(:meeting, + recurring_meeting: schedule.recurring_meeting, + start_time: evaluator.meeting_start_time || schedule.start_time, + project: schedule.recurring_meeting.project) + end end end end diff --git a/modules/meeting/spec/services/recurring_meetings/ical_service_spec.rb b/modules/meeting/spec/services/recurring_meetings/ical_service_spec.rb new file mode 100644 index 000000000000..56aefc77d51e --- /dev/null +++ b/modules/meeting/spec/services/recurring_meetings/ical_service_spec.rb @@ -0,0 +1,148 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +require "spec_helper" + +RSpec.describe RecurringMeetings::ICalService, type: :model do # rubocop:disable RSpec/SpecFilePathFormat + shared_let(:user) do + create(:user, firstname: "Bob", lastname: "Barker", + mail: "bob@example.com", preferences: { time_zone: "America/New_York" }) + end + shared_let(:user2) { create(:user, firstname: "Foo", lastname: "Fooer", mail: "foo@example.com") } + shared_let(:project) { create(:project, name: "My Project", identifier: "my-project") } + + shared_let(:series) do + create(:recurring_meeting, + author: user, + project:, + title: "Weekly", + frequency: "weekly", + start_time: DateTime.parse("2024-12-01T10:00:00Z"), + end_date: "2025-12-01") + end + + let(:template) { series.template } + let(:service) { described_class.new(user:, series:) } + let(:result) { service.call.result } + + let(:parsed_ical) { Icalendar::Calendar.parse(result).first } + let(:parsed_events) { parsed_ical.events } + + let(:series_event) { parsed_events.detect { |evt| evt.recurrence_id.nil? } } + let(:series_ical) { series_event.to_ical } + + let(:standard_zone) { Icalendar::Calendar.parse(result).first.timezones.first.standards.first } + let(:daylight_zone) { Icalendar::Calendar.parse(result).first.timezones.first.daylights.first } + + before do + template.update!( + location: "https://example.com/meet/important-meeting", + duration: 1.5 + ) + template.participants << MeetingParticipant.new(user:) + template.participants << MeetingParticipant.new(user: user2) + end + + describe "exported series" do + it "contains serise and template information" do + expect(parsed_events.count).to eq(1) + expect(series_ical).to include("LOCATION:https://example.com/meet/important-meeting") + expect(series_ical).to include("SUMMARY:[My Project] Weekly") + expect(series_ical).to include("UID:#{Setting.host_name}-meeting-series-#{series.id}") + expect(series_ical).to include("ATTENDEE;CN=Bob Barker;EMAIL=bob@example.com;PARTSTAT=NEEDS-ACTION;RSVP=TRU") + expect(series_ical).to include("ATTENDEE;CN=Foo Fooer;EMAIL=foo@example.com;PARTSTAT=NEEDS-ACTION;RSVP=TRUE") + end + end + + describe "cancelled schedules" do + shared_let(:cancelled_schedule1) do + create(:scheduled_meeting, + :cancelled, + recurring_meeting: series, + start_time: DateTime.parse("2024-12-08T10:00:00Z")) + end + + shared_let(:cancelled_schedule2) do + create(:scheduled_meeting, + :cancelled, + recurring_meeting: series, + start_time: DateTime.parse("2024-12-24T10:00:00Z")) + end + + it "excludes them as EXDATE", :aggregate_failures do + expect(parsed_events.count).to eq(1) + expect(series_ical).to include("EXDATE;TZID=America/New_York:20241208T050000") + expect(series_ical).to include("EXDATE;TZID=America/New_York:20241224T050000") + end + end + + describe "instantiated schedules" do + shared_let(:schedule) do + create(:scheduled_meeting, + :persisted, + recurring_meeting: series, + start_time: DateTime.parse("2024-12-08T10:00:00Z")) + end + + shared_let(:schedule2) do + create(:scheduled_meeting, + :persisted, + recurring_meeting: series, + start_time: DateTime.parse("2024-12-08T10:00:00Z") + 10.weeks) + end + + shared_let(:moved_schedule) do + create(:scheduled_meeting, + :persisted, + recurring_meeting: series, + start_time: DateTime.parse("2024-12-15T10:00:00Z"), + meeting_start_time: DateTime.parse("2024-12-16T11:30:00Z")) + end + + it "creates additional events", :aggregate_failures do + expect(parsed_events.count).to eq(4) + + first = parsed_events.detect { |evt| evt.recurrence_id == schedule.start_time }.to_ical + second = parsed_events.detect { |evt| evt.recurrence_id == schedule2.start_time }.to_ical + # Moved schedule still has the original start time as recurrence id + moved = parsed_events.detect { |evt| evt.recurrence_id == moved_schedule.start_time }.to_ical + + expect(first).to include("DTSTART;TZID=America/New_York:20241208T050000") + expect(first).to include("DTEND;TZID=America/New_York:20241208T060000") + expect(first).to include("URL:http://#{Setting.host_name}/projects/my-project/meetings/#{schedule.meeting_id}") + + expect(second).to include("DTSTART;TZID=America/New_York:20250216T050000") + expect(second).to include("DTEND;TZID=America/New_York:20250216T060000") + expect(second).to include("URL:http://#{Setting.host_name}/projects/my-project/meetings/#{schedule2.meeting_id}") + + expect(moved).to include("DTSTART;TZID=America/New_York:20241216T063000") + expect(moved).to include("DTEND;TZID=America/New_York:20241216T073000") + expect(moved).to include("URL:http://#{Setting.host_name}/projects/my-project/meetings/#{moved_schedule.meeting_id}") + end + end +end From 0d09c400fa969703c2988803ca8448e4b92c6f92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Thu, 5 Dec 2024 20:32:42 +0100 Subject: [PATCH 2/3] Add better uid and prodid --- modules/meeting/app/services/meetings/ical_helpers.rb | 5 +++++ .../meeting/app/services/recurring_meetings/ical_service.rb | 2 +- .../spec/services/recurring_meetings/ical_service_spec.rb | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/modules/meeting/app/services/meetings/ical_helpers.rb b/modules/meeting/app/services/meetings/ical_helpers.rb index feebf05e86b2..9812c612a18c 100644 --- a/modules/meeting/app/services/meetings/ical_helpers.rb +++ b/modules/meeting/app/services/meetings/ical_helpers.rb @@ -39,6 +39,7 @@ def ical_event(start_time, &) def build_icalendar(start_time) ::Icalendar::Calendar.new.tap do |calendar| + calendar.prodid = "-//OpenProject GmbH//#{OpenProject::VERSION}//Meeting//EN" ical_timezone = timezone.tzinfo.ical_timezone start_time calendar.add_timezone ical_timezone end @@ -65,6 +66,10 @@ def add_attendees(event, meeting) end end + def ical_uid(suffix) + "#{Setting.app_title}-#{Setting.host_name}-#{suffix}".dasherize + end + def ical_datetime(time, timezone_id) Icalendar::Values::DateTime.new time.in_time_zone(timezone_id), "tzid" => timezone_id end diff --git a/modules/meeting/app/services/recurring_meetings/ical_service.rb b/modules/meeting/app/services/recurring_meetings/ical_service.rb index 3c6583858706..b68633d16b4e 100644 --- a/modules/meeting/app/services/recurring_meetings/ical_service.rb +++ b/modules/meeting/app/services/recurring_meetings/ical_service.rb @@ -119,7 +119,7 @@ def upcoming_instantiated_schedules end def base_series_attributes(event) # rubocop:disable Metrics/AbcSize - event.uid = "#{Setting.host_name}-meeting-series-#{series.id}" + event.uid = ical_uid("meeting-series-#{series.id}") event.rrule = schedule.rrules.first.to_ical # We currently only have one recurrence rule event.summary = "[#{series.project.name}] #{series.title}" event.description = "[#{series.project.name}] #{I18n.t(:label_meeting_series)}: #{series.title}" diff --git a/modules/meeting/spec/services/recurring_meetings/ical_service_spec.rb b/modules/meeting/spec/services/recurring_meetings/ical_service_spec.rb index 56aefc77d51e..e8554a1c631b 100644 --- a/modules/meeting/spec/services/recurring_meetings/ical_service_spec.rb +++ b/modules/meeting/spec/services/recurring_meetings/ical_service_spec.rb @@ -73,7 +73,7 @@ expect(parsed_events.count).to eq(1) expect(series_ical).to include("LOCATION:https://example.com/meet/important-meeting") expect(series_ical).to include("SUMMARY:[My Project] Weekly") - expect(series_ical).to include("UID:#{Setting.host_name}-meeting-series-#{series.id}") + expect(series_ical).to include("UID:#{Setting.app_title}-#{Setting.host_name}-meeting-series-#{series.id}") expect(series_ical).to include("ATTENDEE;CN=Bob Barker;EMAIL=bob@example.com;PARTSTAT=NEEDS-ACTION;RSVP=TRU") expect(series_ical).to include("ATTENDEE;CN=Foo Fooer;EMAIL=foo@example.com;PARTSTAT=NEEDS-ACTION;RSVP=TRUE") end From 8f169ae6d3b0bd4cb1bd7280ae76b803f7b3e727 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Fri, 6 Dec 2024 08:06:44 +0100 Subject: [PATCH 3/3] Adapt specs to changed end_time end_time is now end_date start of day, so excluding any meetings on that day --- .../app/controllers/recurring_meetings_controller.rb | 3 ++- .../meeting/spec/factories/recurring_meeting_factory.rb | 6 +++--- modules/meeting/spec/models/recurring_meeting_spec.rb | 7 +++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/modules/meeting/app/controllers/recurring_meetings_controller.rb b/modules/meeting/app/controllers/recurring_meetings_controller.rb index eab8d64e4788..258b5d45796d 100644 --- a/modules/meeting/app/controllers/recurring_meetings_controller.rb +++ b/modules/meeting/app/controllers/recurring_meetings_controller.rb @@ -5,7 +5,8 @@ class RecurringMeetingsController < ApplicationController include OpTurbo::FlashStreamHelper include OpTurbo::DialogStreamHelper - before_action :find_meeting, only: %i[show update details_dialog destroy edit init delete_scheduled template_completed download_ics] + before_action :find_meeting, + only: %i[show update details_dialog destroy edit init delete_scheduled template_completed download_ics] before_action :find_optional_project, only: %i[index show new create update details_dialog destroy edit delete_scheduled] before_action :authorize_global, only: %i[index new create] before_action :authorize, except: %i[index new create] diff --git a/modules/meeting/spec/factories/recurring_meeting_factory.rb b/modules/meeting/spec/factories/recurring_meeting_factory.rb index 3cc41f03f3a0..225fa3841890 100644 --- a/modules/meeting/spec/factories/recurring_meeting_factory.rb +++ b/modules/meeting/spec/factories/recurring_meeting_factory.rb @@ -47,9 +47,9 @@ # create template template = create(:structured_meeting_template, - author: recurring_meeting.author, - recurring_meeting:, - project:) + author: recurring_meeting.author, + recurring_meeting:, + project:) # create agenda item create(:meeting_agenda_item, meeting: template, title: "My template item") diff --git a/modules/meeting/spec/models/recurring_meeting_spec.rb b/modules/meeting/spec/models/recurring_meeting_spec.rb index 415b9c9c2472..0f78aa5ca9b2 100644 --- a/modules/meeting/spec/models/recurring_meeting_spec.rb +++ b/modules/meeting/spec/models/recurring_meeting_spec.rb @@ -38,7 +38,7 @@ it "schedules daily", :aggregate_failures do expect(subject.first_occurrence).to eq Time.zone.tomorrow + 10.hours - expect(subject.last_occurrence).to eq Time.zone.tomorrow + 1.week + 10.hours + expect(subject.last_occurrence).to eq Time.zone.tomorrow + 6.days + 10.hours occurrence_in_two_days = Time.zone.today + 2.days + 10.hours Timecop.freeze(Time.zone.tomorrow + 11.hours) do @@ -106,7 +106,7 @@ it "schedules weekly", :aggregate_failures do expect(subject.first_occurrence).to eq Time.zone.tomorrow + 10.hours - expect(subject.last_occurrence).to eq Time.zone.tomorrow + 4.weeks + 10.hours + expect(subject.last_occurrence).to eq Time.zone.tomorrow + 3.weeks + 10.hours following_occurrence = Time.zone.tomorrow + 7.days + 10.hours Timecop.freeze(Time.zone.tomorrow + 11.hours) do @@ -118,8 +118,7 @@ Time.zone.tomorrow + 10.hours, Time.zone.tomorrow + 7.days + 10.hours, Time.zone.tomorrow + 14.days + 10.hours, - Time.zone.tomorrow + 21.days + 10.hours, - Time.zone.tomorrow + 28.days + 10.hours + Time.zone.tomorrow + 21.days + 10.hours ] Timecop.freeze(Time.zone.tomorrow + 5.weeks) do