Skip to content

Commit 156794f

Browse files
Send out mail when recurring meeting template completed (opf#17492)
* ICS improvements for meeting occurrences * Add series mailer implementation * Change sequence to lock_version * Set filename to occurrence if present * Refactor occurence to use actual meeting
1 parent 2cb0fc1 commit 156794f

File tree

18 files changed

+433
-37
lines changed

18 files changed

+433
-37
lines changed

modules/meeting/app/components/meetings/header_component.html.erb

+1-1
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@
6868
end
6969

7070
menu.with_item(label: t(:label_icalendar_download),
71-
href: download_ics_meeting_path(@meeting)) do |item|
71+
href: ics_download_path) do |item|
7272
item.with_leading_visual_icon(icon: :download)
7373
end
7474

modules/meeting/app/components/meetings/header_component.rb

+8
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,14 @@ def check_for_updates_interval
5252
10_000
5353
end
5454

55+
def ics_download_path
56+
if @series
57+
download_ics_recurring_meeting_path(@series, occurrence_id: @meeting.id)
58+
else
59+
download_ics_meeting_path(@meeting)
60+
end
61+
end
62+
5563
private
5664

5765
def delete_enabled?

modules/meeting/app/components/recurring_meetings/row_component.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ def ical_action(menu)
163163
return unless instantiated? && !cancelled?
164164

165165
menu.with_item(label: I18n.t(:label_icalendar_download),
166-
href: download_ics_meeting_path(meeting),
166+
href: download_ics_recurring_meeting_path(model.recurring_meeting, occurrence_id: model.id),
167167
content_arguments: {
168168
data: { turbo: false }
169169
}) do |item|

modules/meeting/app/components/recurring_meetings/show_page_header_component.html.erb

+17-3
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,24 @@
3030
},
3131
'aria-label': t(:label_recurring_meeting_series_edit),
3232
test_selector: "edit-meeting-details-button",
33-
)
33+
) do |item|
34+
item.with_leading_visual_icon(icon: :pencil)
35+
end
3436

3537
menu.with_item(
3638
label: t(:label_icalendar_download),
3739
href: download_ics_recurring_meeting_path(@meeting)
38-
)
40+
) do |item|
41+
item.with_leading_visual_icon(icon: :calendar)
42+
end
43+
44+
menu.with_item(
45+
label: t('meeting.label_mail_all_participants'),
46+
href: notify_recurring_meeting_path(@meeting),
47+
form_arguments: { method: :post }
48+
) do |item|
49+
item.with_leading_visual_icon(icon: :mail)
50+
end
3951

4052
menu.with_item(
4153
label: I18n.t(:label_recurring_meeting_series_delete),
@@ -44,7 +56,9 @@
4456
form_arguments: {
4557
method: :delete, data: { confirm: t("text_are_you_sure"), turbo: 'false' }
4658
}
47-
)
59+
) do |item|
60+
item.with_leading_visual_icon(icon: :trash)
61+
end
4862
end
4963
end
5064
end %>

modules/meeting/app/controllers/recurring_meetings_controller.rb

+38-7
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@ class RecurringMeetingsController < ApplicationController
77
include OpTurbo::DialogStreamHelper
88

99
before_action :find_meeting,
10-
only: %i[show update details_dialog destroy edit init delete_scheduled template_completed download_ics]
11-
before_action :find_optional_project, only: %i[index show new create update details_dialog destroy edit delete_scheduled]
10+
only: %i[show update details_dialog destroy edit init
11+
delete_scheduled template_completed download_ics notify]
12+
before_action :find_optional_project,
13+
only: %i[index show new create update details_dialog destroy edit delete_scheduled notify]
1214
before_action :authorize_global, only: %i[index new create]
1315
before_action :authorize, except: %i[index new create]
1416
before_action :get_scheduled_meeting, only: %i[delete_scheduled]
@@ -150,6 +152,7 @@ def template_completed
150152
.call(start_time: @first_occurrence.to_time)
151153

152154
if call.success?
155+
deliver_invitation_mails
153156
flash[:success] = I18n.t("recurring_meeting.occurrence.first_created")
154157
else
155158
flash[:error] = call.message
@@ -168,18 +171,46 @@ def delete_scheduled
168171
redirect_to polymorphic_path([@project, @recurring_meeting]), status: :see_other
169172
end
170173

171-
def download_ics
172-
::RecurringMeetings::ICalService
173-
.new(user: current_user, series: @recurring_meeting)
174-
.call
174+
def download_ics # rubocop:disable Metrics/AbcSize
175+
service = ::RecurringMeetings::ICalService.new(user: current_user, series: @recurring_meeting)
176+
filename, result =
177+
if params[:occurrence_id].present?
178+
occurrence = @recurring_meeting.meetings.find_by(id: params[:occurrence_id])
179+
["#{@recurring_meeting.title} - #{occurrence.start_time.to_date.iso8601}",
180+
service.generate_occurrence(occurrence)]
181+
else
182+
[@recurring_meeting.title, service.generate_series]
183+
end
184+
185+
result
175186
.on_failure { |call| render_500(message: call.message) }
176187
.on_success do |call|
177-
send_data call.result, filename: filename_for_content_disposition("#{@recurring_meeting.title}.ics")
188+
send_data call.result, filename: filename_for_content_disposition("#{filename}.ics")
178189
end
179190
end
180191

192+
def notify
193+
deliver_invitation_mails
194+
flash[:notice] = I18n.t(:notice_successful_notification)
195+
redirect_to action: :show
196+
end
197+
181198
private
182199

200+
def deliver_invitation_mails
201+
@recurring_meeting
202+
.template
203+
.participants
204+
.invited
205+
.find_each do |participant|
206+
MeetingSeriesMailer.template_completed(
207+
@recurring_meeting,
208+
participant.user,
209+
User.current
210+
).deliver_later
211+
end
212+
end
213+
183214
def upcoming_meetings(count:)
184215
meetings = @recurring_meeting
185216
.scheduled_instances(upcoming: true)

modules/meeting/app/mailers/meeting_mailer.rb

+13-3
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,7 @@ def icalendar_notification(meeting, user, _actor, **)
7272

7373
def with_attached_ics(meeting, user)
7474
User.execute_as(user) do
75-
call = ::Meetings::ICalService
76-
.new(user:, meeting: @meeting)
77-
.call
75+
call = ics_service_call(meeting, user)
7876

7977
call.on_success do
8078
attachments["meeting.ics"] = call.result
@@ -88,6 +86,18 @@ def with_attached_ics(meeting, user)
8886
end
8987
end
9088

89+
def ics_service_call(meeting, user)
90+
if meeting.recurring?
91+
::RecurringMeetings::ICalService
92+
.new(user:, series: meeting.recurring_meeting)
93+
.generate_occurrence(meeting.scheduled_meeting)
94+
else
95+
::Meetings::ICalService
96+
.new(user:, meeting:)
97+
.call
98+
end
99+
end
100+
91101
def set_headers(meeting)
92102
open_project_headers "Project" => meeting.project.identifier, "Meeting-Id" => meeting.id
93103
headers["Content-Type"] = 'text/calendar; charset=utf-8; method="PUBLISH"; name="meeting.ics"'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
#-- copyright
2+
# OpenProject is an open source project management software.
3+
# Copyright (C) the OpenProject GmbH
4+
#
5+
# This program is free software; you can redistribute it and/or
6+
# modify it under the terms of the GNU General Public License version 3.
7+
#
8+
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
9+
# Copyright (C) 2006-2013 Jean-Philippe Lang
10+
# Copyright (C) 2010-2013 the ChiliProject Team
11+
#
12+
# This program is free software; you can redistribute it and/or
13+
# modify it under the terms of the GNU General Public License
14+
# as published by the Free Software Foundation; either version 2
15+
# of the License, or (at your option) any later version.
16+
#
17+
# This program is distributed in the hope that it will be useful,
18+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
19+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20+
# GNU General Public License for more details.
21+
#
22+
# You should have received a copy of the GNU General Public License
23+
# along with this program; if not, write to the Free Software
24+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
25+
#
26+
# See COPYRIGHT and LICENSE files for more details.
27+
#++
28+
29+
class MeetingSeriesMailer < UserMailer
30+
def template_completed(series, user, actor)
31+
@actor = actor
32+
@series = series
33+
@template = series.template
34+
@next_occurrence = series.next_occurrence&.to_time
35+
@user = user
36+
37+
set_headers(series)
38+
39+
with_attached_ics(series, user) do
40+
subject = I18n.t("meeting.email.series.title", title: series.title, project_name: series.project.name)
41+
mail(to: user, subject:)
42+
end
43+
end
44+
45+
private
46+
47+
def with_attached_ics(series, user)
48+
User.execute_as(user) do
49+
call = ::RecurringMeetings::ICalService
50+
.new(user:, series: series)
51+
.generate_series
52+
53+
call.on_success do
54+
attachments["meeting.ics"] = call.result
55+
56+
yield
57+
end
58+
59+
call.on_failure do
60+
Rails.logger.error { "Failed to create ICS attachment for meeting #{series.id}: #{call.message}" }
61+
end
62+
end
63+
end
64+
65+
def set_headers(series)
66+
open_project_headers "Project" => series.project.identifier, "Meeting-Id" => series.id
67+
headers["Content-Type"] = 'text/calendar; charset=utf-8; method="PUBLISH"; name="meeting.ics"'
68+
headers["Content-Transfer-Encoding"] = "8bit"
69+
end
70+
end

modules/meeting/app/models/meeting.rb

+4
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,10 @@ class Meeting < ApplicationRecord
117117
closed: 5
118118
}
119119

120+
def recurring?
121+
recurring_meeting_id.present?
122+
end
123+
120124
##
121125
# Cache key for detecting changes to be shown to the user
122126
def changed_hash

modules/meeting/app/services/recurring_meetings/ical_service.rb

+31-16
Original file line numberDiff line numberDiff line change
@@ -43,23 +43,45 @@ class ICalService
4343
def initialize(series:, user:)
4444
@user = user
4545
@series = series
46+
@schedule = series.schedule
4647
@url_helpers = OpenProject::StaticRouting::StaticUrlHelpers.new
4748
end
4849

49-
def call # rubocop:disable Metrics/AbcSize
50-
User.execute_as(user) do
51-
@timezone = Time.zone || Time.zone_default
52-
@calendar = build_icalendar(series.start_time)
53-
@schedule = series.schedule
54-
ServiceResult.success(result: generate_ical)
50+
def generate_series
51+
ical_result(series) do
52+
series_event
53+
occurrences_events
5554
end
5655
rescue StandardError => e
57-
Rails.logger.error("Failed to generate ICS for meeting series #{@series.id}: #{e.message}")
56+
Rails.logger.error("Failed to generate ICS for meeting series #{series.id}: #{e.message}")
57+
ServiceResult.failure(message: e.message)
58+
end
59+
60+
def generate_occurrence(meeting)
61+
# Get the time the meeting was scheduled to take place
62+
scheduled_meeting = meeting.scheduled_meeting
63+
ical_result(scheduled_meeting) do
64+
occurrence_event(scheduled_meeting.start_time, meeting)
65+
end
66+
rescue StandardError => e
67+
Rails.logger.error("Failed to generate ICS for meeting #{meeting.id}: #{e.message}")
5868
ServiceResult.failure(message: e.message)
5969
end
6070

6171
private
6272

73+
def ical_result(meeting)
74+
User.execute_as(user) do
75+
@timezone = Time.zone || Time.zone_default
76+
@calendar = build_icalendar(meeting.start_time)
77+
78+
yield
79+
80+
calendar.publish
81+
ServiceResult.success(result: calendar.to_ical)
82+
end
83+
end
84+
6385
def tzinfo
6486
timezone.tzinfo
6587
end
@@ -68,18 +90,11 @@ def tzid
6890
tzinfo.canonical_identifier
6991
end
7092

71-
def generate_ical
72-
series_event
73-
occurrences_events
74-
75-
calendar.publish
76-
calendar.to_ical
77-
end
78-
7993
def series_event # rubocop:disable Metrics/AbcSize
8094
calendar.event do |e|
8195
base_series_attributes(e)
8296

97+
e.rrule = schedule.rrules.first.to_ical # We currently only have one recurrence rule
8398
e.dtstart = ical_datetime template.start_time, tzid
8499
e.dtend = ical_datetime template.end_time, tzid
85100
e.url = url_helpers.project_recurring_meeting_url(series.project, series)
@@ -105,6 +120,7 @@ def occurrence_event(schedule_start_time, meeting) # rubocop:disable Metrics/Abc
105120
e.dtend = ical_datetime meeting.end_time, tzid
106121
e.url = url_helpers.project_meeting_url(meeting.project, meeting)
107122
e.location = meeting.location.presence
123+
e.sequence = meeting.lock_version
108124

109125
add_attendees(e, meeting)
110126
end
@@ -120,7 +136,6 @@ def upcoming_instantiated_schedules
120136

121137
def base_series_attributes(event) # rubocop:disable Metrics/AbcSize
122138
event.uid = ical_uid("meeting-series-#{series.id}")
123-
event.rrule = schedule.rrules.first.to_ical # We currently only have one recurrence rule
124139
event.summary = "[#{series.project.name}] #{series.title}"
125140
event.description = "[#{series.project.name}] #{I18n.t(:label_meeting_series)}: #{series.title}"
126141
event.organizer = ical_organizer(series)

0 commit comments

Comments
 (0)