Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Send out mail when recurring meeting template completed #17492

Merged
merged 5 commits into from
Jan 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@
end

menu.with_item(label: t(:label_icalendar_download),
href: download_ics_meeting_path(@meeting)) do |item|
href: ics_download_path) do |item|
item.with_leading_visual_icon(icon: :download)
end

Expand Down
8 changes: 8 additions & 0 deletions modules/meeting/app/components/meetings/header_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,14 @@ def check_for_updates_interval
10_000
end

def ics_download_path
if @series
download_ics_recurring_meeting_path(@series, occurrence_id: @meeting.id)
else
download_ics_meeting_path(@meeting)
end
end

private

def delete_enabled?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ def ical_action(menu)
return unless instantiated? && !cancelled?

menu.with_item(label: I18n.t(:label_icalendar_download),
href: download_ics_meeting_path(meeting),
href: download_ics_recurring_meeting_path(model.recurring_meeting, occurrence_id: model.id),
content_arguments: {
data: { turbo: false }
}) do |item|
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,24 @@
},
'aria-label': t(:label_recurring_meeting_series_edit),
test_selector: "edit-meeting-details-button",
)
) do |item|
item.with_leading_visual_icon(icon: :pencil)
end

menu.with_item(
label: t(:label_icalendar_download),
href: download_ics_recurring_meeting_path(@meeting)
)
) do |item|
item.with_leading_visual_icon(icon: :calendar)
end

menu.with_item(
label: t('meeting.label_mail_all_participants'),
href: notify_recurring_meeting_path(@meeting),
form_arguments: { method: :post }
) do |item|
item.with_leading_visual_icon(icon: :mail)
end

menu.with_item(
label: I18n.t(:label_recurring_meeting_series_delete),
Expand All @@ -44,7 +56,9 @@
form_arguments: {
method: :delete, data: { confirm: t("text_are_you_sure"), turbo: 'false' }
}
)
) do |item|
item.with_leading_visual_icon(icon: :trash)
end
end
end
end %>
45 changes: 38 additions & 7 deletions modules/meeting/app/controllers/recurring_meetings_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ class RecurringMeetingsController < ApplicationController
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_optional_project, only: %i[index show new create update details_dialog destroy edit delete_scheduled]
only: %i[show update details_dialog destroy edit init
delete_scheduled template_completed download_ics notify]
before_action :find_optional_project,
only: %i[index show new create update details_dialog destroy edit delete_scheduled notify]
before_action :authorize_global, only: %i[index new create]
before_action :authorize, except: %i[index new create]
before_action :get_scheduled_meeting, only: %i[delete_scheduled]
Expand Down Expand Up @@ -150,6 +152,7 @@ def template_completed
.call(start_time: @first_occurrence.to_time)

if call.success?
deliver_invitation_mails
flash[:success] = I18n.t("recurring_meeting.occurrence.first_created")
else
flash[:error] = call.message
Expand All @@ -168,18 +171,46 @@ 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
def download_ics # rubocop:disable Metrics/AbcSize
service = ::RecurringMeetings::ICalService.new(user: current_user, series: @recurring_meeting)
filename, result =
if params[:occurrence_id].present?
occurrence = @recurring_meeting.meetings.find_by(id: params[:occurrence_id])
["#{@recurring_meeting.title} - #{occurrence.start_time.to_date.iso8601}",
service.generate_occurrence(occurrence)]
else
[@recurring_meeting.title, service.generate_series]
end

result
.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")
send_data call.result, filename: filename_for_content_disposition("#{filename}.ics")
end
end

def notify
deliver_invitation_mails
flash[:notice] = I18n.t(:notice_successful_notification)
redirect_to action: :show
end

private

def deliver_invitation_mails
@recurring_meeting
.template
.participants
.invited
.find_each do |participant|
MeetingSeriesMailer.template_completed(
@recurring_meeting,
participant.user,
User.current
).deliver_later
end
end

def upcoming_meetings(count:)
meetings = @recurring_meeting
.scheduled_instances(upcoming: true)
Expand Down
16 changes: 13 additions & 3 deletions modules/meeting/app/mailers/meeting_mailer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,7 @@ def icalendar_notification(meeting, user, _actor, **)

def with_attached_ics(meeting, user)
User.execute_as(user) do
call = ::Meetings::ICalService
.new(user:, meeting: @meeting)
.call
call = ics_service_call(meeting, user)

call.on_success do
attachments["meeting.ics"] = call.result
Expand All @@ -88,6 +86,18 @@ def with_attached_ics(meeting, user)
end
end

def ics_service_call(meeting, user)
if meeting.recurring?
::RecurringMeetings::ICalService
.new(user:, series: meeting.recurring_meeting)
.generate_occurrence(meeting.scheduled_meeting)
else
::Meetings::ICalService
.new(user:, meeting:)
.call
end
end

def set_headers(meeting)
open_project_headers "Project" => meeting.project.identifier, "Meeting-Id" => meeting.id
headers["Content-Type"] = 'text/calendar; charset=utf-8; method="PUBLISH"; name="meeting.ics"'
Expand Down
70 changes: 70 additions & 0 deletions modules/meeting/app/mailers/meeting_series_mailer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#-- copyright

Check notice on line 1 in modules/meeting/app/mailers/meeting_series_mailer.rb

View workflow job for this annotation

GitHub Actions / rubocop

[rubocop] modules/meeting/app/mailers/meeting_series_mailer.rb#L1 <Style/FrozenStringLiteralComment>

Missing magic comment `# frozen_string_literal: true`.
Raw output
modules/meeting/app/mailers/meeting_series_mailer.rb:1:1: C: Style/FrozenStringLiteralComment: Missing magic comment `# frozen_string_literal: true`.
# 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.
#++

class MeetingSeriesMailer < UserMailer
def template_completed(series, user, actor)
@actor = actor
@series = series
@template = series.template
@next_occurrence = series.next_occurrence&.to_time
@user = user

set_headers(series)

with_attached_ics(series, user) do
subject = I18n.t("meeting.email.series.title", title: series.title, project_name: series.project.name)
mail(to: user, subject:)
end
end

private

def with_attached_ics(series, user)
User.execute_as(user) do
call = ::RecurringMeetings::ICalService
.new(user:, series: series)
.generate_series

call.on_success do
attachments["meeting.ics"] = call.result

yield
end

call.on_failure do
Rails.logger.error { "Failed to create ICS attachment for meeting #{series.id}: #{call.message}" }
end
end
end

def set_headers(series)
open_project_headers "Project" => series.project.identifier, "Meeting-Id" => series.id
headers["Content-Type"] = 'text/calendar; charset=utf-8; method="PUBLISH"; name="meeting.ics"'
headers["Content-Transfer-Encoding"] = "8bit"
end
end
4 changes: 4 additions & 0 deletions modules/meeting/app/models/meeting.rb
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ class Meeting < ApplicationRecord
closed: 5
}

def recurring?
recurring_meeting_id.present?
end

##
# Cache key for detecting changes to be shown to the user
def changed_hash
Expand Down
47 changes: 31 additions & 16 deletions modules/meeting/app/services/recurring_meetings/ical_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,23 +43,45 @@ class ICalService
def initialize(series:, user:)
@user = user
@series = series
@schedule = series.schedule
@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)
def generate_series
ical_result(series) do
series_event
occurrences_events
end
rescue StandardError => e
Rails.logger.error("Failed to generate ICS for meeting series #{@series.id}: #{e.message}")
Rails.logger.error("Failed to generate ICS for meeting series #{series.id}: #{e.message}")
ServiceResult.failure(message: e.message)
end

def generate_occurrence(meeting)
# Get the time the meeting was scheduled to take place
scheduled_meeting = meeting.scheduled_meeting
ical_result(scheduled_meeting) do
occurrence_event(scheduled_meeting.start_time, meeting)
end
rescue StandardError => e
Rails.logger.error("Failed to generate ICS for meeting #{meeting.id}: #{e.message}")
ServiceResult.failure(message: e.message)
end

private

def ical_result(meeting)
User.execute_as(user) do
@timezone = Time.zone || Time.zone_default
@calendar = build_icalendar(meeting.start_time)

yield

calendar.publish
ServiceResult.success(result: calendar.to_ical)
end
end

def tzinfo
timezone.tzinfo
end
Expand All @@ -68,18 +90,11 @@ 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.rrule = schedule.rrules.first.to_ical # We currently only have one recurrence rule
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)
Expand All @@ -105,6 +120,7 @@ def occurrence_event(schedule_start_time, meeting) # rubocop:disable Metrics/Abc
e.dtend = ical_datetime meeting.end_time, tzid
e.url = url_helpers.project_meeting_url(meeting.project, meeting)
e.location = meeting.location.presence
e.sequence = meeting.lock_version

add_attendees(e, meeting)
end
Expand All @@ -120,7 +136,6 @@ def upcoming_instantiated_schedules

def base_series_attributes(event) # rubocop:disable Metrics/AbcSize
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}"
event.organizer = ical_organizer(series)
Expand Down
Loading
Loading