From 5356118cf6d953cd2abf887a61a2a8439814de01 Mon Sep 17 00:00:00 2001 From: Andreas Pfohl Date: Tue, 12 Apr 2022 15:15:56 +0200 Subject: [PATCH] [#40941] Meeting Time in iCalendar is wrong https://community.openproject.org/work_packages/40941 --- lib/core_extensions.rb | 2 + lib/core_extensions/time_with_zone.rb | 36 ++++++++ .../app/controllers/meetings_controller.rb | 15 ++-- modules/meeting/app/mailers/meeting_mailer.rb | 82 ++++++++++++------- .../icalendar_notification.html.erb | 2 +- .../icalendar_notification.text.erb | 2 +- .../spec/mailers/meeting_mailer_spec.rb | 5 ++ 7 files changed, 103 insertions(+), 41 deletions(-) create mode 100644 lib/core_extensions/time_with_zone.rb diff --git a/lib/core_extensions.rb b/lib/core_extensions.rb index 2e7f61a97d..4da78f71a1 100644 --- a/lib/core_extensions.rb +++ b/lib/core_extensions.rb @@ -27,5 +27,7 @@ #++ require 'core_extensions/string' +require 'core_extensions/time_with_zone' ::String.prepend CoreExtensions::String +::ActiveSupport::TimeWithZone.include CoreExtensions::TimeWithZone diff --git a/lib/core_extensions/time_with_zone.rb b/lib/core_extensions/time_with_zone.rb new file mode 100644 index 0000000000..45f15a3a34 --- /dev/null +++ b/lib/core_extensions/time_with_zone.rb @@ -0,0 +1,36 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2022 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. +#++ + +module CoreExtensions + module TimeWithZone + def utc_offest_for_timezone(timezone) + period = timezone.period_for_local(self) + period.offset.utc_total_offset + end + end +end diff --git a/modules/meeting/app/controllers/meetings_controller.rb b/modules/meeting/app/controllers/meetings_controller.rb index 97fc51aa04..5ee8a0fde9 100644 --- a/modules/meeting/app/controllers/meetings_controller.rb +++ b/modules/meeting/app/controllers/meetings_controller.rb @@ -45,7 +45,7 @@ class MeetingsController < ApplicationController # from params => today's page otherwise => first page as fallback tomorrows_meetings_count = scope.from_tomorrow.count - @page_of_today = 1 + tomorrows_meetings_count / per_page_param + @page_of_today = 1 + (tomorrows_meetings_count / per_page_param) page = params['page'] ? page_param : @page_of_today @@ -119,18 +119,15 @@ class MeetingsController < ApplicationController private - def set_time_zone - old_time_zone = Time.zone + def set_time_zone(&block) zone = User.current.time_zone if zone.nil? - localzone = Time.now.utc_offset - localzone -= 3600 if Time.now.dst? + localzone = Time.current.utc_offset + localzone -= 3600 if Time.current.dst? zone = ::ActiveSupport::TimeZone[localzone] end - Time.zone = zone - yield - ensure - Time.zone = old_time_zone + + Time.use_zone(zone, &block) end def find_project diff --git a/modules/meeting/app/mailers/meeting_mailer.rb b/modules/meeting/app/mailers/meeting_mailer.rb index 5b94a6f802..253fd8363f 100644 --- a/modules/meeting/app/mailers/meeting_mailer.rb +++ b/modules/meeting/app/mailers/meeting_mailer.rb @@ -48,41 +48,63 @@ class MeetingMailer < UserMailer @meeting = content.meeting @content_type = content_type - open_project_headers 'Project' => @meeting.project.identifier, - 'Meeting-Id' => @meeting.id + set_headers @meeting + + User.execute_as(user) do + timezone = Time.zone || Time.zone_default + + @formatted_timezone = format_timezone_offset timezone, @meeting.start_time + + attachments['meeting.ics'] = generate_ical timezone, @meeting, @content_type + mail(to: user.mail, subject: ical_subject(@meeting, @content_type)) + end + end + + private + + 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"' headers['Content-Transfer-Encoding'] = '8bit' + end - author = Icalendar::Values::CalAddress.new("mailto:#{@meeting.author.mail}", - cn: @meeting.author.name) + def format_timezone_offset(timezone, time) + offset = ::ActiveSupport::TimeZone.seconds_to_utc_offset time.utc_offest_for_timezone(timezone), true + "(GMT#{offset}) #{timezone.name}" + end - # Create a calendar with an event (standard method) - entry = ::Icalendar::Calendar.new + def ical_subject(meeting, content_type) + "[#{meeting.project.name}] #{I18n.t(:"label_#{content_type}")}: #{meeting.title}" + end - User.execute_as(user) do - subject = "[#{@meeting.project.name}] #{I18n.t(:"label_#{@content_type}")}: #{@meeting.title}" - timezone = Time.zone || Time.zone_default - # Get the tzinfo object from the rails timezone - tzinfo = timezone.tzinfo - # Get the global identifier like Europe/Berlin - tzid = tzinfo.canonical_identifier - entry.add_timezone tzinfo.ical_timezone(@meeting.start_time) - - entry.event do |e| - e.dtstart = Icalendar::Values::DateTime.new @meeting.start_time, 'tzid' => tzid - e.dtend = Icalendar::Values::DateTime.new @meeting.end_time, 'tzid' => tzid - e.url = meeting_url(@meeting) - e.summary = "[#{@meeting.project.name}] #{@meeting.title}" - e.description = subject - e.uid = "#{@meeting.id}@#{@meeting.project.identifier}" - e.organizer = author - end - - # add needed 'METHOD:PUBLISH' to ics file - entry.publish - - attachments['meeting.ics'] = entry.to_ical - mail(to: user.mail, subject: subject) + # rubocop:disable Metrics/AbcSize + def generate_ical(timezone, meeting, content_type) + calendar = ::Icalendar::Calendar.new + + tzinfo = timezone.tzinfo + calendar.add_timezone tzinfo.ical_timezone(meeting.start_time) + tzid = tzinfo.canonical_identifier + + calendar.event do |e| + e.dtstart = ical_datetime meeting.start_time, tzid + e.dtend = ical_datetime meeting.end_time, tzid + e.url = meeting_url(meeting) + e.summary = "[#{meeting.project.name}] #{meeting.title}" + e.description = ical_subject(meeting, content_type) + e.uid = "#{meeting.id}@#{meeting.project.identifier}" + e.organizer = ical_organizer meeting end + + calendar.publish + calendar.to_ical + end + # rubocop:enable Metrics/AbcSize + + 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 diff --git a/modules/meeting/app/views/meeting_mailer/icalendar_notification.html.erb b/modules/meeting/app/views/meeting_mailer/icalendar_notification.html.erb index 76750d317d..1aea9386ab 100644 --- a/modules/meeting/app/views/meeting_mailer/icalendar_notification.html.erb +++ b/modules/meeting/app/views/meeting_mailer/icalendar_notification.html.erb @@ -4,7 +4,7 @@

<%= t(:text_notificiation_invited) %>