diff --git a/app/models/custom_value.rb b/app/models/custom_value.rb index 6999d7a93b..34ca86925f 100644 --- a/app/models/custom_value.rb +++ b/app/models/custom_value.rb @@ -61,6 +61,10 @@ class CustomValue < ApplicationRecord end end + def default? + value == custom_field.default_value + end + protected def validate_presence_of_required_value diff --git a/app/workers/notifications/create_date_alerts_notifications_job.rb b/app/workers/notifications/create_date_alerts_notifications_job.rb index cea3195d82..351e12203a 100644 --- a/app/workers/notifications/create_date_alerts_notifications_job.rb +++ b/app/workers/notifications/create_date_alerts_notifications_job.rb @@ -23,41 +23,17 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # -# See docs/COPYRIGHT.rdoc for more details. +# See COPYRIGHT and LICENSE files for more details. #++ module Notifications - # Creates date alerts for users whose local time is 1:00 am. - class CreateDateAlertsNotificationsJob < Cron::CronJob - # runs every quarter of an hour, so 00:00, 00:15,..., 15:30, 15:45, 16:00, ... - self.cron_expression = '*/15 * * * *' - - def perform + class CreateDateAlertsNotificationsJob < ApplicationJob + def perform(user) return unless EnterpriseToken.allows_to?(:date_alerts) - service = Service.new(times_from_scheduled_to_execution) - service.call - end - - # Returns times from scheduled execution time to current time in 15 minutes - # steps. - # - # As scheduled execution time can be different from current time by more - # than 15 minutes when workers are busy, all times at 15 minutes interval - # between scheduled time and current time need to be considered to match - # with 1:00am in a time zone. - def times_from_scheduled_to_execution - time = scheduled_time - times = [] - begin - times << time - time += 15.minutes - end while time < Time.current - times - end - - def scheduled_time - self.class.delayed_job.run_at.then { |t| t.change(min: t.min / 15 * 15) } + Service + .new(user) + .call end end end diff --git a/app/workers/notifications/create_date_alerts_notifications_job/alertable_work_packages.rb b/app/workers/notifications/create_date_alerts_notifications_job/alertable_work_packages.rb index ac29d28f76..f1a07a611c 100644 --- a/app/workers/notifications/create_date_alerts_notifications_job/alertable_work_packages.rb +++ b/app/workers/notifications/create_date_alerts_notifications_job/alertable_work_packages.rb @@ -23,7 +23,7 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # -# See docs/COPYRIGHT.rdoc for more details. +# See COPYRIGHT and LICENSE files for more details. #++ class Notifications::CreateDateAlertsNotificationsJob::AlertableWorkPackages @@ -34,26 +34,27 @@ class Notifications::CreateDateAlertsNotificationsJob::AlertableWorkPackages end def alertable_for_start - find_alertables - .filter_map { |row| row["id"] if row["start_alert"] } - .then { |ids| WorkPackage.where(id: ids) } + alertable_for("start_alert") end def alertable_for_due - find_alertables - .filter_map { |row| row["id"] if row["due_alert"] } - .then { |ids| WorkPackage.where(id: ids) } + alertable_for("due_alert") end private + def alertable_for(alert) + find_alertables + .filter_map { |row| row["id"] if row[alert] } + .then { |ids| WorkPackage.where(id: ids) } + end + def find_alertables @find_alertables ||= ActiveRecord::Base.connection.execute(query).to_a end def query today = Arel::Nodes::build_quoted(Date.current).to_sql - alertable_durations = Arel::Nodes::Grouping.new(UserPreferences::ParamsContract::DATE_ALERT_DURATIONS.compact).to_sql alertables = alertable_work_packages .select(:id, @@ -61,9 +62,9 @@ class Notifications::CreateDateAlertsNotificationsJob::AlertableWorkPackages "work_packages.start_date - #{today} AS start_delta", "work_packages.due_date - #{today} AS due_delta", "#{today} - work_packages.due_date AS overdue_delta") - .where("work_packages.start_date - #{today} IN #{alertable_durations} " \ - "OR work_packages.due_date - #{today} IN #{alertable_durations} " \ - "OR #{today} - work_packages.due_date > 0") + .where("work_packages.start_date IN #{alertable_dates} " \ + "OR work_packages.due_date IN #{alertable_dates} " \ + "OR work_packages.due_date < #{today}") <<~SQL.squish WITH @@ -123,4 +124,12 @@ class Notifications::CreateDateAlertsNotificationsJob::AlertableWorkPackages join_dependency = work_packages.construct_join_dependency([:status], Arel::Nodes::OuterJoin) work_packages.joins(join_dependency) end + + def alertable_dates + dates = UserPreferences::ParamsContract::DATE_ALERT_DURATIONS + .compact + .map { |offset| Arel::Nodes::build_quoted(Date.current + offset.days) } + + Arel::Nodes::Grouping.new(dates).to_sql + end end diff --git a/app/workers/notifications/create_date_alerts_notifications_job/service.rb b/app/workers/notifications/create_date_alerts_notifications_job/service.rb index e0cb84d642..8aaa44a6f8 100644 --- a/app/workers/notifications/create_date_alerts_notifications_job/service.rb +++ b/app/workers/notifications/create_date_alerts_notifications_job/service.rb @@ -26,36 +26,23 @@ # See COPYRIGHT and LICENSE files for more details. #++ -# Creates date alerts notifications for users whose local time is 1am for the -# given run_times. class Notifications::CreateDateAlertsNotificationsJob::Service - attr_reader :run_times - - # @param run_times [Array] the times for which the service is run. - # Must be multiple of 15 minutes (xx:00, xx:15, xx:30, or xx:45). - def initialize(run_times) - @run_times = run_times + def initialize(user) + @user = user end def call return unless EnterpriseToken.allows_to?(:date_alerts) - time_zones = time_zones_covering_1am_local_time - return if time_zones.empty? - - # warning: there may be a subtle bug here: if many run_times are given, time - # zones will have different time shifting. This should be ok: as the period - # covered is small this should not have any impact. If the period is more - # than 23h, then the day will change. - Time.use_zone(time_zones.first) do - User.with_time_zone(time_zones).find_each do |user| - send_date_alert_notifications(user) - end + Time.use_zone(user.time_zone) do + send_date_alert_notifications(user) end end private + attr_accessor :user + def send_date_alert_notifications(user) alertables = Notifications::CreateDateAlertsNotificationsJob::AlertableWorkPackages.new(user) create_date_alert_notifications(user, alertables.alertable_for_start, :date_alert_start_date) @@ -70,6 +57,8 @@ class Notifications::CreateDateAlertsNotificationsJob::Service end def mark_previous_notifications_as_read(user, work_packages, reason) + return if work_packages.empty? + Notification .where(recipient: user, reason:, @@ -86,20 +75,4 @@ class Notifications::CreateDateAlertsNotificationsJob::Service reason: ) end - - def time_zones_covering_1am_local_time - UserPreferences::UpdateContract - .assignable_time_zones - .select { |time_zone| executing_at_1am_for_timezone?(time_zone) } - .map { |time_zone| time_zone.tzinfo.canonical_zone.name } - end - - def executing_at_1am_for_timezone?(time_zone) - run_times.any? { |time| is_1am?(time, time_zone) } - end - - def is_1am?(time, time_zone) - local_time = time.in_time_zone(time_zone) - local_time.strftime('%H:%M') == '01:00' - end end diff --git a/app/workers/notifications/schedule_date_alerts_notifications_job.rb b/app/workers/notifications/schedule_date_alerts_notifications_job.rb new file mode 100644 index 0000000000..dc5406c788 --- /dev/null +++ b/app/workers/notifications/schedule_date_alerts_notifications_job.rb @@ -0,0 +1,63 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2023 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 docs/COPYRIGHT.rdoc for more details. +#++ + +module Notifications + # Creates date alert jobs for users whose local time is 1:00 am. + class ScheduleDateAlertsNotificationsJob < Cron::CronJob + # runs every quarter of an hour, so 00:00, 00:15,..., 15:30, 15:45, 16:00, ... + self.cron_expression = '*/15 * * * *' + + def perform + return unless EnterpriseToken.allows_to?(:date_alerts) + + service = Service.new(times_from_scheduled_to_execution) + service.call + end + + # Returns times from scheduled execution time to current time in 15 minutes + # steps. + # + # As scheduled execution time can be different from current time by more + # than 15 minutes when workers are busy, all times at 15 minutes interval + # between scheduled time and current time need to be considered to match + # with 1:00am in a time zone. + def times_from_scheduled_to_execution + time = scheduled_time + times = [] + begin + times << time + time += 15.minutes + end while time < Time.current + times + end + + def scheduled_time + self.class.delayed_job.run_at.then { |t| t.change(min: t.min / 15 * 15) } + end + end +end diff --git a/app/workers/notifications/schedule_date_alerts_notifications_job/service.rb b/app/workers/notifications/schedule_date_alerts_notifications_job/service.rb new file mode 100644 index 0000000000..384c424a4e --- /dev/null +++ b/app/workers/notifications/schedule_date_alerts_notifications_job/service.rb @@ -0,0 +1,74 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2023 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. +#++ + +# Creates date alerts notifications for users whose local time is 1am for the +# given run_times. +class Notifications::ScheduleDateAlertsNotificationsJob::Service + attr_reader :run_times + + # @param run_times [Array] the times for which the service is run. + # Must be multiple of 15 minutes (xx:00, xx:15, xx:30, or xx:45). + def initialize(run_times) + @run_times = run_times + end + + def call + return unless EnterpriseToken.allows_to?(:date_alerts) + + users_at_1am_with_notification_settings.find_each do |user| + Notifications::CreateDateAlertsNotificationsJob.perform_later(user) + end + end + + private + + def time_zones_covering_1am_local_time + UserPreferences::UpdateContract + .assignable_time_zones + .select { |time_zone| executing_at_1am_for_timezone?(time_zone) } + .map { |time_zone| time_zone.tzinfo.canonical_zone.name } + end + + def executing_at_1am_for_timezone?(time_zone) + run_times.any? { |time| is_1am?(time, time_zone) } + end + + def is_1am?(time, time_zone) + local_time = time.in_time_zone(time_zone) + local_time.strftime('%H:%M') == '01:00' + end + + def users_at_1am_with_notification_settings + User + .with_time_zone(time_zones_covering_1am_local_time) + .not_locked + .where("EXISTS (SELECT 1 FROM notification_settings " \ + "WHERE user_id = users.id AND " \ + "(overdue IS NOT NULL OR start_date IS NOT NULL OR due_date IS NOT NULL))") + end +end diff --git a/config/initializers/cronjobs.rb b/config/initializers/cronjobs.rb index 12ab76221c..086811b26f 100644 --- a/config/initializers/cronjobs.rb +++ b/config/initializers/cronjobs.rb @@ -2,14 +2,14 @@ OpenProject::Application.configure do |application| application.config.to_prepare do - ::Cron::CronJob.register! ::Cron::ClearOldSessionsJob, - ::Cron::ClearTmpCacheJob, - ::Cron::ClearUploadedFilesJob, - ::OAuth::CleanupJob, - ::PaperTrailAudits::CleanupJob, - ::Attachments::CleanupUncontaineredJob, - ::Notifications::CreateDateAlertsNotificationsJob, - ::Notifications::ScheduleReminderMailsJob, - ::Ldap::SynchronizationJob + Cron::CronJob.register! Cron::ClearOldSessionsJob, + Cron::ClearTmpCacheJob, + Cron::ClearUploadedFilesJob, + OAuth::CleanupJob, + PaperTrailAudits::CleanupJob, + Attachments::CleanupUncontaineredJob, + Notifications::ScheduleDateAlertsNotificationsJob, + Notifications::ScheduleReminderMailsJob, + Ldap::SynchronizationJob end end diff --git a/db/migrate/20230105073117_remove_renamed_date_alert_job.rb b/db/migrate/20230105073117_remove_renamed_date_alert_job.rb new file mode 100644 index 0000000000..ad480960b9 --- /dev/null +++ b/db/migrate/20230105073117_remove_renamed_date_alert_job.rb @@ -0,0 +1,37 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2023 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +class RemoveRenamedDateAlertJob < ActiveRecord::Migration[6.0] + def up + # The job has been renamed to Notifications::ScheduleDateAlertsNotificationsJob. + # The new job will be added on restarting the application. + Delayed::Job + .where('handler LIKE ?', "%job_class: Notifications::CreateDateAlertsNotificationsJob%") + .delete_all + end +end diff --git a/db/migrate/20230105134940_work_package_date_indices.rb b/db/migrate/20230105134940_work_package_date_indices.rb new file mode 100644 index 0000000000..9235e61a57 --- /dev/null +++ b/db/migrate/20230105134940_work_package_date_indices.rb @@ -0,0 +1,6 @@ +class WorkPackageDateIndices < ActiveRecord::Migration[7.0] + def change + add_index :work_packages, :start_date + add_index :work_packages, :due_date + end +end diff --git a/docs/system-admin-guide/users-permissions/README.md b/docs/system-admin-guide/users-permissions/README.md index 2726fdd1b0..a6956221c0 100644 --- a/docs/system-admin-guide/users-permissions/README.md +++ b/docs/system-admin-guide/users-permissions/README.md @@ -7,7 +7,7 @@ keywords: users, permissions, roles, groups, avatars --- # Users and permissions -Manage users, placeholder users and permissions in OpenProject. +In this section of the System Administration guide you can learn how to manage users, placeholder users and permissions in OpenProject. ## Overview diff --git a/docs/system-admin-guide/users-permissions/settings/README.md b/docs/system-admin-guide/users-permissions/settings/README.md index 816bb2d57f..e2f588248b 100644 --- a/docs/system-admin-guide/users-permissions/settings/README.md +++ b/docs/system-admin-guide/users-permissions/settings/README.md @@ -10,9 +10,9 @@ keywords: user settings The user settings sections covers general settings, such as the default language, user deletion and user consent. -User settings are accessible by administrators from the OpenProject administration. +User settings are accessible by administrators from the OpenProject **Administration**. -![user and permissions settings](image-20200211140959585.png) +![user and permissions settings](openproject_admin_guide_user_settings.png) | Topic | Content | | ------------------------------------------- | -------------------------- | @@ -24,7 +24,7 @@ User settings are accessible by administrators from the OpenProject administrati ![image-20191104163417641](image-20191104163417641.png) -The default preferences section covers default user settings. +The **Default preferences** section covers default user settings. Here, you can specify the default language for new users as well as the default time zone. @@ -32,17 +32,17 @@ The default language is displayed for users when they first sign into OpenProjec You can also choose if success notifications (e.g. on the work package page) should be hidden automatically. -These settings can be changed by users in their "My Account" page later on. +These settings can be changed by users in their **My Account** page later on. ## User deletion ![image-20191104163546817](image-20191104163546817.png) -In the user deletion section you can determine who should be able to delete user accounts. +In the **User deletion** section you can determine who should be able to delete user accounts. -By default, only admins are able to delete accounts. If this option is activated, admins can navigate to the user list, select a user account and click on the "Delete" option on the upper right side to delete an account. +By default, only admins are able to delete accounts. If this option is activated, admins can navigate to the user list, select a user account and click on the **Delete** option on the upper right side to delete an account. -Additionally, you can select the option "Users allowed to delete their accounts". If this option is activated, users can delete their own user accounts from the "My account" page. +Additionally, you can select the option **Users allowed to delete their accounts**. If this option is activated, users can delete their own user accounts from the **My account** page. If you want to prevent users from deleting their own accounts, it is recommended to deactivate this option. @@ -50,8 +50,8 @@ If you want to prevent users from deleting their own accounts, it is recommended ![image-20191104163858457](image-20191104163858457.png) -Data privacy and security is a priority in OpenProject. In order to comply with GDPR regulation, a consent form can be configured in OpenProject. When the option "Consent required" is checked, a user consent form is shown for users when they sign into OpenProject for the first time. +Data privacy and security is a priority in OpenProject. In order to comply with GDPR regulation, a consent form can be configured in OpenProject. When the option **Consent required** is checked, a user consent form is shown for users when they sign into OpenProject for the first time. -By default, OpenProject's privacy and security policy is referenced in the consent form. If you have any additional information you would like your users to consent to, you can link it in the consent information text. +By default, OpenProject's privacy and security policy is referenced in the consent form. If you have any additional information you would like your users to consent to, you can link it in the **Consent information text** section. Furthermore, you can provide the email address of a consent contact. This user can then be notified when a data change or data removal is required. diff --git a/docs/system-admin-guide/users-permissions/settings/openproject_admin_guide_user_settings.png b/docs/system-admin-guide/users-permissions/settings/openproject_admin_guide_user_settings.png new file mode 100644 index 0000000000..234e5d9fa4 Binary files /dev/null and b/docs/system-admin-guide/users-permissions/settings/openproject_admin_guide_user_settings.png differ diff --git a/docs/system-admin-guide/users-permissions/users/README.md b/docs/system-admin-guide/users-permissions/users/README.md index 0d14780fc3..9644d89cd1 100644 --- a/docs/system-admin-guide/users-permissions/users/README.md +++ b/docs/system-admin-guide/users-permissions/users/README.md @@ -31,7 +31,7 @@ In the Community edition there is no limit to the number of users. In Enterprise The User list is where users are managed. They can be added, edited or deleted from this list, which can be filtered if required. -![user list](image-20200211141841492.png) +![openproject_system_admin_guide_users_list](openproject_system_admin_guide_users_list.png) Column headers can be clicked to toggle sort direction. Arrows indicate sort order, up for ascending (a-z/0-9) and down for descending (z-a/9-0). Paging controls are shown at the bottom of the list. You will also see whether a user is a system administrator in OpenProject. @@ -42,7 +42,7 @@ At the top of the user list is a filter box. Filter by status or name, then clic * **Status** - select from Active, All or Locked Temporarily. Each selection shows the number of users. * **Name** - enter any text; this can contain a "%" wild card for 0 or more characters. The filter applies to user name, first name, last name and email address. -![filter users](image-20200115155456033.png) +![Filter users in OpenProject](image-20200115155456033.png) ## Lock and unlock users @@ -52,9 +52,9 @@ If you are using [Enterprise cloud](../../../enterprise-guide/enterprise-cloud-g > **Note**: The previous activities from a locked user will still be displayed in the system. -![System-admin-guide_lock-users](System-admin-guide_lock-users.png) +![Lock users in OpenProject](open_project_system_admin_lock_user_permanently.png) -If a user has repeated failed logins the user will be locked temporarily and a "Reset failed logins" link is shown in the user list. Click the link to unlock it now, or wait and it will be unlocked automatically. Have a look at the section [Other authentication settings](../../authentication/authentication-settings/#other-authentication-settings) for failed attempts and time blocked. +If a user has repeated failed logins the user will be locked temporarily and a **Reset failed logins" link will be shown in the user list. Click the link to unlock it right away, or wait and it will be unlocked automatically. Have a look at the section [Other authentication settings](../../authentication/authentication-settings/#other-authentication-settings) for failed attempts and time blocked. ## Create users @@ -62,9 +62,9 @@ New users can be created and configured by an administrator or by the users them ### Invite user (as administrator) -In the user list, click the **+User** button to open the "New user" form. +In the user list, click the **+User** button to open the **New user** form. -![new user](image-20200115155855409.png) +![Create a new user in OpenProject](openproject_system_guide_create_user.png) Enter the email address, first name, and last name of the new user. Tick the box to make them a system administrator user. @@ -75,28 +75,28 @@ When adding the last of multiple users you can click on **Create** or click the ### Create user (via self-registration) -To allow users to create their own user accounts allow self-registration in the [authentication settings](../../authentication/authentication-settings). A person can then create their own user from the home page by clicking on the "Sign in" button (top right), then on the "Create a new account" link in the sign in box. +To allow users to create their own user accounts allow self-registration in the [authentication settings](../../authentication/authentication-settings). A person can then create their own user from the home page by clicking on the **Sign in** button (top right), then on the **Create a new account** link in the sign in box. Enter values in all fields (they cannot be left blank). The email field must be a valid email address that is not used in this system. Click the **Create** button. Depending on the [settings](../../authentication/authentication-settings) the account is created but it could be that it still needs to be activated by an administrator. #### Activate users -Open the user list. If a user has created their own account (and it has not been activated automatically) it is shown in the user list with an "Activate" link on the right. Click this link and continue to add details to this user as below. There is also an "Activate" button at the top of the user's details page. +Open the user list. If a user has created their own account (and it has not been activated automatically) it is shown in the user list with an **Activate** link on the right. Click this link and continue to add details to this user as below. There is also an **Activate** button at the top of the user's details page. ### Set initial details -You can edit the details of a newly created user. Useful fields might be Username, Language and Time zone. You might also fill Projects, Groups and Rates, or leave these to the "Project creator". +You can edit the details of a newly created user. Useful fields might be **Username**, **Language** and **Time zone**. You might also fill Projects, Groups and Rates, or leave these to the **Project creator**. Also consider the [authentication](#authentication) settings. See [Manage user settings](#manage-user-settings) for full details. ### Resend user invitation via email -If a user did not receive the email invitation or shall change their authentication method to email, you can send the invitation to the user again if needed. In the user list, click on the user name to whom you want to resend the email with the invitation link to the system. +If a user did not receive the email invitation or shall change their authentication method to email, you can send the invitation to the user again if needed. In the user list, click on the name of the user to whom you want to resend the email with the invitation link to the system. In the top right, click the **Send invitation** button in order to send the email once again. -![Sys-admin-resend-invitation](Sys-admin-resend-invitation.png) +![Send user invitation in OpenProject](openproject_system_guide_send_user_invitation.png) ### Delete user invitations diff --git a/docs/system-admin-guide/users-permissions/users/open_project_system_admin_lock_user_permanently.png b/docs/system-admin-guide/users-permissions/users/open_project_system_admin_lock_user_permanently.png new file mode 100644 index 0000000000..7e75356a0a Binary files /dev/null and b/docs/system-admin-guide/users-permissions/users/open_project_system_admin_lock_user_permanently.png differ diff --git a/docs/system-admin-guide/users-permissions/users/openproject_system_admin_guide_users_list.png b/docs/system-admin-guide/users-permissions/users/openproject_system_admin_guide_users_list.png new file mode 100644 index 0000000000..91944a36d5 Binary files /dev/null and b/docs/system-admin-guide/users-permissions/users/openproject_system_admin_guide_users_list.png differ diff --git a/docs/system-admin-guide/users-permissions/users/openproject_system_guide_create_user.png b/docs/system-admin-guide/users-permissions/users/openproject_system_guide_create_user.png new file mode 100644 index 0000000000..8efe7a1f26 Binary files /dev/null and b/docs/system-admin-guide/users-permissions/users/openproject_system_guide_create_user.png differ diff --git a/docs/system-admin-guide/users-permissions/users/openproject_system_guide_send_user_invitation.png b/docs/system-admin-guide/users-permissions/users/openproject_system_guide_send_user_invitation.png new file mode 100644 index 0000000000..6fc69fa52f Binary files /dev/null and b/docs/system-admin-guide/users-permissions/users/openproject_system_guide_send_user_invitation.png differ diff --git a/lib_static/plugins/acts_as_customizable/lib/acts_as_customizable.rb b/lib_static/plugins/acts_as_customizable/lib/acts_as_customizable.rb index 676fdc2107..0e207a7040 100644 --- a/lib_static/plugins/acts_as_customizable/lib/acts_as_customizable.rb +++ b/lib_static/plugins/acts_as_customizable/lib/acts_as_customizable.rb @@ -256,6 +256,9 @@ module Redmine # Skip when the old value equals the new value (no change happened). next cfv_changes if value_was == cfv.value + # Skip when the new value is the default value + next cfv_changes if value_was.nil? && cfv.default? + cfv_changes.merge("custom_field_#{cfv.custom_field_id}": [value_was, cfv.value]) end end diff --git a/spec/features/notifications/notification_center/notification_center_date_alerts_spec.rb b/spec/features/notifications/notification_center/notification_center_date_alerts_spec.rb index eabf31810f..cda9c5eddd 100644 --- a/spec/features/notifications/notification_center/notification_center_date_alerts_spec.rb +++ b/spec/features/notifications/notification_center/notification_center_date_alerts_spec.rb @@ -166,13 +166,15 @@ describe "Notification center date alerts", js: true, with_settings: { journal_a end def run_create_date_alerts_notifications_job - create_date_alerts_service = Notifications::CreateDateAlertsNotificationsJob::Service.new([timezone_time('1:00', time_zone)]) + create_date_alerts_service = Notifications::ScheduleDateAlertsNotificationsJob::Service + .new([timezone_time('1:00', time_zone)]) travel_to(timezone_time('1:04', time_zone)) create_date_alerts_service.call end before do run_create_date_alerts_notifications_job + perform_enqueued_jobs login_as user visit notifications_center_path end diff --git a/spec/models/work_package/work_package_acts_as_customizable_spec.rb b/spec/models/work_package/work_package_acts_as_customizable_spec.rb index e60ee940b5..7040082ae4 100644 --- a/spec/models/work_package/work_package_acts_as_customizable_spec.rb +++ b/spec/models/work_package/work_package_acts_as_customizable_spec.rb @@ -135,5 +135,16 @@ describe WorkPackage, 'acts_as_customizable' do before do setup_custom_field(custom_field) end + + context 'with a default value' do + before do + custom_field.update! default_value: 'foobar' + model_instance.custom_values.destroy_all + end + + it 'returns no changes' do + expect(model_instance.custom_field_changes).to be_empty + end + end end end diff --git a/spec/workers/notifications/create_date_alerts_notifications_job_spec.rb b/spec/workers/notifications/create_date_alerts_notifications_job_spec.rb index dbedbc5d5a..598eb83a2f 100644 --- a/spec/workers/notifications/create_date_alerts_notifications_job_spec.rb +++ b/spec/workers/notifications/create_date_alerts_notifications_job_spec.rb @@ -23,7 +23,7 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # -# See docs/COPYRIGHT.rdoc for more details. +# See COPYRIGHT and LICENSE files for more details. #++ require 'spec_helper' @@ -48,43 +48,19 @@ describe Notifications::CreateDateAlertsNotificationsJob, type: :job, with_ee: % shared_let(:in_3_days) { today + 3.days } shared_let(:in_7_days) { today + 7.days } - shared_let(:user_paris) do + shared_let(:user) do create( :user, firstname: 'Paris', preferences: { time_zone: timezone_paris.name } ) end - shared_let(:user_berlin) do - create( - :user, - firstname: 'Berlin', - preferences: { time_zone: timezone_berlin.name } - ) - end - shared_let(:user_kathmandu) do - create( - :user, - firstname: 'Kathmandu', - preferences: { time_zone: timezone_kathmandu.name } - ) - end shared_let(:alertable_work_packages) do - create_list(:work_package, 2, project:, author: user_paris) - end - - let(:scheduled_job) do - described_class.ensure_scheduled! - described_class.delayed_job + create_list(:work_package, 2, project:, author: user) end - before do - # We need to access the job as stored in the database to get at the run_at time persisted there - allow(ActiveJob::Base) - .to receive(:queue_adapter) - .and_return(ActiveJob::QueueAdapters::DelayedJobAdapter.new) - end + let(:job) { described_class } define :have_a_start_date_alert_notification_for do |work_package| match do |user| @@ -113,9 +89,9 @@ describe Notifications::CreateDateAlertsNotificationsJob, type: :job, with_ee: % when WorkPackage then %i[id start_date due_date assigned_to_id responsible_id] end formatted_pairs = object - .slice(*keys) - .map { |k, v| "#{k}: #{v.is_a?(Date) ? v.to_s : v.inspect}" } - .join(', ') + .slice(*keys) + .map { |k, v| "#{k}: #{v.is_a?(Date) ? v.to_s : v.inspect}" } + .join(', ') "#<#{formatted_pairs}>" end end @@ -147,19 +123,15 @@ describe Notifications::CreateDateAlertsNotificationsJob, type: :job, with_ee: % when WorkPackage then %i[id start_date due_date assigned_to_id responsible_id] end formatted_pairs = object - .slice(*keys) - .map { |k, v| "#{k}: #{v.is_a?(Date) ? v.to_s : v.inspect}" } - .join(', ') + .slice(*keys) + .map { |k, v| "#{k}: #{v.is_a?(Date) ? v.to_s : v.inspect}" } + .join(', ') "#<#{formatted_pairs}>" end end - def set_scheduled_time(run_at) - scheduled_job.update_column(:run_at, run_at) - end - def alertable_work_package(attributes = {}) - assignee = attributes.slice(:responsible, :responsible_id).any? ? nil : user_paris + assignee = attributes.slice(:responsible, :responsible_id).any? ? nil : user attributes = attributes.reverse_merge( start_date: in_1_day, assigned_to: assignee @@ -179,9 +151,8 @@ describe Notifications::CreateDateAlertsNotificationsJob, type: :job, with_ee: % end def run_job(scheduled_at: '1:00', local_time: '1:04', timezone: timezone_paris) - set_scheduled_time(timezone_time(scheduled_at, timezone)) travel_to(timezone_time(local_time, timezone)) do - scheduled_job.reload.invoke_job + job.perform_now(user) yield end @@ -193,57 +164,63 @@ describe Notifications::CreateDateAlertsNotificationsJob, type: :job, with_ee: % closed_work_package = alertable_work_package(status: status_closed) run_job do - expect(user_paris).to have_a_start_date_alert_notification_for(open_work_package) - expect(user_paris).not_to have_a_start_date_alert_notification_for(closed_work_package) + expect(user).to have_a_start_date_alert_notification_for(open_work_package) + expect(user).not_to have_a_start_date_alert_notification_for(closed_work_package) end end - it 'creates date alert notifications only for users whose local time is 1:00 am when the job is executed' do - work_package_for_paris_user = alertable_work_package(assigned_to: user_paris, - start_date: timezone_paris.today + 1.day) - work_package_for_kathmandu_user = alertable_work_package(assigned_to: user_kathmandu, - start_date: timezone_kathmandu.today + 1.day) + it 'creates date alert notifications if user is assigned to the work package' do + work_package_assigned = alertable_work_package(assigned_to: user) - run_job(timezone: timezone_paris) do - expect(user_paris).to have_a_start_date_alert_notification_for(work_package_for_paris_user) - expect(user_kathmandu).not_to have_a_start_date_alert_notification_for(work_package_for_kathmandu_user) + run_job do + expect(user).to have_a_start_date_alert_notification_for(work_package_assigned) end + end - # change timezone to cover Kathmandu - run_job(timezone: timezone_kathmandu) do - expect(user_kathmandu).to have_a_start_date_alert_notification_for(work_package_for_kathmandu_user) + it 'creates date alert notifications if user is accountable of the work package' do + work_package_accountable = alertable_work_package(responsible: user) + + run_job do + expect(user).to have_a_start_date_alert_notification_for(work_package_accountable) end end - it 'creates date alert notifications if user is assigned to or accountable of the work package' do - work_package_assigned = alertable_work_package(assigned_to: user_paris) - work_package_accountable = alertable_work_package(responsible: user_paris) + it 'creates date alert notifications if user is watcher of the work package' do + work_package_watched = alertable_work_package(responsible: nil) + build(:watcher, watchable: work_package_watched, user:).save(validate: false) run_job do - expect(user_paris).to have_a_start_date_alert_notification_for(work_package_assigned) - expect(user_paris).to have_a_start_date_alert_notification_for(work_package_accountable) + expect(user).to have_a_start_date_alert_notification_for(work_package_watched) end end - it 'creates start and finish date alert notifications based on user notification settings' do - user_paris.notification_settings.first.update( + it 'creates start date alert notifications based on user notification settings' do + user.notification_settings.first.update( start_date: 1, due_date: nil ) - user_berlin.notification_settings.first.update( + work_package = alertable_work_package(assigned_to: user, + start_date: in_1_day, + due_date: in_3_days) + + run_job do + expect(user).to have_a_start_date_alert_notification_for(work_package) + expect(user).not_to have_a_due_date_alert_notification_for(work_package) + end + end + + it 'creates due date alert notifications based on user notification settings' do + user.notification_settings.first.update( start_date: nil, due_date: 3 ) - work_package = alertable_work_package(assigned_to: user_paris, - responsible: user_berlin, + work_package = alertable_work_package(assigned_to: user, start_date: in_1_day, due_date: in_3_days) run_job do - expect(user_paris).to have_a_start_date_alert_notification_for(work_package) - expect(user_paris).not_to have_a_due_date_alert_notification_for(work_package) - expect(user_berlin).not_to have_a_start_date_alert_notification_for(work_package) - expect(user_berlin).to have_a_due_date_alert_notification_for(work_package) + expect(user).not_to have_a_start_date_alert_notification_for(work_package) + expect(user).to have_a_due_date_alert_notification_for(work_package) end end @@ -252,7 +229,7 @@ describe Notifications::CreateDateAlertsNotificationsJob, type: :job, with_ee: % work_package = alertable_work_package run_job do - expect(user_paris).not_to have_a_start_date_alert_notification_for(work_package) + expect(user).not_to have_a_start_date_alert_notification_for(work_package) end end end @@ -260,52 +237,52 @@ describe Notifications::CreateDateAlertsNotificationsJob, type: :job, with_ee: % context 'when project notification settings are defined for a user' do it 'creates date alert notifications using these settings for work packages of the project' do # global notification settings - user_paris.notification_settings.first.update( + user.notification_settings.first.update( start_date: 1, due_date: nil ) # project notifications settings - user_paris.notification_settings.create( + user.notification_settings.create( project:, start_date: nil, due_date: 7 ) - silent_work_package = alertable_work_package(assigned_to: user_paris, + silent_work_package = alertable_work_package(assigned_to: user, project:, start_date: in_1_day, due_date: in_1_day) - noisy_work_package = alertable_work_package(assigned_to: user_paris, + noisy_work_package = alertable_work_package(assigned_to: user, project:, start_date: in_7_days, due_date: in_7_days) run_job do - expect(user_paris).not_to have_a_start_date_alert_notification_for(silent_work_package) - expect(user_paris).not_to have_a_due_date_alert_notification_for(silent_work_package) - expect(user_paris).not_to have_a_start_date_alert_notification_for(noisy_work_package) - expect(user_paris).to have_a_due_date_alert_notification_for(noisy_work_package) + expect(user).not_to have_a_start_date_alert_notification_for(silent_work_package) + expect(user).not_to have_a_due_date_alert_notification_for(silent_work_package) + expect(user).not_to have_a_start_date_alert_notification_for(noisy_work_package) + expect(user).to have_a_due_date_alert_notification_for(noisy_work_package) end end end context 'with existing date alerts' do it 'marks them as read when new ones are created' do - work_package = alertable_work_package(assigned_to: user_paris, + work_package = alertable_work_package(assigned_to: user, start_date: in_1_day, due_date: in_1_day) existing_start_notification = create(:notification, resource: work_package, - recipient: user_paris, + recipient: user, reason: :date_alert_start_date) existing_due_notification = create(:notification, resource: work_package, - recipient: user_paris, + recipient: user, reason: :date_alert_due_date) run_job do expect(existing_start_notification.reload).to have_attributes(read_ian: true) expect(existing_due_notification.reload).to have_attributes(read_ian: true) - unread_date_alert_notifications = Notification.where(recipient: user_paris, + unread_date_alert_notifications = Notification.where(recipient: user, read_ian: false, resource: work_package) expect(unread_date_alert_notifications.pluck(:reason)) @@ -315,27 +292,27 @@ describe Notifications::CreateDateAlertsNotificationsJob, type: :job, with_ee: % # rubocop:disable RSpec/ExampleLength it 'does not mark them as read when if no new notifications are created' do - work_package_start = alertable_work_package(assigned_to: user_paris, + work_package_start = alertable_work_package(assigned_to: user, start_date: in_1_day, due_date: nil) - work_package_due = alertable_work_package(assigned_to: user_paris, + work_package_due = alertable_work_package(assigned_to: user, start_date: nil, due_date: in_1_day) existing_wp_start_start_notif = create(:notification, reason: :date_alert_start_date, - recipient: user_paris, + recipient: user, resource: work_package_start) existing_wp_start_due_notif = create(:notification, reason: :date_alert_due_date, - recipient: user_paris, + recipient: user, resource: work_package_start) existing_wp_due_start_notif = create(:notification, reason: :date_alert_start_date, - recipient: user_paris, + recipient: user, resource: work_package_due) existing_wp_due_due_notif = create(:notification, reason: :date_alert_due_date, - recipient: user_paris, + recipient: user, resource: work_package_due) run_job do @@ -347,45 +324,5 @@ describe Notifications::CreateDateAlertsNotificationsJob, type: :job, with_ee: % end # rubocop:enable RSpec/ExampleLength end - - context 'when scheduled and executed at 01:00 am local time' do - it 'creates a start date alert notification for a user in the same time zone' do - work_package = alertable_work_package - - run_job(scheduled_at: '1:00', local_time: '1:00') do - expect(user_paris).to have_a_start_date_alert_notification_for(work_package) - end - end - end - - context 'when scheduled and executed at 01:14 am local time' do - it 'creates a start date alert notification for a user in the same time zone' do - work_package = alertable_work_package - - run_job(scheduled_at: '1:14', local_time: '1:14') do - expect(user_paris).to have_a_start_date_alert_notification_for(work_package) - end - end - end - - context 'when scheduled and executed at 01:15 am local time' do - it 'does not create a start date alert notification for a user in the same time zone' do - work_package = alertable_work_package - - run_job(scheduled_at: '1:15', local_time: '1:15') do - expect(user_paris).not_to have_a_start_date_alert_notification_for(work_package) - end - end - end - - context 'when scheduled at 01:00 am local time and executed at 01:37 am local time' do - it 'creates a start date alert notification for a user in the same time zone' do - work_package = alertable_work_package - - run_job(scheduled_at: '1:00', local_time: '1:37') do - expect(user_paris).to have_a_start_date_alert_notification_for(work_package) - end - end - end end end diff --git a/spec/workers/notifications/schedule_date_alerts_notifications_job_spec.rb b/spec/workers/notifications/schedule_date_alerts_notifications_job_spec.rb new file mode 100644 index 0000000000..b256b040fb --- /dev/null +++ b/spec/workers/notifications/schedule_date_alerts_notifications_job_spec.rb @@ -0,0 +1,301 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2023 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 docs/COPYRIGHT.rdoc for more details. +#++ + +require 'spec_helper' + +describe Notifications::ScheduleDateAlertsNotificationsJob, type: :job, with_ee: %i[date_alerts] do + include ActiveSupport::Testing::TimeHelpers + + shared_let(:project) { create(:project, name: 'main') } + + # Paris and Berlin are both UTC+01:00 (CET) or UTC+02:00 (CEST) + shared_let(:timezone_paris) { ActiveSupport::TimeZone['Europe/Paris'] } + # Kathmandu is UTC+05:45 (no DST) + shared_let(:timezone_kathmandu) { ActiveSupport::TimeZone['Asia/Kathmandu'] } + + shared_let(:user_paris) do + create( + :user, + firstname: 'Paris', + preferences: { time_zone: timezone_paris.name } + ) + end + shared_let(:user_kathmandu) do + create( + :user, + firstname: 'Kathmandu', + preferences: { time_zone: timezone_kathmandu.name } + ) + end + + let(:schedule_job) do + described_class.ensure_scheduled! + described_class.delayed_job + end + + before do + # We need to access the job as stored in the database to get at the run_at time persisted there + allow(ActiveJob::Base) + .to receive(:queue_adapter) + .and_return(ActiveJob::QueueAdapters::DelayedJobAdapter.new) + schedule_job + end + + def set_scheduled_time(run_at) + schedule_job.update_column(:run_at, run_at) + end + + # Converts "hh:mm" into { hour: h, min: m } + def time_hash(time) + %i[hour min].zip(time.split(':', 2).map(&:to_i)).to_h + end + + def timezone_time(time, timezone) + timezone.now.change(time_hash(time)) + end + + def run_job(scheduled_at: '1:00', local_time: '1:04', timezone: timezone_paris) + set_scheduled_time(timezone_time(scheduled_at, timezone)) + travel_to(timezone_time(local_time, timezone)) do + schedule_job.reload.invoke_job + + yield if block_given? + end + end + + def deserialized_of_job(job) + deserializer_class = Class.new do + include(ActiveJob::Arguments) + end + + deserializer = deserializer_class.new + + deserializer.deserialize(job.payload_object.job_data).to_h + end + + def expect_job(job, klass, *arguments) + job_data = deserialized_of_job(job) + expect(job_data['job_class']) + .to eql klass + expect(job_data['arguments']) + .to match_array arguments + end + + shared_examples_for 'job execution creates date alerts creation job' do + let(:timezone) { timezone_paris } + let(:scheduled_at) { '1:00' } + let(:local_time) { '1:04' } + let(:user) { user_paris } + + it 'creates the job for the user' do + expect do + run_job(timezone:, scheduled_at:, local_time:) do + expect_job(Delayed::Job.last, "Notifications::CreateDateAlertsNotificationsJob", user) + end + end.to change(Delayed::Job, :count).by 1 + end + end + + shared_examples_for 'job execution creates no date alerts creation job' do + let(:timezone) { timezone_paris } + let(:scheduled_at) { '1:00' } + let(:local_time) { '1:04' } + + it 'creates no job' do + expect do + run_job(timezone:, scheduled_at:, local_time:) + end.not_to change(Delayed::Job, :count) + end + end + + describe '#perform' do + context 'for users whose local time is 1:00 am (UTC+1) when the job is executed' do + it_behaves_like 'job execution creates date alerts creation job' do + let(:timezone) { timezone_paris } + let(:scheduled_at) { '1:00' } + let(:local_time) { '1:04' } + let(:user) { user_paris } + end + end + + context 'for users whose local time is 1:00 am (UTC+05:45) when the job is executed' do + it_behaves_like 'job execution creates date alerts creation job' do + let(:timezone) { timezone_kathmandu } + let(:scheduled_at) { '1:00' } + let(:local_time) { '1:04' } + let(:user) { user_kathmandu } + end + end + + context 'without enterprise token', with_ee: false do + it_behaves_like 'job execution creates no date alerts creation job' do + let(:timezone) { timezone_paris } + let(:scheduled_at) { '1:00' } + let(:local_time) { '1:04' } + end + end + + context 'when scheduled and executed at 01:00 am local time' do + it_behaves_like 'job execution creates date alerts creation job' do + let(:timezone) { timezone_paris } + let(:scheduled_at) { '1:00' } + let(:local_time) { '1:00' } + let(:user) { user_paris } + end + end + + context 'when scheduled and executed at 01:14 am local time' do + it_behaves_like 'job execution creates date alerts creation job' do + let(:timezone) { timezone_paris } + let(:scheduled_at) { '1:14' } + let(:local_time) { '1:14' } + let(:user) { user_paris } + end + end + + context 'when scheduled and executed at 01:15 am local time' do + it_behaves_like 'job execution creates no date alerts creation job' do + let(:timezone) { timezone_paris } + let(:scheduled_at) { '1:15' } + let(:local_time) { '1:15' } + end + end + + context 'when scheduled at 01:00 am local time and executed at 01:37 am local time' do + it_behaves_like 'job execution creates date alerts creation job' do + let(:timezone) { timezone_paris } + let(:scheduled_at) { '1:00' } + let(:local_time) { '1:37' } + let(:user) { user_paris } + end + end + + context 'with a user having only due_date active in notification settings' do + before do + NotificationSetting + .where(user: user_paris) + .update_all(due_date: 1, + start_date: nil, + overdue: nil) + end + + it_behaves_like 'job execution creates date alerts creation job' do + let(:timezone) { timezone_paris } + let(:scheduled_at) { '1:00' } + let(:local_time) { '1:00' } + let(:user) { user_paris } + end + end + + context 'with a user having only start_date active in notification settings' do + before do + NotificationSetting + .where(user: user_paris) + .update_all(due_date: nil, + start_date: 1, + overdue: nil) + end + + it_behaves_like 'job execution creates date alerts creation job' do + let(:timezone) { timezone_paris } + let(:scheduled_at) { '1:00' } + let(:local_time) { '1:00' } + let(:user) { user_paris } + end + end + + context 'with a user having only overdue active in notification settings' do + before do + NotificationSetting + .where(user: user_paris) + .update_all(due_date: nil, + start_date: nil, + overdue: 1) + end + + it_behaves_like 'job execution creates date alerts creation job' do + let(:timezone) { timezone_paris } + let(:scheduled_at) { '1:00' } + let(:local_time) { '1:00' } + let(:user) { user_paris } + end + end + + context 'without a user having notification settings' do + before do + NotificationSetting + .where(user: user_paris) + .update_all(due_date: nil, + start_date: nil, + overdue: nil) + end + + it_behaves_like 'job execution creates no date alerts creation job' do + let(:timezone) { timezone_paris } + let(:scheduled_at) { '1:00' } + let(:local_time) { '1:00' } + end + end + + context 'with a user having only a project active notification settings' do + before do + NotificationSetting + .where(user: user_paris) + .update_all(due_date: nil, + start_date: nil, + overdue: nil) + + NotificationSetting + .create(user: user_paris, + project: create(:project), + due_date: 1, + start_date: nil, + overdue: nil) + end + + it_behaves_like 'job execution creates date alerts creation job' do + let(:timezone) { timezone_paris } + let(:scheduled_at) { '1:00' } + let(:local_time) { '1:00' } + let(:user) { user_paris } + end + end + + context 'with a locked user' do + before do + user_paris.locked! + end + + it_behaves_like 'job execution creates no date alerts creation job' do + let(:timezone) { timezone_paris } + let(:scheduled_at) { '1:00' } + let(:local_time) { '1:00' } + end + end + end +end