##// END OF EJS Templates
New custom fields of existing issues are not initialized with their default value (#21074)....
Jean-Philippe Lang -
r14391:e6112a9710cb
parent child
Show More
@@ -1,50 +1,50
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
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
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.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class CustomValue < ActiveRecord::Base
18 class CustomValue < ActiveRecord::Base
19 belongs_to :custom_field
19 belongs_to :custom_field
20 belongs_to :customized, :polymorphic => true
20 belongs_to :customized, :polymorphic => true
21 attr_protected :id
21 attr_protected :id
22
22
23 def initialize(attributes=nil, *args)
23 def initialize(attributes=nil, *args)
24 super
24 super
25 if new_record? && custom_field && (customized_type.blank? || (customized && customized.new_record?))
25 if new_record? && custom_field && !attributes.key?(:value)
26 self.value ||= custom_field.default_value
26 self.value ||= custom_field.default_value
27 end
27 end
28 end
28 end
29
29
30 # Returns true if the boolean custom value is true
30 # Returns true if the boolean custom value is true
31 def true?
31 def true?
32 self.value == '1'
32 self.value == '1'
33 end
33 end
34
34
35 def editable?
35 def editable?
36 custom_field.editable?
36 custom_field.editable?
37 end
37 end
38
38
39 def visible?
39 def visible?
40 custom_field.visible?
40 custom_field.visible?
41 end
41 end
42
42
43 def required?
43 def required?
44 custom_field.is_required?
44 custom_field.is_required?
45 end
45 end
46
46
47 def to_s
47 def to_s
48 value.to_s
48 value.to_s
49 end
49 end
50 end
50 end
@@ -1,170 +1,170
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
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
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.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 module Redmine
18 module Redmine
19 module Acts
19 module Acts
20 module Customizable
20 module Customizable
21 def self.included(base)
21 def self.included(base)
22 base.extend ClassMethods
22 base.extend ClassMethods
23 end
23 end
24
24
25 module ClassMethods
25 module ClassMethods
26 def acts_as_customizable(options = {})
26 def acts_as_customizable(options = {})
27 return if self.included_modules.include?(Redmine::Acts::Customizable::InstanceMethods)
27 return if self.included_modules.include?(Redmine::Acts::Customizable::InstanceMethods)
28 cattr_accessor :customizable_options
28 cattr_accessor :customizable_options
29 self.customizable_options = options
29 self.customizable_options = options
30 has_many :custom_values, lambda {includes(:custom_field).order("#{CustomField.table_name}.position")},
30 has_many :custom_values, lambda {includes(:custom_field).order("#{CustomField.table_name}.position")},
31 :as => :customized,
31 :as => :customized,
32 :dependent => :delete_all,
32 :dependent => :delete_all,
33 :validate => false
33 :validate => false
34
34
35 send :include, Redmine::Acts::Customizable::InstanceMethods
35 send :include, Redmine::Acts::Customizable::InstanceMethods
36 validate :validate_custom_field_values
36 validate :validate_custom_field_values
37 after_save :save_custom_field_values
37 after_save :save_custom_field_values
38 end
38 end
39 end
39 end
40
40
41 module InstanceMethods
41 module InstanceMethods
42 def self.included(base)
42 def self.included(base)
43 base.extend ClassMethods
43 base.extend ClassMethods
44 base.send :alias_method_chain, :reload, :custom_fields
44 base.send :alias_method_chain, :reload, :custom_fields
45 end
45 end
46
46
47 def available_custom_fields
47 def available_custom_fields
48 CustomField.where("type = '#{self.class.name}CustomField'").sorted.to_a
48 CustomField.where("type = '#{self.class.name}CustomField'").sorted.to_a
49 end
49 end
50
50
51 # Sets the values of the object's custom fields
51 # Sets the values of the object's custom fields
52 # values is an array like [{'id' => 1, 'value' => 'foo'}, {'id' => 2, 'value' => 'bar'}]
52 # values is an array like [{'id' => 1, 'value' => 'foo'}, {'id' => 2, 'value' => 'bar'}]
53 def custom_fields=(values)
53 def custom_fields=(values)
54 values_to_hash = values.inject({}) do |hash, v|
54 values_to_hash = values.inject({}) do |hash, v|
55 v = v.stringify_keys
55 v = v.stringify_keys
56 if v['id'] && v.has_key?('value')
56 if v['id'] && v.has_key?('value')
57 hash[v['id']] = v['value']
57 hash[v['id']] = v['value']
58 end
58 end
59 hash
59 hash
60 end
60 end
61 self.custom_field_values = values_to_hash
61 self.custom_field_values = values_to_hash
62 end
62 end
63
63
64 # Sets the values of the object's custom fields
64 # Sets the values of the object's custom fields
65 # values is a hash like {'1' => 'foo', 2 => 'bar'}
65 # values is a hash like {'1' => 'foo', 2 => 'bar'}
66 def custom_field_values=(values)
66 def custom_field_values=(values)
67 values = values.stringify_keys
67 values = values.stringify_keys
68
68
69 custom_field_values.each do |custom_field_value|
69 custom_field_values.each do |custom_field_value|
70 key = custom_field_value.custom_field_id.to_s
70 key = custom_field_value.custom_field_id.to_s
71 if values.has_key?(key)
71 if values.has_key?(key)
72 value = values[key]
72 value = values[key]
73 if value.is_a?(Array)
73 if value.is_a?(Array)
74 value = value.reject(&:blank?).map(&:to_s).uniq
74 value = value.reject(&:blank?).map(&:to_s).uniq
75 if value.empty?
75 if value.empty?
76 value << ''
76 value << ''
77 end
77 end
78 else
78 else
79 value = value.to_s
79 value = value.to_s
80 end
80 end
81 custom_field_value.value = value
81 custom_field_value.value = value
82 end
82 end
83 end
83 end
84 @custom_field_values_changed = true
84 @custom_field_values_changed = true
85 end
85 end
86
86
87 def custom_field_values
87 def custom_field_values
88 @custom_field_values ||= available_custom_fields.collect do |field|
88 @custom_field_values ||= available_custom_fields.collect do |field|
89 x = CustomFieldValue.new
89 x = CustomFieldValue.new
90 x.custom_field = field
90 x.custom_field = field
91 x.customized = self
91 x.customized = self
92 if field.multiple?
92 if field.multiple?
93 values = custom_values.select { |v| v.custom_field == field }
93 values = custom_values.select { |v| v.custom_field == field }
94 if values.empty?
94 if values.empty?
95 values << custom_values.build(:customized => self, :custom_field => field, :value => nil)
95 values << custom_values.build(:customized => self, :custom_field => field)
96 end
96 end
97 x.value = values.map(&:value)
97 x.value = values.map(&:value)
98 else
98 else
99 cv = custom_values.detect { |v| v.custom_field == field }
99 cv = custom_values.detect { |v| v.custom_field == field }
100 cv ||= custom_values.build(:customized => self, :custom_field => field, :value => nil)
100 cv ||= custom_values.build(:customized => self, :custom_field => field)
101 x.value = cv.value
101 x.value = cv.value
102 end
102 end
103 x.value_was = x.value.dup if x.value
103 x.value_was = x.value.dup if x.value
104 x
104 x
105 end
105 end
106 end
106 end
107
107
108 def visible_custom_field_values
108 def visible_custom_field_values
109 custom_field_values.select(&:visible?)
109 custom_field_values.select(&:visible?)
110 end
110 end
111
111
112 def custom_field_values_changed?
112 def custom_field_values_changed?
113 @custom_field_values_changed == true
113 @custom_field_values_changed == true
114 end
114 end
115
115
116 def custom_value_for(c)
116 def custom_value_for(c)
117 field_id = (c.is_a?(CustomField) ? c.id : c.to_i)
117 field_id = (c.is_a?(CustomField) ? c.id : c.to_i)
118 custom_values.detect {|v| v.custom_field_id == field_id }
118 custom_values.detect {|v| v.custom_field_id == field_id }
119 end
119 end
120
120
121 def custom_field_value(c)
121 def custom_field_value(c)
122 field_id = (c.is_a?(CustomField) ? c.id : c.to_i)
122 field_id = (c.is_a?(CustomField) ? c.id : c.to_i)
123 custom_field_values.detect {|v| v.custom_field_id == field_id }.try(:value)
123 custom_field_values.detect {|v| v.custom_field_id == field_id }.try(:value)
124 end
124 end
125
125
126 def validate_custom_field_values
126 def validate_custom_field_values
127 if new_record? || custom_field_values_changed?
127 if new_record? || custom_field_values_changed?
128 custom_field_values.each(&:validate_value)
128 custom_field_values.each(&:validate_value)
129 end
129 end
130 end
130 end
131
131
132 def save_custom_field_values
132 def save_custom_field_values
133 target_custom_values = []
133 target_custom_values = []
134 custom_field_values.each do |custom_field_value|
134 custom_field_values.each do |custom_field_value|
135 if custom_field_value.value.is_a?(Array)
135 if custom_field_value.value.is_a?(Array)
136 custom_field_value.value.each do |v|
136 custom_field_value.value.each do |v|
137 target = custom_values.detect {|cv| cv.custom_field == custom_field_value.custom_field && cv.value == v}
137 target = custom_values.detect {|cv| cv.custom_field == custom_field_value.custom_field && cv.value == v}
138 target ||= custom_values.build(:customized => self, :custom_field => custom_field_value.custom_field, :value => v)
138 target ||= custom_values.build(:customized => self, :custom_field => custom_field_value.custom_field, :value => v)
139 target_custom_values << target
139 target_custom_values << target
140 end
140 end
141 else
141 else
142 target = custom_values.detect {|cv| cv.custom_field == custom_field_value.custom_field}
142 target = custom_values.detect {|cv| cv.custom_field == custom_field_value.custom_field}
143 target ||= custom_values.build(:customized => self, :custom_field => custom_field_value.custom_field)
143 target ||= custom_values.build(:customized => self, :custom_field => custom_field_value.custom_field)
144 target.value = custom_field_value.value
144 target.value = custom_field_value.value
145 target_custom_values << target
145 target_custom_values << target
146 end
146 end
147 end
147 end
148 self.custom_values = target_custom_values
148 self.custom_values = target_custom_values
149 custom_values.each(&:save)
149 custom_values.each(&:save)
150 @custom_field_values_changed = false
150 @custom_field_values_changed = false
151 true
151 true
152 end
152 end
153
153
154 def reset_custom_values!
154 def reset_custom_values!
155 @custom_field_values = nil
155 @custom_field_values = nil
156 @custom_field_values_changed = true
156 @custom_field_values_changed = true
157 end
157 end
158
158
159 def reload_with_custom_fields(*args)
159 def reload_with_custom_fields(*args)
160 @custom_field_values = nil
160 @custom_field_values = nil
161 @custom_field_values_changed = false
161 @custom_field_values_changed = false
162 reload_without_custom_fields(*args)
162 reload_without_custom_fields(*args)
163 end
163 end
164
164
165 module ClassMethods
165 module ClassMethods
166 end
166 end
167 end
167 end
168 end
168 end
169 end
169 end
170 end
170 end
@@ -1,782 +1,782
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
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
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.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../../test_helper', __FILE__)
18 require File.expand_path('../../../test_helper', __FILE__)
19
19
20 class Redmine::ApiTest::IssuesTest < Redmine::ApiTest::Base
20 class Redmine::ApiTest::IssuesTest < Redmine::ApiTest::Base
21 fixtures :projects,
21 fixtures :projects,
22 :users,
22 :users,
23 :roles,
23 :roles,
24 :members,
24 :members,
25 :member_roles,
25 :member_roles,
26 :issues,
26 :issues,
27 :issue_statuses,
27 :issue_statuses,
28 :issue_relations,
28 :issue_relations,
29 :versions,
29 :versions,
30 :trackers,
30 :trackers,
31 :projects_trackers,
31 :projects_trackers,
32 :issue_categories,
32 :issue_categories,
33 :enabled_modules,
33 :enabled_modules,
34 :enumerations,
34 :enumerations,
35 :attachments,
35 :attachments,
36 :workflows,
36 :workflows,
37 :custom_fields,
37 :custom_fields,
38 :custom_values,
38 :custom_values,
39 :custom_fields_projects,
39 :custom_fields_projects,
40 :custom_fields_trackers,
40 :custom_fields_trackers,
41 :time_entries,
41 :time_entries,
42 :journals,
42 :journals,
43 :journal_details,
43 :journal_details,
44 :queries,
44 :queries,
45 :attachments
45 :attachments
46
46
47 test "GET /issues.xml should contain metadata" do
47 test "GET /issues.xml should contain metadata" do
48 get '/issues.xml'
48 get '/issues.xml'
49 assert_select 'issues[type=array][total_count=?][limit="25"][offset="0"]',
49 assert_select 'issues[type=array][total_count=?][limit="25"][offset="0"]',
50 assigns(:issue_count).to_s
50 assigns(:issue_count).to_s
51 end
51 end
52
52
53 test "GET /issues.xml with nometa param should not contain metadata" do
53 test "GET /issues.xml with nometa param should not contain metadata" do
54 get '/issues.xml?nometa=1'
54 get '/issues.xml?nometa=1'
55 assert_select 'issues[type=array]:not([total_count]):not([limit]):not([offset])'
55 assert_select 'issues[type=array]:not([total_count]):not([limit]):not([offset])'
56 end
56 end
57
57
58 test "GET /issues.xml with nometa header should not contain metadata" do
58 test "GET /issues.xml with nometa header should not contain metadata" do
59 get '/issues.xml', {}, {'X-Redmine-Nometa' => '1'}
59 get '/issues.xml', {}, {'X-Redmine-Nometa' => '1'}
60 assert_select 'issues[type=array]:not([total_count]):not([limit]):not([offset])'
60 assert_select 'issues[type=array]:not([total_count]):not([limit]):not([offset])'
61 end
61 end
62
62
63 test "GET /issues.xml with offset and limit" do
63 test "GET /issues.xml with offset and limit" do
64 get '/issues.xml?offset=2&limit=3'
64 get '/issues.xml?offset=2&limit=3'
65
65
66 assert_equal 3, assigns(:limit)
66 assert_equal 3, assigns(:limit)
67 assert_equal 2, assigns(:offset)
67 assert_equal 2, assigns(:offset)
68 assert_select 'issues issue', 3
68 assert_select 'issues issue', 3
69 end
69 end
70
70
71 test "GET /issues.xml with relations" do
71 test "GET /issues.xml with relations" do
72 get '/issues.xml?include=relations'
72 get '/issues.xml?include=relations'
73
73
74 assert_response :success
74 assert_response :success
75 assert_equal 'application/xml', @response.content_type
75 assert_equal 'application/xml', @response.content_type
76
76
77 assert_select 'issue id', :text => '3' do
77 assert_select 'issue id', :text => '3' do
78 assert_select '~ relations relation', 1
78 assert_select '~ relations relation', 1
79 assert_select '~ relations relation[id="2"][issue_id="2"][issue_to_id="3"][relation_type=relates]'
79 assert_select '~ relations relation[id="2"][issue_id="2"][issue_to_id="3"][relation_type=relates]'
80 end
80 end
81
81
82 assert_select 'issue id', :text => '1' do
82 assert_select 'issue id', :text => '1' do
83 assert_select '~ relations'
83 assert_select '~ relations'
84 assert_select '~ relations relation', 0
84 assert_select '~ relations relation', 0
85 end
85 end
86 end
86 end
87
87
88 test "GET /issues.xml with invalid query params" do
88 test "GET /issues.xml with invalid query params" do
89 get '/issues.xml', {:f => ['start_date'], :op => {:start_date => '='}}
89 get '/issues.xml', {:f => ['start_date'], :op => {:start_date => '='}}
90
90
91 assert_response :unprocessable_entity
91 assert_response :unprocessable_entity
92 assert_equal 'application/xml', @response.content_type
92 assert_equal 'application/xml', @response.content_type
93 assert_select 'errors error', :text => "Start date cannot be blank"
93 assert_select 'errors error', :text => "Start date cannot be blank"
94 end
94 end
95
95
96 test "GET /issues.xml with custom field filter" do
96 test "GET /issues.xml with custom field filter" do
97 get '/issues.xml',
97 get '/issues.xml',
98 {:set_filter => 1, :f => ['cf_1'], :op => {:cf_1 => '='}, :v => {:cf_1 => ['MySQL']}}
98 {:set_filter => 1, :f => ['cf_1'], :op => {:cf_1 => '='}, :v => {:cf_1 => ['MySQL']}}
99
99
100 expected_ids = Issue.visible.
100 expected_ids = Issue.visible.
101 joins(:custom_values).
101 joins(:custom_values).
102 where(:custom_values => {:custom_field_id => 1, :value => 'MySQL'}).map(&:id)
102 where(:custom_values => {:custom_field_id => 1, :value => 'MySQL'}).map(&:id)
103 assert expected_ids.any?
103 assert expected_ids.any?
104
104
105 assert_select 'issues > issue > id', :count => expected_ids.count do |ids|
105 assert_select 'issues > issue > id', :count => expected_ids.count do |ids|
106 ids.each { |id| assert expected_ids.delete(id.children.first.content.to_i) }
106 ids.each { |id| assert expected_ids.delete(id.children.first.content.to_i) }
107 end
107 end
108 end
108 end
109
109
110 test "GET /issues.xml with custom field filter (shorthand method)" do
110 test "GET /issues.xml with custom field filter (shorthand method)" do
111 get '/issues.xml', {:cf_1 => 'MySQL'}
111 get '/issues.xml', {:cf_1 => 'MySQL'}
112
112
113 expected_ids = Issue.visible.
113 expected_ids = Issue.visible.
114 joins(:custom_values).
114 joins(:custom_values).
115 where(:custom_values => {:custom_field_id => 1, :value => 'MySQL'}).map(&:id)
115 where(:custom_values => {:custom_field_id => 1, :value => 'MySQL'}).map(&:id)
116 assert expected_ids.any?
116 assert expected_ids.any?
117
117
118 assert_select 'issues > issue > id', :count => expected_ids.count do |ids|
118 assert_select 'issues > issue > id', :count => expected_ids.count do |ids|
119 ids.each { |id| assert expected_ids.delete(id.children.first.content.to_i) }
119 ids.each { |id| assert expected_ids.delete(id.children.first.content.to_i) }
120 end
120 end
121 end
121 end
122
122
123 def test_index_should_include_issue_attributes
123 def test_index_should_include_issue_attributes
124 get '/issues.xml'
124 get '/issues.xml'
125 assert_select 'issues>issue>is_private', :text => 'false'
125 assert_select 'issues>issue>is_private', :text => 'false'
126 end
126 end
127
127
128 def test_index_should_allow_timestamp_filtering
128 def test_index_should_allow_timestamp_filtering
129 Issue.delete_all
129 Issue.delete_all
130 Issue.generate!(:subject => '1').update_column(:updated_on, Time.parse("2014-01-02T10:25:00Z"))
130 Issue.generate!(:subject => '1').update_column(:updated_on, Time.parse("2014-01-02T10:25:00Z"))
131 Issue.generate!(:subject => '2').update_column(:updated_on, Time.parse("2014-01-02T12:13:00Z"))
131 Issue.generate!(:subject => '2').update_column(:updated_on, Time.parse("2014-01-02T12:13:00Z"))
132
132
133 get '/issues.xml',
133 get '/issues.xml',
134 {:set_filter => 1, :f => ['updated_on'], :op => {:updated_on => '<='},
134 {:set_filter => 1, :f => ['updated_on'], :op => {:updated_on => '<='},
135 :v => {:updated_on => ['2014-01-02T12:00:00Z']}}
135 :v => {:updated_on => ['2014-01-02T12:00:00Z']}}
136 assert_select 'issues>issue', :count => 1
136 assert_select 'issues>issue', :count => 1
137 assert_select 'issues>issue>subject', :text => '1'
137 assert_select 'issues>issue>subject', :text => '1'
138
138
139 get '/issues.xml',
139 get '/issues.xml',
140 {:set_filter => 1, :f => ['updated_on'], :op => {:updated_on => '>='},
140 {:set_filter => 1, :f => ['updated_on'], :op => {:updated_on => '>='},
141 :v => {:updated_on => ['2014-01-02T12:00:00Z']}}
141 :v => {:updated_on => ['2014-01-02T12:00:00Z']}}
142 assert_select 'issues>issue', :count => 1
142 assert_select 'issues>issue', :count => 1
143 assert_select 'issues>issue>subject', :text => '2'
143 assert_select 'issues>issue>subject', :text => '2'
144
144
145 get '/issues.xml',
145 get '/issues.xml',
146 {:set_filter => 1, :f => ['updated_on'], :op => {:updated_on => '>='},
146 {:set_filter => 1, :f => ['updated_on'], :op => {:updated_on => '>='},
147 :v => {:updated_on => ['2014-01-02T08:00:00Z']}}
147 :v => {:updated_on => ['2014-01-02T08:00:00Z']}}
148 assert_select 'issues>issue', :count => 2
148 assert_select 'issues>issue', :count => 2
149 end
149 end
150
150
151 test "GET /issues.xml with filter" do
151 test "GET /issues.xml with filter" do
152 get '/issues.xml?status_id=5'
152 get '/issues.xml?status_id=5'
153
153
154 expected_ids = Issue.visible.where(:status_id => 5).map(&:id)
154 expected_ids = Issue.visible.where(:status_id => 5).map(&:id)
155 assert expected_ids.any?
155 assert expected_ids.any?
156
156
157 assert_select 'issues > issue > id', :count => expected_ids.count do |ids|
157 assert_select 'issues > issue > id', :count => expected_ids.count do |ids|
158 ids.each { |id| assert expected_ids.delete(id.children.first.content.to_i) }
158 ids.each { |id| assert expected_ids.delete(id.children.first.content.to_i) }
159 end
159 end
160 end
160 end
161
161
162 test "GET /issues.json with filter" do
162 test "GET /issues.json with filter" do
163 get '/issues.json?status_id=5'
163 get '/issues.json?status_id=5'
164
164
165 json = ActiveSupport::JSON.decode(response.body)
165 json = ActiveSupport::JSON.decode(response.body)
166 status_ids_used = json['issues'].collect {|j| j['status']['id'] }
166 status_ids_used = json['issues'].collect {|j| j['status']['id'] }
167 assert_equal 3, status_ids_used.length
167 assert_equal 3, status_ids_used.length
168 assert status_ids_used.all? {|id| id == 5 }
168 assert status_ids_used.all? {|id| id == 5 }
169 end
169 end
170
170
171 test "GET /issues/:id.xml with journals" do
171 test "GET /issues/:id.xml with journals" do
172 get '/issues/1.xml?include=journals'
172 get '/issues/1.xml?include=journals'
173
173
174 assert_select 'issue journals[type=array]' do
174 assert_select 'issue journals[type=array]' do
175 assert_select 'journal[id="1"]' do
175 assert_select 'journal[id="1"]' do
176 assert_select 'details[type=array]' do
176 assert_select 'details[type=array]' do
177 assert_select 'detail[name=status_id]' do
177 assert_select 'detail[name=status_id]' do
178 assert_select 'old_value', :text => '1'
178 assert_select 'old_value', :text => '1'
179 assert_select 'new_value', :text => '2'
179 assert_select 'new_value', :text => '2'
180 end
180 end
181 end
181 end
182 end
182 end
183 end
183 end
184 end
184 end
185
185
186 test "GET /issues/:id.xml with journals should format timestamps in ISO 8601" do
186 test "GET /issues/:id.xml with journals should format timestamps in ISO 8601" do
187 get '/issues/1.xml?include=journals'
187 get '/issues/1.xml?include=journals'
188
188
189 iso_date = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/
189 iso_date = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/
190 assert_select 'issue>created_on', :text => iso_date
190 assert_select 'issue>created_on', :text => iso_date
191 assert_select 'issue>updated_on', :text => iso_date
191 assert_select 'issue>updated_on', :text => iso_date
192 assert_select 'issue journal>created_on', :text => iso_date
192 assert_select 'issue journal>created_on', :text => iso_date
193 end
193 end
194
194
195 test "GET /issues/:id.xml with custom fields" do
195 test "GET /issues/:id.xml with custom fields" do
196 get '/issues/3.xml'
196 get '/issues/3.xml'
197
197
198 assert_select 'issue custom_fields[type=array]' do
198 assert_select 'issue custom_fields[type=array]' do
199 assert_select 'custom_field[id="1"]' do
199 assert_select 'custom_field[id="1"]' do
200 assert_select 'value', :text => 'MySQL'
200 assert_select 'value', :text => 'MySQL'
201 end
201 end
202 end
202 end
203 assert_nothing_raised do
203 assert_nothing_raised do
204 Hash.from_xml(response.body).to_xml
204 Hash.from_xml(response.body).to_xml
205 end
205 end
206 end
206 end
207
207
208 test "GET /issues/:id.xml with multi custom fields" do
208 test "GET /issues/:id.xml with multi custom fields" do
209 field = CustomField.find(1)
209 field = CustomField.find(1)
210 field.update_attribute :multiple, true
210 field.update_attribute :multiple, true
211 issue = Issue.find(3)
211 issue = Issue.find(3)
212 issue.custom_field_values = {1 => ['MySQL', 'Oracle']}
212 issue.custom_field_values = {1 => ['MySQL', 'Oracle']}
213 issue.save!
213 issue.save!
214
214
215 get '/issues/3.xml'
215 get '/issues/3.xml'
216 assert_response :success
216 assert_response :success
217
217
218 assert_select 'issue custom_fields[type=array]' do
218 assert_select 'issue custom_fields[type=array]' do
219 assert_select 'custom_field[id="1"]' do
219 assert_select 'custom_field[id="1"]' do
220 assert_select 'value[type=array] value', 2
220 assert_select 'value[type=array] value', 2
221 end
221 end
222 end
222 end
223 xml = Hash.from_xml(response.body)
223 xml = Hash.from_xml(response.body)
224 custom_fields = xml['issue']['custom_fields']
224 custom_fields = xml['issue']['custom_fields']
225 assert_kind_of Array, custom_fields
225 assert_kind_of Array, custom_fields
226 field = custom_fields.detect {|f| f['id'] == '1'}
226 field = custom_fields.detect {|f| f['id'] == '1'}
227 assert_kind_of Hash, field
227 assert_kind_of Hash, field
228 assert_equal ['MySQL', 'Oracle'], field['value'].sort
228 assert_equal ['MySQL', 'Oracle'], field['value'].sort
229 end
229 end
230
230
231 test "GET /issues/:id.json with multi custom fields" do
231 test "GET /issues/:id.json with multi custom fields" do
232 field = CustomField.find(1)
232 field = CustomField.find(1)
233 field.update_attribute :multiple, true
233 field.update_attribute :multiple, true
234 issue = Issue.find(3)
234 issue = Issue.find(3)
235 issue.custom_field_values = {1 => ['MySQL', 'Oracle']}
235 issue.custom_field_values = {1 => ['MySQL', 'Oracle']}
236 issue.save!
236 issue.save!
237
237
238 get '/issues/3.json'
238 get '/issues/3.json'
239 assert_response :success
239 assert_response :success
240
240
241 json = ActiveSupport::JSON.decode(response.body)
241 json = ActiveSupport::JSON.decode(response.body)
242 custom_fields = json['issue']['custom_fields']
242 custom_fields = json['issue']['custom_fields']
243 assert_kind_of Array, custom_fields
243 assert_kind_of Array, custom_fields
244 field = custom_fields.detect {|f| f['id'] == 1}
244 field = custom_fields.detect {|f| f['id'] == 1}
245 assert_kind_of Hash, field
245 assert_kind_of Hash, field
246 assert_equal ['MySQL', 'Oracle'], field['value'].sort
246 assert_equal ['MySQL', 'Oracle'], field['value'].sort
247 end
247 end
248
248
249 test "GET /issues/:id.xml with empty value for multi custom field" do
249 test "GET /issues/:id.xml with empty value for multi custom field" do
250 field = CustomField.find(1)
250 field = CustomField.find(1)
251 field.update_attribute :multiple, true
251 field.update_attribute :multiple, true
252 issue = Issue.find(3)
252 issue = Issue.find(3)
253 issue.custom_field_values = {1 => ['']}
253 issue.custom_field_values = {1 => ['']}
254 issue.save!
254 issue.save!
255
255
256 get '/issues/3.xml'
256 get '/issues/3.xml'
257
257
258 assert_select 'issue custom_fields[type=array]' do
258 assert_select 'issue custom_fields[type=array]' do
259 assert_select 'custom_field[id="1"]' do
259 assert_select 'custom_field[id="1"]' do
260 assert_select 'value[type=array]:empty'
260 assert_select 'value[type=array]:empty'
261 end
261 end
262 end
262 end
263 xml = Hash.from_xml(response.body)
263 xml = Hash.from_xml(response.body)
264 custom_fields = xml['issue']['custom_fields']
264 custom_fields = xml['issue']['custom_fields']
265 assert_kind_of Array, custom_fields
265 assert_kind_of Array, custom_fields
266 field = custom_fields.detect {|f| f['id'] == '1'}
266 field = custom_fields.detect {|f| f['id'] == '1'}
267 assert_kind_of Hash, field
267 assert_kind_of Hash, field
268 assert_equal [], field['value']
268 assert_equal [], field['value']
269 end
269 end
270
270
271 test "GET /issues/:id.json with empty value for multi custom field" do
271 test "GET /issues/:id.json with empty value for multi custom field" do
272 field = CustomField.find(1)
272 field = CustomField.find(1)
273 field.update_attribute :multiple, true
273 field.update_attribute :multiple, true
274 issue = Issue.find(3)
274 issue = Issue.find(3)
275 issue.custom_field_values = {1 => ['']}
275 issue.custom_field_values = {1 => ['']}
276 issue.save!
276 issue.save!
277
277
278 get '/issues/3.json'
278 get '/issues/3.json'
279 assert_response :success
279 assert_response :success
280 json = ActiveSupport::JSON.decode(response.body)
280 json = ActiveSupport::JSON.decode(response.body)
281 custom_fields = json['issue']['custom_fields']
281 custom_fields = json['issue']['custom_fields']
282 assert_kind_of Array, custom_fields
282 assert_kind_of Array, custom_fields
283 field = custom_fields.detect {|f| f['id'] == 1}
283 field = custom_fields.detect {|f| f['id'] == 1}
284 assert_kind_of Hash, field
284 assert_kind_of Hash, field
285 assert_equal [], field['value'].sort
285 assert_equal [], field['value'].sort
286 end
286 end
287
287
288 test "GET /issues/:id.xml with attachments" do
288 test "GET /issues/:id.xml with attachments" do
289 get '/issues/3.xml?include=attachments'
289 get '/issues/3.xml?include=attachments'
290
290
291 assert_select 'issue attachments[type=array]' do
291 assert_select 'issue attachments[type=array]' do
292 assert_select 'attachment', 4
292 assert_select 'attachment', 4
293 assert_select 'attachment id', :text => '1' do
293 assert_select 'attachment id', :text => '1' do
294 assert_select '~ filename', :text => 'error281.txt'
294 assert_select '~ filename', :text => 'error281.txt'
295 assert_select '~ content_url', :text => 'http://www.example.com/attachments/download/1/error281.txt'
295 assert_select '~ content_url', :text => 'http://www.example.com/attachments/download/1/error281.txt'
296 end
296 end
297 end
297 end
298 end
298 end
299
299
300 test "GET /issues/:id.xml with subtasks" do
300 test "GET /issues/:id.xml with subtasks" do
301 issue = Issue.generate_with_descendants!(:project_id => 1)
301 issue = Issue.generate_with_descendants!(:project_id => 1)
302 get "/issues/#{issue.id}.xml?include=children"
302 get "/issues/#{issue.id}.xml?include=children"
303
303
304 assert_select 'issue id', :text => issue.id.to_s do
304 assert_select 'issue id', :text => issue.id.to_s do
305 assert_select '~ children[type=array] > issue', 2
305 assert_select '~ children[type=array] > issue', 2
306 assert_select '~ children[type=array] > issue > children', 1
306 assert_select '~ children[type=array] > issue > children', 1
307 end
307 end
308 end
308 end
309
309
310 test "GET /issues/:id.json with subtasks" do
310 test "GET /issues/:id.json with subtasks" do
311 issue = Issue.generate_with_descendants!(:project_id => 1)
311 issue = Issue.generate_with_descendants!(:project_id => 1)
312 get "/issues/#{issue.id}.json?include=children"
312 get "/issues/#{issue.id}.json?include=children"
313
313
314 json = ActiveSupport::JSON.decode(response.body)
314 json = ActiveSupport::JSON.decode(response.body)
315 assert_equal 2, json['issue']['children'].size
315 assert_equal 2, json['issue']['children'].size
316 assert_equal 1, json['issue']['children'].select {|child| child.key?('children')}.size
316 assert_equal 1, json['issue']['children'].select {|child| child.key?('children')}.size
317 end
317 end
318
318
319 def test_show_should_include_issue_attributes
319 def test_show_should_include_issue_attributes
320 get '/issues/1.xml'
320 get '/issues/1.xml'
321 assert_select 'issue>is_private', :text => 'false'
321 assert_select 'issue>is_private', :text => 'false'
322 end
322 end
323
323
324 test "GET /issues/:id.xml?include=watchers should include watchers" do
324 test "GET /issues/:id.xml?include=watchers should include watchers" do
325 Watcher.create!(:user_id => 3, :watchable => Issue.find(1))
325 Watcher.create!(:user_id => 3, :watchable => Issue.find(1))
326
326
327 get '/issues/1.xml?include=watchers', {}, credentials('jsmith')
327 get '/issues/1.xml?include=watchers', {}, credentials('jsmith')
328
328
329 assert_response :ok
329 assert_response :ok
330 assert_equal 'application/xml', response.content_type
330 assert_equal 'application/xml', response.content_type
331 assert_select 'issue' do
331 assert_select 'issue' do
332 assert_select 'watchers', Issue.find(1).watchers.count
332 assert_select 'watchers', Issue.find(1).watchers.count
333 assert_select 'watchers' do
333 assert_select 'watchers' do
334 assert_select 'user[id="3"]'
334 assert_select 'user[id="3"]'
335 end
335 end
336 end
336 end
337 end
337 end
338
338
339 test "POST /issues.xml should create an issue with the attributes" do
339 test "POST /issues.xml should create an issue with the attributes" do
340
340
341 payload = <<-XML
341 payload = <<-XML
342 <?xml version="1.0" encoding="UTF-8" ?>
342 <?xml version="1.0" encoding="UTF-8" ?>
343 <issue>
343 <issue>
344 <project_id>1</project_id>
344 <project_id>1</project_id>
345 <tracker_id>2</tracker_id>
345 <tracker_id>2</tracker_id>
346 <status_id>3</status_id>
346 <status_id>3</status_id>
347 <subject>API test</subject>
347 <subject>API test</subject>
348 </issue>
348 </issue>
349 XML
349 XML
350
350
351 assert_difference('Issue.count') do
351 assert_difference('Issue.count') do
352 post '/issues.xml', payload, {"CONTENT_TYPE" => 'application/xml'}.merge(credentials('jsmith'))
352 post '/issues.xml', payload, {"CONTENT_TYPE" => 'application/xml'}.merge(credentials('jsmith'))
353 end
353 end
354 issue = Issue.order('id DESC').first
354 issue = Issue.order('id DESC').first
355 assert_equal 1, issue.project_id
355 assert_equal 1, issue.project_id
356 assert_equal 2, issue.tracker_id
356 assert_equal 2, issue.tracker_id
357 assert_equal 3, issue.status_id
357 assert_equal 3, issue.status_id
358 assert_equal 'API test', issue.subject
358 assert_equal 'API test', issue.subject
359
359
360 assert_response :created
360 assert_response :created
361 assert_equal 'application/xml', @response.content_type
361 assert_equal 'application/xml', @response.content_type
362 assert_select 'issue > id', :text => issue.id.to_s
362 assert_select 'issue > id', :text => issue.id.to_s
363 end
363 end
364
364
365 test "POST /issues.xml with watcher_user_ids should create issue with watchers" do
365 test "POST /issues.xml with watcher_user_ids should create issue with watchers" do
366 assert_difference('Issue.count') do
366 assert_difference('Issue.count') do
367 post '/issues.xml',
367 post '/issues.xml',
368 {:issue => {:project_id => 1, :subject => 'Watchers',
368 {:issue => {:project_id => 1, :subject => 'Watchers',
369 :tracker_id => 2, :status_id => 3, :watcher_user_ids => [3, 1]}}, credentials('jsmith')
369 :tracker_id => 2, :status_id => 3, :watcher_user_ids => [3, 1]}}, credentials('jsmith')
370 assert_response :created
370 assert_response :created
371 end
371 end
372 issue = Issue.order('id desc').first
372 issue = Issue.order('id desc').first
373 assert_equal 2, issue.watchers.size
373 assert_equal 2, issue.watchers.size
374 assert_equal [1, 3], issue.watcher_user_ids.sort
374 assert_equal [1, 3], issue.watcher_user_ids.sort
375 end
375 end
376
376
377 test "POST /issues.xml with failure should return errors" do
377 test "POST /issues.xml with failure should return errors" do
378 assert_no_difference('Issue.count') do
378 assert_no_difference('Issue.count') do
379 post '/issues.xml', {:issue => {:project_id => 1}}, credentials('jsmith')
379 post '/issues.xml', {:issue => {:project_id => 1}}, credentials('jsmith')
380 end
380 end
381
381
382 assert_select 'errors error', :text => "Subject cannot be blank"
382 assert_select 'errors error', :text => "Subject cannot be blank"
383 end
383 end
384
384
385 test "POST /issues.json should create an issue with the attributes" do
385 test "POST /issues.json should create an issue with the attributes" do
386
386
387 payload = <<-JSON
387 payload = <<-JSON
388 {
388 {
389 "issue": {
389 "issue": {
390 "project_id": "1",
390 "project_id": "1",
391 "tracker_id": "2",
391 "tracker_id": "2",
392 "status_id": "3",
392 "status_id": "3",
393 "subject": "API test"
393 "subject": "API test"
394 }
394 }
395 }
395 }
396 JSON
396 JSON
397
397
398 assert_difference('Issue.count') do
398 assert_difference('Issue.count') do
399 post '/issues.json', payload, {"CONTENT_TYPE" => 'application/json'}.merge(credentials('jsmith'))
399 post '/issues.json', payload, {"CONTENT_TYPE" => 'application/json'}.merge(credentials('jsmith'))
400 end
400 end
401
401
402 issue = Issue.order('id DESC').first
402 issue = Issue.order('id DESC').first
403 assert_equal 1, issue.project_id
403 assert_equal 1, issue.project_id
404 assert_equal 2, issue.tracker_id
404 assert_equal 2, issue.tracker_id
405 assert_equal 3, issue.status_id
405 assert_equal 3, issue.status_id
406 assert_equal 'API test', issue.subject
406 assert_equal 'API test', issue.subject
407 end
407 end
408
408
409 test "POST /issues.json without tracker_id should accept custom fields" do
409 test "POST /issues.json without tracker_id should accept custom fields" do
410 field = IssueCustomField.generate!(
410 field = IssueCustomField.generate!(
411 :field_format => 'list',
411 :field_format => 'list',
412 :multiple => true,
412 :multiple => true,
413 :possible_values => ["V1", "V2", "V3"],
413 :possible_values => ["V1", "V2", "V3"],
414 :default_value => "V2",
414 :default_value => "V2",
415 :is_for_all => true,
415 :is_for_all => true,
416 :trackers => Tracker.all.to_a
416 :trackers => Tracker.all.to_a
417 )
417 )
418
418
419 payload = <<-JSON
419 payload = <<-JSON
420 {
420 {
421 "issue": {
421 "issue": {
422 "project_id": "1",
422 "project_id": "1",
423 "subject": "Multivalued custom field",
423 "subject": "Multivalued custom field",
424 "custom_field_values":{"#{field.id}":["V1","V3"]}
424 "custom_field_values":{"#{field.id}":["V1","V3"]}
425 }
425 }
426 }
426 }
427 JSON
427 JSON
428
428
429 assert_difference('Issue.count') do
429 assert_difference('Issue.count') do
430 post '/issues.json', payload, {"CONTENT_TYPE" => 'application/json'}.merge(credentials('jsmith'))
430 post '/issues.json', payload, {"CONTENT_TYPE" => 'application/json'}.merge(credentials('jsmith'))
431 end
431 end
432
432
433 assert_response :created
433 assert_response :created
434 issue = Issue.order('id DESC').first
434 issue = Issue.order('id DESC').first
435 assert_equal ["V1", "V3"], issue.custom_field_value(field).sort
435 assert_equal ["V1", "V3"], issue.custom_field_value(field).sort
436 end
436 end
437
437
438 test "POST /issues.json with omitted custom field should set default value" do
438 test "POST /issues.json with omitted custom field should set default value" do
439 field = IssueCustomField.generate!(:default_value => "Default")
439 field = IssueCustomField.generate!(:default_value => "Default")
440
440
441 issue = new_record(Issue) do
441 issue = new_record(Issue) do
442 post '/issues.json',
442 post '/issues.json',
443 {:issue => {:project_id => 1, :subject => 'API', :custom_field_values => {}}},
443 {:issue => {:project_id => 1, :subject => 'API', :custom_field_values => {}}},
444 credentials('jsmith')
444 credentials('jsmith')
445 end
445 end
446 assert_equal "Default", issue.custom_field_value(field)
446 assert_equal "Default", issue.custom_field_value(field)
447 end
447 end
448
448
449 test "POST /issues.json with custom field set to blank should not set default value" do
449 test "POST /issues.json with custom field set to blank should not set default value" do
450 field = IssueCustomField.generate!(:default_value => "Default")
450 field = IssueCustomField.generate!(:default_value => "Default")
451
451
452 issue = new_record(Issue) do
452 issue = new_record(Issue) do
453 post '/issues.json',
453 post '/issues.json',
454 {:issue => {:project_id => 1, :subject => 'API', :custom_field_values => {field.id.to_s => ""}}},
454 {:issue => {:project_id => 1, :subject => 'API', :custom_field_values => {field.id.to_s => ""}}},
455 credentials('jsmith')
455 credentials('jsmith')
456 end
456 end
457 assert_equal "", issue.custom_field_value(field)
457 assert_equal "", issue.custom_field_value(field)
458 end
458 end
459
459
460 test "POST /issues.json with failure should return errors" do
460 test "POST /issues.json with failure should return errors" do
461 assert_no_difference('Issue.count') do
461 assert_no_difference('Issue.count') do
462 post '/issues.json', {:issue => {:project_id => 1}}, credentials('jsmith')
462 post '/issues.json', {:issue => {:project_id => 1}}, credentials('jsmith')
463 end
463 end
464
464
465 json = ActiveSupport::JSON.decode(response.body)
465 json = ActiveSupport::JSON.decode(response.body)
466 assert json['errors'].include?("Subject cannot be blank")
466 assert json['errors'].include?("Subject cannot be blank")
467 end
467 end
468
468
469 test "POST /issues.json with invalid project_id should respond with 422" do
469 test "POST /issues.json with invalid project_id should respond with 422" do
470 post '/issues.json', {:issue => {:project_id => 999, :subject => "API"}}, credentials('jsmith')
470 post '/issues.json', {:issue => {:project_id => 999, :subject => "API"}}, credentials('jsmith')
471 assert_response 422
471 assert_response 422
472 end
472 end
473
473
474 test "PUT /issues/:id.xml" do
474 test "PUT /issues/:id.xml" do
475 assert_difference('Journal.count') do
475 assert_difference('Journal.count') do
476 put '/issues/6.xml',
476 put '/issues/6.xml',
477 {:issue => {:subject => 'API update', :notes => 'A new note'}},
477 {:issue => {:subject => 'API update', :notes => 'A new note'}},
478 credentials('jsmith')
478 credentials('jsmith')
479 end
479 end
480
480
481 issue = Issue.find(6)
481 issue = Issue.find(6)
482 assert_equal "API update", issue.subject
482 assert_equal "API update", issue.subject
483 journal = Journal.last
483 journal = Journal.last
484 assert_equal "A new note", journal.notes
484 assert_equal "A new note", journal.notes
485 end
485 end
486
486
487 test "PUT /issues/:id.xml with custom fields" do
487 test "PUT /issues/:id.xml with custom fields" do
488 put '/issues/3.xml',
488 put '/issues/3.xml',
489 {:issue => {:custom_fields => [
489 {:issue => {:custom_fields => [
490 {'id' => '1', 'value' => 'PostgreSQL' },
490 {'id' => '1', 'value' => 'PostgreSQL' },
491 {'id' => '2', 'value' => '150'}
491 {'id' => '2', 'value' => '150'}
492 ]}},
492 ]}},
493 credentials('jsmith')
493 credentials('jsmith')
494
494
495 issue = Issue.find(3)
495 issue = Issue.find(3)
496 assert_equal '150', issue.custom_value_for(2).value
496 assert_equal '150', issue.custom_value_for(2).value
497 assert_equal 'PostgreSQL', issue.custom_value_for(1).value
497 assert_equal 'PostgreSQL', issue.custom_value_for(1).value
498 end
498 end
499
499
500 test "PUT /issues/:id.xml with multi custom fields" do
500 test "PUT /issues/:id.xml with multi custom fields" do
501 field = CustomField.find(1)
501 field = CustomField.find(1)
502 field.update_attribute :multiple, true
502 field.update_attribute :multiple, true
503
503
504 put '/issues/3.xml',
504 put '/issues/3.xml',
505 {:issue => {:custom_fields => [
505 {:issue => {:custom_fields => [
506 {'id' => '1', 'value' => ['MySQL', 'PostgreSQL'] },
506 {'id' => '1', 'value' => ['MySQL', 'PostgreSQL'] },
507 {'id' => '2', 'value' => '150'}
507 {'id' => '2', 'value' => '150'}
508 ]}},
508 ]}},
509 credentials('jsmith')
509 credentials('jsmith')
510
510
511 issue = Issue.find(3)
511 issue = Issue.find(3)
512 assert_equal '150', issue.custom_value_for(2).value
512 assert_equal '150', issue.custom_value_for(2).value
513 assert_equal ['MySQL', 'PostgreSQL'], issue.custom_field_value(1).sort
513 assert_equal ['MySQL', 'PostgreSQL'], issue.custom_field_value(1).sort
514 end
514 end
515
515
516 test "PUT /issues/:id.xml with project change" do
516 test "PUT /issues/:id.xml with project change" do
517 put '/issues/3.xml',
517 put '/issues/3.xml',
518 {:issue => {:project_id => 2, :subject => 'Project changed'}},
518 {:issue => {:project_id => 2, :subject => 'Project changed'}},
519 credentials('jsmith')
519 credentials('jsmith')
520
520
521 issue = Issue.find(3)
521 issue = Issue.find(3)
522 assert_equal 2, issue.project_id
522 assert_equal 2, issue.project_id
523 assert_equal 'Project changed', issue.subject
523 assert_equal 'Project changed', issue.subject
524 end
524 end
525
525
526 test "PUT /issues/:id.xml with notes only" do
526 test "PUT /issues/:id.xml with notes only" do
527 assert_difference('Journal.count') do
527 assert_difference('Journal.count') do
528 put '/issues/6.xml',
528 put '/issues/6.xml',
529 {:issue => {:notes => 'Notes only'}},
529 {:issue => {:notes => 'Notes only'}},
530 credentials('jsmith')
530 credentials('jsmith')
531 end
531 end
532
532
533 journal = Journal.last
533 journal = Journal.last
534 assert_equal "Notes only", journal.notes
534 assert_equal "Notes only", journal.notes
535 end
535 end
536
536
537 test "PUT /issues/:id.json with omitted custom field should not change blank value to default value" do
537 test "PUT /issues/:id.json with omitted custom field should not change blank value to default value" do
538 field = IssueCustomField.generate!(:default_value => "Default")
538 field = IssueCustomField.generate!(:default_value => "Default")
539 issue = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {field.id.to_s => ""})
539 issue = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {field.id.to_s => ""})
540 assert_equal "", issue.reload.custom_field_value(field)
540 assert_equal "", issue.reload.custom_field_value(field)
541
541
542 assert_difference('Journal.count') do
542 assert_difference('Journal.count') do
543 put "/issues/#{issue.id}.json",
543 put "/issues/#{issue.id}.json",
544 {:issue => {:custom_field_values => {}, :notes => 'API'}},
544 {:issue => {:custom_field_values => {}, :notes => 'API'}},
545 credentials('jsmith')
545 credentials('jsmith')
546 end
546 end
547
547
548 assert_equal "", issue.reload.custom_field_value(field)
548 assert_equal "", issue.reload.custom_field_value(field)
549 end
549 end
550
550
551 test "PUT /issues/:id.json with custom field set to blank should not change blank value to default value" do
551 test "PUT /issues/:id.json with custom field set to blank should not change blank value to default value" do
552 field = IssueCustomField.generate!(:default_value => "Default")
552 field = IssueCustomField.generate!(:default_value => "Default")
553 issue = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {field.id.to_s => ""})
553 issue = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {field.id.to_s => ""})
554 assert_equal "", issue.reload.custom_field_value(field)
554 assert_equal "", issue.reload.custom_field_value(field)
555
555
556 assert_difference('Journal.count') do
556 assert_difference('Journal.count') do
557 put "/issues/#{issue.id}.json",
557 put "/issues/#{issue.id}.json",
558 {:issue => {:custom_field_values => {field.id.to_s => ""}, :notes => 'API'}},
558 {:issue => {:custom_field_values => {field.id.to_s => ""}, :notes => 'API'}},
559 credentials('jsmith')
559 credentials('jsmith')
560 end
560 end
561
561
562 assert_equal "", issue.reload.custom_field_value(field)
562 assert_equal "", issue.reload.custom_field_value(field)
563 end
563 end
564
564
565 test "PUT /issues/:id.json with tracker change and omitted custom field specific to that tracker does not set default value" do
565 test "PUT /issues/:id.json with tracker change and omitted custom field specific to that tracker should set default value" do
566 field = IssueCustomField.generate!(:default_value => "Default", :tracker_ids => [2])
566 field = IssueCustomField.generate!(:default_value => "Default", :tracker_ids => [2])
567 issue = Issue.generate!(:project_id => 1, :tracker_id => 1)
567 issue = Issue.generate!(:project_id => 1, :tracker_id => 1)
568
568
569 assert_difference('Journal.count') do
569 assert_difference('Journal.count') do
570 put "/issues/#{issue.id}.json",
570 put "/issues/#{issue.id}.json",
571 {:issue => {:tracker_id => 2, :custom_field_values => {}, :notes => 'API'}},
571 {:issue => {:tracker_id => 2, :custom_field_values => {}, :notes => 'API'}},
572 credentials('jsmith')
572 credentials('jsmith')
573 end
573 end
574
574
575 assert_equal 2, issue.reload.tracker_id
575 assert_equal 2, issue.reload.tracker_id
576 assert_nil issue.reload.custom_field_value(field)
576 assert_equal "Default", issue.reload.custom_field_value(field)
577 end
577 end
578
578
579 test "PUT /issues/:id.json with tracker change and custom field specific to that tracker set to blank should not set default value" do
579 test "PUT /issues/:id.json with tracker change and custom field specific to that tracker set to blank should not set default value" do
580 field = IssueCustomField.generate!(:default_value => "Default", :tracker_ids => [2])
580 field = IssueCustomField.generate!(:default_value => "Default", :tracker_ids => [2])
581 issue = Issue.generate!(:project_id => 1, :tracker_id => 1)
581 issue = Issue.generate!(:project_id => 1, :tracker_id => 1)
582
582
583 assert_difference('Journal.count') do
583 assert_difference('Journal.count') do
584 put "/issues/#{issue.id}.json",
584 put "/issues/#{issue.id}.json",
585 {:issue => {:tracker_id => 2, :custom_field_values => {field.id.to_s => ""}, :notes => 'API'}},
585 {:issue => {:tracker_id => 2, :custom_field_values => {field.id.to_s => ""}, :notes => 'API'}},
586 credentials('jsmith')
586 credentials('jsmith')
587 end
587 end
588
588
589 assert_equal 2, issue.reload.tracker_id
589 assert_equal 2, issue.reload.tracker_id
590 assert_equal "", issue.reload.custom_field_value(field)
590 assert_equal "", issue.reload.custom_field_value(field)
591 end
591 end
592
592
593 test "PUT /issues/:id.xml with failed update" do
593 test "PUT /issues/:id.xml with failed update" do
594 put '/issues/6.xml', {:issue => {:subject => ''}}, credentials('jsmith')
594 put '/issues/6.xml', {:issue => {:subject => ''}}, credentials('jsmith')
595
595
596 assert_response :unprocessable_entity
596 assert_response :unprocessable_entity
597 assert_select 'errors error', :text => "Subject cannot be blank"
597 assert_select 'errors error', :text => "Subject cannot be blank"
598 end
598 end
599
599
600 test "PUT /issues/:id.json" do
600 test "PUT /issues/:id.json" do
601 assert_difference('Journal.count') do
601 assert_difference('Journal.count') do
602 put '/issues/6.json',
602 put '/issues/6.json',
603 {:issue => {:subject => 'API update', :notes => 'A new note'}},
603 {:issue => {:subject => 'API update', :notes => 'A new note'}},
604 credentials('jsmith')
604 credentials('jsmith')
605
605
606 assert_response :ok
606 assert_response :ok
607 assert_equal '', response.body
607 assert_equal '', response.body
608 end
608 end
609
609
610 issue = Issue.find(6)
610 issue = Issue.find(6)
611 assert_equal "API update", issue.subject
611 assert_equal "API update", issue.subject
612 journal = Journal.last
612 journal = Journal.last
613 assert_equal "A new note", journal.notes
613 assert_equal "A new note", journal.notes
614 end
614 end
615
615
616 test "PUT /issues/:id.json with failed update" do
616 test "PUT /issues/:id.json with failed update" do
617 put '/issues/6.json', {:issue => {:subject => ''}}, credentials('jsmith')
617 put '/issues/6.json', {:issue => {:subject => ''}}, credentials('jsmith')
618
618
619 assert_response :unprocessable_entity
619 assert_response :unprocessable_entity
620 json = ActiveSupport::JSON.decode(response.body)
620 json = ActiveSupport::JSON.decode(response.body)
621 assert json['errors'].include?("Subject cannot be blank")
621 assert json['errors'].include?("Subject cannot be blank")
622 end
622 end
623
623
624 test "DELETE /issues/:id.xml" do
624 test "DELETE /issues/:id.xml" do
625 assert_difference('Issue.count', -1) do
625 assert_difference('Issue.count', -1) do
626 delete '/issues/6.xml', {}, credentials('jsmith')
626 delete '/issues/6.xml', {}, credentials('jsmith')
627
627
628 assert_response :ok
628 assert_response :ok
629 assert_equal '', response.body
629 assert_equal '', response.body
630 end
630 end
631 assert_nil Issue.find_by_id(6)
631 assert_nil Issue.find_by_id(6)
632 end
632 end
633
633
634 test "DELETE /issues/:id.json" do
634 test "DELETE /issues/:id.json" do
635 assert_difference('Issue.count', -1) do
635 assert_difference('Issue.count', -1) do
636 delete '/issues/6.json', {}, credentials('jsmith')
636 delete '/issues/6.json', {}, credentials('jsmith')
637
637
638 assert_response :ok
638 assert_response :ok
639 assert_equal '', response.body
639 assert_equal '', response.body
640 end
640 end
641 assert_nil Issue.find_by_id(6)
641 assert_nil Issue.find_by_id(6)
642 end
642 end
643
643
644 test "POST /issues/:id/watchers.xml should add watcher" do
644 test "POST /issues/:id/watchers.xml should add watcher" do
645 assert_difference 'Watcher.count' do
645 assert_difference 'Watcher.count' do
646 post '/issues/1/watchers.xml', {:user_id => 3}, credentials('jsmith')
646 post '/issues/1/watchers.xml', {:user_id => 3}, credentials('jsmith')
647
647
648 assert_response :ok
648 assert_response :ok
649 assert_equal '', response.body
649 assert_equal '', response.body
650 end
650 end
651 watcher = Watcher.order('id desc').first
651 watcher = Watcher.order('id desc').first
652 assert_equal Issue.find(1), watcher.watchable
652 assert_equal Issue.find(1), watcher.watchable
653 assert_equal User.find(3), watcher.user
653 assert_equal User.find(3), watcher.user
654 end
654 end
655
655
656 test "DELETE /issues/:id/watchers/:user_id.xml should remove watcher" do
656 test "DELETE /issues/:id/watchers/:user_id.xml should remove watcher" do
657 Watcher.create!(:user_id => 3, :watchable => Issue.find(1))
657 Watcher.create!(:user_id => 3, :watchable => Issue.find(1))
658
658
659 assert_difference 'Watcher.count', -1 do
659 assert_difference 'Watcher.count', -1 do
660 delete '/issues/1/watchers/3.xml', {}, credentials('jsmith')
660 delete '/issues/1/watchers/3.xml', {}, credentials('jsmith')
661
661
662 assert_response :ok
662 assert_response :ok
663 assert_equal '', response.body
663 assert_equal '', response.body
664 end
664 end
665 assert_equal false, Issue.find(1).watched_by?(User.find(3))
665 assert_equal false, Issue.find(1).watched_by?(User.find(3))
666 end
666 end
667
667
668 def test_create_issue_with_uploaded_file
668 def test_create_issue_with_uploaded_file
669 token = xml_upload('test_create_with_upload', credentials('jsmith'))
669 token = xml_upload('test_create_with_upload', credentials('jsmith'))
670 attachment = Attachment.find_by_token(token)
670 attachment = Attachment.find_by_token(token)
671
671
672 # create the issue with the upload's token
672 # create the issue with the upload's token
673 assert_difference 'Issue.count' do
673 assert_difference 'Issue.count' do
674 post '/issues.xml',
674 post '/issues.xml',
675 {:issue => {:project_id => 1, :subject => 'Uploaded file',
675 {:issue => {:project_id => 1, :subject => 'Uploaded file',
676 :uploads => [{:token => token, :filename => 'test.txt',
676 :uploads => [{:token => token, :filename => 'test.txt',
677 :content_type => 'text/plain'}]}},
677 :content_type => 'text/plain'}]}},
678 credentials('jsmith')
678 credentials('jsmith')
679 assert_response :created
679 assert_response :created
680 end
680 end
681 issue = Issue.order('id DESC').first
681 issue = Issue.order('id DESC').first
682 assert_equal 1, issue.attachments.count
682 assert_equal 1, issue.attachments.count
683 assert_equal attachment, issue.attachments.first
683 assert_equal attachment, issue.attachments.first
684
684
685 attachment.reload
685 attachment.reload
686 assert_equal 'test.txt', attachment.filename
686 assert_equal 'test.txt', attachment.filename
687 assert_equal 'text/plain', attachment.content_type
687 assert_equal 'text/plain', attachment.content_type
688 assert_equal 'test_create_with_upload'.size, attachment.filesize
688 assert_equal 'test_create_with_upload'.size, attachment.filesize
689 assert_equal 2, attachment.author_id
689 assert_equal 2, attachment.author_id
690
690
691 # get the issue with its attachments
691 # get the issue with its attachments
692 get "/issues/#{issue.id}.xml", :include => 'attachments'
692 get "/issues/#{issue.id}.xml", :include => 'attachments'
693 assert_response :success
693 assert_response :success
694 xml = Hash.from_xml(response.body)
694 xml = Hash.from_xml(response.body)
695 attachments = xml['issue']['attachments']
695 attachments = xml['issue']['attachments']
696 assert_kind_of Array, attachments
696 assert_kind_of Array, attachments
697 assert_equal 1, attachments.size
697 assert_equal 1, attachments.size
698 url = attachments.first['content_url']
698 url = attachments.first['content_url']
699 assert_not_nil url
699 assert_not_nil url
700
700
701 # download the attachment
701 # download the attachment
702 get url
702 get url
703 assert_response :success
703 assert_response :success
704 assert_equal 'test_create_with_upload', response.body
704 assert_equal 'test_create_with_upload', response.body
705 end
705 end
706
706
707 def test_create_issue_with_multiple_uploaded_files_as_xml
707 def test_create_issue_with_multiple_uploaded_files_as_xml
708 token1 = xml_upload('File content 1', credentials('jsmith'))
708 token1 = xml_upload('File content 1', credentials('jsmith'))
709 token2 = xml_upload('File content 2', credentials('jsmith'))
709 token2 = xml_upload('File content 2', credentials('jsmith'))
710
710
711 payload = <<-XML
711 payload = <<-XML
712 <?xml version="1.0" encoding="UTF-8" ?>
712 <?xml version="1.0" encoding="UTF-8" ?>
713 <issue>
713 <issue>
714 <project_id>1</project_id>
714 <project_id>1</project_id>
715 <tracker_id>1</tracker_id>
715 <tracker_id>1</tracker_id>
716 <subject>Issue with multiple attachments</subject>
716 <subject>Issue with multiple attachments</subject>
717 <uploads type="array">
717 <uploads type="array">
718 <upload>
718 <upload>
719 <token>#{token1}</token>
719 <token>#{token1}</token>
720 <filename>test1.txt</filename>
720 <filename>test1.txt</filename>
721 </upload>
721 </upload>
722 <upload>
722 <upload>
723 <token>#{token2}</token>
723 <token>#{token2}</token>
724 <filename>test1.txt</filename>
724 <filename>test1.txt</filename>
725 </upload>
725 </upload>
726 </uploads>
726 </uploads>
727 </issue>
727 </issue>
728 XML
728 XML
729
729
730 assert_difference 'Issue.count' do
730 assert_difference 'Issue.count' do
731 post '/issues.xml', payload, {"CONTENT_TYPE" => 'application/xml'}.merge(credentials('jsmith'))
731 post '/issues.xml', payload, {"CONTENT_TYPE" => 'application/xml'}.merge(credentials('jsmith'))
732 assert_response :created
732 assert_response :created
733 end
733 end
734 issue = Issue.order('id DESC').first
734 issue = Issue.order('id DESC').first
735 assert_equal 2, issue.attachments.count
735 assert_equal 2, issue.attachments.count
736 end
736 end
737
737
738 def test_create_issue_with_multiple_uploaded_files_as_json
738 def test_create_issue_with_multiple_uploaded_files_as_json
739 token1 = json_upload('File content 1', credentials('jsmith'))
739 token1 = json_upload('File content 1', credentials('jsmith'))
740 token2 = json_upload('File content 2', credentials('jsmith'))
740 token2 = json_upload('File content 2', credentials('jsmith'))
741
741
742 payload = <<-JSON
742 payload = <<-JSON
743 {
743 {
744 "issue": {
744 "issue": {
745 "project_id": "1",
745 "project_id": "1",
746 "tracker_id": "1",
746 "tracker_id": "1",
747 "subject": "Issue with multiple attachments",
747 "subject": "Issue with multiple attachments",
748 "uploads": [
748 "uploads": [
749 {"token": "#{token1}", "filename": "test1.txt"},
749 {"token": "#{token1}", "filename": "test1.txt"},
750 {"token": "#{token2}", "filename": "test2.txt"}
750 {"token": "#{token2}", "filename": "test2.txt"}
751 ]
751 ]
752 }
752 }
753 }
753 }
754 JSON
754 JSON
755
755
756 assert_difference 'Issue.count' do
756 assert_difference 'Issue.count' do
757 post '/issues.json', payload, {"CONTENT_TYPE" => 'application/json'}.merge(credentials('jsmith'))
757 post '/issues.json', payload, {"CONTENT_TYPE" => 'application/json'}.merge(credentials('jsmith'))
758 assert_response :created
758 assert_response :created
759 end
759 end
760 issue = Issue.order('id DESC').first
760 issue = Issue.order('id DESC').first
761 assert_equal 2, issue.attachments.count
761 assert_equal 2, issue.attachments.count
762 end
762 end
763
763
764 def test_update_issue_with_uploaded_file
764 def test_update_issue_with_uploaded_file
765 token = xml_upload('test_upload_with_upload', credentials('jsmith'))
765 token = xml_upload('test_upload_with_upload', credentials('jsmith'))
766 attachment = Attachment.find_by_token(token)
766 attachment = Attachment.find_by_token(token)
767
767
768 # update the issue with the upload's token
768 # update the issue with the upload's token
769 assert_difference 'Journal.count' do
769 assert_difference 'Journal.count' do
770 put '/issues/1.xml',
770 put '/issues/1.xml',
771 {:issue => {:notes => 'Attachment added',
771 {:issue => {:notes => 'Attachment added',
772 :uploads => [{:token => token, :filename => 'test.txt',
772 :uploads => [{:token => token, :filename => 'test.txt',
773 :content_type => 'text/plain'}]}},
773 :content_type => 'text/plain'}]}},
774 credentials('jsmith')
774 credentials('jsmith')
775 assert_response :ok
775 assert_response :ok
776 assert_equal '', @response.body
776 assert_equal '', @response.body
777 end
777 end
778
778
779 issue = Issue.find(1)
779 issue = Issue.find(1)
780 assert_include attachment, issue.attachments
780 assert_include attachment, issue.attachments
781 end
781 end
782 end
782 end
@@ -1,39 +1,49
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
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
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.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19
19
20 class CustomValueTest < ActiveSupport::TestCase
20 class CustomValueTest < ActiveSupport::TestCase
21 fixtures :custom_fields, :custom_values, :users
21 fixtures :custom_fields, :custom_values, :users
22
22
23 def test_default_value
23 def test_new_without_value_should_set_default_value
24 field = CustomField.find_by_default_value('Default string')
24 field = CustomField.generate!(:default_value => 'Default string')
25 assert_not_nil field
26
25
27 v = CustomValue.new(:custom_field => field)
26 v = CustomValue.new(:custom_field => field)
28 assert_equal 'Default string', v.value
27 assert_equal 'Default string', v.value
28 end
29
30 def test_new_with_value_should_not_set_default_value
31 field = CustomField.generate!(:default_value => 'Default string')
32
33 v = CustomValue.new(:custom_field => field, :value => 'String')
34 assert_equal 'String', v.value
35 end
36
37 def test_new_with_nil_value_should_not_set_default_value
38 field = CustomField.generate!(:default_value => 'Default string')
29
39
30 v = CustomValue.new(:custom_field => field, :value => 'Not empty')
40 v = CustomValue.new(:custom_field => field, :value => nil)
31 assert_equal 'Not empty', v.value
41 assert_nil v.value
32 end
42 end
33
43
34 def test_sti_polymorphic_association
44 def test_sti_polymorphic_association
35 # Rails uses top level sti class for polymorphic association. See #3978.
45 # Rails uses top level sti class for polymorphic association. See #3978.
36 assert !User.find(4).custom_values.empty?
46 assert !User.find(4).custom_values.empty?
37 assert !CustomValue.find(2).customized.nil?
47 assert !CustomValue.find(2).customized.nil?
38 end
48 end
39 end
49 end
General Comments 0
You need to be logged in to leave comments. Login now