[#43819] Basic administration for non working days (excluding danger zone)

https://community.openproject.org/work_packages/43819

WorkingDays as Setting
pull/11254/head
Dombi Attila 2 years ago
parent 16e68736e3
commit bb107a7223
  1. 4
      .rubocop.yml
  2. 47
      app/contracts/settings/working_days_params_contract.rb
  3. 25
      app/controllers/admin/settings/working_days_settings_controller.rb
  4. 6
      app/controllers/admin/settings_controller.rb
  5. 3
      app/helpers/settings_helper.rb
  6. 18
      app/models/day.rb
  7. 28
      app/models/week_day.rb
  8. 22
      app/services/settings/update_service.rb
  9. 12
      app/services/work_packages/shared/working_days.rb
  10. 55
      app/views/admin/settings/working_days_settings/show.html.erb
  11. 5
      config/constants/settings/definitions.rb
  12. 6
      config/initializers/menus.rb
  13. 12
      config/locales/en.yml
  14. 21
      config/routes.rb
  15. 21
      db/migrate/20220909153412_drop_week_days.rb
  16. 12
      lib/api/v3/days/week_api.rb
  17. 4
      lib/api/v3/days/week_day_representer.rb
  18. 10
      modules/calendar/spec/features/calendar_dates_spec.rb
  19. 10
      modules/team_planner/spec/features/team_planner_dates_spec.rb
  20. 48
      spec/contracts/settings/working_days_params_contract_spec.rb
  21. 6
      spec/contracts/work_packages/base_contract_spec.rb
  22. 37
      spec/factories/week_day_factory.rb
  23. 86
      spec/features/admin/working_days_spec.rb
  24. 8
      spec/features/menu_items/admin_menu_item_spec.rb
  25. 2
      spec/features/work_packages/datepicker/datepicker_logic_spec.rb
  26. 3
      spec/features/work_packages/details/workdays_spec.rb
  27. 7
      spec/features/work_packages/timeline/timeline_dates_spec.rb
  28. 1
      spec/lib/api/v3/days/day_collection_representer_spec.rb
  29. 2
      spec/lib/api/v3/days/day_representer_spec.rb
  30. 2
      spec/lib/api/v3/days/week_day_collection_representer_spec.rb
  31. 6
      spec/lib/api/v3/days/week_day_representer_spec.rb
  32. 22
      spec/models/day_spec.rb
  33. 1
      spec/models/setting_spec.rb
  34. 8
      spec/models/week_day_spec.rb
  35. 2
      spec/requests/api/v3/days/day_spec.rb
  36. 3
      spec/requests/api/v3/days/week_show_resource_spec.rb
  37. 1
      spec/requests/api/v3/days/week_spec.rb
  38. 4
      spec/services/work_packages/set_attributes_service_spec.rb
  39. 8
      spec/services/work_packages/set_schedule_service_working_days_spec.rb
  40. 4
      spec/services/work_packages/shared/shared_examples_days.rb
  41. 6
      spec/services/work_packages/shared/working_days_spec.rb
  42. 78
      spec/support/settings.rb
  43. 2
      spec/support_spec/schedule_helpers/chart_representer_spec.rb
  44. 2
      spec/support_spec/schedule_helpers/chart_spec.rb
  45. 23
      spec/workers/work_packages/apply_working_days_change_job_spec.rb

@ -79,7 +79,6 @@ Lint/Void:
Lint/AmbiguousBlockAssociation:
AllowedMethods: [change]
Metrics/ClassLength:
Enabled: false
@ -282,6 +281,9 @@ Style/EvenOdd:
Style/FormatString:
Enabled: false
Style/FormatStringToken:
AllowedMethods: [redirect]
Style/GlobalVars:
Enabled: false

@ -0,0 +1,47 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2022 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
module Settings
class WorkingDaysParamsContract < ::ParamsContract
include RequiresAdminGuard
validate :working_days_are_present
protected
def working_days_are_present
if working_days.empty?
errors.add :base, :working_days_are_missing
end
end
def working_days
params[:working_days]
end
end
end

@ -0,0 +1,25 @@
module Admin::Settings
class WorkingDaysSettingsController < ::Admin::SettingsController
current_menu_item [:show] do
:working_days
end
def default_breadcrumb
t(:label_working_days)
end
def show_local_breadcrumb
true
end
def settings_params
settings = super
settings[:working_days] = settings[:working_days].compact_blank.map(&:to_i).uniq
settings
end
def contract_options
{ params_contract: Settings::WorkingDaysParamsContract }
end
end
end

@ -51,7 +51,7 @@ module Admin
return unless params[:settings]
call = ::Settings::UpdateService
.new(user: current_user)
.new(user: current_user, contract_options:)
.call(settings_params)
call.on_success { flash[:notice] = t(:notice_successful_update) }
@ -93,5 +93,9 @@ module Admin
def settings_params
permitted_params.settings.to_h
end
def contract_options
{}
end
end
end

@ -76,8 +76,9 @@ module SettingsHelper
end
def setting_multiselect(setting, choices, options = {})
direction = options.delete(:direction) || :vertical
setting_label(setting, options) +
content_tag(:span, class: 'form--field-container -vertical') do
content_tag(:span, class: "form--field-container -#{direction}") do
hidden = with_empty_unless_writable(setting) do
hidden_field_tag("settings[#{setting}][]", '')
end

@ -29,12 +29,6 @@
class Day < ApplicationRecord
include Tableless
belongs_to :week_day,
inverse_of: false,
class_name: 'WeekDay',
foreign_key: :day_of_week,
primary_key: :day
has_many :non_working_days,
inverse_of: false,
class_name: 'NonWorkingDay',
@ -53,7 +47,6 @@ class Day < ApplicationRecord
from = today.at_beginning_of_month
to = today.next_month.at_end_of_month
from_range(from:, to:)
.includes(:week_day)
.includes(:non_working_days)
.order("days.id")
end
@ -68,16 +61,21 @@ class Day < ApplicationRecord
to_char(dd, 'YYYYMMDD')::integer id,
date_trunc('day', dd)::date date,
extract(isodow from dd) day_of_week,
(COALESCE(week_days.working, TRUE) AND non_working_days.id IS NULL)::bool working
(COALESCE(POSITION(extract(isodow from dd)::text IN settings.value) > 0, TRUE)
AND non_working_days.id IS NULL)::bool working
FROM
generate_series( '#{from}'::timestamp,
'#{to}'::timestamp,
'1 day'::interval) dd
LEFT JOIN week_days
ON extract(isodow from dd) = week_days.day
LEFT JOIN settings
ON settings.name = 'working_days'
LEFT JOIN non_working_days
ON dd = non_working_days.date
) days
SQL
end
def week_day
WeekDay.new(day: day_of_week)
end
end

@ -1,6 +1,32 @@
class WeekDay < ApplicationRecord
class WeekDay
DAY_RANGE = Array(1..7)
attr_accessor :day
class << self
def find_by!(day:)
raise ActiveRecord::RecordNotFound, "Couldn't find WeekDay with day #{day}" unless day.in?(DAY_RANGE)
new(day:)
end
def all
DAY_RANGE.map do |day|
new(day:)
end
end
end
def initialize(day:)
self.day = day
end
def name
day_names = I18n.t('date.day_names')
day_names[day % 7]
end
def working
Setting.working_days.empty? || day.in?(Setting.working_days)
end
end

@ -27,11 +27,23 @@
#++
class Settings::UpdateService < ::BaseServices::BaseContracted
def initialize(user:)
def initialize(user:, contract_options: {})
super user:,
contract_options:,
contract_class: Settings::UpdateContract
end
def validate_params(params)
if contract_options[:params_contract]
contract = contract_options[:params_contract].new(model, user, params:)
ServiceResult.new success: contract.valid?,
errors: contract.errors,
result: model
else
super
end
end
def after_validate(params, call)
params.each do |name, value|
Setting[name] = derive_value(value)
@ -44,11 +56,9 @@ class Settings::UpdateService < ::BaseServices::BaseContracted
def derive_value(value)
case value
when Array
# remove blank values in array settings
value.delete_if(&:blank?)
when Hash
value.delete_if { |_, v| v.blank? }
when Array, Hash
# remove blank values in array, hash settings
value.compact_blank!
else
value.strip
end

@ -146,13 +146,15 @@ module WorkPackages
# To accomodate both versions 0-6, 1-7, an array of 8 elements is created
# where array[0] = array[7] = value for Sunday
#
# Because the database table for WeekDay could be empty or incomplete
# (like in tests), the initial array is built with all days considered
# working (value is `true`)
# Since Setting.working_days can be empty, the initial array is
# built with all days considered working (value is `true`)
@working_week_days = [true] * 8
WeekDay.pluck(:day, :working).each do |day, working|
@working_week_days[day] = working
WeekDay.all.each do |week_day|
@working_week_days[week_day.day] = week_day.working
end
@working_week_days[0] = @working_week_days[7] # value for Sunday is present at index 0 AND index 7
@working_week_days
end

@ -0,0 +1,55 @@
<%#-- copyright
OpenProject is an open source project management software.
Copyright (C) 2012-2022 the OpenProject GmbH
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License version 3.
OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
Copyright (C) 2006-2013 Jean-Philippe Lang
Copyright (C) 2010-2013 the ChiliProject Team
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
See COPYRIGHT and LICENSE files for more details.
++#%>
<% content_for :header_tags do %>
<meta name="required_script" content="administration_settings" />
<% end %>
<%= toolbar title: t(:label_working_days) %>
<div class="op-toast -warning">
<div class="op-toast--content">
<p><%= t("working_days.warning") %></p>
</div>
</div>
<%= styled_form_tag(admin_settings_working_days_path, method: :patch) do %>
<section class="form--section">
<p>
<%= t("working_days.info").html_safe %>
</p>
<div class="form--field" id="setting_working_days">
<%= setting_multiselect :working_days,
I18n.t('date.day_names').rotate.zip(WeekDay::DAY_RANGE),
direction: :horizontal,
label: false %>
</div>
</section>
<%= styled_button_tag t(:button_save), class: '-highlight -with-icon icon-checkmark' %>
<% end %>

@ -986,6 +986,11 @@ Settings::Definition.define do
add :work_package_startdate_is_adddate,
default: false
add :working_days,
format: :array,
allowed: Array(1..7),
default: Array(1..5) # Sat, Sun being non-working days
add :youtube_channel,
default: 'https://www.youtube.com/c/OpenProjectCommunity',
writable: false

@ -276,6 +276,12 @@ Redmine::MenuManager.map :admin_menu do |menu|
if: Proc.new { User.current.admin? },
icon: 'icon2 icon-enumerations'
menu.push :working_days,
{ controller: '/admin/settings/working_days_settings', action: :show },
if: Proc.new { User.current.admin? },
caption: :label_working_days,
icon: 'icon2 icon-calendar'
menu.push :settings,
{ controller: '/admin/settings/general_settings', action: :show },
if: Proc.new { User.current.admin? },

@ -793,6 +793,10 @@ en:
attributes:
permissions:
dependency_missing: "need to also include '%{dependency}' as '%{permission}' is selected."
setting:
attributes:
base:
working_days_are_missing: 'At least one working day needs to be specified.'
time_entry:
attributes:
hours:
@ -2007,6 +2011,7 @@ en:
label_workflow: "Workflow"
label_workflow_plural: "Workflows"
label_workflow_summary: "Summary"
label_working_days: "Working days"
label_x_closed_work_packages_abbr:
one: "1 closed"
other: "%{count} closed"
@ -2668,7 +2673,6 @@ en:
status: "Entire row by Status"
type: "Entire row by Type"
priority: "Entire row by Priority"
text_formatting:
markdown: 'Markdown'
plain: 'Plain text'
@ -3099,6 +3103,12 @@ en:
info: "Deleting the work package is an irreversible action."
title: "Delete the work package"
working_days:
info: >
<b>Define days considered part of the work week</b><br/>
Days that are not selected are skipped when scheduling work packages (and not included in the day count). These can be overriden at a work-package level.
warning: >
Changing which days of the week are considered working days can affect the start and finish days of all work packages in all projects in this instance.
nothing_to_preview: "Nothing to preview"
api_v3:

@ -71,7 +71,7 @@ OpenProject::Application.routes.draw do
# forward requests to the proxy
if FrontendAssetHelper.assets_proxied?
match '/assets/frontend/*appendix',
to: redirect(FrontendAssetHelper.cli_proxy + "/assets/frontend/%{appendix}", status: 307),
to: redirect("#{FrontendAssetHelper.cli_proxy}/assets/frontend/%{appendix}", status: 307),
format: false,
via: :all
end
@ -143,10 +143,7 @@ OpenProject::Application.routes.draw do
resources :custom_fields, except: :show do
member do
match "options/:option_id",
to: "custom_fields#delete_option",
via: :delete,
as: :delete_option_of
delete "options/:option_id", to: "custom_fields#delete_option", as: :delete_option_of
post :reorder_alphabetical
end
@ -192,7 +189,7 @@ OpenProject::Application.routes.draw do
end
member do
get "settings", to: redirect('projects/%{id}/settings/general/') # rubocop:disable Style/FormatStringToken
get "settings", to: redirect('projects/%{id}/settings/general/')
get :copy
@ -278,7 +275,7 @@ OpenProject::Application.routes.draw do
resources :members, only: %i[index create update destroy], shallow: true do
collection do
match :autocomplete_for_member, via: %i[get]
get :autocomplete_for_member
end
end
@ -306,7 +303,7 @@ OpenProject::Application.routes.draw do
%w{diff annotate changes entry browse}.each do |action|
get "(/revisions/:rev)/#{action}(/*repo_path)",
format: 'html',
action: action,
action:,
constraints: { rev: /[\w0-9.\-_]+/, repo_path: /.*/ },
as: "#{action}_revision"
end
@ -401,6 +398,7 @@ OpenProject::Application.routes.draw do
resource :mail_notifications, controller: '/admin/settings/mail_notifications_settings', only: %i[show update]
resource :api, controller: '/admin/settings/api_settings', only: %i[show update]
resource :work_packages, controller: '/admin/settings/work_packages_settings', only: %i[show update]
resource :working_days, controller: '/admin/settings/working_days_settings', only: %i[show update]
resource :users, controller: '/admin/settings/users_settings', only: %i[show update]
# Redirect /settings to general settings
@ -468,7 +466,7 @@ OpenProject::Application.routes.draw do
member do
get '/edit(/:tab)' => 'users#edit', as: 'edit'
match '/change_status/:change_action' => 'users#change_status_info', via: :get, as: 'change_status_info'
get '/change_status/:change_action' => 'users#change_status_info', as: 'change_status_info'
post :change_status
post :resend_invitation
get :deletion_info
@ -535,9 +533,8 @@ OpenProject::Application.routes.draw do
# alternate routes for the current user
scope 'my' do
match '/deletion_info' => 'users#deletion_info', via: :get, as: 'delete_my_account_info'
match '/oauth/revoke_application/:application_id' => 'oauth/grants#revoke_application', via: :post,
as: 'revoke_my_oauth_application'
get '/deletion_info' => 'users#deletion_info', as: 'delete_my_account_info'
post '/oauth/revoke_application/:application_id' => 'oauth/grants#revoke_application', as: 'revoke_my_oauth_application'
end
scope controller: 'my' do

@ -0,0 +1,21 @@
class DropWeekDays < ActiveRecord::Migration[7.0]
def up
drop_table :week_days
end
def down
create_table :week_days do |t|
t.integer :day, null: false
t.boolean :working, null: false, default: true
t.timestamps
end
execute <<-SQL.squish
ALTER TABLE week_days
ADD CONSTRAINT unique_day_number UNIQUE (day);
ALTER TABLE week_days
ADD CHECK (day >= 1 AND day <=7);
SQL
end
end

@ -31,10 +31,14 @@ module API::V3::Days
helpers ::API::Utilities::UrlPropsParsingHelper
resources :week do
get &::API::V3::Utilities::Endpoints::Index.new(model: WeekDay,
render_representer: WeekDayCollectionRepresenter,
self_path: -> { api_v3_paths.days_week })
.mount
get do
self_link = api_v3_paths.days_week
week_days = WeekDay.all
WeekDayCollectionRepresenter.new(week_days,
self_link:,
current_user:)
end
route_param :day, type: Integer, desc: 'WeekDay ID' do
after_validation do
@week_day = WeekDay.find_by!(day: declared_params[:day])

@ -39,5 +39,9 @@ module API::V3::Days
def _type
'WeekDay'
end
def json_key_part_represented
[represented.day, Setting.working_days]
end
end
end

@ -50,7 +50,7 @@ describe 'Calendar non working days', type: :feature, js: true do
end
context 'with week days defined' do
let!(:week_days) { create :week_days }
let(:week_days) { week_with_saturday_and_sunday_as_weekend }
it 'renders sat and sun as non working' do
expect(page).to have_selector('.fc-day-sat.fc-non-working-day', minimum: 1, wait: 10)
@ -76,13 +76,7 @@ describe 'Calendar non working days', type: :feature, js: true do
end
context 'with all days marked as weekend' do
let!(:week_days) do
days = create(:week_with_saturday_and_sunday_as_weekend)
WeekDay.update_all(working: false)
days
end
let(:week_days) { week_with_no_working_days }
it 'renders all as non working' do
expect(page).to have_selector('.fc-day-sat.fc-non-working-day', minimum: 1, wait: 10)

@ -37,7 +37,7 @@ describe 'Team planner working days', type: :feature, js: true do
include_context 'with team planner full access'
context 'with week days defined' do
let!(:week_days) { create :week_days }
let!(:week_days) { week_with_saturday_and_sunday_as_weekend }
it 'renders sat and sun as non working' do
team_planner.visit!
@ -72,13 +72,7 @@ describe 'Team planner working days', type: :feature, js: true do
end
context 'with all days marked as weekend' do
let!(:week_days) do
days = create(:week_with_saturday_and_sunday_as_weekend)
WeekDay.update_all(working: false)
days
end
let!(:week_days) { week_with_no_working_days }
it 'renders all as non working' do
team_planner.visit!

@ -0,0 +1,48 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2022 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
require 'spec_helper'
require 'contracts/shared/model_contract_shared_context'
describe Settings::WorkingDaysParamsContract do
include_context 'ModelContract shared context'
let(:setting) { Setting }
let(:current_user) { build_stubbed(:admin) }
let(:params) { { working_days: [1] } }
let(:contract) do
described_class.new(setting, current_user, params:)
end
it_behaves_like 'contract is valid for active admins and invalid for regular users'
context 'without working days' do
let(:params) { { working_days: [] } }
include_examples 'contract is invalid', base: :working_days_are_missing
end
end

@ -600,7 +600,7 @@ describe WorkPackages::BaseContract do
context 'when setting duration and dates while covering non-working days' do
before do
create(:week_with_saturday_and_sunday_as_weekend)
week_with_saturday_and_sunday_as_weekend
work_package.ignore_non_working_days = false
work_package.duration = 6
work_package.start_date = "2022-08-22"
@ -623,7 +623,7 @@ describe WorkPackages::BaseContract do
context 'when setting duration and dates while covering non-working days and duration is too small' do
before do
create(:week_with_saturday_and_sunday_as_weekend)
week_with_saturday_and_sunday_as_weekend
work_package.ignore_non_working_days = false
work_package.duration = 1
work_package.start_date = "2022-08-22"
@ -646,7 +646,7 @@ describe WorkPackages::BaseContract do
context 'when setting duration and dates while covering non-working days and duration is too big' do
before do
create(:week_with_saturday_and_sunday_as_weekend)
week_with_saturday_and_sunday_as_weekend
work_package.ignore_non_working_days = false
work_package.duration = 99
work_package.start_date = "2022-08-22"

@ -28,45 +28,14 @@
FactoryBot.define do
factory :week_day do
sequence :day, [1, 2, 3, 4, 5, 6, 7].cycle
working { day < 6 }
# hack to reuse the day if it already exists in database
to_create do |instance|
instance.attributes = WeekDay.find_or_create_by(instance.attributes.slice("day", "working")).attributes
instance.instance_variable_set('@new_record', false)
end
trait :tuesday do
day { 2 }
end
end
# Factory to create all 7 week days at once, Saturday and Sunday being weekend days
factory :week_with_saturday_and_sunday_as_weekend, aliases: [:week_days], parent: :week do
working_days { %w[monday tuesday wednesday thursday friday] }
end
# Factory to create all 7 week days at once
#
# use +working: ['monday', 'tuesday', ...]+ to define which days of the week
# will be working days. By default, all days are working days.
factory :week, class: 'Array' do
transient do
working_days { %w[monday tuesday wednesday thursday friday saturday sunday] }
end
# Skip the create callback to be able to use non-AR models. Otherwise FactoryBot will
# try to call #save! on any created object.
skip_create
sequence :day, [1, 2, 3, 4, 5, 6, 7].cycle
initialize_with do
%w[monday tuesday wednesday thursday friday saturday sunday]
.map.with_index do |day_name, i|
day = i + 1
working = working_days.include?(day_name)
create(:week_day, day:, working:)
end
new(day:)
end
end
end

@ -0,0 +1,86 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2022 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
require 'spec_helper'
describe 'Working Days', type: :feature do
shared_let(:week_days) { week_with_saturday_and_sunday_as_weekend }
shared_let(:admin) { create :admin }
before do
login_as(admin)
visit admin_settings_working_days_path
end
it 'contains all defined days from the settings' do
WeekDay.all.each do |day|
expect(page).to have_selector('label', text: day.name)
if day.working
expect(page).to have_checked_field day.name
end
end
end
it 'updates the values and saves the settings' do
expect(Setting.working_days).to eq([1, 2, 3, 4, 5])
uncheck 'Monday'
uncheck 'Friday'
click_on 'Save'
expect(page).to have_selector('.flash.notice', text: 'Successful update.')
expect(page).to have_unchecked_field 'Monday'
expect(page).to have_unchecked_field 'Friday'
expect(page).to have_unchecked_field 'Saturday'
expect(page).to have_unchecked_field 'Sunday'
expect(page).to have_checked_field 'Tuesday'
expect(page).to have_checked_field 'Wednesday'
expect(page).to have_checked_field 'Thursday'
expect(Setting.working_days).to eq([2, 3, 4])
end
it 'shows error when no working days are set' do
uncheck 'Monday'
uncheck 'Tuesday'
uncheck 'Wednesday'
uncheck 'Thursday'
uncheck 'Friday'
click_on 'Save'
expect(page).to have_selector('.flash.error', text: 'At least one working day needs to be specified.')
# Restore the checkboxes to their valid state
expect(page).to have_checked_field 'Monday'
expect(page).to have_checked_field 'Tuesday'
expect(page).to have_checked_field 'Wednesday'
expect(page).to have_checked_field 'Thursday'
expect(page).to have_checked_field 'Friday'
expect(page).to have_unchecked_field 'Saturday'
expect(page).to have_unchecked_field 'Sunday'
expect(Setting.working_days).to eq([1, 2, 3, 4, 5])
end
end

@ -44,8 +44,8 @@ describe 'Admin menu items', js: true do
visit admin_index_path
expect(page).to have_selector('[data-qa-selector="menu-blocks--container"]')
expect(page).to have_selector('[data-qa-selector="menu-block"]', count: 19)
expect(page).to have_selector('[data-qa-selector="op-menu--item-action"]', count: 20) # All plus 'overview'
expect(page).to have_selector('[data-qa-selector="menu-block"]', count: 20)
expect(page).to have_selector('[data-qa-selector="op-menu--item-action"]', count: 21) # All plus 'overview'
end
end
@ -57,10 +57,10 @@ describe 'Admin menu items', js: true do
visit admin_index_path
expect(page).to have_selector('[data-qa-selector="menu-blocks--container"]')
expect(page).to have_selector('[data-qa-selector="menu-block"]', count: 18)
expect(page).to have_selector('[data-qa-selector="menu-block"]', count: 19)
expect(page).not_to have_selector('[data-qa-selector="menu-block"]', text: I18n.t('timelines.admin_menu.colors'))
expect(page).to have_selector('[data-qa-selector="op-menu--item-action"]', count: 19) # All plus 'overview'
expect(page).to have_selector('[data-qa-selector="op-menu--item-action"]', count: 20) # All plus 'overview'
expect(page).not_to have_selector('[data-qa-selector="op-menu--item-action"]', text: I18n.t('timelines.admin_menu.colors'))
end
end

@ -42,7 +42,7 @@ describe 'Datepicker modal logic test cases (WP #43539)',
shared_let(:milestone_wp) { create :work_package, project:, type: type_milestone }
# assume sat+sun are non working days
shared_let(:weekdays) { create :week_days }
shared_let(:week_days) { week_with_saturday_and_sunday_as_weekend }
let(:work_packages_page) { Pages::FullWorkPackage.new(work_package, project) }
let(:wp_table) { Pages::WorkPackagesTable.new(project) }

@ -44,7 +44,6 @@ describe 'Work packages datepicker workdays',
let(:combined_date) { work_packages_page.edit_field(:combinedDate) }
before do
week_days
login_as(user)
work_packages_page.visit!
@ -55,7 +54,7 @@ describe 'Work packages datepicker workdays',
end
context 'with default work days' do
let(:week_days) { create :week_days }
shared_let(:working_days) { week_with_saturday_and_sunday_as_weekend }
it 'shows them as disabled' do
expect(page).to have_selector('.dayContainer', count: 2)

@ -65,7 +65,6 @@ RSpec.describe 'Work package timeline date formatting',
subject: 'Work Package ignoring non working days'
end
let(:week_days) { nil }
let(:wp_timeline) { Pages::WorkPackagesTimeline.new(project) }
let!(:query_tl) do
query = build(:query, user: current_user, project:)
@ -88,7 +87,6 @@ RSpec.describe 'Work package timeline date formatting',
end
before do
week_days
login_as current_user
wp_timeline.visit_query query_tl
@ -125,7 +123,8 @@ RSpec.describe 'Work package timeline date formatting',
context 'with weekdays defined' do
let(:current_user) { create :admin, language: 'en' }
let(:week_days) { create :week_days }
shared_let(:week_days) { week_with_saturday_and_sunday_as_weekend }
it 'shows them as disabled' do
expect_date_week work_package.start_date.iso8601, '01'
@ -160,8 +159,8 @@ RSpec.describe 'Work package timeline date formatting',
end
describe 'setting dates' do
shared_let(:week_days) { week_with_saturday_and_sunday_as_weekend }
let(:current_user) { create :admin }
let(:week_days) { create :week_days }
let(:row) { wp_timeline.timeline_row work_package_with_non_working_days.id }
shared_examples "sets dates, duration and displays bar" do

@ -29,7 +29,6 @@
require 'spec_helper'
describe ::API::V3::Days::DayCollectionRepresenter do
let!(:week_days) { create(:week_with_saturday_and_sunday_as_weekend) }
let(:days) do
[
build(:day, date: Date.new(2022, 12, 27)),

@ -41,7 +41,7 @@ describe ::API::V3::Days::DayRepresenter do
subject(:generated) { representer.to_json }
before do
create(:week_day, :tuesday, working:)
set_week_days('tuesday', working:)
end
it 'has _type: Day' do

@ -29,7 +29,7 @@
require 'spec_helper'
describe ::API::V3::Days::WeekDayCollectionRepresenter do
let(:week_days) { build(:week_days) }
let(:week_days) { WeekDay.all }
let(:representer) do
described_class.new(week_days,
self_link: '/api/v3/days/week',

@ -29,7 +29,7 @@
require 'spec_helper'
describe ::API::V3::Days::WeekDayRepresenter do
let(:week_day) { build_stubbed(:week_day, day: 1) }
let(:week_day) { build(:week_day, day: 1) }
let(:representer) { described_class.new(week_day, current_user: instance_double(User, name: 'current_user')) }
describe '#to_json' do
@ -99,8 +99,8 @@ describe ::API::V3::Days::WeekDayRepresenter do
end
end
it 'changes when the week_day is updated' do
week_day.updated_at = 20.seconds.from_now
it 'changes when the Setting is updated' do
set_week_days('tuesday')
expect(representer.json_cache_key)
.not_to eql former_cache_key

@ -23,8 +23,8 @@ describe Day, type: :model do
)
end
it 'eager loads week_day relation' do
expect(days).to(be_all { |d| d.association(:week_day).loaded? })
it 'loads week_day method' do
expect(days).to(be_all { |d| d.week_day.present? })
end
it 'eager loads non_working_days relation' do
@ -43,16 +43,16 @@ describe Day, type: :model do
expect(days.first.day_of_week % 7).to eq(today.at_beginning_of_month.wday) # wday is from 0-6
end
it 'does not have a name' do
expect(days.first.name).to be_nil
it 'loads the name attribute' do
expect(days.first.name).to eq(today.at_beginning_of_month.strftime("%A"))
end
end
context 'for collection with multiple non-working days' do
shared_let(:week_days) { week_with_saturday_and_sunday_as_weekend }
let(:non_working_dates) { [date_range.begin, date_range.begin + 1.day] }
before do
create(:week_with_saturday_and_sunday_as_weekend)
non_working_dates.each { |date| create(:non_working_day, date:) }
end
@ -78,10 +78,6 @@ describe Day, type: :model do
end
context 'with the weekday present' do
before do
create(:week_day, day: 6)
end
it 'loads the name attribute' do
expect(subject.name).to eq('Saturday')
end
@ -89,9 +85,7 @@ describe Day, type: :model do
describe '#working' do
context 'when the week day is non-working' do
before do
create(:week_day, day: 6, working: false)
end
shared_let(:working_days) { week_with_no_working_days }
it 'is false' do
expect(subject.working).to be_falsy
@ -109,9 +103,7 @@ describe Day, type: :model do
end
context 'when the week day is working' do
before do
create(:week_day, day: 6, working: true)
end
shared_let(:working_days) { reset_working_week_days('saturday') }
it 'is true' do
expect(subject.working).to be_truthy

@ -31,6 +31,7 @@ require 'spec_helper'
describe Setting, type: :model do
before do
described_class.clear_cache
described_class.destroy_all
end
after do

@ -3,11 +3,11 @@ require 'rails_helper'
RSpec.describe WeekDay, type: :model do
describe '#name' do
it 'returns the translated week day name' do
expect(described_class.create(day: 1).name).to eq('Monday')
expect(described_class.create(day: 7).name).to eq('Sunday')
expect(described_class.new(day: 1).name).to eq('Monday')
expect(described_class.new(day: 7).name).to eq('Sunday')
I18n.with_locale(:de) do
expect(described_class.create(day: 3).name).to eq('Mittwoch')
expect(described_class.create(day: 4).name).to eq('Donnerstag')
expect(described_class.new(day: 3).name).to eq('Mittwoch')
expect(described_class.new(day: 4).name).to eq('Donnerstag')
end
end
end

@ -32,13 +32,13 @@ describe ::API::V3::Days::DaysAPI,
type: :request do
include API::V3::Utilities::PathHelper
shared_let(:working_days) { week_with_saturday_and_sunday_as_weekend }
let(:parsed_response) { JSON.parse(last_response.body) }
let(:filters) { [] }
current_user { user }
before do
create(:week_with_saturday_and_sunday_as_weekend)
get api_v3_paths.path_for :days, filters:
end

@ -33,8 +33,7 @@ describe ::API::V3::Days::WeekAPI,
type: :request do
include API::V3::Utilities::PathHelper
let(:week_day) { create(:week_day, day: 1) }
let(:path) { api_v3_paths.days_week_day(week_day.day) }
let(:path) { api_v3_paths.days_week_day(1) }
current_user { user }
subject { last_response.body }

@ -38,7 +38,6 @@ describe ::API::V3::Days::WeekAPI,
current_user { user }
before do
create(:week_with_saturday_and_sunday_as_weekend)
get api_v3_paths.days_week
end

@ -1039,7 +1039,7 @@ describe WorkPackages::SetAttributesService,
end
context 'with non-working days' do
shared_let(:week_days) { create(:week_with_saturday_and_sunday_as_weekend) }
shared_let(:working_days) { week_with_saturday_and_sunday_as_weekend }
let(:monday) { Time.zone.today.beginning_of_week }
let(:tuesday) { monday + 1.day }
let(:wednesday) { monday + 2.days }
@ -1643,12 +1643,12 @@ describe WorkPackages::SetAttributesService,
end
context 'when the soonest start date is a non-working day' do
shared_let(:working_days) { week_with_saturday_and_sunday_as_weekend }
let(:saturday) { Time.zone.today.beginning_of_week.next_occurring(:saturday) }
let(:next_monday) { saturday.next_occurring(:monday) }
let(:soonest_start) { saturday }
before do
create(:week_with_saturday_and_sunday_as_weekend)
work_package.ignore_non_working_days = false
end

@ -31,7 +31,7 @@ require 'spec_helper'
describe WorkPackages::SetScheduleService, 'working days' do
create_shared_association_defaults_for_work_package_factory
shared_let(:week_days) { create(:week_with_saturday_and_sunday_as_weekend) }
shared_let(:week_days) { week_with_saturday_and_sunday_as_weekend }
let(:instance) do
described_class.new(user:, work_package:)
@ -1023,10 +1023,10 @@ describe WorkPackages::SetScheduleService, 'working days' do
end
def set_non_working_week_days(*days)
days.each do |day|
wday = %w[xxx monday tuesday wednesday thursday friday saturday sunday].index(day.downcase)
WeekDay.find_by!(day: wday).update(working: false)
non_working_days = days.map do |day|
%w[xxx monday tuesday wednesday thursday friday saturday sunday].index(day.downcase)
end
Setting.working_days -= non_working_days
end
context 'when moving forward due to days and predecessor due date now being non-working days' do

@ -29,7 +29,7 @@
Date::DATE_FORMATS[:wday_date] = '%a %-d %b %Y' # Fri 5 Aug 2022
RSpec.shared_context 'with weekend days Saturday and Sunday' do
shared_let(:week_days) { create(:week_with_saturday_and_sunday_as_weekend) }
shared_let(:week_days) { week_with_saturday_and_sunday_as_weekend }
end
RSpec.shared_context 'with non working days Christmas 2022 and new year 2023' do
@ -41,7 +41,7 @@ RSpec.shared_context 'with no working days' do
include_context 'with weekend days Saturday and Sunday'
before do
WeekDay.update_all(working: false)
week_with_no_working_days
end
end

@ -181,7 +181,8 @@ RSpec.describe WorkPackages::Shared::WorkingDays do
describe '#add_days' do
it 'when positive, adds the number of working days to the date, ignoring non-working days' do
create(:week_day, day: 5, working: false)
# Friday is a non working week day
set_non_working_week_days('friday')
create(:non_working_day, date: wednesday_2022_08_03)
# Wednesday is skipped (non working day)
@ -195,7 +196,8 @@ RSpec.describe WorkPackages::Shared::WorkingDays do
end
it 'when negative, removes the number of working days to the date, ignoring non-working days' do
create(:week_day, day: 5, working: false)
# Friday is a non working week day
set_non_working_week_days('friday')
create(:non_working_day, date: sunday_2022_07_31)
# Sunday is skipped (non working day)

@ -0,0 +1,78 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2022 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
def week_with_saturday_and_sunday_as_weekend
Setting.working_days = Array(1..5)
end
def week_with_all_days_working
Setting.working_days = Array(1..7)
end
def week_with_no_working_days
# This a hack to make all days non-working,
# because we don't allow that by definition
Setting.working_days = [false]
end
def set_non_working_week_days(*days)
week_days = get_week_days(*days)
Setting.working_days -= week_days
end
def set_working_week_days(*days)
week_days = get_week_days(*days)
Setting.working_days += week_days
end
def set_week_days(*days, working: true)
if working
set_working_week_days(*days)
else
set_non_working_week_days(*days)
end
end
def reset_working_week_days(*days)
week_days = get_week_days(*days)
Setting.working_days = week_days
end
def get_week_days(*days)
days.map do |day|
%w[xxx monday tuesday wednesday thursday friday saturday sunday].index(day.downcase)
end
end
RSpec.configure do |config|
config.before(:suite) do
# The test suite assumes the default of all days working.
# Since the Setting default is with Sat-Sun non-working, we update it before the tests.
week_with_all_days_working
end
end

@ -39,7 +39,7 @@ describe ScheduleHelpers::ChartRepresenter do
let(:sunday) { Date.new(2022, 6, 26) }
describe '#normalized_to_s' do
let!(:week_days) { create(:week_with_saturday_and_sunday_as_weekend) }
shared_let(:week_days) { week_with_saturday_and_sunday_as_weekend }
context 'when both charts have different work packages items and/or order' do
def to_first_columns(charts)

@ -157,7 +157,7 @@ describe ScheduleHelpers::Chart do
end
describe '#to_s' do
let!(:week_days) { create(:week_with_saturday_and_sunday_as_weekend) }
shared_let(:week_days) { week_with_saturday_and_sunday_as_weekend }
context 'with a chart built from ascii representation' do
let(:chart) do

@ -33,22 +33,7 @@ RSpec.describe WorkPackages::ApplyWorkingDaysChangeJob do
shared_let(:user) { create(:user) }
let!(:week) { create(:week_with_saturday_and_sunday_as_weekend) }
def set_non_working_week_days(*days)
set_week_days(*days, working: false)
end
def set_working_week_days(*days)
set_week_days(*days, working: true)
end
def set_week_days(*days, working:)
days.each do |day|
wday = %w[xxx monday tuesday wednesday thursday friday saturday sunday].index(day.downcase)
WeekDay.find_by!(day: wday).update(working:)
end
end
let!(:week) { week_with_saturday_and_sunday_as_weekend }
context 'when a work package includes a date that is now a non-working day' do
let_schedule(<<~CHART)
@ -138,7 +123,7 @@ RSpec.describe WorkPackages::ApplyWorkingDaysChangeJob do
end
context 'when a follower has a predecessor with dates covering a day that is now a working day' do
let!(:week) { create(:week, working_days: ['monday', 'tuesday', 'thursday', 'friday']) }
let!(:week) { reset_working_week_days('monday', 'tuesday', 'thursday', 'friday') }
let_schedule(<<~CHART)
days | MTWTFSS |
@ -161,7 +146,7 @@ RSpec.describe WorkPackages::ApplyWorkingDaysChangeJob do
end
xcontext 'when a follower has a predecessor with a non-working day between them that is now a working day' do
let!(:week) { create(:week, working_days: ['monday', 'tuesday', 'thursday', 'friday']) }
let!(:week) { reset_working_week_days('monday', 'tuesday', 'thursday', 'friday') }
let_schedule(<<~CHART)
days | MTWTFSS |
@ -245,7 +230,7 @@ RSpec.describe WorkPackages::ApplyWorkingDaysChangeJob do
end
xcontext 'when having multiple work packages following each other, and having days becoming working days' do
let!(:week) { create(:week, working_days: ['monday', 'thursday']) }
let!(:week) { reset_working_week_days('monday', 'thursday') }
let_schedule(<<~CHART)
days | MTWTFSSmtwtfssmtwtfss |

Loading…
Cancel
Save