Project

General

Profile

Feature #1459 » 0001-Allow-non-working-dates-such-as-holidays-to-be-regis.patch

Go MAEDA, 2026-10-08 10:29

View differences:

app/assets/stylesheets/application.css
1256 1256
input#content_comments {inline-size: 99%}
1257 1257
span.pagination {margin-inline-start: 3px; color: var(--oc-gray-6); display: block;}
1258
.non-working-dates-year {display: block; margin-block-end: 1em;}
1259
.non-working-dates-year span.pagination {display: inline-block;}
1260
.non-working-dates-year .pagination ul.pages li {margin-block-end: 0;}
1258 1261
.pagination ul.pages {
1259 1262
  margin-block: 0;
1260 1263
  margin-inline: 0 5px;
app/assets/stylesheets/responsive.css
879 879
  }
880 880
  div.issue .next-prev-links .pagination ul.pages li.previous,
881 881
  div.issue .next-prev-links .pagination ul.pages li.position,
882
  div.issue .next-prev-links .pagination ul.pages li.next {
882
  div.issue .next-prev-links .pagination ul.pages li.next,
883
  .non-working-dates-year .pagination ul.pages li.previous,
884
  .non-working-dates-year .pagination ul.pages li.next {
883 885
    display: inline-block;
884 886
    inline-size: auto;
885 887
    overflow: visible;
app/controllers/context_menus/non_working_dates_controller.rb
1
# frozen_string_literal: true
2

  
3
# Redmine - project management software
4
# Copyright (C) 2006-  Jean-Philippe Lang
5
#
6
# This program is free software; you can redistribute it and/or
7
# modify it under the terms of the GNU General Public License
8
# as published by the Free Software Foundation; either version 2
9
# of the License, or (at your option) any later version.
10
#
11
# This program is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
# GNU General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with this program; if not, write to the Free Software
18
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
19

  
20
module ContextMenus
21
  class NonWorkingDatesController < BaseController
22
    before_action :require_admin
23
    before_action :find_non_working_dates
24

  
25
    def index
26
      render_context_menu 'non_working_dates'
27
    end
28

  
29
    private
30

  
31
    def find_non_working_dates
32
      @non_working_dates = NonWorkingDate.where(id: params[:id] || params[:ids]).to_a
33
      (render_404; return) if @non_working_dates.empty?
34

  
35
      if @non_working_dates.size == 1
36
        @non_working_date = @non_working_dates.first
37
      end
38
    end
39
  end
40
end
app/controllers/non_working_dates_controller.rb
1
# frozen_string_literal: true
2

  
3
# Redmine - project management software
4
# Copyright (C) 2006-  Jean-Philippe Lang
5
#
6
# This program is free software; you can redistribute it and/or
7
# modify it under the terms of the GNU General Public License
8
# as published by the Free Software Foundation; either version 2
9
# of the License, or (at your option) any later version.
10
#
11
# This program is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
# GNU General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with this program; if not, write to the Free Software
18
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
19

  
20
class NonWorkingDatesController < ApplicationController
21
  layout 'admin'
22
  self.main_menu = false
23
  menu_item :settings
24

  
25
  before_action :require_admin
26
  before_action :find_non_working_date, :only => [:edit, :update, :destroy]
27

  
28
  def index
29
    @year = params[:year].to_i if /\A\d{4}\z/.match?(params[:year].to_s)
30
    @year ||= User.current.today.year
31
    years = NonWorkingDate.years
32
    @years = (years | [@year, User.current.today.year]).sort
33
    @previous_year = @year - 1 if years.any? {|year| year < @year}
34
    @next_year = @year + 1 if years.any? {|year| year > @year}
35
    @non_working_dates = NonWorkingDate.in_year(@year).sorted.to_a
36
    @non_working_date_count = NonWorkingDate.count
37
  end
38

  
39
  def new
40
    @non_working_date = NonWorkingDate.new
41
  end
42

  
43
  def create
44
    @non_working_date = NonWorkingDate.new
45
    @non_working_date.safe_attributes = params[:non_working_date]
46
    if @non_working_date.save
47
      flash[:notice] = l(:notice_successful_create)
48
      if params[:continue]
49
        redirect_to new_non_working_date_path
50
      else
51
        redirect_to non_working_dates_path(:year => @non_working_date.date.year)
52
      end
53
    else
54
      render :action => 'new'
55
    end
56
  end
57

  
58
  def edit
59
  end
60

  
61
  def update
62
    @non_working_date.safe_attributes = params[:non_working_date]
63
    if @non_working_date.save
64
      flash[:notice] = l(:notice_successful_update)
65
      redirect_to non_working_dates_path(:year => @non_working_date.date.year)
66
    else
67
      render :action => 'edit'
68
    end
69
  end
70

  
71
  def destroy
72
    @non_working_date.destroy
73
    flash[:notice] = l(:notice_successful_delete)
74
    redirect_to non_working_dates_path(:year => @non_working_date.date.year)
75
  end
76

  
77
  def bulk_destroy
78
    NonWorkingDate.where(:id => params[:ids]).delete_all
79
    NonWorkingDate.clear_dates_cache
80
    flash[:notice] = l(:notice_successful_delete)
81
    redirect_back_or_default non_working_dates_path
82
  end
83

  
84
  private
85

  
86
  def find_non_working_date
87
    @non_working_date = NonWorkingDate.find(params[:id])
88
  rescue ActiveRecord::RecordNotFound
89
    render_404
90
  end
91
end
app/helpers/gantt_helper.rb
173 173
        Period.new(
174 174
          date: date,
175 175
          width: zoom,
176
          non_working: @gantt.non_working_week_days.include?(date.cwday),
176
          non_working: @gantt.non_working_day?(date),
177 177
          spans_body: spans_body,
178 178
          last: date == @gantt.date_to
179 179
        )
app/models/non_working_date.rb
1
# frozen_string_literal: true
2

  
3
# Redmine - project management software
4
# Copyright (C) 2006-  Jean-Philippe Lang
5
#
6
# This program is free software; you can redistribute it and/or
7
# modify it under the terms of the GNU General Public License
8
# as published by the Free Software Foundation; either version 2
9
# of the License, or (at your option) any later version.
10
#
11
# This program is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
# GNU General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with this program; if not, write to the Free Software
18
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
19

  
20
class NonWorkingDate < ApplicationRecord
21
  include Redmine::SafeAttributes
22

  
23
  class Current < ActiveSupport::CurrentAttributes
24
    attribute :sorted_dates, :dates
25
  end
26

  
27
  after_save {self.class.clear_dates_cache}
28
  after_destroy {self.class.clear_dates_cache}
29
  after_rollback {self.class.clear_dates_cache}
30

  
31
  validates_presence_of :date
32
  validates_uniqueness_of :date
33
  validates :date, :date => true
34
  validates_length_of :name, :maximum => 255
35

  
36
  scope :sorted, lambda {order(:date)}
37
  scope :in_year, lambda {|year| where(:date => Date.new(year, 1, 1)..Date.new(year, 12, 31))}
38

  
39
  safe_attributes 'date', 'name'
40

  
41
  # Returns the registered dates as a sorted Array.
42
  # The result is cached for the duration of the current request.
43
  def self.sorted_dates
44
    Current.sorted_dates ||= order(:date).pluck(:date)
45
  end
46

  
47
  # Returns the registered dates as a Set.
48
  # The result is cached for the duration of the current request.
49
  def self.dates
50
    Current.dates ||= sorted_dates.to_set
51
  end
52

  
53
  def self.clear_dates_cache
54
    Current.reset
55
  end
56

  
57
  # Returns the years that have at least one non-working date
58
  def self.years
59
    pluck(:date).map(&:year).uniq.sort
60
  end
61
end
app/views/context_menus/non_working_dates.html.erb
1
<ul>
2
  <% if @non_working_date %>
3
    <li>
4
      <%= context_menu_link sprite_icon('edit', l(:button_edit)), edit_non_working_date_path(@non_working_date), class: 'icon icon-edit' %>
5
    </li>
6
  <% end %>
7
  <li>
8
    <%= context_menu_link sprite_icon('del', l(:button_delete)),
9
                          {controller: 'non_working_dates', action: 'bulk_destroy', ids: @non_working_dates.map(&:id), back_url: @back},
10
                          method: :delete, data: {confirm: l(:text_are_you_sure)}, class: 'icon icon-del' %>
11
  </li>
12
</ul>
app/views/non_working_dates/_form.html.erb
1
<%= error_messages_for 'non_working_date' %>
2

  
3
<div class="box tabular">
4
<p><%= f.date_field :date, :size => 10, :required => true %><%= calendar_for('non_working_date_date') %></p>
5
<p><%= f.text_field :name, :size => 40 %></p>
6
</div>
app/views/non_working_dates/edit.html.erb
1
<%= title [l(:label_non_working_date_plural), non_working_dates_path(:year => @non_working_date.date_was.year)], format_date(@non_working_date.date_was) %>
2

  
3
<%= labelled_form_for @non_working_date do |f| %>
4
  <%= render :partial => 'form', :locals => {:f => f} %>
5
  <%= submit_tag l(:button_save) %>
6
<% end %>
app/views/non_working_dates/index.html.erb
1
<div class="contextual">
2
<%= link_to(sprite_icon('add', l(:label_non_working_date_new)), new_non_working_date_path, :class => 'icon icon-add') %>
3
</div>
4

  
5
<h2><%= l(:label_non_working_date_plural) %></h2>
6

  
7
<%= form_tag(non_working_dates_path, :method => :get, :class => 'non-working-dates-year') do %>
8
  <%= label_tag 'year', l(:label_year) %>
9
  <%= select_tag 'year', options_for_select(@years, @year), :onchange => 'this.form.submit();' %>
10
  <span class="pagination">
11
    <ul class="pages">
12
      <li class="previous<%= ' page' if @previous_year %>">
13
        <%= link_to_if(@previous_year, "\xc2\xab #{l(:label_previous)}", non_working_dates_path(:year => @previous_year), :title => @previous_year, :accesskey => accesskey(:previous)) {|text| content_tag('span', text)} %>
14
      </li><li class="next<%= ' page' if @next_year %>">
15
        <%= link_to_if(@next_year, "#{l(:label_next)} \xc2\xbb", non_working_dates_path(:year => @next_year), :title => @next_year, :accesskey => accesskey(:next)) {|text| content_tag('span', text)} %>
16
      </li>
17
    </ul>
18
  </span>
19
<% end %>
20

  
21
<% if @non_working_dates.any? %>
22
<%= form_tag({}, :data => {:cm_url => non_working_dates_context_menu_path}) do -%>
23
<%= hidden_field_tag 'back_url', url_for(:params => request.query_parameters), :id => nil %>
24
<table class="list odd-even non_working_dates">
25
  <thead><tr>
26
  <th class="checkbox hide-when-print">
27
    <%= check_box_tag 'check_all', '', false, :class => 'toggle-selection',
28
          :title => "#{l(:button_check_all)} / #{l(:button_uncheck_all)}" %>
29
  </th>
30
  <th><%= l(:field_date) %></th>
31
  <th><%= l(:field_name) %></th>
32
  <th></th>
33
  </tr></thead>
34
  <tbody>
35
<% @non_working_dates.each do |non_working_date| %>
36
  <tr id="non-working-date-<%= non_working_date.id %>" class="hascontextmenu <%= cycle("odd", "even") %>">
37
  <td class="checkbox hide-when-print"><%= check_box_tag("ids[]", non_working_date.id, false, :id => nil) %></td>
38
  <td class="date"><%= link_to "#{format_date(non_working_date.date)} (#{abbr_day_name(non_working_date.date.cwday)})", edit_non_working_date_path(non_working_date) %></td>
39
  <td class="name"><%= non_working_date.name %></td>
40
  <td class="buttons">
41
    <%= link_to_context_menu %>
42
  </td>
43
  </tr>
44
<% end %>
45
  </tbody>
46
</table>
47
<% end -%>
48
<span class="pagination"><span class="items">(<%= @non_working_dates.size %>/<%= @non_working_date_count %>)</span></span>
49
<%= context_menu %>
50
<% else %>
51
<p class="nodata"><%= l(:label_no_data) %></p>
52
<% end %>
53

  
54
<% html_title(l(:label_non_working_date_plural)) -%>
app/views/non_working_dates/new.html.erb
1
<%= title [l(:label_non_working_date_plural), non_working_dates_path], l(:label_non_working_date_new) %>
2

  
3
<%= labelled_form_for @non_working_date do |f| %>
4
  <%= render :partial => 'form', :locals => {:f => f} %>
5
  <%= submit_tag l(:button_create) %>
6
  <%= submit_tag l(:button_create_and_continue), :name => 'continue' %>
7
<% end %>
app/views/settings/_issues.html.erb
50 50
<p><%= setting_select :issue_done_ratio_interval, [5, 10].collect {|i| ["#{i} %", i]} %></p>
51
<p><%= setting_multiselect :non_working_week_days, (1..7).map {|d| [day_name(d), d.to_s]}, :inline => true %></p>
51
<p>
52
  <%= setting_multiselect :non_working_week_days, (1..7).map {|d| [day_name(d), d.to_s]}, :inline => true %>
53
  <em class="info"><%= t(:text_non_working_dates_html, non_working_dates_path: non_working_dates_path) %></em>
54
</p>
52 55
<p><%= setting_text_field :issues_export_limit, :size => 6 %></p>
config/locales/en.yml
285 285
  mail_destroy_project_with_subprojects_successful: Project %{value} and its subprojects were deleted successfully.
286
  field_date: Date
286 287
  field_name: Name
287 288
  field_description: Description
288 289
  field_summary: Summary
......
690 691
  label_custom_field_new: New custom field
691 692
  label_enumerations: Enumerations
692 693
  label_enumeration_new: New value
694
  label_non_working_date_plural: Non-working dates
695
  label_non_working_date_new: New non-working date
693 696
  label_information: Information
694 697
  label_information_plural: Information
695 698
  label_register: Register
......
1440 1443
  description_wiki_subpages_reassign: Choose new parent page
1441 1444
  text_repository_identifier_info: 'Only lower case letters (a-z), numbers, dashes and underscores are allowed.<br />Once saved, the identifier cannot be changed.'
1442 1445
  text_login_required_html: When not requiring authentication, public projects and their contents are openly available on the network. You can <a href="%{anonymous_role_path}">edit the applicable permissions</a>.
1446
  text_non_working_dates_html: 'You can add non-working dates such as holidays: <a href="%{non_working_dates_path}">Manage non-working dates</a>'
1443 1447
  label_login_required_yes: "Yes"
1444 1448
  label_login_required_no: "No, allow anonymous access to public projects"
1445 1449
  text_project_is_public_non_member: Public projects and their contents are available to all logged-in users.
config/routes.rb
371 371
  end
372 372
  resources :enumerations, :except => :show
373 373
  match 'enumerations/:type', :to => 'enumerations#index', :via => :get
374
  match '/non_working_dates/context_menu', :to => 'context_menus/non_working_dates#index', :as => :non_working_dates_context_menu, :via => [:get, :post]
375
  resources :non_working_dates, :except => :show do
376
    collection do
377
      delete 'bulk_destroy'
378
    end
379
  end
374 380
  get '(projects/:id)/search', :controller => 'search', :action => 'index', :as => 'search'
db/migrate/20261005121927_create_non_working_dates.rb
1
class CreateNonWorkingDates < ActiveRecord::Migration[8.1]
2
  def change
3
    create_table :non_working_dates do |t|
4
      t.date :date, :null => false
5
      t.string :name
6
    end
7
    add_index :non_working_dates, :date, :unique => true
8
  end
9
end
lib/redmine/helpers/calendar.rb
59 59
      def day_css_classes(day)
60 60
        css = day.month==month ? +'this-month' : +'other-month'
61 61
        css << " today" if User.current.today == day
62
        css << " nwday" if non_working_week_days.include?(day.cwday)
62
        css << " nwday" if non_working_day?(day)
63 63
        css
64 64
      end
lib/redmine/helpers/gantt.rb
549 549
            height = g_height + header_height - 1
550 550
            (@date_from..date_to).each do |g_date|
551 551
              width =  zoom
552
              gc.fill(non_working_week_days.include?(g_date.cwday) ? '#eee' : 'white')
552
              gc.fill(non_working_day?(g_date) ? '#eee' : 'white')
553 553
              gc.stroke('#ddd')
554 554
              gc.strokewidth(1)
555 555
              gc.draw('rectangle %d,%d %d,%d' % [
......
669 669
            width = zoom
670 670
            pdf.SetY(y_start + header_height * 2)
671 671
            pdf.SetX(left)
672
            pdf.SetTextColor(non_working_week_days.include?(g_date.cwday) ? 150 : 0)
672
            pdf.SetTextColor(non_working_day?(g_date) ? 150 : 0)
673 673
            pdf.RDMCell(width, height, day_num.day.to_s, "LTR", 0, "C")
674 674
            left = left + width
675 675
            day_num = day_num + 1
......
684 684
            width = zoom
685 685
            pdf.SetY(y_start + header_height * (show_day_num ? 3 : 2))
686 686
            pdf.SetX(left)
687
            pdf.SetTextColor(non_working_week_days.include?(g_date.cwday) ? 150 : 0)
687
            pdf.SetTextColor(non_working_day?(g_date) ? 150 : 0)
688 688
            pdf.RDMCell(width, height, day_name(g_date.cwday).first, "LTR", 0, "C")
689 689
            left = left + width
690 690
          end
lib/redmine/utils/date_calculation.rb
33 33
              result += 1
34 34
            end
35 35
          end
36
          result
36
          result - non_working_dates_count(from, to)
37 37
        else
38 38
          0
39 39
        end
......
42 42
      # Adds working days to the given date
43 43
      def add_working_days(date, working_days)
44 44
        if working_days > 0
45
          weeks = working_days / (7 - non_working_week_days.size)
46
          result = weeks * 7
47
          days_left = working_days - weeks * (7 - non_working_week_days.size)
48
          cwday = date.cwday
49
          while days_left > 0
50
            cwday += 1
51
            unless non_working_week_days.include?(((cwday - 1) % 7) + 1)
52
              days_left -= 1
53
            end
54
            result += 1
45
          result = add_working_week_days(date, working_days)
46
          # Each registered non working date that was passed over
47
          # pushes the result back by one more working day
48
          from = date
49
          while (skipped = non_working_dates_count(from + 1, result + 1)) > 0
50
            from = result
51
            result = add_working_week_days(result, skipped)
55 52
          end
56
          next_working_date(date + result)
53
          next_working_date(result)
57 54
        else
58 55
          date
59 56
        end
......
61 58
      # Returns the date of the first day on or after the given date that is a working day
62 59
      def next_working_date(date)
60
        date += 1 while non_working_day?(date)
61
        date
62
      end
63

  
64
      # Returns true if the given date is a non working day
65
      def non_working_day?(date)
66
        non_working_week_days.include?(date.cwday) || NonWorkingDate.dates.include?(date)
67
      end
68

  
69
      # Adds working days to the given date,
70
      # taking only non working week days into account
71
      def add_working_week_days(date, working_days)
72
        weeks = working_days / (7 - non_working_week_days.size)
73
        result = weeks * 7
74
        days_left = working_days - weeks * (7 - non_working_week_days.size)
63 75
        cwday = date.cwday
64
        days = 0
65
        days += 1 while non_working_week_days.include?(((cwday + days - 1) % 7) + 1)
66
        date + days
76
        while days_left > 0
77
          cwday += 1
78
          unless non_working_week_days.include?(((cwday - 1) % 7) + 1)
79
            days_left -= 1
80
          end
81
          result += 1
82
        end
83
        date + result
84
      end
85

  
86
      # Returns the number of registered non working dates that are on or after from
87
      # and before to, and that do not fall on a non working week day
88
      def non_working_dates_count(from, to)
89
        dates = NonWorkingDate.sorted_dates
90
        first = dates.bsearch_index {|date| date >= from} || dates.size
91
        last = dates.bsearch_index {|date| date >= to} || dates.size
92
        dates[first...last].count {|date| non_working_week_days.exclude?(date.cwday)}
67 93
      end
68 94
      # Returns the index of non working week days (1=monday, 7=sunday)
test/functional/calendars_controller_test.rb
277 277
      assert_select 'li.nwday', :text => /^10/
278 278
    end
279 279
  end
280

  
281
  def test_show_calendar_should_mark_non_working_dates
282
    NonWorkingDate.create!(:date => '2016-12-29')
283
    get(
284
      :show,
285
      :params => {
286
        :month => '12',
287
        :year => '2016'
288
      }
289
    )
290
    assert_response :success
291

  
292
    assert_select 'li.nwday', 11
293
    assert_select 'li.nwday', :text => /^29/
294
  end
280 295
end
test/functional/context_menus/non_working_dates_controller_test.rb
1
# frozen_string_literal: true
2

  
3
# Redmine - project management software
4
# Copyright (C) 2006-  Jean-Philippe Lang
5
#
6
# This program is free software; you can redistribute it and/or
7
# modify it under the terms of the GNU General Public License
8
# as published by the Free Software Foundation; either version 2
9
# of the License, or (at your option) any later version.
10
#
11
# This program is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
# GNU General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with this program; if not, write to the Free Software
18
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
19

  
20
require_relative '../../test_helper'
21

  
22
module ContextMenus
23
  class NonWorkingDatesControllerTest < Redmine::ControllerTest
24
    def setup
25
      User.current = nil
26
      @request.session[:user_id] = 1 # admin
27
    end
28

  
29
    def test_context_menu_with_one_non_working_date
30
      non_working_date = NonWorkingDate.create!(:date => '2026-06-25')
31

  
32
      get :index, :params => {:ids => [non_working_date.id]}
33
      assert_response :success
34
      assert_select 'a.icon-edit[href=?]', "/non_working_dates/#{non_working_date.id}/edit"
35
      assert_select 'a.icon-del[href$=?]', "/non_working_dates/bulk_destroy?ids%5B%5D=#{non_working_date.id}"
36
    end
37

  
38
    def test_context_menu_with_multiple_non_working_dates
39
      ids = %w(2026-08-13 2026-08-14).map {|date| NonWorkingDate.create!(:date => date).id}
40

  
41
      get :index, :params => {:ids => ids}
42
      assert_response :success
43
      assert_select 'a.icon-edit', 0
44
      assert_select 'a.icon-del[href$=?]', "/non_working_dates/bulk_destroy?ids%5B%5D=#{ids[0]}&ids%5B%5D=#{ids[1]}"
45
    end
46

  
47
    def test_context_menu_by_non_admin_should_respond_with_403
48
      @request.session[:user_id] = 2
49
      get :index, :params => {:ids => [NonWorkingDate.create!(:date => '2026-06-25').id]}
50
      assert_response :forbidden
51
    end
52
  end
53
end
test/functional/gantts_controller_test.rb
146 146
    assert_select 'a', :text => /Private child of eCookbook/, :count => 0
147 147
  end
148
  def test_gantt_should_mark_non_working_dates
149
    NonWorkingDate.create!(:date => '2016-12-29')
150

  
151
    get :show, :params => {:year => '2016', :month => '12', :months => '1', :zoom => '4'}
152
    assert_response :success
153

  
154
    # 9 week-end days and 1 non-working date, in both the day number and the day name rows
155
    assert_select 'div.gantt-period-non-working', 20
156
    assert_select 'div.gantt-period-non-working', :text => '29'
157
  end
158

  
148 159
  def test_gantt_should_display_relations
149 160
    IssueRelation.delete_all
150 161
    issue1 = Issue.generate!(:start_date => 1.day.from_now.to_date, :due_date => 3.day.from_now.to_date)
test/functional/non_working_dates_controller_test.rb
1
# frozen_string_literal: true
2

  
3
# Redmine - project management software
4
# Copyright (C) 2006-  Jean-Philippe Lang
5
#
6
# This program is free software; you can redistribute it and/or
7
# modify it under the terms of the GNU General Public License
8
# as published by the Free Software Foundation; either version 2
9
# of the License, or (at your option) any later version.
10
#
11
# This program is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
# GNU General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with this program; if not, write to the Free Software
18
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
19

  
20
require_relative '../test_helper'
21

  
22
class NonWorkingDatesControllerTest < Redmine::ControllerTest
23
  def setup
24
    User.current = nil
25
    @request.session[:user_id] = 1 # admin
26
  end
27

  
28
  def test_index_should_list_non_working_dates_of_the_current_year_by_default
29
    year = User.find(1).today.year
30
    NonWorkingDate.create!(:date => Date.new(year, 6, 25), :name => 'Company Foundation Day')
31
    NonWorkingDate.create!(:date => Date.new(year - 1, 8, 13), :name => 'Summer Shutdown')
32

  
33
    get :index
34
    assert_response :success
35
    assert_select 'table.non_working_dates tbody tr', 1
36
    assert_select 'table.non_working_dates td.name', :text => 'Company Foundation Day'
37
    assert_select 'form[data-cm-url=?] input[name=?]', '/non_working_dates/context_menu', 'ids[]', 1
38
    assert_select 'span.pagination span.items', :text => '(1/2)'
39
    assert_select 'form.non-working-dates-year' do
40
      assert_select 'select[name=year] option', 2
41
      assert_select 'select[name=year] option[selected=selected]', :text => year.to_s
42
      assert_select 'li.previous a[href=?]', "/non_working_dates?year=#{year - 1}"
43
      assert_select 'li.next span', :text => /Next/
44
      assert_select 'li.next a', 0
45
    end
46
  end
47

  
48
  def test_index_with_year
49
    NonWorkingDate.create!(:date => '2020-12-30', :name => 'Year-end Shutdown')
50
    NonWorkingDate.create!(:date => '2021-12-30')
51

  
52
    get :index, :params => {:year => '2020'}
53
    assert_response :success
54
    assert_select 'table.non_working_dates tbody tr', 1
55
    assert_select 'table.non_working_dates td.name', :text => 'Year-end Shutdown'
56
  end
57

  
58
  def test_index_by_non_admin_should_respond_with_403
59
    @request.session[:user_id] = 2
60
    get :index
61
    assert_response :forbidden
62
  end
63

  
64
  def test_create
65
    assert_difference 'NonWorkingDate.count' do
66
      post :create, :params => {:non_working_date => {:date => '2026-06-25', :name => 'Company Foundation Day'}}
67
    end
68
    assert_redirected_to '/non_working_dates?year=2026'
69
    non_working_date = NonWorkingDate.order(:id).last
70
    assert_equal Date.new(2026, 6, 25), non_working_date.date
71
    assert_equal 'Company Foundation Day', non_working_date.name
72
  end
73

  
74
  def test_create_and_continue
75
    assert_difference 'NonWorkingDate.count' do
76
      post :create, :params => {:non_working_date => {:date => '2026-06-25'}, :continue => 'Create and continue'}
77
    end
78
    assert_redirected_to '/non_working_dates/new'
79
  end
80

  
81
  def test_create_with_duplicate_date_should_fail
82
    NonWorkingDate.create!(:date => '2026-06-25')
83

  
84
    assert_no_difference 'NonWorkingDate.count' do
85
      post :create, :params => {:non_working_date => {:date => '2026-06-25'}}
86
    end
87
    assert_response :success
88
    assert_select_error /Date has already been taken/
89
  end
90

  
91
  def test_update
92
    non_working_date = NonWorkingDate.create!(:date => '2026-08-13', :name => 'Summer Shutdown')
93

  
94
    put :update, :params => {:id => non_working_date.id, :non_working_date => {:date => '2027-08-12', :name => 'Summer Holidays'}}
95
    assert_redirected_to '/non_working_dates?year=2027'
96
    non_working_date.reload
97
    assert_equal Date.new(2027, 8, 12), non_working_date.date
98
    assert_equal 'Summer Holidays', non_working_date.name
99
  end
100

  
101
  def test_destroy
102
    non_working_date = NonWorkingDate.create!(:date => '2026-06-25')
103

  
104
    assert_difference 'NonWorkingDate.count', -1 do
105
      delete :destroy, :params => {:id => non_working_date.id}
106
    end
107
    assert_redirected_to '/non_working_dates?year=2026'
108
  end
109

  
110
  def test_bulk_destroy
111
    first, second, third = %w(2026-08-12 2026-08-13 2026-08-14).map {|date| NonWorkingDate.create!(:date => date)}
112

  
113
    assert_difference 'NonWorkingDate.count', -2 do
114
      delete :bulk_destroy, :params => {:ids => [first.id, third.id], :back_url => '/non_working_dates?year=2026'}
115
    end
116
    assert_redirected_to '/non_working_dates?year=2026'
117
    assert_equal [second], NonWorkingDate.all.to_a
118
  end
119
end
test/helpers/gantt_helper_test.rb
171 171
                   months: 1, zoom: 3, number_of_rows: 0, non_working_week_days: [6, 7])
172 172
    gantt = stub(
173 173
      date_from: date_from, date_to: date_to, months: months, zoom: zoom,
174
      number_of_rows: number_of_rows, non_working_week_days: non_working_week_days
174
      number_of_rows: number_of_rows
175 175
    )
176
    gantt.define_singleton_method(:non_working_day?) {|date| non_working_week_days.include?(date.cwday)}
176 177
    GanttHelper::ChartLayout.new(gantt)
177 178
  end
178 179
end
test/unit/issue_test.rb
2468 2468
    end
2469 2469
  end
2470
  def test_rescheduling_an_issue_should_skip_non_working_dates
2471
    NonWorkingDate.create!(:date => '2012-10-18')
2472
    NonWorkingDate.create!(:date => '2012-10-22')
2473
    with_settings :non_working_week_days => %w(6 7) do
2474
      issue1 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
2475
      issue2 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
2476
      IssueRelation.create!(:issue_from => issue1, :issue_to => issue2,
2477
                            :relation_type => IssueRelation::TYPE_PRECEDES)
2478
      issue2.reload
2479
      assert_equal Date.parse('2012-10-19'), issue2.start_date
2480
      assert_equal Date.parse('2012-10-24'), issue2.due_date
2481
    end
2482
  end
2483

  
2470 2484
  def test_rescheduling_an_issue_to_an_earlier_due_date_should_reschedule_following_issue
2471 2485
    issue1 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
2472 2486
    issue2 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
test/unit/lib/redmine/utils/date_calculation_test.rb
68 68
    end
69 69
  end
70
  def test_working_days_with_non_working_dates
71
    NonWorkingDate.create!(:date => '2012-10-10')
72
    # Registered on a non-working week day, should not be counted twice
73
    NonWorkingDate.create!(:date => '2012-10-13')
74
    with_settings :non_working_week_days => %w(6 7) do
75
      assert_working_days 13, '2012-10-09', '2012-10-27'
76
      assert_working_days  1, '2012-10-09', '2012-10-11'
77
      assert_working_days  0, '2012-10-10', '2012-10-11'
78
      assert_working_days  3, '2012-10-11', '2012-10-16'
79
    end
80
  end
81

  
82
  def test_add_working_days_with_non_working_dates
83
    NonWorkingDate.create!(:date => '2012-10-11')
84
    NonWorkingDate.create!(:date => '2012-10-15')
85
    with_settings :non_working_week_days => %w(6 7) do
86
      assert_add_working_days '2012-10-10', '2012-10-10', 0
87
      assert_add_working_days '2012-10-12', '2012-10-10', 1
88
      assert_add_working_days '2012-10-16', '2012-10-10', 2
89
      assert_add_working_days '2012-10-17', '2012-10-10', 3
90
      # From a non-working date
91
      assert_add_working_days '2012-10-16', '2012-10-13', 1
92
    end
93
  end
94

  
70 95
  def assert_working_days(expected_days, from, to)
71 96
    assert_equal expected_days, working_days(from.to_date, to.to_date)
72 97
  end
test/unit/non_working_date_test.rb
1
# frozen_string_literal: true
2

  
3
# Redmine - project management software
4
# Copyright (C) 2006-  Jean-Philippe Lang
5
#
6
# This program is free software; you can redistribute it and/or
7
# modify it under the terms of the GNU General Public License
8
# as published by the Free Software Foundation; either version 2
9
# of the License, or (at your option) any later version.
10
#
11
# This program is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
# GNU General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with this program; if not, write to the Free Software
18
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
19

  
20
require_relative '../test_helper'
21

  
22
class NonWorkingDateTest < ActiveSupport::TestCase
23
  def test_dates_should_be_cached_and_refreshed_on_change
24
    non_working_date = NonWorkingDate.create!(:date => '2026-08-13')
25
    assert_equal [Date.new(2026, 8, 13)], NonWorkingDate.sorted_dates
26
    assert_no_queries {NonWorkingDate.sorted_dates}
27

  
28
    non_working_date.update!(:date => '2026-08-14')
29
    assert_equal [Date.new(2026, 8, 14)], NonWorkingDate.sorted_dates
30

  
31
    non_working_date.destroy
32
    assert_empty NonWorkingDate.sorted_dates
33
  end
34
end
(7-7/8)