Feature #1459 » 0002-Import-and-export-non-working-dates-as-CSV-files.patch
| app/assets/stylesheets/application.css | ||
|---|---|---|
| 2249 | 2249 |
stroke: var(--oc-yellow-8); |
| 2250 | 2250 |
} |
| 2251 |
.icon-info svg.icon-svg {
|
|
| 2252 |
stroke: var(--oc-indigo-9); |
|
| 2253 |
} |
|
| 2254 | ||
| 2251 | 2255 |
.icon-only span {
|
| 2252 | 2256 |
display: none; |
| 2253 | 2257 |
} |
| app/controllers/non_working_dates_controller.rb | ||
|---|---|---|
| 22 | 22 |
self.main_menu = false |
| 23 | 23 |
menu_item :settings |
| 24 |
include NonWorkingDatesHelper |
|
| 25 | ||
| 24 | 26 |
before_action :require_admin |
| 25 | 27 |
before_action :find_non_working_date, :only => [:edit, :update, :destroy] |
| 26 | 28 |
def index |
| 27 |
@year = params[:year].to_i if /\A\d{4}\z/.match?(params[:year].to_s)
|
|
| 28 |
@year ||= User.current.today.year |
|
| 29 |
years = NonWorkingDate.years |
|
| 30 |
@years = (years | [@year, User.current.today.year]).sort |
|
| 31 |
@previous_year = @year - 1 if years.any? {|year| year < @year}
|
|
| 32 |
@next_year = @year + 1 if years.any? {|year| year > @year}
|
|
| 33 |
@non_working_dates = NonWorkingDate.in_year(@year).sorted.to_a |
|
| 34 |
@non_working_date_count = NonWorkingDate.count |
|
| 29 |
respond_to do |format| |
|
| 30 |
format.html do |
|
| 31 |
@year = params[:year].to_i if /\A\d{4}\z/.match?(params[:year].to_s)
|
|
| 32 |
@year ||= User.current.today.year |
|
| 33 |
years = NonWorkingDate.years |
|
| 34 |
@years = (years | [@year, User.current.today.year]).sort |
|
| 35 |
@previous_year = @year - 1 if years.any? {|year| year < @year}
|
|
| 36 |
@next_year = @year + 1 if years.any? {|year| year > @year}
|
|
| 37 |
@non_working_dates = NonWorkingDate.in_year(@year).sorted.to_a |
|
| 38 |
@non_working_date_count = NonWorkingDate.count |
|
| 39 |
end |
|
| 40 |
format.csv do |
|
| 41 |
send_data(non_working_dates_to_csv(NonWorkingDate.sorted), :type => 'text/csv; header=present', :filename => 'non_working_dates.csv') |
|
| 42 |
end |
|
| 43 |
end |
|
| 35 | 44 |
end |
| 36 | 45 |
def new |
| app/helpers/non_working_dates_helper.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 NonWorkingDatesHelper |
|
| 21 |
def non_working_dates_to_csv(non_working_dates) |
|
| 22 |
Redmine::Export::CSV.generate(:encoding => params[:encoding], :field_separator => params[:field_separator]) do |csv| |
|
| 23 |
csv << [l(:field_date), l(:field_name)] |
|
| 24 |
non_working_dates.each do |non_working_date| |
|
| 25 |
csv << [format_date(non_working_date.date), non_working_date.name] |
|
| 26 |
end |
|
| 27 |
end |
|
| 28 |
end |
|
| 29 |
end |
|
| app/models/non_working_date_import.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 NonWorkingDateImport < Import |
|
| 21 |
AUTO_MAPPABLE_FIELDS = {
|
|
| 22 |
'date' => 'field_date', |
|
| 23 |
'name' => 'field_name' |
|
| 24 |
} |
|
| 25 | ||
| 26 |
def self.menu_item |
|
| 27 |
:settings |
|
| 28 |
end |
|
| 29 | ||
| 30 |
def self.layout |
|
| 31 |
'admin' |
|
| 32 |
end |
|
| 33 | ||
| 34 |
def self.authorized?(user) |
|
| 35 |
user.admin? |
|
| 36 |
end |
|
| 37 | ||
| 38 |
# Returns the objects that were imported |
|
| 39 |
def saved_objects |
|
| 40 |
NonWorkingDate.where(:id => saved_items.pluck(:obj_id)).sorted |
|
| 41 |
end |
|
| 42 | ||
| 43 |
def mappable_custom_fields |
|
| 44 |
[] |
|
| 45 |
end |
|
| 46 | ||
| 47 |
private |
|
| 48 | ||
| 49 |
def build_object(row, item) |
|
| 50 |
NonWorkingDate.new( |
|
| 51 |
:date => row_date(row, 'date'), |
|
| 52 |
:name => row_value(row, 'name') |
|
| 53 |
) |
|
| 54 |
end |
|
| 55 |
end |
|
| app/views/imports/_non_working_dates_mapping.html.erb | ||
|---|---|---|
| 1 |
<fieldset class="box tabular"> |
|
| 2 |
<legend><%= l(:label_fields_mapping) %></legend> |
|
| 3 |
<p> |
|
| 4 |
<label for="import_mapping_date"><%= l(:field_date) %></label> |
|
| 5 |
<%= mapping_select_tag @import, 'date', :required => true %> |
|
| 6 |
</p> |
|
| 7 |
<p> |
|
| 8 |
<label for="import_mapping_name"><%= l(:field_name) %></label> |
|
| 9 |
<%= mapping_select_tag @import, 'name' %> |
|
| 10 |
</p> |
|
| 11 |
</fieldset> |
|
| app/views/imports/_non_working_dates_saved_objects.html.erb | ||
|---|---|---|
| 1 |
<table id="saved-items" class="list"> |
|
| 2 |
<thead> |
|
| 3 |
<tr> |
|
| 4 |
<th><%= t(:field_date) %></th> |
|
| 5 |
<th><%= t(:field_name) %></th> |
|
| 6 |
</tr> |
|
| 7 |
</thead> |
|
| 8 |
<tbody> |
|
| 9 |
<% saved_objects.each do |non_working_date| %> |
|
| 10 |
<tr> |
|
| 11 |
<td><%= link_to format_date(non_working_date.date), edit_non_working_date_path(non_working_date) %></td> |
|
| 12 |
<td><%= non_working_date.name %></td> |
|
| 13 |
</tr> |
|
| 14 |
<% end %> |
|
| 15 |
</tbody> |
|
| 16 |
</table> |
|
| 17 |
<p><%= link_to l(:label_non_working_date_plural), non_working_dates_path(:year => saved_objects.first.date.year) %></p> |
|
| app/views/non_working_dates/index.html.erb | ||
|---|---|---|
| 1 | 1 |
<div class="contextual"> |
| 2 | 2 |
<%= link_to(sprite_icon('add', l(:label_non_working_date_new)), new_non_working_date_path, :class => 'icon icon-add') %>
|
| 3 |
<%= link_to(sprite_icon('import', l(:button_import)), new_non_working_dates_import_path, :class => 'icon icon-import') %>
|
|
| 3 | 4 |
</div> |
| 4 | 5 |
<h2><%= l(:label_non_working_date_plural) %></h2> |
| ... | ... | |
| 51 | 52 |
<p class="nodata"><%= l(:label_no_data) %></p> |
| 52 | 53 |
<% end %> |
| 54 |
<% if @non_working_date_count > 0 %> |
|
| 55 |
<% other_formats_links do |f| %> |
|
| 56 |
<%= f.link_to_with_query_parameters 'CSV', {}, :onclick => "showModal('csv-export-options', '330px'); return false;" %>
|
|
| 57 |
<% end %> |
|
| 58 |
<div id="csv-export-options" style="display: none;"> |
|
| 59 |
<h3 class="title"><%= l(:label_export_options, :export_format => 'CSV') %></h3> |
|
| 60 |
<%= form_tag(non_working_dates_path(:format => 'csv'), :method => :get, :id => 'csv-export-form') do %> |
|
| 61 |
<%= export_csv_encoding_select_tag %> |
|
| 62 |
<%= export_csv_separator_select_tag %> |
|
| 63 |
<p class="icon icon-info"><%= sprite_icon('help', l(:text_non_working_dates_export_all_years)) %></p>
|
|
| 64 |
<p class="buttons"> |
|
| 65 |
<%= submit_tag l(:button_export), :name => nil, :onclick => 'hideModal(this);', :data => {:disable_with => false} %>
|
|
| 66 |
<%= link_to_function l(:button_cancel), 'hideModal(this);' %> |
|
| 67 |
</p> |
|
| 68 |
<% end %> |
|
| 69 |
</div> |
|
| 70 |
<% end %> |
|
| 71 | ||
| 53 | 72 |
<% html_title(l(:label_non_working_date_plural)) -%> |
| config/locales/en.yml | ||
|---|---|---|
| 1444 | 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.' |
| 1445 | 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 | 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>'
|
| 1447 |
text_non_working_dates_export_all_years: Non-working dates of all years will be exported. |
|
| 1447 | 1448 |
label_login_required_yes: "Yes" |
| 1448 | 1449 |
label_login_required_no: "No, allow anonymous access to public projects" |
| 1449 | 1450 |
text_project_is_public_non_member: Public projects and their contents are available to all logged-in users. |
| 1450 | 1451 |
text_project_is_public_anonymous: Public projects and their contents are openly available on the network. |
| 1451 | 1452 |
label_import_time_entries: Import time entries |
| 1452 | 1453 |
label_import_users: Import users |
| 1454 |
label_import_non_working_dates: Import non-working dates |
|
| 1453 | 1455 |
sudo_mode_new_info_html: <strong>What's happening?</strong> You need to reconfirm your password before taking any administrative actions, this ensures your account stays protected. |
| 1454 | 1456 |
twofa__totp__name: Authenticator app |
| config/routes.rb | ||
|---|---|---|
| 85 | 85 |
:defaults => {:type => 'TimeEntryImport'}, :as => 'new_time_entries_import'
|
| 86 | 86 |
get '/users/imports/new', :to => 'imports#new', |
| 87 | 87 |
:defaults => {:type => 'UserImport'}, :as => 'new_users_import'
|
| 88 |
get '/non_working_dates/imports/new', :to => 'imports#new', |
|
| 89 |
:defaults => {:type => 'NonWorkingDateImport'}, :as => 'new_non_working_dates_import'
|
|
| 88 | 90 |
post '/imports', :to => 'imports#create', :as => 'imports' |
| 89 | 91 |
get '/imports/:id', :to => 'imports#show', :as => 'import' |
| 90 | 92 |
match '/imports/:id/settings', :to => 'imports#settings', :via => [:get, :post], :as => 'import_settings' |
| test/fixtures/files/import_non_working_dates.csv | ||
|---|---|---|
| 1 |
date,name |
|
| 2 |
2026-06-25,Company Foundation Day |
|
| 3 |
2026-08-13,Summer Shutdown |
|
| 4 |
2026-12-30,Year-end Shutdown |
|
| test/functional/imports_controller_test.rb | ||
|---|---|---|
| 502 | 502 |
assert_select 'table#unsaved-items' |
| 503 | 503 |
assert_select 'table#unsaved-items tbody tr', import.unsaved_items.count |
| 504 | 504 |
end |
| 505 | ||
| 506 |
def test_import_non_working_dates |
|
| 507 |
@request.session[:user_id] = 1 |
|
| 508 |
import = NonWorkingDateImport.new |
|
| 509 |
import.user_id = 1 |
|
| 510 |
import.file = uploaded_test_file('import_non_working_dates.csv', 'text/csv')
|
|
| 511 |
import.settings = {
|
|
| 512 |
'separator' => ',', 'wrapper' => '"', 'encoding' => 'UTF-8', 'date_format' => '%Y-%m-%d', |
|
| 513 |
'mapping' => {'date' => '0', 'name' => '1'}
|
|
| 514 |
} |
|
| 515 |
import.save! |
|
| 516 | ||
| 517 |
get(:mapping, :params => {:id => import.to_param})
|
|
| 518 |
assert_response :success |
|
| 519 |
assert_select 'select[name=?]', 'import_settings[mapping][date]' |
|
| 520 |
assert_select 'select[name=?]', 'import_settings[mapping][name]' |
|
| 521 | ||
| 522 |
assert_difference 'NonWorkingDate.count', 3 do |
|
| 523 |
post(:run, :params => {:id => import.to_param})
|
|
| 524 |
end |
|
| 525 |
assert_redirected_to "/imports/#{import.to_param}"
|
|
| 526 | ||
| 527 |
get(:show, :params => {:id => import.to_param})
|
|
| 528 |
assert_response :success |
|
| 529 |
assert_select 'table#saved-items tbody tr', 3 |
|
| 530 |
assert_select 'a[href=?]', '/non_working_dates?year=2026', :text => 'Non-working dates' |
|
| 531 |
end |
|
| 505 | 532 |
end |
| test/functional/non_working_dates_controller_test.rb | ||
|---|---|---|
| 36 | 36 |
assert_select 'table.non_working_dates td.name', :text => 'Company Foundation Day' |
| 37 | 37 |
assert_select 'form[data-cm-url=?] input[name=?]', '/non_working_dates/context_menu', 'ids[]', 1 |
| 38 | 38 |
assert_select 'span.pagination span.items', :text => '(1/2)' |
| 39 |
assert_select '#csv-export-options p', :text => 'Non-working dates of all years will be exported.' |
|
| 39 | 40 |
assert_select 'form.non-working-dates-year' do |
| 40 | 41 |
assert_select 'select[name=year] option', 2 |
| 41 | 42 |
assert_select 'select[name=year] option[selected=selected]', :text => year.to_s |
| ... | ... | |
| 55 | 56 |
assert_select 'table.non_working_dates td.name', :text => 'Year-end Shutdown' |
| 56 | 57 |
end |
| 58 |
def test_index_csv_should_export_non_working_dates_of_all_years |
|
| 59 |
NonWorkingDate.create!(:date => '2021-06-25', :name => 'Company Foundation Day') |
|
| 60 |
NonWorkingDate.create!(:date => '2020-12-30') |
|
| 61 | ||
| 62 |
with_settings :date_format => '%Y-%m-%d' do |
|
| 63 |
get :index, :params => {:year => '2021', :format => 'csv'}
|
|
| 64 |
end |
|
| 65 |
assert_response :success |
|
| 66 |
assert_equal 'text/csv; header=present', @response.media_type |
|
| 67 |
assert_equal ['Date,Name', '2020-12-30,', '2021-06-25,Company Foundation Day'], @response.body.lines.map(&:chomp) |
|
| 68 |
end |
|
| 69 | ||
| 57 | 70 |
def test_index_by_non_admin_should_respond_with_403 |
| 58 | 71 |
@request.session[:user_id] = 2 |
| 59 | 72 |
get :index |
| test/unit/non_working_date_import_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 NonWorkingDateImportTest < ActiveSupport::TestCase |
|
| 23 |
include Redmine::I18n |
|
| 24 | ||
| 25 |
def setup |
|
| 26 |
set_language_if_valid 'en' |
|
| 27 |
User.current = nil |
|
| 28 |
end |
|
| 29 | ||
| 30 |
def test_run_should_import_dates_and_names |
|
| 31 |
import = generate_import_with_mapping |
|
| 32 |
first, second, third = new_records(NonWorkingDate, 3) {import.run}
|
|
| 33 | ||
| 34 |
assert_equal Date.new(2026, 6, 25), first.date |
|
| 35 |
assert_equal 'Company Foundation Day', first.name |
|
| 36 |
assert_equal Date.new(2026, 8, 13), second.date |
|
| 37 |
assert_equal 'Summer Shutdown', second.name |
|
| 38 |
assert_equal Date.new(2026, 12, 30), third.date |
|
| 39 |
assert_equal 'Year-end Shutdown', third.name |
|
| 40 |
end |
|
| 41 | ||
| 42 |
def test_run_should_not_import_already_registered_dates |
|
| 43 |
NonWorkingDate.create!(:date => '2026-08-13', :name => 'Existing') |
|
| 44 |
import = generate_import_with_mapping |
|
| 45 | ||
| 46 |
assert_difference 'NonWorkingDate.count', 2 do |
|
| 47 |
import.run |
|
| 48 |
end |
|
| 49 |
assert_equal 'Existing', NonWorkingDate.find_by(:date => '2026-08-13').name |
|
| 50 |
item = import.unsaved_items.first |
|
| 51 |
assert_equal 2, item.position |
|
| 52 |
assert_include 'Date has already been taken', item.message |
|
| 53 |
end |
|
| 54 | ||
| 55 |
protected |
|
| 56 | ||
| 57 |
def generate_import_with_mapping |
|
| 58 |
import = NonWorkingDateImport.new |
|
| 59 |
import.user_id = 1 |
|
| 60 |
import.file = uploaded_test_file('import_non_working_dates.csv', 'text/csv')
|
|
| 61 |
import.settings = {
|
|
| 62 |
'separator' => ',', 'wrapper' => '"', 'encoding' => 'UTF-8', 'date_format' => '%Y-%m-%d', |
|
| 63 |
'mapping' => {'date' => '0', 'name' => '1'}
|
|
| 64 |
} |
|
| 65 |
import.save! |
|
| 66 |
import |
|
| 67 |
end |
|
| 68 |
end |
|
- « Previous
- 1
- …
- 6
- 7
- 8
- Next »