From 63214c9327f9d39589a6ebefe38676be84e324a9 Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Sun, 10 Feb 2019 12:01:38 +0100 Subject: [PATCH] Fixes #1589 - No localised time displayed in trigger (autoreply/slack/...) notifications. --- .../area_item_default_timezone.coffee | 35 ++ .../app/controllers/getting_started.coffee | 1 + .../widget/default_timezone.coffee | 36 ++ .../javascripts/app/lib/app_post/i18n.coffee | 11 + app/controllers/calendars_controller.rb | 9 +- app/controllers/getting_started_controller.rb | 5 + app/models/ticket.rb | 11 +- app/models/transaction/notification.rb | 2 +- app/models/transaction/slack.rb | 3 +- app/models/translation.rb | 84 ++++ config/routes/calendar.rb | 13 +- ...20190208000001_setting_timezone_default.rb | 27 ++ db/seeds/settings.rb | 20 + lib/notification_factory/mailer.rb | 33 +- lib/notification_factory/renderer.rb | 37 +- lib/notification_factory/slack.rb | 32 +- .../notification_factory/renderer.rb | 2 +- spec/models/translation_spec.rb | 78 +++- spec/requests/calendar_spec.rb | 13 + .../notification_factory_renderer_test.rb | 403 ++++++++++-------- ...otification_factory_slack_template_test.rb | 28 +- test/unit/ticket_notification_test.rb | 30 +- 22 files changed, 696 insertions(+), 217 deletions(-) create mode 100644 app/assets/javascripts/app/controllers/_settings/area_item_default_timezone.coffee create mode 100644 app/assets/javascripts/app/controllers/widget/default_timezone.coffee create mode 100644 db/migrate/20190208000001_setting_timezone_default.rb diff --git a/app/assets/javascripts/app/controllers/_settings/area_item_default_timezone.coffee b/app/assets/javascripts/app/controllers/_settings/area_item_default_timezone.coffee new file mode 100644 index 000000000..b6933eed5 --- /dev/null +++ b/app/assets/javascripts/app/controllers/_settings/area_item_default_timezone.coffee @@ -0,0 +1,35 @@ +class App.SettingsAreaItemDefaultTimezone extends App.SettingsAreaItem + result: {} + + render: => + @fetchTimezones() + + localRender: (data) => + options = {} + for timezone, offset of data.timezones + if !offset.toString().match(/(\+|\-)/) + offset = "+#{offset}" + options[timezone] = "#{timezone} (GMT#{offset})" + configure_attributes = [ + { name: 'timezone_default', display: '', tag: 'searchable_select', null: false, class: 'input', options: options, default: @setting.state_current.value }, + ] + + @html App.view(@template)( + setting: @setting + ) + + new App.ControllerForm( + el: @el.find('.form-item') + model: { configure_attributes: configure_attributes, className: '' } + autofocus: false + ) + + fetchTimezones: => + @ajax( + id: 'calendar_timezones' + type: 'GET' + url: "#{@apiPath}/calendars/timezones" + success: (data) => + @result = data + @localRender(data) + ) diff --git a/app/assets/javascripts/app/controllers/getting_started.coffee b/app/assets/javascripts/app/controllers/getting_started.coffee index 263c0e570..f3817117b 100644 --- a/app/assets/javascripts/app/controllers/getting_started.coffee +++ b/app/assets/javascripts/app/controllers/getting_started.coffee @@ -340,6 +340,7 @@ class Base extends App.WizardFullScreen @params = @formParam(e.target) @params.logo = @logoPreview.attr('src') @params.locale_default = App.i18n.detectBrowserLocale() + @params.timezone_default = App.i18n.detectBrowserTimezone() store = (logoResizeDataUrl) => @params.logo_resize = logoResizeDataUrl diff --git a/app/assets/javascripts/app/controllers/widget/default_timezone.coffee b/app/assets/javascripts/app/controllers/widget/default_timezone.coffee new file mode 100644 index 000000000..a85b327c1 --- /dev/null +++ b/app/assets/javascripts/app/controllers/widget/default_timezone.coffee @@ -0,0 +1,36 @@ +class DefaultTimezone extends App.Controller + constructor: -> + super + + check = => + timezone = App.i18n.detectBrowserTimezone() + return if !timezone + + # check system timezone_default + if _.isEmpty(@Config('timezone_default')) && @permissionCheck('admin.system') + App.Setting.fetchFull( + => @updateSetting(timezone) + force: false + ) + + # prepare user based timezone + # check current user timezone + #preferences = App.Session.get('preferences') + #return if !preferences + #return if !_.isEmpty(preferences.timezone) + #@ajax( + # id: "i18n-set-user-timezone" + # type: 'PUT' + # url: "#{App.Config.get('api_path')}/users/preferences" + # data: JSON.stringify(timezone: timezone) + # processData: true + #) + + App.Event.bind('auth:login', (session) => + @delay(check, 8500, 'default_timezone') + ) + + updateSetting: (timezone) -> + App.Setting.set('timezone_default', timezone) + +App.Config.set('default_timezone', DefaultTimezone, 'Widgets') diff --git a/app/assets/javascripts/app/lib/app_post/i18n.coffee b/app/assets/javascripts/app/lib/app_post/i18n.coffee index 2cbdec418..c8bc27895 100644 --- a/app/assets/javascripts/app/lib/app_post/i18n.coffee +++ b/app/assets/javascripts/app/lib/app_post/i18n.coffee @@ -103,6 +103,17 @@ class App.i18n window.navigator.userLanguage || window.navigator.language || 'en-us' + @detectBrowserTimezone: -> + return if !window.Intl + return if !window.Intl.DateTimeFormat + DateTimeFormat = Intl.DateTimeFormat() + return if !DateTimeFormat + return if !DateTimeFormat.resolvedOptions + resolvedOptions = DateTimeFormat.resolvedOptions() + return if !resolvedOptions + return if !resolvedOptions.timeZone + resolvedOptions.timeZone + class _i18nSingleton extends Spine.Module @include App.LogInclude diff --git a/app/controllers/calendars_controller.rb b/app/controllers/calendars_controller.rb index c08ed2e71..e16e188ad 100644 --- a/app/controllers/calendars_controller.rb +++ b/app/controllers/calendars_controller.rb @@ -1,7 +1,8 @@ # Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ class CalendarsController < ApplicationController - prepend_before_action { authentication_check(permission: 'admin.calendar') } + prepend_before_action -> { authentication_check(permission: 'admin.calendar') }, only: %i[init index show create update destroy] + prepend_before_action -> { authentication_check(permission: 'admin') }, only: %i[timezones] def init assets = {} @@ -41,4 +42,10 @@ class CalendarsController < ApplicationController model_destroy_render(Calendar, params) end + def timezones + render json: { + timezones: Calendar.timezones + } + end + end diff --git a/app/controllers/getting_started_controller.rb b/app/controllers/getting_started_controller.rb index 367e231bf..46ab046df 100644 --- a/app/controllers/getting_started_controller.rb +++ b/app/controllers/getting_started_controller.rb @@ -148,6 +148,11 @@ curl http://localhost/api/v1/getting_started -v -u #{login}:#{password} settings[:locale_default] = params[:locale_default] end + # add timezone_default + if params[:timezone_default].present? + settings[:timezone_default] = params[:timezone_default] + end + if messages.present? render json: { result: 'invalid', diff --git a/app/models/ticket.rb b/app/models/ticket.rb index 28dd42579..740120b55 100644 --- a/app/models/ticket.rb +++ b/app/models/ticket.rb @@ -1543,10 +1543,13 @@ result end objects = build_notification_template_objects(article) - body = NotificationFactory::Renderer.new(objects, 'en-en', value['body'], false) - .render - .html2text - .tr(' ', ' ') # convert non-breaking space to simple space + body = NotificationFactory::Renderer.new( + objects: objects, + locale: 'en-en', + timezone: Setting.get('timezone_default'), + template: value['body'], + escape: false + ).render.html2text.tr(' ', ' ') # convert non-breaking space to simple space # attributes content_type is not needed for SMS article = Ticket::Article.create( diff --git a/app/models/transaction/notification.rb b/app/models/transaction/notification.rb index 36d653bd3..ec0d41f5e 100644 --- a/app/models/transaction/notification.rb +++ b/app/models/transaction/notification.rb @@ -242,7 +242,7 @@ class Transaction::Notification # only show allowed attributes attribute_list = ObjectManager::Attribute.by_object_as_hash('Ticket', user) - #puts "AL #{attribute_list.inspect}" + user_related_changes = {} @item[:changes].each do |key, value| diff --git a/app/models/transaction/slack.rb b/app/models/transaction/slack.rb index 5b972f792..b6b716ee8 100644 --- a/app/models/transaction/slack.rb +++ b/app/models/transaction/slack.rb @@ -84,7 +84,8 @@ class Transaction::Slack result = NotificationFactory::Slack.template( template: template, - locale: user[:preferences][:locale], + locale: user[:preferences][:locale] || Setting.get('locale_default'), + timezone: user[:preferences][:timezone] || Setting.get('timezone_default'), objects: { ticket: ticket, article: article, diff --git a/app/models/translation.rb b/app/models/translation.rb index acef5a6e5..9f627b5fb 100644 --- a/app/models/translation.rb +++ b/app/models/translation.rb @@ -215,6 +215,90 @@ translate strings in ruby context, e. g. for notifications =begin +translate timestampes in ruby context, e. g. for notifications + + translated = Translation.timestamp('de-de', 'Europe/Berlin', '2018-10-10T10:00:00Z0') + +or + + translated = Translation.timestamp('de-de', 'Europe/Berlin', Time.zone.parse('2018-10-10T10:00:00Z0')) + +=end + + def self.timestamp(locale, timezone, timestamp) + + if timestamp.class == String + begin + timestamp_parsed = Time.zone.parse(timestamp) + return timestamp.to_s if !timestamp_parsed + + timestamp = timestamp_parsed + rescue + return timestamp.to_s + end + end + + record = Translation.where(locale: locale, source: 'timestamp', format: 'time').pluck(:target).first + return timestamp.to_s if !record + + begin + timestamp = timestamp.in_time_zone(timezone) + rescue + return timestamp.to_s + end + record.sub!('dd', format('%02d', timestamp.day)) + record.sub!('d', timestamp.day.to_s) + record.sub!('mm', format('%02d', timestamp.month)) + record.sub!('m', timestamp.month.to_s) + record.sub!('yyyy', timestamp.year.to_s) + record.sub!('yy', timestamp.year.to_s.last(2)) + record.sub!('SS', format('%02d', timestamp.sec.to_s)) + record.sub!('MM', format('%02d', timestamp.min.to_s)) + record.sub!('HH', format('%02d', timestamp.hour.to_s)) + "#{record} (#{timezone})" + end + +=begin + +translate date in ruby context, e. g. for notifications + + translated = Translation.date('de-de', '2018-10-10') + +or + + translated = Translation.date('de-de', Date.parse('2018-10-10')) + +=end + + def self.date(locale, date) + + if date.class == String + begin + date_parsed = Date.parse(date) + return date.to_s if !date_parsed + + date = date_parsed + rescue + return date.to_s + end + end + + return date.to_s if date.class != Date + + record = Translation.where(locale: locale, source: 'date', format: 'time').pluck(:target).first + return date.to_s if !record + + record.sub!('dd', format('%02d', date.day)) + record.sub!('d', date.day.to_s) + record.sub!('mm', format('%02d', date.month)) + record.sub!('m', date.month.to_s) + record.sub!('yyyy', date.year.to_s) + record.sub!('yy', date.year.to_s.last(2)) + record + end + +=begin + load translations from local all: diff --git a/config/routes/calendar.rb b/config/routes/calendar.rb index d83201939..caff7b5d4 100644 --- a/config/routes/calendar.rb +++ b/config/routes/calendar.rb @@ -2,11 +2,12 @@ Zammad::Application.routes.draw do api_path = Rails.configuration.api_path # calendars - match api_path + '/calendars_init', to: 'calendars#init', via: :get - match api_path + '/calendars', to: 'calendars#index', via: :get - match api_path + '/calendars/:id', to: 'calendars#show', via: :get - match api_path + '/calendars', to: 'calendars#create', via: :post - match api_path + '/calendars/:id', to: 'calendars#update', via: :put - match api_path + '/calendars/:id', to: 'calendars#destroy', via: :delete + match api_path + '/calendars_init', to: 'calendars#init', via: :get + match api_path + '/calendars/timezones', to: 'calendars#timezones', via: :get + match api_path + '/calendars', to: 'calendars#index', via: :get + match api_path + '/calendars/:id', to: 'calendars#show', via: :get + match api_path + '/calendars', to: 'calendars#create', via: :post + match api_path + '/calendars/:id', to: 'calendars#update', via: :put + match api_path + '/calendars/:id', to: 'calendars#destroy', via: :delete end diff --git a/db/migrate/20190208000001_setting_timezone_default.rb b/db/migrate/20190208000001_setting_timezone_default.rb new file mode 100644 index 000000000..564563e48 --- /dev/null +++ b/db/migrate/20190208000001_setting_timezone_default.rb @@ -0,0 +1,27 @@ +class SettingTimezoneDefault < ActiveRecord::Migration[5.1] + def up + # return if it's a new setup + return if !Setting.find_by(name: 'system_init_done') + + Setting.create_if_not_exists( + title: 'Timezone', + name: 'timezone_default', + area: 'System::Branding', + description: 'Defines the system default timezone.', + options: { + form: [ + { + name: 'timezone_default', + } + ], + }, + state: '', + preferences: { + prio: 9, + controller: 'SettingsAreaItemDefaultTimezone', + permission: ['admin.system'], + }, + frontend: true + ) + end +end diff --git a/db/seeds/settings.rb b/db/seeds/settings.rb index 6d90cafb9..778538f0c 100644 --- a/db/seeds/settings.rb +++ b/db/seeds/settings.rb @@ -176,6 +176,26 @@ Setting.create_if_not_exists( }, frontend: true ) +Setting.create_if_not_exists( + title: 'Timezone', + name: 'timezone_default', + area: 'System::Branding', + description: 'Defines the system default timezone.', + options: { + form: [ + { + name: 'timezone_default', + } + ], + }, + state: '', + preferences: { + prio: 9, + controller: 'SettingsAreaItemDefaultTimezone', + permission: ['admin.system'], + }, + frontend: true +) Setting.create_or_update( title: 'Pretty Date', name: 'pretty_date_format', diff --git a/lib/notification_factory/mailer.rb b/lib/notification_factory/mailer.rb index ee90b0bed..463e3bd87 100644 --- a/lib/notification_factory/mailer.rb +++ b/lib/notification_factory/mailer.rb @@ -228,6 +228,7 @@ retunes result = NotificationFactory::Mailer.template( template: 'password_reset', locale: 'en-us', + timezone: 'America/Santiago', objects: { recipient: User.find(2), }, @@ -236,6 +237,7 @@ retunes result = NotificationFactory::Mailer.template( templateInline: "Invitation to \#{config.product_name} at \#{config.fqdn}", locale: 'en-us', + timezone: 'America/Santiago', objects: { recipient: User.find(2), }, @@ -247,6 +249,7 @@ only raw subject/body result = NotificationFactory::Mailer.template( template: 'password_reset', locale: 'en-us', + timezone: 'America/Santiago', objects: { recipient: User.find(2), }, @@ -266,7 +269,13 @@ returns def self.template(data) if data[:templateInline] - return NotificationFactory::Renderer.new(data[:objects], data[:locale], data[:templateInline], data[:quote]).render + return NotificationFactory::Renderer.new( + objects: data[:objects], + locale: data[:locale], + timezone: data[:timezone], + template: data[:templateInline], + escape: data[:quote] + ).render end template = NotificationFactory.template_read( @@ -276,8 +285,19 @@ returns type: 'mailer', ) - message_subject = NotificationFactory::Renderer.new(data[:objects], data[:locale], template[:subject], false).render - message_body = NotificationFactory::Renderer.new(data[:objects], data[:locale], template[:body]).render + message_subject = NotificationFactory::Renderer.new( + objects: data[:objects], + locale: data[:locale], + timezone: data[:timezone], + template: template[:subject], + escape: false + ).render + message_body = NotificationFactory::Renderer.new( + objects: data[:objects], + locale: data[:locale], + timezone: data[:timezone], + template: template[:body] + ).render if !data[:raw] application_template = NotificationFactory.application_template_read( @@ -286,7 +306,12 @@ returns ) data[:objects][:message] = message_body data[:objects][:standalone] = data[:standalone] - message_body = NotificationFactory::Renderer.new(data[:objects], data[:locale], application_template).render + message_body = NotificationFactory::Renderer.new( + objects: data[:objects], + locale: data[:locale], + timezone: data[:timezone], + template: application_template + ).render end { subject: message_subject, diff --git a/lib/notification_factory/renderer.rb b/lib/notification_factory/renderer.rb index bc7f5dfa8..eaf23d9af 100644 --- a/lib/notification_factory/renderer.rb +++ b/lib/notification_factory/renderer.rb @@ -5,27 +5,30 @@ class NotificationFactory::Renderer examples how to use message_subject = NotificationFactory::Renderer.new( - { + objects: { ticket: Ticket.first, }, - 'de-de', - 'some template #{ticket.title} {config.fqdn}', - false + locale: 'de-de', + timezone: 'America/Port-au-Prince', + template: 'some template #{ticket.title} {config.fqdn}', + escape: false ).render message_body = NotificationFactory::Renderer.new( - { + objects: { ticket: Ticket.first, }, - 'de-de', - 'some template #{ticket.title} #{config.fqdn}', + locale: 'de-de', + timezone: 'America/Port-au-Prince', + template: 'some template #{ticket.title} #{config.fqdn}', ).render =end - def initialize(objects, locale, template, escape = true) + def initialize(objects:, locale: nil, timezone: nil, template:, escape: true) @objects = objects @locale = locale || Setting.get('locale_default') || 'en-us' + @timezone = timezone || Setting.get('timezone_default') @template = NotificationFactory::Template.new(template, escape) @escape = escape end @@ -141,7 +144,8 @@ examples how to use else value end - escaping(placeholder, escape) + + escaping(convert_to_timezone(placeholder), escape) end # c - config @@ -159,15 +163,22 @@ examples how to use end # h - htmlEscape - # h('fqdn', htmlEscape) - def h(key) - return key if !key + # h(htmlEscape) + def h(value) + return value if !value - CGI.escapeHTML(key.to_s) + CGI.escapeHTML(convert_to_timezone(value).to_s) end private + def convert_to_timezone(value) + return Translation.timestamp(@locale, @timezone, value) if value.class == ActiveSupport::TimeWithZone + return Translation.date(@locale, value) if value.class == Date + + value + end + def escaping(key, escape) return key if escape == false return key if escape.nil? && !@escape diff --git a/lib/notification_factory/slack.rb b/lib/notification_factory/slack.rb index 78f0814e5..85542268e 100644 --- a/lib/notification_factory/slack.rb +++ b/lib/notification_factory/slack.rb @@ -5,6 +5,7 @@ class NotificationFactory::Slack result = NotificationFactory::Slack.template( template: 'ticket_update', locale: 'en-us', + timezone: 'Europe/Berlin', objects: { recipient: User.find(2), ticket: Ticket.find(1) @@ -23,7 +24,12 @@ returns def self.template(data) if data[:templateInline] - return NotificationFactory::Renderer.new(data[:objects], data[:locale], data[:templateInline]).render + return NotificationFactory::Renderer.new( + objects: data[:objects], + locale: data[:locale], + timezone: data[:timezone], + template: data[:templateInline] + ).render end template = NotificationFactory.template_read( @@ -33,8 +39,20 @@ returns type: 'slack', ) - message_subject = NotificationFactory::Renderer.new(data[:objects], data[:locale], template[:subject], false).render - message_body = NotificationFactory::Renderer.new(data[:objects], data[:locale], template[:body], false).render + message_subject = NotificationFactory::Renderer.new( + objects: data[:objects], + locale: data[:locale], + timezone: data[:timezone], + template: template[:subject], + escape: false + ).render + message_body = NotificationFactory::Renderer.new( + objects: data[:objects], + locale: data[:locale], + timezone: data[:timezone], + template: template[:body], + escape: false + ).render if !data[:raw] application_template = NotificationFactory.application_template_read( @@ -43,7 +61,13 @@ returns ) data[:objects][:message] = message_body data[:objects][:standalone] = data[:standalone] - message_body = NotificationFactory::Renderer.new(data[:objects], data[:locale], application_template, false).render + message_body = NotificationFactory::Renderer.new( + objects: data[:objects], + locale: data[:locale], + timezone: data[:timezone], + template: application_template, + escape: false + ).render end { subject: message_subject.strip!, diff --git a/spec/factories/notification_factory/renderer.rb b/spec/factories/notification_factory/renderer.rb index abc251d92..c957878c1 100644 --- a/spec/factories/notification_factory/renderer.rb +++ b/spec/factories/notification_factory/renderer.rb @@ -5,6 +5,6 @@ FactoryBot.define do template '' escape true - initialize_with { new(objects, locale, template, escape) } + initialize_with { new(objects: objects, locale: locale, template: template, escape: escape) } end end diff --git a/spec/models/translation_spec.rb b/spec/models/translation_spec.rb index 94e2be0cd..1036cb801 100644 --- a/spec/models/translation_spec.rb +++ b/spec/models/translation_spec.rb @@ -7,7 +7,7 @@ RSpec.describe Translation do Translation.sync('de-de') end - context 'default translations' do + context 'default string translations' do it 'en with existing word' do expect(Translation.translate('en', 'New')).to eq('New') @@ -31,6 +31,82 @@ RSpec.describe Translation do end + context 'default timestamp translations' do + + it 'de-de with array' do + expect(Translation.timestamp('de-de', 'Europe/Berlin', ['some value'])).to eq('["some value"]') + end + + it 'not_existing with timestamp as string' do + expect(Translation.timestamp('not_existing', 'Europe/Berlin', '2018-10-10T10:00:00Z0')).to eq('2018-10-10 10:00:00 UTC') + end + + it 'not_existing with time object' do + expect(Translation.timestamp('not_existing', 'Europe/Berlin', Time.zone.parse('2018-10-10T10:00:00Z0'))).to eq('2018-10-10 10:00:00 UTC') + end + + it 'not_existing with invalid timestamp string' do + expect(Translation.timestamp('not_existing', 'Europe/Berlin', 'something')).to eq('something') + end + + it 'en-us with invalid time zone' do + expect(Translation.timestamp('en-us', 'Europe/Berlin', '2018-10-10T10:00:00Z0')).to eq('10/10/2018 12:00 (Europe/Berlin)') + end + + it 'en-us with timestamp as string' do + expect(Translation.timestamp('en-us', 'Europe/Berlin', '2018-10-10T10:00:00Z0')).to eq('10/10/2018 12:00 (Europe/Berlin)') + end + + it 'en-us with time object' do + expect(Translation.timestamp('en-us', 'Europe/Berlin', Time.zone.parse('2018-10-10T10:00:00Z0'))).to eq('10/10/2018 12:00 (Europe/Berlin)') + end + + it 'de-de with timestamp as string' do + expect(Translation.timestamp('de-de', 'Europe/Berlin', '2018-10-10T10:00:00Z0')).to eq('10.10.2018 12:00 (Europe/Berlin)') + end + + it 'de-de with time object' do + expect(Translation.timestamp('de-de', 'Europe/Berlin', Time.zone.parse('2018-10-10T10:00:00Z0'))).to eq('10.10.2018 12:00 (Europe/Berlin)') + end + + end + + context 'default date translations' do + + it 'de-de with array' do + expect(Translation.date('de-de', ['some value'])).to eq('["some value"]') + end + + it 'not_existing with date as string' do + expect(Translation.date('not_existing', '2018-10-10')).to eq('2018-10-10') + end + + it 'not_existing with date object' do + expect(Translation.date('not_existing', Date.parse('2018-10-10'))).to eq('2018-10-10') + end + + it 'not_existing with invalid data as string' do + expect(Translation.date('not_existing', 'something')).to eq('something') + end + + it 'en-us with date as string' do + expect(Translation.date('en-us', '2018-10-10')).to eq('10/10/2018') + end + + it 'en-us with date object' do + expect(Translation.date('en-us', Date.parse('2018-10-10'))).to eq('10/10/2018') + end + + it 'de-de with date as string' do + expect(Translation.date('de-de', '2018-10-10')).to eq('10.10.2018') + end + + it 'de-de with date object' do + expect(Translation.date('de-de', Date.parse('2018-10-10'))).to eq('10.10.2018') + end + + end + context 'remote_translation_need_update? tests' do it 'translation is still the same' do diff --git a/spec/requests/calendar_spec.rb b/spec/requests/calendar_spec.rb index d2b900996..5f811e7ab 100644 --- a/spec/requests/calendar_spec.rb +++ b/spec/requests/calendar_spec.rb @@ -20,6 +20,12 @@ RSpec.describe 'Calendars', type: :request do expect(json_response).to be_a_kind_of(Hash) expect(json_response['error']).to eq('authentication failed') + + get '/api/v1/calendars/timezones', as: :json + expect(response).to have_http_status(401) + + expect(json_response).to be_a_kind_of(Hash) + expect(json_response['error']).to eq('authentication failed') end it 'does calendar index with admin' do @@ -58,6 +64,13 @@ RSpec.describe 'Calendars', type: :request do expect(json_response['timezones']['America/Sitka']).to be_between(-9, -8) expect(json_response['timezones']['Europe/Berlin']).to be_between(1, 2) expect(json_response['assets']).to be_truthy + + # timezones + get '/api/v1/calendars/timezones', as: :json + expect(response).to have_http_status(200) + expect(json_response).to be_a_kind_of(Hash) + expect(json_response['timezones']).to be_a_kind_of(Hash) + expect(json_response['timezones']['America/New_York']).to be_truthy end end diff --git a/test/unit/notification_factory_renderer_test.rb b/test/unit/notification_factory_renderer_test.rb index 759891d40..be5f7e249 100644 --- a/test/unit/notification_factory_renderer_test.rb +++ b/test/unit/notification_factory_renderer_test.rb @@ -39,149 +39,164 @@ class NotificationFactoryRendererTest < ActiveSupport::TestCase template = "\#{ticket.title}" result = described_class.new( - { + objects: { ticket: ticket, }, - 'en-us', - template, + locale: 'en-us', + timezone: 'Europe/Berlin', + template: template, ).render assert_equal(CGI.escapeHTML(ticket.title), result) template = "\#{ticket.created_at}" result = described_class.new( - { + objects: { ticket: ticket, }, - 'en-us', - template, + locale: 'en-us', + timezone: 'Europe/Berlin', + template: template, ).render - assert_equal(ticket.created_at.to_s, result) + assert_equal('11/12/2016 13:00 (Europe/Berlin)', result) template = "\#{ticket.created_by.firstname}" result = described_class.new( - { + objects: { ticket: ticket, }, - 'en-us', - template, + locale: 'en-us', + timezone: 'Europe/Berlin', + template: template, ).render assert_equal('CurrentUser<b>xxx</b>', result) template = "\#{ticket.updated_at}" result = described_class.new( - { + objects: { ticket: ticket, }, - 'en-us', - template, + locale: 'en-us', + timezone: 'Europe/Berlin', + template: template, ).render - assert_equal(ticket.updated_at.to_s, result) + assert_equal('11/12/2016 15:00 (Europe/Berlin)', result) template = "\#{ticket.updated_by.firstname}" result = described_class.new( - { + objects: { ticket: ticket, }, - 'en-us', - template, + locale: 'en-us', + timezone: 'Europe/Berlin', + template: template, ).render assert_equal('CurrentUser<b>xxx</b>', result) template = "\#{ticket.owner.firstname}" result = described_class.new( - { + objects: { ticket: ticket, }, - 'en-us', - template, + locale: 'en-us', + timezone: 'Europe/Berlin', + template: template, ).render assert_equal('Owner<b>xxx</b>', result) template = "\#{ticket. title}" result = described_class.new( - { + objects: { ticket: ticket, }, - 'en-us', - template, + locale: 'en-us', + timezone: 'Europe/Berlin', + template: template, ).render assert_equal(CGI.escapeHTML(ticket.title), result) template = "\#{ticket.\n title}" result = described_class.new( - { + objects: { ticket: ticket, }, - 'en-us', - template, + locale: 'en-us', + timezone: 'Europe/Berlin', + template: template, ).render assert_equal(CGI.escapeHTML(ticket.title), result) template = "\#{ticket.\t title}" result = described_class.new( - { + objects: { ticket: ticket, }, - 'en-us', - template, + locale: 'en-us', + timezone: 'Europe/Berlin', + template: template, ).render assert_equal(CGI.escapeHTML(ticket.title), result) template = "\#{ticket.\t\n title\t}" result = described_class.new( - { + objects: { ticket: ticket, }, - 'en-us', - template, + locale: 'en-us', + timezone: 'Europe/Berlin', + template: template, ).render assert_equal(CGI.escapeHTML(ticket.title), result) template = "\#{ticket.\" title\t}" result = described_class.new( - { + objects: { ticket: ticket, }, - 'en-us', - template, + locale: 'en-us', + timezone: 'Europe/Berlin', + template: template, ).render assert_equal(CGI.escapeHTML(ticket.title), result) template = "\#{ticket.\" title}" result = described_class.new( - { + objects: { ticket: ticket, }, - 'en-us', - template, + locale: 'en-us', + timezone: 'Europe/Berlin', + template: template, ).render assert_equal(CGI.escapeHTML(ticket.title), result) template = "some test
\#{article.body}" result = described_class.new( - { + objects: { article: article_html1, }, - 'en-us', - template, + locale: 'en-us', + timezone: 'Europe/Berlin', + template: template, ).render assert_equal('some test
> test hello
> some new line
', result) result = described_class.new( - { + objects: { article: article_plain1, }, - 'en-us', - template, + locale: 'en-us', + timezone: 'Europe/Berlin', + template: template, ).render assert_equal('some test
> test <b>hello</b>
> some new line
', result) result = described_class.new( - { + objects: { article: article_plain2, }, - 'en-us', - template, + locale: 'en-us', + timezone: 'Europe/Berlin', + template: template, ).render assert_equal('some test
> test <b>hello</b>
> some new line
', result) @@ -192,11 +207,12 @@ class NotificationFactoryRendererTest < ActiveSupport::TestCase setting = 'fqdn' template = "\#{config.#{setting}}" result = described_class.new( - { + objects: { ticket: ticket, }, - 'en-us', - template, + locale: 'en-us', + timezone: 'Europe/Berlin', + template: template, ).render assert_equal(Setting.get(setting), result) @@ -204,11 +220,12 @@ class NotificationFactoryRendererTest < ActiveSupport::TestCase setting2 = 'product_name' template = "some \#{config.#{setting1}} and \#{config.#{setting2}}" result = described_class.new( - { + objects: { ticket: ticket, }, - 'en-us', - template, + locale: 'en-us', + timezone: 'Europe/Berlin', + template: template, ).render assert_equal("some #{Setting.get(setting1)} and #{Setting.get(setting2)}", result) @@ -216,11 +233,12 @@ class NotificationFactoryRendererTest < ActiveSupport::TestCase setting2 = 'product_name' template = "some \#{ config.#{setting1}} and \#{\tconfig.#{setting2}}" result = described_class.new( - { + objects: { ticket: ticket, }, - 'en-us', - template, + locale: 'en-us', + timezone: 'Europe/Berlin', + template: template, ).render assert_equal("some #{Setting.get(setting1)} and #{Setting.get(setting2)}", result) end @@ -230,41 +248,45 @@ class NotificationFactoryRendererTest < ActiveSupport::TestCase #template = "<%= t 'new' %>" template = "\#{t('new')}" result = described_class.new( - { + objects: { ticket: ticket, }, - 'de-de', - template, + locale: 'de-de', + timezone: 'Europe/Berlin', + template: template, ).render assert_equal('neu', result) template = "some text \#{t('new')} and \#{t('open')}" result = described_class.new( - { + objects: { ticket: ticket, }, - 'de-de', - template, + locale: 'de-de', + timezone: 'Europe/Berlin', + template: template, ).render assert_equal('some text neu and offen', result) template = "some text \#{t('new') } and \#{ t('open')}" result = described_class.new( - { + objects: { ticket: ticket, }, - 'de-de', - template, + locale: 'de-de', + timezone: 'Europe/Berlin', + template: template, ).render assert_equal('some text neu and offen', result) template = "some text \#{\nt('new') } and \#{ t('open')\t}" result = described_class.new( - { + objects: { ticket: ticket, }, - 'de-de', - template, + locale: 'de-de', + timezone: 'Europe/Berlin', + template: template, ).render assert_equal('some text neu and offen', result) @@ -275,11 +297,12 @@ class NotificationFactoryRendererTest < ActiveSupport::TestCase template = "\#{t(ticket.state.name)}" result = described_class.new( - { + objects: { ticket: ticket, }, - 'de-de', - template, + locale: 'de-de', + timezone: 'Europe/Berlin', + template: template, ).render assert_equal('neu', result) @@ -289,111 +312,122 @@ class NotificationFactoryRendererTest < ActiveSupport::TestCase template = "\#{}" result = described_class.new( - { + objects: { ticket: ticket, }, - 'en-us', - template, + locale: 'en-us', + timezone: 'Europe/Berlin', + template: template, ).render assert_equal(CGI.escapeHTML('#{no such object}'), result) template = "\#{notexsiting.notexsiting}" result = described_class.new( - { + objects: { ticket: ticket, }, - 'en-us', - template, + locale: 'en-us', + timezone: 'Europe/Berlin', + template: template, ).render assert_equal(CGI.escapeHTML('#{notexsiting / no such object}'), result) template = "\#{ticket.notexsiting}" result = described_class.new( - { + objects: { ticket: ticket, }, - 'en-us', - template, + locale: 'en-us', + timezone: 'Europe/Berlin', + template: template, ).render assert_equal(CGI.escapeHTML('#{ticket.notexsiting / no such method}'), result) template = "\#{ticket.}" result = described_class.new( - { + objects: { ticket: ticket, }, - 'en-us', - template, + locale: 'en-us', + timezone: 'Europe/Berlin', + template: template, ).render assert_equal(CGI.escapeHTML('#{ticket. / no such method}'), result) template = "\#{ticket.title.notexsiting}" result = described_class.new( - { + objects: { ticket: ticket, }, - 'en-us', - template, + locale: 'en-us', + timezone: 'Europe/Berlin', + template: template, ).render assert_equal(CGI.escapeHTML('#{ticket.title.notexsiting / no such method}'), result) template = "\#{ticket.notexsiting.notexsiting}" result = described_class.new( - { + objects: { ticket: ticket, }, - 'en-us', - template, + locale: 'en-us', + timezone: 'Europe/Berlin', + template: template, ).render assert_equal(CGI.escapeHTML('#{ticket.notexsiting / no such method}'), result) template = "\#{notexsiting}" result = described_class.new( - { + objects: { ticket: ticket, }, - 'en-us', - template, + locale: 'en-us', + timezone: 'Europe/Berlin', + template: template, ).render assert_equal(CGI.escapeHTML('#{notexsiting / no such object}'), result) template = "\#{notexsiting.}" result = described_class.new( - { + objects: { ticket: ticket, }, - 'en-us', - template, + locale: 'en-us', + timezone: 'Europe/Berlin', + template: template, ).render assert_equal(CGI.escapeHTML('#{notexsiting / no such object}'), result) template = "\#{string}" result = described_class.new( - { + objects: { string: 'some string', }, - 'en-us', - template, + locale: 'en-us', + timezone: 'Europe/Berlin', + template: template, ).render assert_equal(CGI.escapeHTML('some string'), result) template = "\#{fixum}" result = described_class.new( - { + objects: { fixum: 123, }, - 'en-us', - template, + locale: 'en-us', + timezone: 'Europe/Berlin', + template: template, ).render assert_equal(CGI.escapeHTML('123'), result) template = "\#{float}" result = described_class.new( - { + objects: { float: 123.99, }, - 'en-us', - template, + locale: 'en-us', + timezone: 'Europe/Berlin', + template: template, ).render assert_equal(CGI.escapeHTML('123.99'), result) @@ -403,181 +437,199 @@ class NotificationFactoryRendererTest < ActiveSupport::TestCase template = "\#{ticket.title `echo 1`}" result = described_class.new( - { + objects: { ticket: ticket, }, - 'en-us', - template, + locale: 'en-us', + timezone: 'Europe/Berlin', + template: template, ).render assert_equal(CGI.escapeHTML('#{ticket.title`echo1` / not allowed}'), result) template = "\#{ticket.destroy}" result = described_class.new( - { + objects: { ticket: ticket, }, - 'en-us', - template, + locale: 'en-us', + timezone: 'Europe/Berlin', + template: template, ).render assert_equal(CGI.escapeHTML('#{ticket.destroy / not allowed}'), result) template = "\#{ticket.save}" result = described_class.new( - { + objects: { ticket: ticket, }, - 'en-us', - template, + locale: 'en-us', + timezone: 'Europe/Berlin', + template: template, ).render assert_equal(CGI.escapeHTML('#{ticket.save / not allowed}'), result) template = "\#{ticket.update}" result = described_class.new( - { + objects: { ticket: ticket, }, - 'en-us', - template, + locale: 'en-us', + timezone: 'Europe/Berlin', + template: template, ).render assert_equal(CGI.escapeHTML('#{ticket.update / not allowed}'), result) template = "\#{ticket.create}" result = described_class.new( - { + objects: { ticket: ticket, }, - 'en-us', - template, + locale: 'en-us', + timezone: 'Europe/Berlin', + template: template, ).render assert_equal(CGI.escapeHTML('#{ticket.create / not allowed}'), result) template = "\#{ticket.delete}" result = described_class.new( - { + objects: { ticket: ticket, }, - 'en-us', - template, + locale: 'en-us', + timezone: 'Europe/Berlin', + template: template, ).render assert_equal(CGI.escapeHTML('#{ticket.delete / not allowed}'), result) template = "\#{ticket.remove}" result = described_class.new( - { + objects: { ticket: ticket, }, - 'en-us', - template, + locale: 'en-us', + timezone: 'Europe/Berlin', + template: template, ).render assert_equal(CGI.escapeHTML('#{ticket.remove / not allowed}'), result) template = "\#{ticket.drop}" result = described_class.new( - { + objects: { ticket: ticket, }, - 'en-us', - template, + locale: 'en-us', + timezone: 'Europe/Berlin', + template: template, ).render assert_equal(CGI.escapeHTML('#{ticket.drop / not allowed}'), result) template = "\#{ticket.create}" result = described_class.new( - { + objects: { ticket: ticket, }, - 'en-us', - template, + locale: 'en-us', + timezone: 'Europe/Berlin', + template: template, ).render assert_equal(CGI.escapeHTML('#{ticket.create / not allowed}'), result) template = "\#{ticket.new}" result = described_class.new( - { + objects: { ticket: ticket, }, - 'en-us', - template, + locale: 'en-us', + timezone: 'Europe/Berlin', + template: template, ).render assert_equal(CGI.escapeHTML('#{ticket.new / not allowed}'), result) template = "\#{ticket.update_att}" result = described_class.new( - { + objects: { ticket: ticket, }, - 'en-us', - template, + locale: 'en-us', + timezone: 'Europe/Berlin', + template: template, ).render assert_equal(CGI.escapeHTML('#{ticket.update_att / not allowed}'), result) template = "\#{ticket.all}" result = described_class.new( - { + objects: { ticket: ticket, }, - 'en-us', - template, + locale: 'en-us', + timezone: 'Europe/Berlin', + template: template, ).render assert_equal(CGI.escapeHTML('#{ticket.all / not allowed}'), result) template = "\#{ticket.find}" result = described_class.new( - { + objects: { ticket: ticket, }, - 'en-us', - template, + locale: 'en-us', + timezone: 'Europe/Berlin', + template: template, ).render assert_equal(CGI.escapeHTML('#{ticket.find / not allowed}'), result) template = "\#{ticket.where}" result = described_class.new( - { + objects: { ticket: ticket, }, - 'en-us', - template, + locale: 'en-us', + timezone: 'Europe/Berlin', + template: template, ).render assert_equal(CGI.escapeHTML('#{ticket.where / not allowed}'), result) template = "\#{ticket. destroy}" result = described_class.new( - { + objects: { ticket: ticket, }, - 'en-us', - template, + locale: 'en-us', + timezone: 'Europe/Berlin', + template: template, ).render assert_equal(CGI.escapeHTML('#{ticket.destroy / not allowed}'), result) template = "\#{ticket.\n destroy}" result = described_class.new( - { + objects: { ticket: ticket, }, - 'en-us', - template, + locale: 'en-us', + timezone: 'Europe/Berlin', + template: template, ).render assert_equal(CGI.escapeHTML("\#{ticket.destroy / not allowed}"), result) template = "\#{ticket.\t destroy}" result = described_class.new( - { + objects: { ticket: ticket, }, - 'en-us', - template, + locale: 'en-us', + timezone: 'Europe/Berlin', + template: template, ).render assert_equal(CGI.escapeHTML("\#{ticket.destroy / not allowed}"), result) template = "\#{ticket.\r destroy}" result = described_class.new( - { + objects: { ticket: ticket, }, - 'en-us', - template, + locale: 'en-us', + timezone: 'Europe/Berlin', + template: template, ).render assert_equal(CGI.escapeHTML("\#{ticket.destroy / not allowed}"), result) @@ -587,51 +639,56 @@ class NotificationFactoryRendererTest < ActiveSupport::TestCase template = "\#{ticket.title.first(3)}" result = described_class.new( - { + objects: { ticket: ticket, }, - 'en-us', - template, + locale: 'en-us', + timezone: 'Europe/Berlin', + template: template, ).render assert_equal(CGI.escapeHTML(''), result) template = "\#{ticket.title.last(4)}" result = described_class.new( - { + objects: { ticket: ticket, }, - 'en-us', - template, + locale: 'en-us', + timezone: 'Europe/Berlin', + template: template, ).render assert_equal(CGI.escapeHTML(''), result) template = "\#{ticket.title.slice(3, 4)}" result = described_class.new( - { + objects: { ticket: ticket, }, - 'en-us', - template, + locale: 'en-us', + timezone: 'Europe/Berlin', + template: template, ).render assert_equal(CGI.escapeHTML("\#{ticket.title.slice(3,4) / invalid parameter: 3,4}"), result) template = "\#{ticket.title.first('some invalid parameter')}" result = described_class.new( - { + objects: { ticket: ticket, }, - 'en-us', - template, + locale: 'en-us', + timezone: 'Europe/Berlin', + template: template, ).render assert_equal("\#{ticket.title.first(someinvalidparameter) / invalid parameter: someinvalidparameter}", result) template = "\#{ticket.title.chomp(`cat /etc/passwd`)}" result = described_class.new( - { + objects: { ticket: ticket, }, - 'en-us', - template, + locale: 'en-us', + timezone: 'Europe/Berlin', + template: template, ).render assert_equal("\#{ticket.title.chomp(`cat/etc/passwd`) / not allowed}", result) end diff --git a/test/unit/notification_factory_slack_template_test.rb b/test/unit/notification_factory_slack_template_test.rb index 2a1b23ae9..411b7e806 100644 --- a/test/unit/notification_factory_slack_template_test.rb +++ b/test/unit/notification_factory_slack_template_test.rb @@ -66,7 +66,8 @@ class NotificationFactorySlackTemplateTest < ActiveSupport::TestCase changes = {} result = NotificationFactory::Slack.template( template: 'ticket_create', - locale: 'es-us', + locale: 'en-us', + timezone: 'Europe/Berlin', objects: { ticket: ticket, article: article, @@ -97,12 +98,14 @@ class NotificationFactorySlackTemplateTest < ActiveSupport::TestCase created_by_id: 1, ) changes = { - state: %w[aaa bbb], - group: %w[xxx yyy], + state: %w[aaa bbb], + group: %w[xxx yyy], + pending_time: [Time.zone.parse('2019-04-01T10:00:00Z0'), Time.zone.parse('2019-04-01T23:00:00Z0')], } result = NotificationFactory::Slack.template( template: 'ticket_update', - locale: 'es-us', + locale: 'en-us', + timezone: 'Europe/Berlin', objects: { ticket: ticket, article: article, @@ -111,14 +114,31 @@ class NotificationFactorySlackTemplateTest < ActiveSupport::TestCase changes: changes, }, ) + assert_match('# Welcome to Zammad!', result[:subject]) assert_match('Userxxx', result[:body]) assert_match('state: aaa -> bbb', result[:body]) assert_match('group: xxx -> yyy', result[:body]) + assert_match('pending_time: 04/01/2019 12:00 (Europe/Berlin) -> 04/02/2019 01:00 (Europe/Berlin)', result[:body]) assert_no_match('Dein', result[:body]) assert_no_match('longname', result[:body]) assert_match('Current User', result[:body]) + # en notification + ticket.escalation_at = Time.zone.parse('2019-04-01T10:00:00Z') + result = NotificationFactory::Slack.template( + template: 'ticket_escalation', + locale: 'en-us', + timezone: 'Europe/Berlin', + objects: { + ticket: ticket, + article: article, + recipient: agent1, + } + ) + + assert_match('# Welcome to Zammad!', result[:subject]) + assert_match('is escalated since "04/01/2019 12:00 (Europe/Berlin)"!', result[:body]) end end diff --git a/test/unit/ticket_notification_test.rb b/test/unit/ticket_notification_test.rb index f1e965127..1934dabe0 100644 --- a/test/unit/ticket_notification_test.rb +++ b/test/unit/ticket_notification_test.rb @@ -2,6 +2,7 @@ require 'test_helper' class TicketNotificationTest < ActiveSupport::TestCase setup do + Setting.set('timezone_default', 'Europe/Berlin') Trigger.create_or_update( name: 'auto reply - new ticket', condition: { @@ -78,7 +79,8 @@ class TicketNotificationTest < ActiveSupport::TestCase roles: roles, groups: groups, preferences: { - locale: 'en-ca', + locale: 'en-us', + timezone: 'America/St_Lucia', }, updated_by_id: 1, created_by_id: 1, @@ -1152,6 +1154,7 @@ class TicketNotificationTest < ActiveSupport::TestCase # en notification result = NotificationFactory::Mailer.template( locale: @agent2.preferences[:locale], + timezone: @agent2.preferences[:timezone], template: 'ticket_update', objects: { ticket: ticket1, @@ -1165,7 +1168,7 @@ class TicketNotificationTest < ActiveSupport::TestCase assert_match(/1 low/, result[:body]) assert_match(/2 normal/, result[:body]) assert_match(/Pending till/, result[:body]) - assert_match(/2015-01-11 23:33:47 UTC/, result[:body]) + assert_match('01/11/2015 19:33 (America/St_Lucia)', result[:body]) assert_match(/update/, result[:body]) assert_no_match(/pending_till/, result[:body]) assert_no_match(/i18n/, result[:body]) @@ -1181,9 +1184,10 @@ class TicketNotificationTest < ActiveSupport::TestCase assert_not(human_changes['pending_time']) assert_not(human_changes['pending_till']) - # de notification + # de & Europe/Berlin notification result = NotificationFactory::Mailer.template( locale: @agent1.preferences[:locale], + timezone: @agent1.preferences[:timezone], template: 'ticket_update', objects: { ticket: ticket1, @@ -1198,7 +1202,7 @@ class TicketNotificationTest < ActiveSupport::TestCase assert_match(/1 niedrig/, result[:body]) assert_match(/2 normal/, result[:body]) assert_match(/Warten/, result[:body]) - assert_match(/2015-01-11 23:33:47 UTC/, result[:body]) + assert_match('12.01.2015 00:33 (Europe/Berlin)', result[:body]) assert_match(/aktualis/, result[:body]) assert_no_match(/pending_till/, result[:body]) assert_no_match(/i18n/, result[:body]) @@ -1229,6 +1233,7 @@ class TicketNotificationTest < ActiveSupport::TestCase # de notification result = NotificationFactory::Mailer.template( locale: @agent1.preferences[:locale], + timezone: @agent1.preferences[:timezone], template: 'ticket_update', objects: { ticket: ticket1, @@ -1254,6 +1259,7 @@ class TicketNotificationTest < ActiveSupport::TestCase # en notification result = NotificationFactory::Mailer.template( locale: @agent2.preferences[:locale], + timezone: @agent2.preferences[:timezone], template: 'ticket_update', objects: { ticket: ticket1, @@ -1276,6 +1282,22 @@ class TicketNotificationTest < ActiveSupport::TestCase assert_no_match(/pending_till/, result[:body]) assert_no_match(/i18n/, result[:body]) + # en notification + ticket1.escalation_at = Time.zone.parse('2019-04-01T10:00:00Z') + result = NotificationFactory::Mailer.template( + locale: @agent2.preferences[:locale], + timezone: @agent2.preferences[:timezone], + template: 'ticket_escalation', + objects: { + ticket: ticket1, + article: article, + recipient: @agent2, + } + ) + + assert_match('Ticket is escalated (some notification template test 1 Bobs\'s resumé', result[:subject]) + assert_match('is escalated since "04/01/2019 06:00 (America/St_Lucia)"!', result[:body]) + end end