redmine_extensions 0.1.14 → 0.1.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/assets/javascripts/redmine_extensions/redmine_extensions.js.orig +648 -0
- data/app/views/easy_entity_assignments/_query_index.html.erb.orig +16 -0
- data/config/locales/cs.yml +4 -1
- data/config/locales/en.yml +1 -0
- data/lib/generators/redmine_extensions/entity/templates/context_menu.html.erb.erb +14 -1
- data/lib/generators/redmine_extensions/entity/templates/controller.rb.erb +40 -6
- data/lib/generators/redmine_extensions/entity/templates/edit.js.erb.erb +5 -5
- data/lib/generators/redmine_extensions/entity/templates/index.js.erb.erb +3 -3
- data/lib/generators/redmine_extensions/entity/templates/new.js.erb.erb +5 -5
- data/lib/generators/redmine_extensions/entity/templates/show.js.erb.erb +3 -3
- data/lib/redmine_extensions/easy_query_helpers/outputs.rb +18 -2
- data/lib/redmine_extensions/query_output.rb +30 -11
- data/lib/redmine_extensions/redmine_patches/controllers/issues_controller_patch.rb +9 -0
- data/lib/redmine_extensions/version.rb +1 -1
- data/lib/redmine_extensions/version.rb.orig +7 -0
- data/spec/redmine/app/controllers/custom_field_enumerations_controller.rb +75 -0
- data/spec/redmine/app/controllers/imports_controller.rb +121 -0
- data/spec/redmine/app/helpers/imports_helper.rb +47 -0
- data/spec/redmine/app/models/custom_field_enumeration.rb +90 -0
- data/spec/redmine/app/models/import.rb +249 -0
- data/spec/redmine/app/models/import_item.rb +22 -0
- data/spec/redmine/app/models/issue_import.rb +186 -0
- data/spec/redmine/app/views/attachments/image.html.erb +3 -0
- data/spec/redmine/app/views/attachments/other.html.erb +3 -0
- data/spec/redmine/app/views/common/_image.html.erb +1 -0
- data/spec/redmine/app/views/common/_other.html.erb +1 -0
- data/spec/redmine/app/views/custom_field_enumerations/create.js.erb +2 -0
- data/spec/redmine/app/views/custom_field_enumerations/destroy.html.erb +14 -0
- data/spec/redmine/app/views/custom_field_enumerations/index.html.erb +47 -0
- data/spec/redmine/app/views/custom_fields/formats/_enumeration.erb +12 -0
- data/spec/redmine/app/views/imports/_fields_mapping.html.erb +90 -0
- data/spec/redmine/app/views/imports/mapping.html.erb +52 -0
- data/spec/redmine/app/views/imports/mapping.js.erb +1 -0
- data/spec/redmine/app/views/imports/new.html.erb +15 -0
- data/spec/redmine/app/views/imports/run.html.erb +20 -0
- data/spec/redmine/app/views/imports/run.js.erb +11 -0
- data/spec/redmine/app/views/imports/settings.html.erb +30 -0
- data/spec/redmine/app/views/imports/show.html.erb +38 -0
- data/spec/redmine/app/views/layouts/_file.html.erb +15 -0
- data/spec/redmine/app/views/mailer/security_notification.html.erb +13 -0
- data/spec/redmine/app/views/mailer/security_notification.text.erb +8 -0
- data/spec/redmine/app/views/mailer/settings_updated.html.erb +14 -0
- data/spec/redmine/app/views/mailer/settings_updated.text.erb +12 -0
- data/spec/redmine/app/views/queries/_query_form.html.erb +62 -0
- data/spec/redmine/app/views/repositories/_changeset.html.erb +40 -0
- data/spec/redmine/app/views/search/index.api.rsb +12 -0
- data/spec/redmine/app/views/settings/_api.html.erb +10 -0
- data/spec/redmine/app/views/settings/_attachments.html.erb +21 -0
- data/spec/redmine/app/views/wiki/_new_modal.html.erb +21 -0
- data/spec/redmine/app/views/wiki/new.html.erb +17 -0
- data/spec/redmine/app/views/wiki/new.js.erb +2 -0
- data/spec/redmine/appveyor.yml +36 -0
- data/spec/redmine/db/migrate/20150725112753_insert_allowed_statuses_for_new_issues.rb +23 -0
- data/spec/redmine/db/migrate/20150730122707_create_imports.rb +13 -0
- data/spec/redmine/db/migrate/20150730122735_create_import_items.rb +10 -0
- data/spec/redmine/db/migrate/20150921204850_change_time_entries_comments_limit_to_1024.rb +9 -0
- data/spec/redmine/db/migrate/20150921210243_change_wiki_contents_comments_limit_to_1024.rb +11 -0
- data/spec/redmine/db/migrate/20151020182334_change_attachments_filesize_limit_to_8.rb +9 -0
- data/spec/redmine/db/migrate/20151020182731_fix_comma_in_user_format_setting_value.rb +13 -0
- data/spec/redmine/db/migrate/20151021184614_change_issue_categories_name_limit_to_60.rb +9 -0
- data/spec/redmine/db/migrate/20151021185456_change_auth_sources_filter_to_text.rb +9 -0
- data/spec/redmine/db/migrate/20151021190616_change_user_preferences_hide_mail_default_to_true.rb +9 -0
- data/spec/redmine/db/migrate/20151024082034_add_tokens_updated_on.rb +10 -0
- data/spec/redmine/db/migrate/20151025072118_create_custom_field_enumerations.rb +10 -0
- data/spec/redmine/db/migrate/20151031095005_add_projects_default_version_id.rb +12 -0
- data/spec/redmine/db/migrate/20160404080304_force_password_reset_during_setup.rb +9 -0
- data/spec/redmine/db/migrate/20160416072926_remove_position_defaults.rb +13 -0
- data/spec/redmine/db/migrate/20160529063352_add_roles_settings.rb +5 -0
- data/spec/redmine/lib/redmine/acts/positioned.rb +118 -0
- data/spec/redmine/lib/redmine/helpers/url.rb +35 -0
- data/spec/redmine/lib/redmine/hook/listener.rb +32 -0
- data/spec/redmine/lib/redmine/hook/view_listener.rb +78 -0
- data/spec/redmine/log/test.log +2 -0
- data/spec/redmine/plugins/dummy_plugin/Gemfile +1 -0
- data/spec/redmine/plugins/dummy_plugin/app/controllers/dummy_autocompletes_controller.rb +4 -0
- data/spec/redmine/plugins/dummy_plugin/app/views/dummy_autocompletes/index.html.erb +3 -0
- data/spec/redmine/plugins/dummy_plugin/config/locales/en.yml +2 -0
- data/spec/redmine/plugins/dummy_plugin/config/routes.rb +1 -0
- data/spec/redmine/plugins/dummy_plugin/init.rb +34 -0
- data/spec/redmine/plugins/dummy_plugin/lib/dummy_plugin/easy_patch/redmine/controllers/issues_controller_patch.example +30 -0
- data/spec/redmine/plugins/dummy_plugin/lib/dummy_plugin/easy_patch/redmine/helpers/issues_helper_patch.example +30 -0
- data/spec/redmine/plugins/dummy_plugin/lib/dummy_plugin/easy_patch/redmine/models/issue_patch.example +30 -0
- data/spec/redmine/plugins/dummy_plugin/lib/dummy_plugin/hooks.rb +5 -0
- data/spec/redmine/plugins/dummy_plugin/lib/dummy_plugin/internals.rb +4 -0
- data/spec/redmine/public/images/jstoolbar/bt_precode.png +0 -0
- data/spec/redmine/public/images/reorder.png +0 -0
- data/spec/redmine/public/javascripts/jquery-1.11.1-ui-1.11.0-ujs-3.1.4.js +21 -0
- data/spec/redmine/public/javascripts/redmine_extensions/application.js +13 -0
- data/spec/redmine/public/javascripts/redmine_extensions/easy_togglers.js +58 -0
- data/spec/redmine/public/javascripts/redmine_extensions/jquery.entityarray.js +130 -0
- data/spec/redmine/public/javascripts/redmine_extensions/redmine_extensions.js +642 -0
- data/spec/redmine/public/javascripts/responsive.js +83 -0
- data/spec/redmine/public/stylesheets/responsive.css +799 -0
- data/spec/redmine/test/fixtures/files/import_dates.csv +4 -0
- data/spec/redmine/test/fixtures/files/import_iso8859-1.csv +3 -0
- data/spec/redmine/test/fixtures/files/import_issues.csv +4 -0
- data/spec/redmine/test/fixtures/ldap/slapd.centos6.conf +24 -0
- data/spec/redmine/test/fixtures/ldap/slapd.ubuntu.12.04.conf +23 -0
- data/spec/redmine/test/fixtures/mail_handler/issue_update_with_cc.eml +19 -0
- data/spec/redmine/test/fixtures/mail_handler/ticket_on_project_given_by_to_header.eml +60 -0
- data/spec/redmine/test/fixtures/mail_handler/ticket_with_text_attachment_iso-8859-2.eml +31 -0
- data/spec/redmine/test/functional/custom_field_enumerations_controller_test.rb +115 -0
- data/spec/redmine/test/functional/imports_controller_test.rb +205 -0
- data/spec/redmine/test/functional/sessions_controller_test.rb +138 -0
- data/spec/redmine/test/integration/api_test/search_test.rb +92 -0
- data/spec/redmine/test/integration/routing/imports_test.rb +36 -0
- data/spec/redmine/test/integration/sessions_test.rb +97 -0
- data/spec/redmine/test/ui/custom_fields_test_ui.rb +38 -0
- data/spec/redmine/test/unit/attachment_transaction_test.rb +76 -0
- data/spec/redmine/test/unit/helpers/journals_helper_test.rb +48 -0
- data/spec/redmine/test/unit/helpers/wiki_helper_test.rb +45 -0
- data/spec/redmine/test/unit/issue_import_test.rb +178 -0
- data/spec/redmine/test/unit/lib/redmine/acts/positioned_with_scope_test.rb +53 -0
- data/spec/redmine/test/unit/lib/redmine/acts/positioned_without_scope_test.rb +55 -0
- data/spec/redmine/test/unit/lib/redmine/field_format/enumeration_format_test.rb +91 -0
- data/spec/support/easy_queries_helpers.rb +0 -0
- metadata +216 -10
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
#
|
|
3
|
+
# Redmine - project management software
|
|
4
|
+
# Copyright (C) 2006-2016 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 ImportsHelper
|
|
21
|
+
def options_for_mapping_select(import, field, options={})
|
|
22
|
+
tags = "".html_safe
|
|
23
|
+
blank_text = options[:required] ? "-- #{l(:actionview_instancetag_blank_option)} --" : " ".html_safe
|
|
24
|
+
tags << content_tag('option', blank_text, :value => '')
|
|
25
|
+
tags << options_for_select(import.columns_options, import.mapping[field])
|
|
26
|
+
if values = options[:values]
|
|
27
|
+
tags << content_tag('option', '--', :disabled => true)
|
|
28
|
+
tags << options_for_select(values.map {|text, value| [text, "value:#{value}"]}, import.mapping[field])
|
|
29
|
+
end
|
|
30
|
+
tags
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def mapping_select_tag(import, field, options={})
|
|
34
|
+
name = "import_settings[mapping][#{field}]"
|
|
35
|
+
select_tag name, options_for_mapping_select(import, field, options), :id => "import_mapping_#{field}"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Returns the options for the date_format setting
|
|
39
|
+
def date_format_options
|
|
40
|
+
Import::DATE_FORMATS.map do |f|
|
|
41
|
+
format = f.gsub('%', '').gsub(/[dmY]/) do
|
|
42
|
+
{'d' => 'DD', 'm' => 'MM', 'Y' => 'YYYY'}[$&]
|
|
43
|
+
end
|
|
44
|
+
[format, f]
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Redmine - project management software
|
|
2
|
+
# Copyright (C) 2006-2016 Jean-Philippe Lang
|
|
3
|
+
#
|
|
4
|
+
# This program is free software; you can redistribute it and/or
|
|
5
|
+
# modify it under the terms of the GNU General Public License
|
|
6
|
+
# as published by the Free Software Foundation; either version 2
|
|
7
|
+
# of the License, or (at your option) any later version.
|
|
8
|
+
#
|
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
# GNU General Public License for more details.
|
|
13
|
+
#
|
|
14
|
+
# You should have received a copy of the GNU General Public License
|
|
15
|
+
# along with this program; if not, write to the Free Software
|
|
16
|
+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
17
|
+
|
|
18
|
+
class CustomFieldEnumeration < ActiveRecord::Base
|
|
19
|
+
include Redmine::SafeAttributes
|
|
20
|
+
|
|
21
|
+
belongs_to :custom_field
|
|
22
|
+
attr_accessible :name, :active, :position
|
|
23
|
+
|
|
24
|
+
validates_presence_of :name, :position, :custom_field_id
|
|
25
|
+
validates_length_of :name, :maximum => 60
|
|
26
|
+
validates_numericality_of :position, :only_integer => true
|
|
27
|
+
before_create :set_position
|
|
28
|
+
|
|
29
|
+
scope :active, lambda { where(:active => true) }
|
|
30
|
+
|
|
31
|
+
safe_attributes 'name',
|
|
32
|
+
'active',
|
|
33
|
+
'position'
|
|
34
|
+
|
|
35
|
+
def to_s
|
|
36
|
+
name.to_s
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def objects_count
|
|
40
|
+
custom_values.count
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def in_use?
|
|
44
|
+
objects_count > 0
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
alias :destroy_without_reassign :destroy
|
|
48
|
+
def destroy(reassign_to=nil)
|
|
49
|
+
if reassign_to
|
|
50
|
+
custom_values.update_all(:value => reassign_to.id.to_s)
|
|
51
|
+
end
|
|
52
|
+
destroy_without_reassign
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def custom_values
|
|
56
|
+
custom_field.custom_values.where(:value => id.to_s)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def self.update_each(custom_field, attributes)
|
|
60
|
+
return unless attributes.is_a?(Hash)
|
|
61
|
+
transaction do
|
|
62
|
+
attributes.each do |enumeration_id, enumeration_attributes|
|
|
63
|
+
enumeration = custom_field.enumerations.find_by_id(enumeration_id)
|
|
64
|
+
if enumeration
|
|
65
|
+
if block_given?
|
|
66
|
+
yield enumeration, enumeration_attributes
|
|
67
|
+
else
|
|
68
|
+
enumeration.attributes = enumeration_attributes
|
|
69
|
+
end
|
|
70
|
+
unless enumeration.save
|
|
71
|
+
raise ActiveRecord::Rollback
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def self.fields_for_order_statement(table=nil)
|
|
79
|
+
table ||= table_name
|
|
80
|
+
columns = ['position']
|
|
81
|
+
columns.uniq.map {|field| "#{table}.#{field}"}
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
def set_position
|
|
87
|
+
max = self.class.where(:custom_field_id => custom_field_id).maximum(:position) || 0
|
|
88
|
+
self.position = max + 1
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
# Redmine - project management software
|
|
2
|
+
# Copyright (C) 2006-2016 Jean-Philippe Lang
|
|
3
|
+
#
|
|
4
|
+
# This program is free software; you can redistribute it and/or
|
|
5
|
+
# modify it under the terms of the GNU General Public License
|
|
6
|
+
# as published by the Free Software Foundation; either version 2
|
|
7
|
+
# of the License, or (at your option) any later version.
|
|
8
|
+
#
|
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
# GNU General Public License for more details.
|
|
13
|
+
#
|
|
14
|
+
# You should have received a copy of the GNU General Public License
|
|
15
|
+
# along with this program; if not, write to the Free Software
|
|
16
|
+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
17
|
+
|
|
18
|
+
require 'csv'
|
|
19
|
+
|
|
20
|
+
class Import < ActiveRecord::Base
|
|
21
|
+
has_many :items, :class_name => 'ImportItem', :dependent => :delete_all
|
|
22
|
+
belongs_to :user
|
|
23
|
+
serialize :settings
|
|
24
|
+
|
|
25
|
+
before_destroy :remove_file
|
|
26
|
+
|
|
27
|
+
validates_presence_of :filename, :user_id
|
|
28
|
+
validates_length_of :filename, :maximum => 255
|
|
29
|
+
|
|
30
|
+
DATE_FORMATS = [
|
|
31
|
+
'%Y-%m-%d',
|
|
32
|
+
'%d/%m/%Y',
|
|
33
|
+
'%m/%d/%Y',
|
|
34
|
+
'%d.%m.%Y',
|
|
35
|
+
'%d-%m-%Y'
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
def initialize(*args)
|
|
39
|
+
super
|
|
40
|
+
self.settings ||= {}
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def file=(arg)
|
|
44
|
+
return unless arg.present? && arg.size > 0
|
|
45
|
+
|
|
46
|
+
self.filename = generate_filename
|
|
47
|
+
Redmine::Utils.save_upload(arg, filepath)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def set_default_settings
|
|
51
|
+
separator = lu(user, :general_csv_separator)
|
|
52
|
+
if file_exists?
|
|
53
|
+
begin
|
|
54
|
+
content = File.read(filepath, 256)
|
|
55
|
+
separator = [',', ';'].sort_by {|sep| content.count(sep) }.last
|
|
56
|
+
rescue Exception => e
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
wrapper = '"'
|
|
60
|
+
encoding = lu(user, :general_csv_encoding)
|
|
61
|
+
|
|
62
|
+
date_format = lu(user, "date.formats.default", :default => "foo")
|
|
63
|
+
date_format = DATE_FORMATS.first unless DATE_FORMATS.include?(date_format)
|
|
64
|
+
|
|
65
|
+
self.settings.merge!(
|
|
66
|
+
'separator' => separator,
|
|
67
|
+
'wrapper' => wrapper,
|
|
68
|
+
'encoding' => encoding,
|
|
69
|
+
'date_format' => date_format
|
|
70
|
+
)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def to_param
|
|
74
|
+
filename
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Returns the full path of the file to import
|
|
78
|
+
# It is stored in tmp/imports with a random hex as filename
|
|
79
|
+
def filepath
|
|
80
|
+
if filename.present? && filename =~ /\A[0-9a-f]+\z/
|
|
81
|
+
File.join(Rails.root, "tmp", "imports", filename)
|
|
82
|
+
else
|
|
83
|
+
nil
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Returns true if the file to import exists
|
|
88
|
+
def file_exists?
|
|
89
|
+
filepath.present? && File.exists?(filepath)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Returns the headers as an array that
|
|
93
|
+
# can be used for select options
|
|
94
|
+
def columns_options(default=nil)
|
|
95
|
+
i = -1
|
|
96
|
+
headers.map {|h| [h, i+=1]}
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Parses the file to import and updates the total number of items
|
|
100
|
+
def parse_file
|
|
101
|
+
count = 0
|
|
102
|
+
read_items {|row, i| count=i}
|
|
103
|
+
update_attribute :total_items, count
|
|
104
|
+
count
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Reads the items to import and yields the given block for each item
|
|
108
|
+
def read_items
|
|
109
|
+
i = 0
|
|
110
|
+
headers = true
|
|
111
|
+
read_rows do |row|
|
|
112
|
+
if i == 0 && headers
|
|
113
|
+
headers = false
|
|
114
|
+
next
|
|
115
|
+
end
|
|
116
|
+
i+= 1
|
|
117
|
+
yield row, i if block_given?
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Returns the count first rows of the file (including headers)
|
|
122
|
+
def first_rows(count=4)
|
|
123
|
+
rows = []
|
|
124
|
+
read_rows do |row|
|
|
125
|
+
rows << row
|
|
126
|
+
break if rows.size >= count
|
|
127
|
+
end
|
|
128
|
+
rows
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Returns an array of headers
|
|
132
|
+
def headers
|
|
133
|
+
first_rows(1).first || []
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Returns the mapping options
|
|
137
|
+
def mapping
|
|
138
|
+
settings['mapping'] || {}
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Imports items and returns the position of the last processed item
|
|
142
|
+
def run(options={})
|
|
143
|
+
max_items = options[:max_items]
|
|
144
|
+
max_time = options[:max_time]
|
|
145
|
+
current = 0
|
|
146
|
+
imported = 0
|
|
147
|
+
resume_after = items.maximum(:position) || 0
|
|
148
|
+
interrupted = false
|
|
149
|
+
started_on = Time.now
|
|
150
|
+
|
|
151
|
+
read_items do |row, position|
|
|
152
|
+
if (max_items && imported >= max_items) || (max_time && Time.now >= started_on + max_time)
|
|
153
|
+
interrupted = true
|
|
154
|
+
break
|
|
155
|
+
end
|
|
156
|
+
if position > resume_after
|
|
157
|
+
item = items.build
|
|
158
|
+
item.position = position
|
|
159
|
+
|
|
160
|
+
if object = build_object(row)
|
|
161
|
+
if object.save
|
|
162
|
+
item.obj_id = object.id
|
|
163
|
+
else
|
|
164
|
+
item.message = object.errors.full_messages.join("\n")
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
item.save!
|
|
169
|
+
imported += 1
|
|
170
|
+
end
|
|
171
|
+
current = position
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
if imported == 0 || interrupted == false
|
|
175
|
+
if total_items.nil?
|
|
176
|
+
update_attribute :total_items, current
|
|
177
|
+
end
|
|
178
|
+
update_attribute :finished, true
|
|
179
|
+
remove_file
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
current
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def unsaved_items
|
|
186
|
+
items.where(:obj_id => nil)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def saved_items
|
|
190
|
+
items.where("obj_id IS NOT NULL")
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
private
|
|
194
|
+
|
|
195
|
+
def read_rows
|
|
196
|
+
return unless file_exists?
|
|
197
|
+
|
|
198
|
+
csv_options = {:headers => false}
|
|
199
|
+
csv_options[:encoding] = settings['encoding'].to_s.presence || 'UTF-8'
|
|
200
|
+
separator = settings['separator'].to_s
|
|
201
|
+
csv_options[:col_sep] = separator if separator.size == 1
|
|
202
|
+
wrapper = settings['wrapper'].to_s
|
|
203
|
+
csv_options[:quote_char] = wrapper if wrapper.size == 1
|
|
204
|
+
|
|
205
|
+
CSV.foreach(filepath, csv_options) do |row|
|
|
206
|
+
yield row if block_given?
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def row_value(row, key)
|
|
211
|
+
if index = mapping[key].presence
|
|
212
|
+
row[index.to_i].presence
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def row_date(row, key)
|
|
217
|
+
if s = row_value(row, key)
|
|
218
|
+
format = settings['date_format']
|
|
219
|
+
format = DATE_FORMATS.first unless DATE_FORMATS.include?(format)
|
|
220
|
+
Date.strptime(s, format) rescue s
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Builds a record for the given row and returns it
|
|
225
|
+
# To be implemented by subclasses
|
|
226
|
+
def build_object(row)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Generates a filename used to store the import file
|
|
230
|
+
def generate_filename
|
|
231
|
+
Redmine::Utils.random_hex(16)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Deletes the import file
|
|
235
|
+
def remove_file
|
|
236
|
+
if file_exists?
|
|
237
|
+
begin
|
|
238
|
+
File.delete filepath
|
|
239
|
+
rescue Exception => e
|
|
240
|
+
logger.error "Unable to delete file #{filepath}: #{e.message}" if logger
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Returns true if value is a string that represents a true value
|
|
246
|
+
def yes?(value)
|
|
247
|
+
value == lu(user, :general_text_yes) || value == '1'
|
|
248
|
+
end
|
|
249
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Redmine - project management software
|
|
2
|
+
# Copyright (C) 2006-2016 Jean-Philippe Lang
|
|
3
|
+
#
|
|
4
|
+
# This program is free software; you can redistribute it and/or
|
|
5
|
+
# modify it under the terms of the GNU General Public License
|
|
6
|
+
# as published by the Free Software Foundation; either version 2
|
|
7
|
+
# of the License, or (at your option) any later version.
|
|
8
|
+
#
|
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
# GNU General Public License for more details.
|
|
13
|
+
#
|
|
14
|
+
# You should have received a copy of the GNU General Public License
|
|
15
|
+
# along with this program; if not, write to the Free Software
|
|
16
|
+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
17
|
+
|
|
18
|
+
class ImportItem < ActiveRecord::Base
|
|
19
|
+
belongs_to :import
|
|
20
|
+
|
|
21
|
+
validates_presence_of :import_id, :position
|
|
22
|
+
end
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# Redmine - project management software
|
|
2
|
+
# Copyright (C) 2006-2016 Jean-Philippe Lang
|
|
3
|
+
#
|
|
4
|
+
# This program is free software; you can redistribute it and/or
|
|
5
|
+
# modify it under the terms of the GNU General Public License
|
|
6
|
+
# as published by the Free Software Foundation; either version 2
|
|
7
|
+
# of the License, or (at your option) any later version.
|
|
8
|
+
#
|
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
# GNU General Public License for more details.
|
|
13
|
+
#
|
|
14
|
+
# You should have received a copy of the GNU General Public License
|
|
15
|
+
# along with this program; if not, write to the Free Software
|
|
16
|
+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
17
|
+
|
|
18
|
+
class IssueImport < Import
|
|
19
|
+
|
|
20
|
+
# Returns the objects that were imported
|
|
21
|
+
def saved_objects
|
|
22
|
+
object_ids = saved_items.pluck(:obj_id)
|
|
23
|
+
objects = Issue.where(:id => object_ids).order(:id).preload(:tracker, :priority, :status)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Returns a scope of projects that user is allowed to
|
|
27
|
+
# import issue to
|
|
28
|
+
def allowed_target_projects
|
|
29
|
+
Project.allowed_to(user, :import_issues)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def project
|
|
33
|
+
project_id = mapping['project_id'].to_i
|
|
34
|
+
allowed_target_projects.find_by_id(project_id) || allowed_target_projects.first
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Returns a scope of trackers that user is allowed to
|
|
38
|
+
# import issue to
|
|
39
|
+
def allowed_target_trackers
|
|
40
|
+
Issue.allowed_target_trackers(project, user)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def tracker
|
|
44
|
+
if mapping['tracker'].to_s =~ /\Avalue:(\d+)\z/
|
|
45
|
+
tracker_id = $1.to_i
|
|
46
|
+
allowed_target_trackers.find_by_id(tracker_id)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Returns true if missing categories should be created during the import
|
|
51
|
+
def create_categories?
|
|
52
|
+
user.allowed_to?(:manage_categories, project) &&
|
|
53
|
+
mapping['create_categories'] == '1'
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Returns true if missing versions should be created during the import
|
|
57
|
+
def create_versions?
|
|
58
|
+
user.allowed_to?(:manage_versions, project) &&
|
|
59
|
+
mapping['create_versions'] == '1'
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def mappable_custom_fields
|
|
63
|
+
if tracker
|
|
64
|
+
issue = Issue.new
|
|
65
|
+
issue.project = project
|
|
66
|
+
issue.tracker = tracker
|
|
67
|
+
issue.editable_custom_field_values(user).map(&:custom_field)
|
|
68
|
+
elsif project
|
|
69
|
+
project.all_issue_custom_fields
|
|
70
|
+
else
|
|
71
|
+
[]
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def build_object(row)
|
|
78
|
+
issue = Issue.new
|
|
79
|
+
issue.author = user
|
|
80
|
+
issue.notify = false
|
|
81
|
+
|
|
82
|
+
tracker_id = nil
|
|
83
|
+
if tracker
|
|
84
|
+
tracker_id = tracker.id
|
|
85
|
+
elsif tracker_name = row_value(row, 'tracker')
|
|
86
|
+
tracker_id = allowed_target_trackers.named(tracker_name).first.try(:id)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
attributes = {
|
|
90
|
+
'project_id' => mapping['project_id'],
|
|
91
|
+
'tracker_id' => tracker_id,
|
|
92
|
+
'subject' => row_value(row, 'subject'),
|
|
93
|
+
'description' => row_value(row, 'description')
|
|
94
|
+
}
|
|
95
|
+
if status_name = row_value(row, 'status')
|
|
96
|
+
if status_id = IssueStatus.named(status_name).first.try(:id)
|
|
97
|
+
attributes['status_id'] = status_id
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
issue.send :safe_attributes=, attributes, user
|
|
101
|
+
|
|
102
|
+
attributes = {}
|
|
103
|
+
if priority_name = row_value(row, 'priority')
|
|
104
|
+
if priority_id = IssuePriority.active.named(priority_name).first.try(:id)
|
|
105
|
+
attributes['priority_id'] = priority_id
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
if issue.project && category_name = row_value(row, 'category')
|
|
109
|
+
if category = issue.project.issue_categories.named(category_name).first
|
|
110
|
+
attributes['category_id'] = category.id
|
|
111
|
+
elsif create_categories?
|
|
112
|
+
category = issue.project.issue_categories.build
|
|
113
|
+
category.name = category_name
|
|
114
|
+
if category.save
|
|
115
|
+
attributes['category_id'] = category.id
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
if assignee_name = row_value(row, 'assigned_to')
|
|
120
|
+
if assignee = Principal.detect_by_keyword(issue.assignable_users, assignee_name)
|
|
121
|
+
attributes['assigned_to_id'] = assignee.id
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
if issue.project && version_name = row_value(row, 'fixed_version')
|
|
125
|
+
if version = issue.project.versions.named(version_name).first
|
|
126
|
+
attributes['fixed_version_id'] = version.id
|
|
127
|
+
elsif create_versions?
|
|
128
|
+
version = issue.project.versions.build
|
|
129
|
+
version.name = version_name
|
|
130
|
+
if version.save
|
|
131
|
+
attributes['fixed_version_id'] = version.id
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
if is_private = row_value(row, 'is_private')
|
|
136
|
+
if yes?(is_private)
|
|
137
|
+
attributes['is_private'] = '1'
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
if parent_issue_id = row_value(row, 'parent_issue_id')
|
|
141
|
+
if parent_issue_id =~ /\A(#)?(\d+)\z/
|
|
142
|
+
parent_issue_id = $2
|
|
143
|
+
if $1
|
|
144
|
+
attributes['parent_issue_id'] = parent_issue_id
|
|
145
|
+
elsif issue_id = items.where(:position => parent_issue_id).first.try(:obj_id)
|
|
146
|
+
attributes['parent_issue_id'] = issue_id
|
|
147
|
+
end
|
|
148
|
+
else
|
|
149
|
+
attributes['parent_issue_id'] = parent_issue_id
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
if start_date = row_date(row, 'start_date')
|
|
153
|
+
attributes['start_date'] = start_date
|
|
154
|
+
end
|
|
155
|
+
if due_date = row_date(row, 'due_date')
|
|
156
|
+
attributes['due_date'] = due_date
|
|
157
|
+
end
|
|
158
|
+
if estimated_hours = row_value(row, 'estimated_hours')
|
|
159
|
+
attributes['estimated_hours'] = estimated_hours
|
|
160
|
+
end
|
|
161
|
+
if done_ratio = row_value(row, 'done_ratio')
|
|
162
|
+
attributes['done_ratio'] = done_ratio
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
attributes['custom_field_values'] = issue.custom_field_values.inject({}) do |h, v|
|
|
166
|
+
value = case v.custom_field.field_format
|
|
167
|
+
when 'date'
|
|
168
|
+
row_date(row, "cf_#{v.custom_field.id}")
|
|
169
|
+
else
|
|
170
|
+
row_value(row, "cf_#{v.custom_field.id}")
|
|
171
|
+
end
|
|
172
|
+
if value
|
|
173
|
+
h[v.custom_field.id.to_s] = v.custom_field.value_from_keyword(value, issue)
|
|
174
|
+
end
|
|
175
|
+
h
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
issue.send :safe_attributes=, attributes, user
|
|
179
|
+
|
|
180
|
+
if issue.tracker_id != tracker_id
|
|
181
|
+
issue.tracker_id = nil
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
issue
|
|
185
|
+
end
|
|
186
|
+
end
|