Feature #1459 » 0001-Allow-non-working-dates-such-as-holidays-to-be-regis.patch
| 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 |
|