Merge pull request #16567 from opf/bug/56771-meeting-timestamp-in-edit-form-not-the-same-as-in-details

Bug/56771 meeting timestamp in edit form not the same as in details
This commit is contained in:
Jens Ulferts
2024-09-26 14:51:04 +02:00
committed by GitHub
50 changed files with 396 additions and 251 deletions
@@ -30,7 +30,7 @@
module Settings
##
# A text field to enter numeric values.
# A select field to select a time zone from.
class TimeZoneSettingComponent < ::ApplicationComponent
options :form, :title
options container_class: "-wide"
+15 -21
View File
@@ -27,31 +27,25 @@
#++
class AnonymousUser < User
validate :validate_unique_anonymous_user, on: :create
# There should be only one AnonymousUser in the database
def validate_unique_anonymous_user
errors.add :base, "An anonymous user already exists." if AnonymousUser.any?
end
def available_custom_fields
[]
end
# Overrides a few properties
def logged?; false end
def builtin?; true end
def admin; false end
include Users::FunctionUser
def name(*_args); I18n.t(:label_user_anonymous) end
def mail; nil end
def self.first
anonymous_user = super
def time_zone; nil end
if anonymous_user.nil?
(anonymous_user = new.tap do |u|
u.lastname = "Anonymous"
u.login = ""
u.firstname = ""
u.mail = ""
u.status = User.statuses[:active]
end).save
def rss_key; nil end
raise "Unable to create the anonymous user." if anonymous_user.new_record?
end
def destroy; false end
anonymous_user
end
end
+2 -16
View File
@@ -1,23 +1,9 @@
class DeletedUser < User
validate :validate_unique_deleted_user, on: :create
# There should be only one DeletedUser in the database
def validate_unique_deleted_user
errors.add :base, "A DeletedUser already exists." if DeletedUser.any?
end
def self.first
super || create(type: to_s, status: statuses[:locked])
end
# Overrides a few properties
def available_custom_fields = []
def logged? = false
def builtin? = true
def admin = false
include Users::FunctionUser
def name(*_args) = I18n.t("user.deleted")
def mail = nil
def time_zone = nil
def rss_key = nil
def destroy = false
end
+1 -1
View File
@@ -84,7 +84,7 @@ module Exports
def csv_export_filename
sane_filename(
"#{Setting.app_title} #{title} \
#{format_time_as_date(Time.zone.now, '%Y-%m-%d')}.csv"
#{format_time_as_date(Time.zone.now, format: '%Y-%m-%d')}.csv"
)
end
end
+23 -19
View File
@@ -31,31 +31,35 @@
#
class SystemUser < User
validate :validate_unique_system_user, on: :create
# There should be only one SystemUser in the database
def validate_unique_system_user
errors.add :base, "A SystemUser already exists." if SystemUser.any?
end
# Overrides a few properties
def logged?; false end
def builtin?; true end
include Users::FunctionUser
def name(*_args); "System" end
def mail; nil end
def time_zone; nil end
def rss_key; nil end
def destroy; false end
def run_given
User.execute_as(self) do
yield self
end
end
def self.first
system_user = super
if system_user.nil?
system_user = new(
firstname: "",
lastname: "System",
login: "",
mail: "",
admin: true,
status: User.statuses[:active],
first_login: false
)
system_user.save
raise "Unable to create the system user." unless system_user.persisted?
end
system_user
end
end
+12 -40
View File
@@ -399,7 +399,7 @@ class User < Principal
end
def log_successful_login
update_attribute(:last_login_on, Time.now)
update_attribute(:last_login_on, Time.current)
end
def pref
@@ -407,7 +407,13 @@ class User < Principal
end
def time_zone
@time_zone ||= (pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[pref.time_zone])
@time_zone ||= ActiveSupport::TimeZone[pref.time_zone] || ActiveSupport::TimeZone["Etc/UTC"]
end
def reload(*)
@time_zone = nil
super
end
def wants_comments_in_reverse_order?
@@ -538,46 +544,12 @@ class User < Principal
# Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only
# one anonymous user per database.
def self.anonymous # rubocop:disable Metrics/AbcSize
RequestStore[:anonymous_user] ||=
begin
anonymous_user = AnonymousUser.first
if anonymous_user.nil?
(anonymous_user = AnonymousUser.new.tap do |u|
u.lastname = "Anonymous"
u.login = ""
u.firstname = ""
u.mail = ""
u.status = User.statuses[:active]
end).save
raise "Unable to create the anonymous user." if anonymous_user.new_record?
end
anonymous_user
end
def self.anonymous
RequestStore[:anonymous_user] ||= AnonymousUser.first
end
def self.system
system_user = SystemUser.first
if system_user.nil?
system_user = SystemUser.new(
firstname: "",
lastname: "System",
login: "",
mail: "",
admin: true,
status: User.statuses[:active],
first_login: false
)
system_user.save(validate: false)
raise "Unable to create the automatic migration user." unless system_user.persisted?
end
system_user
SystemUser.first
end
protected
@@ -689,6 +661,6 @@ class User < Principal
end
def self.default_admin_account_changed?
!User.active.find_by_login("admin").try(:current_password).try(:matches_plaintext?, "admin") # rubocop:disable Rails/DynamicFindBy
!User.active.find_by_login("admin").try(:current_password).try(:matches_plaintext?, "admin")
end
end
+5 -1
View File
@@ -132,7 +132,11 @@ class UserPreference < ApplicationRecord
end
def time_zone
super.presence || Setting.user_default_timezone.presence
super.presence || Setting.user_default_timezone.presence || "Etc/UTC"
end
def time_zone?
settings["time_zone"].present?
end
def daily_reminders
+56
View File
@@ -0,0 +1,56 @@
#-- 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.
#++
module Users::FunctionUser
extend ActiveSupport::Concern
included do
validate :validate_unique_function_user, on: :create
# There should be only one such user in the database
def validate_unique_function_user
errors.add :base, "A #{self.class.name} already exists." if self.class.any?
end
def available_custom_fields = []
def logged? = false
def builtin? = true
def name(*_args); raise NotImplementedError end
def mail = nil
def time_zone; ActiveSupport::TimeZone[Setting.user_default_timezone.presence || "Etc/UTC"] end
def rss_key = nil
def destroy = false
end
end
+1 -1
View File
@@ -127,7 +127,7 @@ module WorkPackage::PDFExport::Page
end
def footer_date
format_time(Time.zone.now, true)
format_time(Time.zone.now)
end
def total_page_nr_text
@@ -48,7 +48,6 @@ See COPYRIGHT and LICENSE files for more details.
<%= render Settings::TimeZoneSettingComponent.new(
"user_default_timezone",
container_class: "-slim",
title: I18n.t("tooltip_user_default_timezone")
)
%>
+1
View File
@@ -30,6 +30,7 @@ See COPYRIGHT and LICENSE files for more details.
<%= render Settings::TimeZoneSettingComponent.new(
"time_zone",
form: pref_fields,
include_blank: false,
container_class: (defined? input_size) ? "-#{input_size}" : "-wide"
)
%>
+33 -17
View File
@@ -115,18 +115,18 @@ module Redmine
/(\[(.+?)\]\((.+?)\))/
end
# Format the time to a date in the user time zone if one is set.
# If none is set and the time is in utc time zone (meaning it came from active record), format the date in the system timezone
# otherwise just use the date in the time zone attached to the time.
def format_time_as_date(time, format = nil)
# Formats the given time as a date string according to the user's time zone and
# optional specified format.
#
# @param time [Time] The time to format.
# @param format [String, nil] The strftime format to use for the date. If nil, the default
# date format from `Setting.date_format` is used.
# @return [String, nil] The formatted date string, or nil if the time is not provided.
def format_time_as_date(time, format: nil)
return nil unless time
zone = User.current.time_zone
local_date = (if zone
time.in_time_zone(zone)
else
time.utc? ? time.localtime : time
end).to_date
local_date = time.in_time_zone(zone).to_date
if format
local_date.strftime(format)
@@ -135,18 +135,34 @@ module Redmine
end
end
def format_time(time, include_date = true)
# Formats the given time as a time string according to the user's time zone
# and optional specified format.
#
# @param time [Time] The time to format.
# @param include_date [Boolean] Whether to include the date in the formatted
# output. Defaults to true.
# @param format [String] The strftime format to use for the time. Defaults
# to the format in `Setting.time_format`.
# @return [String, nil] The formatted time string, or nil if the time is not
# provided.
def format_time(time, include_date: true, format: Setting.time_format)
return nil unless time
time = time.to_time if time.is_a?(String)
zone = User.current.time_zone
local = if zone
time.in_time_zone(zone)
else
(time.utc? ? time.to_time.localtime : time)
end
local = time.in_time_zone(zone)
(include_date ? "#{format_date(local)} " : "") +
(Setting.time_format.blank? ? ::I18n.l(local, format: :time) : local.strftime(Setting.time_format))
(format.blank? ? ::I18n.l(local, format: :time) : local.strftime(format))
end
# Returns the offset to UTC (with utc prepended) currently active
# in the current users time zone. DST is factored in so the offset can
# shift over the course of the year
def formatted_time_zone_offset
# Doing User.current.time_zone and format that will not take heed of DST as it has no notion
# of a current time.
# https://github.com/rails/rails/issues/7297
"UTC#{User.current.time_zone.now.formatted_offset}"
end
def day_name(day)
@@ -43,7 +43,7 @@ module OpenProject::Bim::BcfXml
sane_filename(
"#{Setting.app_title} #{I18n.t(:label_work_package_plural)} \
#{format_time_as_date(Time.now, '%Y-%m-%d')}.bcf"
#{format_time_as_date(Time.current, format: '%Y-%m-%d')}.bcf"
)
end
@@ -39,7 +39,7 @@ module Boards
end
def created_at
safe_join([helpers.format_date(model.created_at), helpers.format_time(model.created_at, false)], " ")
safe_join([helpers.format_date(model.created_at), helpers.format_time(model.created_at, include_date: false)], " ")
end
def type
@@ -47,7 +47,7 @@ module Meetings
end
def start_time
safe_join([helpers.format_date(model.start_time), helpers.format_time(model.start_time, false)], " ")
safe_join([helpers.format_date(model.start_time), helpers.format_time(model.start_time, include_date: false)], " ")
end
def duration
@@ -30,13 +30,13 @@
flex_layout(align_items: :center) do |time|
time.with_column do
render(Primer::Beta::Text.new) do
"#{format_time(@meeting.start_time, false)} - #{format_time(@meeting.end_time, false)}"
"#{format_time(@meeting.start_time, include_date: false)} - #{format_time(@meeting.end_time, include_date:false)}"
end
end
time.with_column(ml: 2) do
render(Primer::Beta::Text.new(color: :subtle, font_size: :small)) do
(User.current.time_zone || Time.zone).to_s[/\((.*?)\)/m, 1]
formatted_time_zone_offset
end
end
end
@@ -45,11 +45,11 @@ module Meetings
private
def start_date_initial_value
@meeting.start_time&.strftime("%Y-%m-%d")
format_time_as_date(@meeting.start_time, format: "%Y-%m-%d")
end
def start_time_initial_value
@meeting.start_time&.strftime("%H:%M")
format_time(@meeting.start_time, include_date: false, format: "%H:%M")
end
end
end
@@ -27,7 +27,6 @@
#++
class MeetingsController < ApplicationController
around_action :set_time_zone
before_action :load_and_authorize_in_optional_project, only: %i[index new show create history]
before_action :verify_activities_module_activated, only: %i[history]
before_action :determine_date_range, only: %i[history]
@@ -96,8 +95,8 @@ class MeetingsController < ApplicationController
if call.success?
text = I18n.t(:notice_successful_create)
if User.current.time_zone.nil?
link = I18n.t(:notice_timezone_missing, zone: Time.zone)
unless User.current.pref.time_zone?
link = I18n.t(:notice_timezone_missing, zone: formatted_time_zone_offset)
text += " #{view_context.link_to(link, { controller: '/my', action: :settings, anchor: 'pref_time_zone' },
class: 'link_to_profile')}"
end
@@ -288,17 +287,6 @@ class MeetingsController < ApplicationController
.paginate(page: page_param, per_page: per_page_param)
end
def set_time_zone(&)
zone = User.current.time_zone
if zone.nil?
localzone = Time.current.utc_offset
localzone -= 3600 if Time.current.dst?
zone = ::ActiveSupport::TimeZone[localzone]
end
Time.use_zone(zone, &)
end
def build_meeting
@meeting = Meeting.new
@meeting.project = @project
@@ -27,6 +27,8 @@
#++
class Meeting::StartTime < ApplicationForm
include Redmine::I18n
form do |meeting_form|
meeting_form.text_field(
name: :start_time_hour,
@@ -36,7 +38,7 @@ class Meeting::StartTime < ApplicationForm
label: Meeting.human_attribute_name(:start_time),
leading_visual: { icon: :clock },
required: true,
caption: Time.zone.to_s[/\((.*?)\)/m, 1]
caption: formatted_time_zone_offset
)
end
@@ -52,7 +52,7 @@ class MeetingAgendaItem::MeetingForm < ApplicationForm
label: "#{meeting.project.name}: " \
"#{meeting.title} " \
"#{format_date(meeting.start_time)} " \
"#{format_time(meeting.start_time, false)}",
"#{format_time(meeting.start_time, include_date: false)}",
value: meeting.id
)
end
@@ -63,8 +63,6 @@ class MeetingMailer < UserMailer
set_headers @meeting
with_attached_ics(meeting, user) do
timezone = Time.zone || Time.zone_default
@formatted_timezone = format_timezone_offset timezone, @meeting.start_time
subject = "[#{@meeting.project.name}] #{@meeting.title}"
mail(to: user, subject:)
end
@@ -95,9 +93,4 @@ class MeetingMailer < UserMailer
headers["Content-Type"] = 'text/calendar; charset=utf-8; method="PUBLISH"; name="meeting.ics"'
headers["Content-Transfer-Encoding"] = "8bit"
end
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
end
@@ -109,8 +109,8 @@ class Activities::MeetingActivityProvider < Activities::BaseActivityProvider
end_time = start_time + event["meeting_duration"].to_f.hours
fstart_with = format_date start_time
fstart_without = format_time start_time, false
fend_without = format_time end_time, false
fstart_without = format_time start_time, include_date: false
fend_without = format_time end_time, include_date: false
"#{I18n.t(:label_meeting)}: #{event['meeting_title']} (#{fstart_with} #{fstart_without}-#{fend_without})"
else
@@ -204,8 +204,8 @@ class Activities::MeetingEventMapper < Activities::EventMapper
end_time = start_time + data[:meeting_duration].to_f.hours
fstart_with = format_date start_time
fstart_without = format_time start_time, false
fend_without = format_time end_time, false
fstart_without = format_time start_time, include_date: false
fend_without = format_time end_time, include_date: false
"#{I18n.t(:label_meeting)}: #{data[:meeting_title]} (#{fstart_with} #{fstart_without}-#{fend_without})"
end
+4 -3
View File
@@ -253,14 +253,15 @@ class Meeting < ApplicationRecord
def set_initial_values
# set defaults
write_attribute(:start_time, Date.tomorrow + 10.hours) if start_time.nil?
# Start date is set to tomorrow at 10 AM (Current users local time)
write_attribute(:start_time, User.current.time_zone.now.at_midnight + 34.hours) if start_time.nil?
self.duration ||= 1
update_derived_fields
end
def update_derived_fields
@start_date = start_time.to_date.iso8601
@start_time_hour = start_time.strftime("%H:%M")
@start_date = format_time_as_date(start_time, format: "%Y-%m-%d")
@start_time_hour = format_time(start_time, include_date: false, format: "%H:%M")
end
private
@@ -35,7 +35,7 @@ module Meeting::Journalized
acts_as_event title: Proc.new { |o|
"#{I18n.t(:label_meeting)}: #{o.title} \
#{format_date o.start_time} \
#{format_time o.start_time, false}-#{format_time o.end_time, false})"
#{format_time o.start_time, include_date: false}-#{format_time o.end_time, include_date: false})"
},
url: Proc.new { |o| { controller: "/meetings", action: "show", id: o } },
author: Proc.new(&:user),
@@ -4,7 +4,7 @@
<p><%= t(:text_notificiation_invited) %></p>
<ul>
<li><%=t :label_meeting_date_time %>: <%= format_time_as_date @meeting.start_time %> <%= format_time @meeting.start_time, false %>-<%= format_time @meeting.end_time, false %> <%= @formatted_timezone %></li>
<li><%=t :label_meeting_date_time %>: <%= format_time_as_date @meeting.start_time %> <%= format_time @meeting.start_time, include_date: false %>-<%= format_time @meeting.end_time, include_date: false %> (<%= formatted_time_zone_offset %>)</li>
<li><%=Meeting.human_attribute_name(:location) %>: <%= @meeting.location %></li>
<li><%=Meeting.human_attribute_name(:participants_invited) %>: <%= @meeting.participants.invited.sort.join("; ") %></li>
<li><%=Meeting.human_attribute_name(:participants_attended) %>: <%= @meeting.participants.attended.sort.join("; ") %></li>
@@ -3,7 +3,7 @@
<%= t(:text_notificiation_invited) %>
<%=t :label_meeting_date_time %>: <%= format_time_as_date @meeting.start_time %> <%= format_time @meeting.start_time, false %>-<%= format_time @meeting.end_time, false %> <%= @formatted_timezone %>
<%=t :label_meeting_date_time %>: <%= format_time_as_date @meeting.start_time %> <%= format_time @meeting.start_time, include_date: false %>-<%= format_time @meeting.end_time, include_date: false %> (<%= formatted_time_zone_offset %>)
<%=Meeting.human_attribute_name(:location) %>: <%= @meeting.location %>
<%=Meeting.human_attribute_name(:participants_invited) %>: <%= @meeting.participants.invited.sort.join("; ") %>
<%=Meeting.human_attribute_name(:participants_attended) %>: <%= @meeting.participants.attended.sort.join("; ") %>
@@ -45,9 +45,9 @@ See COPYRIGHT and LICENSE files for more details.
<%= I18n.t(:label_meeting_date_time) %>
</td>
<td style="<%= placeholder_text_styles %>">
<%= format_time_as_date @meeting.start_time %> <%= format_time @meeting.start_time, false %>
<%= format_time_as_date @meeting.start_time %> <%= format_time @meeting.start_time, include_date: false %>
-
<%= format_time @meeting.end_time, false %> <%= Time.zone %>
<%= format_time @meeting.end_time, include_date: false %> (<%= formatted_time_zone_offset %>)
</td>
</tr>
<% if @meeting.location.present? %>
@@ -32,7 +32,7 @@ See COPYRIGHT and LICENSE files for more details.
<%= @meeting.project.name %>: <%= @meeting.title %> (<%= meeting_url(@meeting) %>)
<%= @meeting.author %>
<%=t :label_meeting_date_time %>: <%= format_time_as_date @meeting.start_time %> <%= format_time @meeting.start_time, false %>-<%= format_time @meeting.end_time, false %> <%= Time.zone %>
<%=t :label_meeting_date_time %>: <%= format_time_as_date @meeting.start_time %> <%= format_time @meeting.start_time, include_date: false %>-<%= format_time @meeting.end_time, include_date: false %> (<%= formatted_time_zone_offset %>)
<%= Meeting.human_attribute_name(:location) %>: <%= @meeting.location %>
<%= Meeting.human_attribute_name(:participants_invited) %>: <%= @meeting.participants.invited.sort.join("; ") %>
<%= Meeting.human_attribute_name(:participants_attended) %>: <%= @meeting.participants.attended.sort.join("; ") %>
@@ -46,9 +46,9 @@ See COPYRIGHT and LICENSE files for more details.
</td>
<td style="<%= placeholder_text_styles %>">
<s>
<%= format_time_as_date @changes[:old_start] %> <%= format_time @changes[:old_start], false %>
<%= format_time_as_date @changes[:old_start] %> <%= format_time @changes[:old_start], include_date: false %>
-
<%= format_time (@changes[:old_start] + @changes[:old_duration].hours), false %> <%= Time.zone %>
<%= format_time (@changes[:old_start] + @changes[:old_duration].hours), include_date: false %> (<%= formatted_time_zone_offset %>)
</s>
</td>
</tr>
@@ -57,9 +57,9 @@ See COPYRIGHT and LICENSE files for more details.
<%= t('meeting.email.rescheduled.new_date_time') %>
</td>
<td style="<%= placeholder_text_styles('font-weight': 'bold') %>">
<%= format_time_as_date @changes[:new_start] %> <%= format_time @changes[:new_start], false %>
<%= format_time_as_date @changes[:new_start] %> <%= format_time @changes[:new_start], include_date: false %>
-
<%= format_time (@changes[:new_start] + @changes[:new_duration].hours), false %> <%= Time.zone %>
<%= format_time (@changes[:new_start] + @changes[:new_duration].hours), include_date: false %> (<%= formatted_time_zone_offset %>)
</td>
</tr>
<% if @meeting.location.present? %>
@@ -35,7 +35,7 @@ See COPYRIGHT and LICENSE files for more details.
title: @meeting.title) %>
<%= t('meeting.email.rescheduled.old_date_time') %>:
<%= format_time_as_date @changes[:old_start] %> <%= format_time @changes[:old_start], false %> - <%= format_time (@changes[:old_start] + @changes[:old_duration]), false %> <%= Time.zone %>
<%= format_time_as_date @changes[:old_start] %> <%= format_time @changes[:old_start], include_date: false %> - <%= format_time (@changes[:old_start] + @changes[:old_duration]), include_date: false %> (<%= formatted_time_zone_offset %>}
<%= t('meeting.email.rescheduled.new_date_time') %>:
<%= format_time_as_date @changes[:new_start] %> <%= format_time @changes[:new_start], false %> - <%= format_time (@changes[:new_start] + @changes[:new_duration]), false %> <%= Time.zone %>
<%= format_time_as_date @changes[:new_start] %> <%= format_time @changes[:new_start], include_date: false %> - <%= format_time (@changes[:new_start] + @changes[:new_duration]), include_date: false %> (<%= formatted_time_zone_offset %>}
@@ -160,7 +160,7 @@ See COPYRIGHT and LICENSE files for more details.
<%= Meeting.human_attribute_name(:start_time) %>
<label lang="en">
<%= t(:label_time_zone) %>
<%= Time.zone.to_s %>
<%= formatted_time_zone_offset %>
</label>
</label>
<%= f.text_field :start_time_hour,
@@ -169,7 +169,7 @@ See COPYRIGHT and LICENSE files for more details.
type: 'time',
no_label: true,
step: 5.minutes,
suffix: Time.zone.to_s,
suffix: formatted_time_zone_offset,
container_class: '-xslim' %>
</div>
</div>
@@ -100,8 +100,8 @@ See COPYRIGHT and LICENSE files for more details.
</div>
<div class="grid-content small-6">
<p>
<strong><%= Meeting.human_attribute_name(:start_time) %></strong>: <%= format_date @meeting.start_time %> <%= format_time @meeting.start_time, false %>
- <%= format_time @meeting.end_time, false %> <%= Time.zone %></p>
<strong><%= Meeting.human_attribute_name(:start_time) %></strong>: <%= format_date @meeting.start_time %> <%= format_time @meeting.start_time, include_date: false %>
- <%= format_time @meeting.end_time, include_date: false %> <%= formatted_time_zone_offset %></p>
</div>
<div class="grid-content small-6">
<p>
@@ -34,7 +34,7 @@ RSpec.describe "Meetings copy", :js, :with_cuprite do
shared_let(:user) do
create(:user,
member_with_permissions: { project => permissions }).tap do |u|
u.pref[:time_zone] = "UTC"
u.pref[:time_zone] = "Etc/UTC"
u.save!
end
@@ -66,7 +66,7 @@ RSpec.describe "Meetings copy", :js, :with_cuprite do
start_of_meeting = start_time.strftime(twelve_hour_format)
end_of_meeting = (start_time + meeting.duration.hours).strftime(twelve_hour_format)
"Start time: #{date} #{start_of_meeting} - #{end_of_meeting} (GMT+00:00) UTC"
"Start time: #{date} #{start_of_meeting} - #{end_of_meeting} UTC+00:00"
end
before do
@@ -33,7 +33,7 @@ require_relative "../support/pages/meetings/index"
RSpec.describe "Meetings new", :js, with_cuprite: false do
shared_let(:project) { create(:project, enabled_module_names: %w[meetings]) }
shared_let(:admin) { create(:admin) }
let(:time_zone) { "utc" }
let(:time_zone) { "Etc/UTC" }
let(:user) do
create(:user,
lastname: "First",
@@ -41,7 +41,7 @@ RSpec.describe "Structured meetings CRUD",
lastname: "First",
member_with_permissions: { project => %i[view_meetings create_meetings edit_meetings delete_meetings manage_agendas
close_meeting_agendas view_work_packages] }).tap do |u|
u.pref[:time_zone] = "utc"
u.pref[:time_zone] = "Etc/UTC"
u.save!
end
@@ -42,7 +42,7 @@ RSpec.describe "Structured meetings CRUD",
lastname: "First",
member_with_permissions: { project => %i[view_meetings create_meetings edit_meetings delete_meetings manage_agendas
view_work_packages] }).tap do |u|
u.pref[:time_zone] = "utc"
u.pref[:time_zone] = "Etc/UTC"
u.save!
end
@@ -41,7 +41,7 @@ RSpec.describe "Structured meetings participants",
lastname: "First",
member_with_permissions: { project => %i[view_meetings create_meetings edit_meetings delete_meetings manage_agendas
close_meeting_agendas view_work_packages] }).tap do |u|
u.pref[:time_zone] = "utc"
u.pref[:time_zone] = "Etc/UTC"
u.save!
end
@@ -42,7 +42,7 @@ RSpec.describe "Structured meetings links caught by turbo",
lastname: "First",
member_with_permissions: { project => %i[view_meetings create_meetings edit_meetings delete_meetings manage_agendas
view_work_packages] }).tap do |u|
u.pref[:time_zone] = "utc"
u.pref[:time_zone] = "Etc/UTC"
u.save!
end
@@ -47,6 +47,8 @@ RSpec.describe MeetingMailer do
let(:meeting_agenda) do
create(:meeting_agenda, meeting:)
end
let(:tokyo_offset) { "UTC#{ActiveSupport::TimeZone['Asia/Tokyo'].now.formatted_offset}" }
let(:berlin_offset) { "UTC#{ActiveSupport::TimeZone['Europe/Berlin'].now.formatted_offset}" }
before do
User.current = author
@@ -88,11 +90,9 @@ RSpec.describe MeetingMailer do
context "with a recipient with another time zone" do
let!(:preference) { watcher1.pref.update(time_zone: "Asia/Tokyo") }
it "renders the mail with the correcet locale" do
expect(mail.text_part.body).to include("Tokyo")
expect(mail.text_part.body).to include("GMT+09:00")
expect(mail.html_part.body).to include("Tokyo")
expect(mail.html_part.body).to include("GMT+09:00")
it "renders the mail with the correct locale" do
expect(mail.text_part.body).to include(tokyo_offset)
expect(mail.html_part.body).to include(tokyo_offset)
expect(mail.to).to contain_exactly(watcher1.mail)
end
@@ -111,8 +111,8 @@ RSpec.describe MeetingMailer do
it "renders the mail with the correct locale" do
expect(mail.html_part.body).to include("11/09/2021 11:00 PM")
expect(mail.html_part.body).to include("12:00 AM (GMT+01:00) Europe/Berlin")
expect(mail.text_part.body).to include("11/09/2021 11:00 PM-12:00 AM (GMT+01:00) Europe/Berlin")
expect(mail.html_part.body).to include("12:00 AM (#{berlin_offset})")
expect(mail.text_part.body).to include("11/09/2021 11:00 PM-12:00 AM (#{berlin_offset})")
expect(mail.to).to contain_exactly(author.mail)
end
@@ -124,9 +124,9 @@ RSpec.describe MeetingMailer do
it "renders the mail with the correct locale" do
expect(mail.html_part.body).to include("11/10/2021 07:00 AM")
expect(mail.html_part.body).to include("08:00 AM (GMT+09:00) Asia/Tokyo")
expect(mail.html_part.body).to include("08:00 AM (#{tokyo_offset})")
expect(mail.text_part.body).to include("11/10/2021 07:00 AM-08:00 AM (GMT+09:00) Asia/Tokyo")
expect(mail.text_part.body).to include("11/10/2021 07:00 AM-08:00 AM (#{tokyo_offset})")
expect(mail.to).to contain_exactly(watcher1.mail)
end
@@ -160,7 +160,7 @@ RSpec.describe MeetingMailer do
expect(body).to include(meeting.project.name)
expect(body).to include(meeting.title)
expect(body).to include(meeting.location)
expect(body).to include("01/19/2021 11:00 AM-12:00 PM (GMT+01:00) Europe/Berlin")
expect(body).to include("01/19/2021 11:00 AM-12:00 PM (#{berlin_offset})")
expect(body).to include(meeting.participants[0].name)
expect(body).to include(meeting.participants[1].name)
end
@@ -174,7 +174,7 @@ RSpec.describe MeetingMailer do
expect(body).to include(meeting.title)
expect(body).to include(meeting.location)
expect(body).to include("01/19/2021 11:00 AM")
expect(body).to include("12:00 PM (GMT+01:00) Europe/Berlin")
expect(body).to include("12:00 PM (#{berlin_offset})")
expect(body).to include(meeting.participants[0].name)
expect(body).to include(meeting.participants[1].name)
end
@@ -207,9 +207,9 @@ RSpec.describe MeetingMailer do
let(:mail) { described_class.icalendar_notification(meeting, watcher1, author) }
it "renders the mail with the correct locale" do
expect(mail.text_part.body).to include("01/19/2021 07:00 PM-08:00 PM (GMT+09:00) Asia/Tokyo")
expect(mail.text_part.body).to include("01/19/2021 07:00 PM-08:00 PM (#{tokyo_offset})")
expect(mail.html_part.body).to include("01/19/2021 07:00 PM")
expect(mail.html_part.body).to include("08:00 PM (GMT+09:00) Asia/Tokyo")
expect(mail.html_part.body).to include("08:00 PM (#{tokyo_offset})")
expect(mail.to).to contain_exactly(watcher1.mail)
end
@@ -227,9 +227,9 @@ RSpec.describe MeetingMailer do
let(:mail) { described_class.icalendar_notification(meeting, author, author) }
it "renders the mail with the correct locale" do
expect(mail.text_part.body).to include("11/09/2021 11:00 PM-12:00 AM (GMT+01:00) Europe/Berlin")
expect(mail.text_part.body).to include("11/09/2021 11:00 PM-12:00 AM (#{berlin_offset})")
expect(mail.html_part.body).to include("11/09/2021 11:00 PM")
expect(mail.html_part.body).to include("12:00 AM (GMT+01:00) Europe/Berlin")
expect(mail.html_part.body).to include("12:00 AM (#{berlin_offset})")
expect(mail.to).to contain_exactly(author.mail)
end
@@ -240,8 +240,8 @@ RSpec.describe MeetingMailer do
let!(:preference) { watcher1.pref.update(time_zone: "Asia/Tokyo") }
it "renders the mail with the correct locale" do
expect(mail.text_part.body).to include("11/10/2021 07:00 AM-08:00 AM (GMT+09:00) Asia/Tokyo")
expect(mail.html_part.body).to include("11/10/2021 07:00 AM-08:00 AM (GMT+09:00) Asia/Tokyo")
expect(mail.text_part.body).to include("11/10/2021 07:00 AM-08:00 AM (#{tokyo_offset})")
expect(mail.html_part.body).to include("11/10/2021 07:00 AM-08:00 AM (#{tokyo_offset})")
expect(mail.to).to contain_exactly(watcher1.mail)
end
@@ -253,8 +253,9 @@ RSpec.describe MeetingMailer do
expect(body).to include(meeting.project.name)
expect(body).to include(meeting.title)
expect(body).to include(i18n.format_date(meeting.start_date))
expect(body).to include(i18n.format_time(meeting.start_time, false))
expect(body).to include(i18n.format_time(meeting.end_time, false))
expect(body).to include(i18n.format_time(meeting.start_time, include_date: false))
expect(body).to include(i18n.format_time(meeting.end_time, include_date: false))
expect(body).to include(i18n.formatted_time_zone_offset)
expect(body).to include(meeting.participants[0].name)
expect(body).to include(meeting.participants[1].name)
end
@@ -79,7 +79,7 @@ module XlsExport
def xls_export_filename
sane_filename(
"#{Setting.app_title} #{spreadsheet_title} \
#{format_time_as_date(Time.zone.now, '%Y-%m-%d')}.xls"
#{format_time_as_date(Time.zone.now, format: '%Y-%m-%d')}.xls"
)
end
end
@@ -62,8 +62,8 @@ RSpec.describe API::V3::UserPreferences::UserPreferenceRepresenter,
context "without a timezone set" do
let(:preference) { build(:user_preference, time_zone: "") }
it "shows the timeZone as nil" do
expect(subject).to be_json_eql(nil.to_json).at_path("timeZone")
it "shows the timeZone as utc" do
expect(subject).to be_json_eql("Etc/UTC".to_json).at_path("timeZone")
end
end
+96 -45
View File
@@ -35,43 +35,35 @@ module OpenProject
let(:format) { "%d/%m/%Y" }
let(:user) { build_stubbed(:user) }
after do
Time.zone = nil
end
describe "#format_time_as_date" do
current_user { build_stubbed(:user, preferences: { time_zone: user_time_zone }) }
describe "with user time zone" do
before do
login_as user
allow(user).to receive(:time_zone).and_return(ActiveSupport::TimeZone["Athens"])
describe "with user time zone" do
let(:user_time_zone) { "Europe/Athens" }
it "returns a date string in the user timezone for a utc timestamp" do
time = ActiveSupport::TimeZone["UTC"].local(2013, 6, 30, 23, 59)
expect(format_time_as_date(time, format:)).to eq "01/07/2013"
end
it "returns a date string in the user timezone for a non-utc timestamp" do
time = ActiveSupport::TimeZone["Berlin"].local(2013, 6, 30, 23, 59)
expect(format_time_as_date(time, format:)).to eq "01/07/2013"
end
end
it "returns a date in the user timezone for a utc timestamp" do
Time.zone = "UTC"
time = Time.zone.local(2013, 6, 30, 23, 59)
expect(format_time_as_date(time, format)).to eq "01/07/2013"
end
describe "without user time zone" do
let(:user_time_zone) { "" }
it "returns a date in the user timezone for a non-utc timestamp" do
Time.zone = "Berlin"
time = Time.zone.local(2013, 6, 30, 23, 59)
expect(format_time_as_date(time, format)).to eq "01/07/2013"
end
end
it "returns a date string in the utc timezone for a utc timestamp" do
time = ActiveSupport::TimeZone["UTC"].local(2013, 6, 30, 23, 59)
expect(format_time_as_date(time, format:)).to eq "30/06/2013"
end
describe "without user time zone" do
before { allow(User.current).to receive(:time_zone).and_return(nil) }
it "returns a date in the local system timezone for a utc timestamp" do
Time.zone = "UTC"
time = Time.zone.local(2013, 6, 30, 23, 59)
allow(time).to receive(:localtime).and_return(ActiveSupport::TimeZone["Athens"].local(2013, 7, 1, 1, 59))
expect(format_time_as_date(time, format)).to eq "01/07/2013"
end
it "returns a date in the original timezone for a non-utc timestamp" do
Time.zone = "Berlin"
time = Time.zone.local(2013, 6, 30, 23, 59)
expect(format_time_as_date(time, format)).to eq "30/06/2013"
it "returns a date string in the utc timezone for a non-utc timestamp" do
time = ActiveSupport::TimeZone["Berlin"].local(2013, 6, 30, 23, 59)
expect(format_time_as_date(time, format:)).to eq "30/06/2013"
end
end
end
@@ -271,7 +263,10 @@ module OpenProject
time_format: "%H %M",
date_format: "%d %m %Y"
} do
let!(:now) { Time.parse("2011-02-20 15:45:22") }
let(:user_time_zone) { "" }
let(:now) { Time.zone.parse("2011-02-20 15:45:22") }
current_user { build_stubbed(:user, preferences: { time_zone: user_time_zone }) }
it "with date and hours" do
expect(format_time(now))
@@ -279,18 +274,33 @@ module OpenProject
end
it "with only hours" do
expect(format_time(now, false))
expect(format_time(now, include_date: false))
.to eql now.strftime("%H %M")
end
it "with a utc to date and hours" do
expect(format_time(now.utc))
.to eql now.localtime.strftime("%d %m %Y %H %M")
it "renders correctly for only hours and when providing a custom format" do
expect(format_time(now, include_date: false, format: "%H:%M"))
.to eql now.strftime("%H:%M")
end
it "with a utce to only hours" do
expect(format_time(now.utc, false))
.to eql now.localtime.strftime("%H %M")
context "with another time zone configured for the user" do
# Kathmandu has a +05:45 offset
let(:user_time_zone) { "Kathmandu" }
it "renders correctly for data and hours" do
expect(format_time(now))
.to eql "20 02 2011 21 30"
end
it "renders correctly for only hours" do
expect(format_time(now, include_date: false))
.to eql "21 30"
end
it "renders correctly for only hours and when providing a custom format" do
expect(format_time(now, include_date: false, format: "%H:%M"))
.to eql "21:30"
end
end
context "with a different format defined", with_settings: {
@@ -303,7 +313,7 @@ module OpenProject
end
it "renders only hours" do
expect(format_time(now, false))
expect(format_time(now, include_date: false))
.to eql "15:45"
end
end
@@ -318,7 +328,7 @@ module OpenProject
end
it "falls back to default for only hours" do
expect(format_time(now, false))
expect(format_time(now, include_date: false))
.to eql "03:45 PM"
end
@@ -333,7 +343,7 @@ module OpenProject
it "raises no error for only hours" do
described_class.with_locale lang do
expect { format_time(now, false) }
expect { format_time(now, include_date: false) }
.not_to raise_error
end
end
@@ -351,7 +361,7 @@ module OpenProject
end
it "falls back to default for only hours" do
expect(format_time(now, false))
expect(format_time(now, include_date: false))
.to eql "03:45 PM"
end
end
@@ -366,12 +376,53 @@ module OpenProject
end
it "falls back to default for only hours" do
expect(format_time(now, false))
expect(format_time(now, include_date: false))
.to eql "15:45"
end
end
end
describe "#formatted_time_zone_offset" do
current_user { build_stubbed(:user, preferences: { time_zone: user_time_zone }) }
let(:user_time_zone) { "" }
let(:berlin_gmt) { ActiveSupport::TimeZone["Europe/Berlin"].now.utc_offset == 7200 ? "UTC+02:00" : "UTC+01:00" }
context "with the current user having set a time zone" do
let(:user_time_zone) { "Europe/Berlin" }
it "renders the time zone of the user" do
expect(formatted_time_zone_offset).to eql berlin_gmt
end
end
context "without the current user having a time zone and no default one configured" do
let(:user_time_zone) { "" }
it "renders the UTC time zone" do
expect(formatted_time_zone_offset).to eql "UTC+00:00"
end
end
context "without the current user having a time zone but with a default one configured",
with_settings: { user_default_timezone: "Europe/Berlin" } do
let(:user_time_zone) { "" }
it "renders the default time zone" do
expect(formatted_time_zone_offset).to eql berlin_gmt
end
end
context "without the current user having a time zone and also a default one configured",
with_settings: { user_default_timezone: "America/Atlanta" } do
let(:user_time_zone) { "Europe/Berlin" }
it "renders the default time zone" do
expect(formatted_time_zone_offset).to eql berlin_gmt
end
end
end
describe "day names" do
valid_languages.each do |lang|
context "for locale #{lang}" do
+1 -1
View File
@@ -48,7 +48,7 @@ RSpec.describe DeletedUser do
end
describe "#time_zone" do
it { expect(user.time_zone).to be_nil }
it { expect(user.time_zone).to eql ActiveSupport::TimeZone["Etc/UTC"] }
end
describe "#rss_key" do
+44
View File
@@ -222,4 +222,48 @@ RSpec.describe UserPreference do
expect(subject[:auto_hide_popups]).to eql(value_auto_hide_popups)
end
end
describe "#time_zone" do
context "with a time zone set and a default configured", with_settings: { user_default_timezone: "America/Los_Angeles" } do
let(:settings) { { "time_zone" => "Africa/Algiers" } }
it "returns the time zone set" do
expect(preference.time_zone).to eql "Africa/Algiers"
end
end
context "with no time zone configured but a default", with_settings: { user_default_timezone: "America/Los_Angeles" } do
it "returns the default time zone" do
expect(preference.time_zone).to eql "America/Los_Angeles"
end
end
context "with neiter a time zone configured nor a default one", with_settings: { user_default_timezone: "" } do
it "returns UTC" do
expect(preference.time_zone).to eql "Etc/UTC"
end
end
end
describe "#time_zone?" do
context "with a time zone set and a default configured", with_settings: { user_default_timezone: "America/Los_Angeles" } do
let(:settings) { { "time_zone" => "Africa/Algiers" } }
it "is true" do
expect(preference).to be_time_zone
end
end
context "with no time zone configured but a default", with_settings: { user_default_timezone: "America/Los_Angeles" } do
it "is false" do
expect(preference).not_to be_time_zone
end
end
context "with neiter a time zone configured nor a default one", with_settings: { user_default_timezone: "" } do
it "is false" do
expect(preference).not_to be_time_zone
end
end
end
end
+33
View File
@@ -823,6 +823,39 @@ RSpec.describe User do
end
end
describe "#time_zone" do
let(:user) { build(:user, preferences:) }
context "with an existing time zone in the prefs" do
let(:preferences) { { "time_zone" => "Europe/Athens" } }
it "returns the matching ActiveSupport::TimeZone" do
expect(user.time_zone)
.to eql ActiveSupport::TimeZone["Europe/Athens"]
end
end
context "with an invalid time zone" do
# Would need to be Etc/UTC or UTC to be valid
let(:preferences) { { "time_zone" => "utc" } }
it "returns the utc ActiveSupport::TimeZone" do
expect(user.time_zone)
.to eql ActiveSupport::TimeZone["Etc/UTC"]
end
end
context "without a time zone" do
# Would need to be Etc/UTC or UTC to be valid
let(:preferences) { {} }
it "returns the utc ActiveSupport::TimeZone" do
expect(user.time_zone)
.to eql ActiveSupport::TimeZone["Etc/UTC"]
end
end
end
describe "#find_by_mail" do
let!(:user1) { create(:user, mail: "foo+test@example.org") }
let!(:user2) { create(:user, mail: "foo@example.org") }
+2 -2
View File
@@ -32,8 +32,8 @@ RSpec.describe User, "default time zone" do
let(:user) { create(:user) }
context "with no system default set" do
it "is not set" do
expect(user.pref.time_zone).to be_nil
it "is still set to Etc/UTC as that will be calculated with internally" do
expect(user.pref.time_zone).to eq "Etc/UTC"
end
end
@@ -78,7 +78,7 @@ RSpec.describe WorkPackage::PDFExport::WorkPackageListToPdf do
end
end
let(:export_time) { DateTime.new(2024, 4, 22, 12, 37) }
let(:export_time_formatted) { format_time(export_time, true) }
let(:export_time_formatted) { format_time(export_time, include_date: true) }
let(:export) do
login_as(user)
work_packages
@@ -62,7 +62,7 @@ RSpec.describe WorkPackage::PDFExport::WorkPackageListToPdf do
member_with_permissions: { project => %w[view_work_packages export_work_packages] })
end
let(:export_time) { DateTime.new(2023, 6, 30, 23, 59) }
let(:export_time_formatted) { format_time(export_time, true) }
let(:export_time_formatted) { format_time(export_time, include_date: true) }
let(:work_package_parent) do
create(:work_package,
project:,
@@ -91,7 +91,7 @@ RSpec.describe WorkPackage::PDFExport::WorkPackageToPdf do
let(:category) { create(:category, project:, name: "Demo") }
let(:version) { create(:version, project:) }
let(:export_time) { DateTime.new(2023, 6, 30, 23, 59) }
let(:export_time_formatted) { format_time(export_time, true) }
let(:export_time_formatted) { format_time(export_time, include_date: true) }
let(:image_path) { Rails.root.join("spec/fixtures/files/image.png") }
let(:priority) { create(:priority_normal) }
let(:image_attachment) { Attachment.new author: user, file: File.open(image_path) }