##// END OF EJS Templates
Adds User and Version custom field format that can be used to reference a project member or version in custom fields (#2096)....
Jean-Philippe Lang -
r5152:1cd6a2aa84db
parent child
Show More
@@ -0,0 +1,65
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 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 File.expand_path('../../test_helper', __FILE__)
19
20 class CustomFieldUserFormatTest < ActiveSupport::TestCase
21 fixtures :custom_fields, :projects, :members, :users, :member_roles, :trackers, :issues
22
23 def setup
24 @field = IssueCustomField.create!(:name => 'Tester', :field_format => 'user')
25 end
26
27 def test_possible_values_with_no_arguments
28 assert_equal [], @field.possible_values
29 assert_equal [], @field.possible_values(nil)
30 end
31
32 def test_possible_values_with_project_resource
33 project = Project.find(1)
34 possible_values = @field.possible_values(project.issues.first)
35 assert possible_values.any?
36 assert_equal project.users.sort.collect(&:id).map(&:to_s), possible_values
37 end
38
39 def test_possible_values_options_with_no_arguments
40 assert_equal [], @field.possible_values_options
41 assert_equal [], @field.possible_values_options(nil)
42 end
43
44 def test_possible_values_options_with_project_resource
45 project = Project.find(1)
46 possible_values_options = @field.possible_values_options(project.issues.first)
47 assert possible_values_options.any?
48 assert_equal project.users.sort.map {|u| [u.name, u.id.to_s]}, possible_values_options
49 end
50
51 def test_cast_blank_value
52 assert_equal nil, @field.cast_value(nil)
53 assert_equal nil, @field.cast_value("")
54 end
55
56 def test_cast_valid_value
57 user = @field.cast_value("2")
58 assert_kind_of User, user
59 assert_equal User.find(2), user
60 end
61
62 def test_cast_invalid_value
63 assert_equal nil, @field.cast_value("187")
64 end
65 end
@@ -1,118 +1,118
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 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 CustomFieldsHelper
18 module CustomFieldsHelper
19
19
20 def custom_fields_tabs
20 def custom_fields_tabs
21 tabs = [{:name => 'IssueCustomField', :partial => 'custom_fields/index', :label => :label_issue_plural},
21 tabs = [{:name => 'IssueCustomField', :partial => 'custom_fields/index', :label => :label_issue_plural},
22 {:name => 'TimeEntryCustomField', :partial => 'custom_fields/index', :label => :label_spent_time},
22 {:name => 'TimeEntryCustomField', :partial => 'custom_fields/index', :label => :label_spent_time},
23 {:name => 'ProjectCustomField', :partial => 'custom_fields/index', :label => :label_project_plural},
23 {:name => 'ProjectCustomField', :partial => 'custom_fields/index', :label => :label_project_plural},
24 {:name => 'VersionCustomField', :partial => 'custom_fields/index', :label => :label_version_plural},
24 {:name => 'VersionCustomField', :partial => 'custom_fields/index', :label => :label_version_plural},
25 {:name => 'UserCustomField', :partial => 'custom_fields/index', :label => :label_user_plural},
25 {:name => 'UserCustomField', :partial => 'custom_fields/index', :label => :label_user_plural},
26 {:name => 'GroupCustomField', :partial => 'custom_fields/index', :label => :label_group_plural},
26 {:name => 'GroupCustomField', :partial => 'custom_fields/index', :label => :label_group_plural},
27 {:name => 'TimeEntryActivityCustomField', :partial => 'custom_fields/index', :label => TimeEntryActivity::OptionName},
27 {:name => 'TimeEntryActivityCustomField', :partial => 'custom_fields/index', :label => TimeEntryActivity::OptionName},
28 {:name => 'IssuePriorityCustomField', :partial => 'custom_fields/index', :label => IssuePriority::OptionName},
28 {:name => 'IssuePriorityCustomField', :partial => 'custom_fields/index', :label => IssuePriority::OptionName},
29 {:name => 'DocumentCategoryCustomField', :partial => 'custom_fields/index', :label => DocumentCategory::OptionName}
29 {:name => 'DocumentCategoryCustomField', :partial => 'custom_fields/index', :label => DocumentCategory::OptionName}
30 ]
30 ]
31 end
31 end
32
32
33 # Return custom field html tag corresponding to its format
33 # Return custom field html tag corresponding to its format
34 def custom_field_tag(name, custom_value)
34 def custom_field_tag(name, custom_value)
35 custom_field = custom_value.custom_field
35 custom_field = custom_value.custom_field
36 field_name = "#{name}[custom_field_values][#{custom_field.id}]"
36 field_name = "#{name}[custom_field_values][#{custom_field.id}]"
37 field_id = "#{name}_custom_field_values_#{custom_field.id}"
37 field_id = "#{name}_custom_field_values_#{custom_field.id}"
38
38
39 field_format = Redmine::CustomFieldFormat.find_by_name(custom_field.field_format)
39 field_format = Redmine::CustomFieldFormat.find_by_name(custom_field.field_format)
40 case field_format.try(:edit_as)
40 case field_format.try(:edit_as)
41 when "date"
41 when "date"
42 text_field_tag(field_name, custom_value.value, :id => field_id, :size => 10) +
42 text_field_tag(field_name, custom_value.value, :id => field_id, :size => 10) +
43 calendar_for(field_id)
43 calendar_for(field_id)
44 when "text"
44 when "text"
45 text_area_tag(field_name, custom_value.value, :id => field_id, :rows => 3, :style => 'width:90%')
45 text_area_tag(field_name, custom_value.value, :id => field_id, :rows => 3, :style => 'width:90%')
46 when "bool"
46 when "bool"
47 hidden_field_tag(field_name, '0') + check_box_tag(field_name, '1', custom_value.true?, :id => field_id)
47 hidden_field_tag(field_name, '0') + check_box_tag(field_name, '1', custom_value.true?, :id => field_id)
48 when "list"
48 when "list"
49 blank_option = custom_field.is_required? ?
49 blank_option = custom_field.is_required? ?
50 (custom_field.default_value.blank? ? "<option value=\"\">--- #{l(:actionview_instancetag_blank_option)} ---</option>" : '') :
50 (custom_field.default_value.blank? ? "<option value=\"\">--- #{l(:actionview_instancetag_blank_option)} ---</option>" : '') :
51 '<option></option>'
51 '<option></option>'
52 select_tag(field_name, blank_option + options_for_select(custom_field.possible_values, custom_value.value), :id => field_id)
52 select_tag(field_name, blank_option + options_for_select(custom_field.possible_values_options(custom_value.customized), custom_value.value), :id => field_id)
53 else
53 else
54 text_field_tag(field_name, custom_value.value, :id => field_id)
54 text_field_tag(field_name, custom_value.value, :id => field_id)
55 end
55 end
56 end
56 end
57
57
58 # Return custom field label tag
58 # Return custom field label tag
59 def custom_field_label_tag(name, custom_value)
59 def custom_field_label_tag(name, custom_value)
60 content_tag "label", custom_value.custom_field.name +
60 content_tag "label", custom_value.custom_field.name +
61 (custom_value.custom_field.is_required? ? " <span class=\"required\">*</span>" : ""),
61 (custom_value.custom_field.is_required? ? " <span class=\"required\">*</span>" : ""),
62 :for => "#{name}_custom_field_values_#{custom_value.custom_field.id}",
62 :for => "#{name}_custom_field_values_#{custom_value.custom_field.id}",
63 :class => (custom_value.errors.empty? ? nil : "error" )
63 :class => (custom_value.errors.empty? ? nil : "error" )
64 end
64 end
65
65
66 # Return custom field tag with its label tag
66 # Return custom field tag with its label tag
67 def custom_field_tag_with_label(name, custom_value)
67 def custom_field_tag_with_label(name, custom_value)
68 custom_field_label_tag(name, custom_value) + custom_field_tag(name, custom_value)
68 custom_field_label_tag(name, custom_value) + custom_field_tag(name, custom_value)
69 end
69 end
70
70
71 def custom_field_tag_for_bulk_edit(name, custom_field)
71 def custom_field_tag_for_bulk_edit(name, custom_field)
72 field_name = "#{name}[custom_field_values][#{custom_field.id}]"
72 field_name = "#{name}[custom_field_values][#{custom_field.id}]"
73 field_id = "#{name}_custom_field_values_#{custom_field.id}"
73 field_id = "#{name}_custom_field_values_#{custom_field.id}"
74 field_format = Redmine::CustomFieldFormat.find_by_name(custom_field.field_format)
74 field_format = Redmine::CustomFieldFormat.find_by_name(custom_field.field_format)
75 case field_format.try(:edit_as)
75 case field_format.try(:edit_as)
76 when "date"
76 when "date"
77 text_field_tag(field_name, '', :id => field_id, :size => 10) +
77 text_field_tag(field_name, '', :id => field_id, :size => 10) +
78 calendar_for(field_id)
78 calendar_for(field_id)
79 when "text"
79 when "text"
80 text_area_tag(field_name, '', :id => field_id, :rows => 3, :style => 'width:90%')
80 text_area_tag(field_name, '', :id => field_id, :rows => 3, :style => 'width:90%')
81 when "bool"
81 when "bool"
82 select_tag(field_name, options_for_select([[l(:label_no_change_option), ''],
82 select_tag(field_name, options_for_select([[l(:label_no_change_option), ''],
83 [l(:general_text_yes), '1'],
83 [l(:general_text_yes), '1'],
84 [l(:general_text_no), '0']]), :id => field_id)
84 [l(:general_text_no), '0']]), :id => field_id)
85 when "list"
85 when "list"
86 select_tag(field_name, options_for_select([[l(:label_no_change_option), '']] + custom_field.possible_values), :id => field_id)
86 select_tag(field_name, options_for_select([[l(:label_no_change_option), '']] + custom_field.possible_values_options), :id => field_id)
87 else
87 else
88 text_field_tag(field_name, '', :id => field_id)
88 text_field_tag(field_name, '', :id => field_id)
89 end
89 end
90 end
90 end
91
91
92 # Return a string used to display a custom value
92 # Return a string used to display a custom value
93 def show_value(custom_value)
93 def show_value(custom_value)
94 return "" unless custom_value
94 return "" unless custom_value
95 format_value(custom_value.value, custom_value.custom_field.field_format)
95 format_value(custom_value.value, custom_value.custom_field.field_format)
96 end
96 end
97
97
98 # Return a string used to display a custom value
98 # Return a string used to display a custom value
99 def format_value(value, field_format)
99 def format_value(value, field_format)
100 Redmine::CustomFieldFormat.format_value(value, field_format) # Proxy
100 Redmine::CustomFieldFormat.format_value(value, field_format) # Proxy
101 end
101 end
102
102
103 # Return an array of custom field formats which can be used in select_tag
103 # Return an array of custom field formats which can be used in select_tag
104 def custom_field_formats_for_select
104 def custom_field_formats_for_select(custom_field)
105 Redmine::CustomFieldFormat.as_select
105 Redmine::CustomFieldFormat.as_select(custom_field.class.customized_class.name)
106 end
106 end
107
107
108 # Renders the custom_values in api views
108 # Renders the custom_values in api views
109 def render_api_custom_values(custom_values, api)
109 def render_api_custom_values(custom_values, api)
110 api.array :custom_fields do
110 api.array :custom_fields do
111 custom_values.each do |custom_value|
111 custom_values.each do |custom_value|
112 api.custom_field :id => custom_value.custom_field_id, :name => custom_value.custom_field.name do
112 api.custom_field :id => custom_value.custom_field_id, :name => custom_value.custom_field.name do
113 api.value custom_value.value
113 api.value custom_value.value
114 end
114 end
115 end
115 end
116 end unless custom_values.empty?
116 end unless custom_values.empty?
117 end
117 end
118 end
118 end
@@ -1,120 +1,149
1 # redMine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 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 CustomField < ActiveRecord::Base
18 class CustomField < ActiveRecord::Base
19 has_many :custom_values, :dependent => :delete_all
19 has_many :custom_values, :dependent => :delete_all
20 acts_as_list :scope => 'type = \'#{self.class}\''
20 acts_as_list :scope => 'type = \'#{self.class}\''
21 serialize :possible_values
21 serialize :possible_values
22
22
23 validates_presence_of :name, :field_format
23 validates_presence_of :name, :field_format
24 validates_uniqueness_of :name, :scope => :type
24 validates_uniqueness_of :name, :scope => :type
25 validates_length_of :name, :maximum => 30
25 validates_length_of :name, :maximum => 30
26 validates_inclusion_of :field_format, :in => Redmine::CustomFieldFormat.available_formats
26 validates_inclusion_of :field_format, :in => Redmine::CustomFieldFormat.available_formats
27
27
28 def initialize(attributes = nil)
28 def initialize(attributes = nil)
29 super
29 super
30 self.possible_values ||= []
30 self.possible_values ||= []
31 end
31 end
32
32
33 def before_validation
33 def before_validation
34 # make sure these fields are not searchable
34 # make sure these fields are not searchable
35 self.searchable = false if %w(int float date bool).include?(field_format)
35 self.searchable = false if %w(int float date bool).include?(field_format)
36 true
36 true
37 end
37 end
38
38
39 def validate
39 def validate
40 if self.field_format == "list"
40 if self.field_format == "list"
41 errors.add(:possible_values, :blank) if self.possible_values.nil? || self.possible_values.empty?
41 errors.add(:possible_values, :blank) if self.possible_values.nil? || self.possible_values.empty?
42 errors.add(:possible_values, :invalid) unless self.possible_values.is_a? Array
42 errors.add(:possible_values, :invalid) unless self.possible_values.is_a? Array
43 end
43 end
44
44
45 # validate default value
45 # validate default value
46 v = CustomValue.new(:custom_field => self.clone, :value => default_value, :customized => nil)
46 v = CustomValue.new(:custom_field => self.clone, :value => default_value, :customized => nil)
47 v.custom_field.is_required = false
47 v.custom_field.is_required = false
48 errors.add(:default_value, :invalid) unless v.valid?
48 errors.add(:default_value, :invalid) unless v.valid?
49 end
49 end
50
50
51 def possible_values_options(obj=nil)
52 case field_format
53 when 'user', 'version'
54 if obj.respond_to?(:project)
55 case field_format
56 when 'user'
57 obj.project.users.sort.collect {|u| [u.to_s, u.id.to_s]}
58 when 'version'
59 obj.project.versions.sort.collect {|u| [u.to_s, u.id.to_s]}
60 end
61 else
62 []
63 end
64 else
65 read_attribute :possible_values
66 end
67 end
68
69 def possible_values(obj=nil)
70 case field_format
71 when 'user'
72 possible_values_options(obj).collect(&:last)
73 else
74 read_attribute :possible_values
75 end
76 end
77
51 # Makes possible_values accept a multiline string
78 # Makes possible_values accept a multiline string
52 def possible_values=(arg)
79 def possible_values=(arg)
53 if arg.is_a?(Array)
80 if arg.is_a?(Array)
54 write_attribute(:possible_values, arg.compact.collect(&:strip).select {|v| !v.blank?})
81 write_attribute(:possible_values, arg.compact.collect(&:strip).select {|v| !v.blank?})
55 else
82 else
56 self.possible_values = arg.to_s.split(/[\n\r]+/)
83 self.possible_values = arg.to_s.split(/[\n\r]+/)
57 end
84 end
58 end
85 end
59
86
60 def cast_value(value)
87 def cast_value(value)
61 casted = nil
88 casted = nil
62 unless value.blank?
89 unless value.blank?
63 case field_format
90 case field_format
64 when 'string', 'text', 'list'
91 when 'string', 'text', 'list'
65 casted = value
92 casted = value
66 when 'date'
93 when 'date'
67 casted = begin; value.to_date; rescue; nil end
94 casted = begin; value.to_date; rescue; nil end
68 when 'bool'
95 when 'bool'
69 casted = (value == '1' ? true : false)
96 casted = (value == '1' ? true : false)
70 when 'int'
97 when 'int'
71 casted = value.to_i
98 casted = value.to_i
72 when 'float'
99 when 'float'
73 casted = value.to_f
100 casted = value.to_f
101 when 'user', 'version'
102 casted = (value.blank? ? nil : field_format.classify.constantize.find_by_id(value.to_i))
74 end
103 end
75 end
104 end
76 casted
105 casted
77 end
106 end
78
107
79 # Returns a ORDER BY clause that can used to sort customized
108 # Returns a ORDER BY clause that can used to sort customized
80 # objects by their value of the custom field.
109 # objects by their value of the custom field.
81 # Returns false, if the custom field can not be used for sorting.
110 # Returns false, if the custom field can not be used for sorting.
82 def order_statement
111 def order_statement
83 case field_format
112 case field_format
84 when 'string', 'text', 'list', 'date', 'bool'
113 when 'string', 'text', 'list', 'date', 'bool'
85 # COALESCE is here to make sure that blank and NULL values are sorted equally
114 # COALESCE is here to make sure that blank and NULL values are sorted equally
86 "COALESCE((SELECT cv_sort.value FROM #{CustomValue.table_name} cv_sort" +
115 "COALESCE((SELECT cv_sort.value FROM #{CustomValue.table_name} cv_sort" +
87 " WHERE cv_sort.customized_type='#{self.class.customized_class.name}'" +
116 " WHERE cv_sort.customized_type='#{self.class.customized_class.name}'" +
88 " AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
117 " AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
89 " AND cv_sort.custom_field_id=#{id} LIMIT 1), '')"
118 " AND cv_sort.custom_field_id=#{id} LIMIT 1), '')"
90 when 'int', 'float'
119 when 'int', 'float'
91 # Make the database cast values into numeric
120 # Make the database cast values into numeric
92 # Postgresql will raise an error if a value can not be casted!
121 # Postgresql will raise an error if a value can not be casted!
93 # CustomValue validations should ensure that it doesn't occur
122 # CustomValue validations should ensure that it doesn't occur
94 "(SELECT CAST(cv_sort.value AS decimal(60,3)) FROM #{CustomValue.table_name} cv_sort" +
123 "(SELECT CAST(cv_sort.value AS decimal(60,3)) FROM #{CustomValue.table_name} cv_sort" +
95 " WHERE cv_sort.customized_type='#{self.class.customized_class.name}'" +
124 " WHERE cv_sort.customized_type='#{self.class.customized_class.name}'" +
96 " AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
125 " AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
97 " AND cv_sort.custom_field_id=#{id} AND cv_sort.value <> '' AND cv_sort.value IS NOT NULL LIMIT 1)"
126 " AND cv_sort.custom_field_id=#{id} AND cv_sort.value <> '' AND cv_sort.value IS NOT NULL LIMIT 1)"
98 else
127 else
99 nil
128 nil
100 end
129 end
101 end
130 end
102
131
103 def <=>(field)
132 def <=>(field)
104 position <=> field.position
133 position <=> field.position
105 end
134 end
106
135
107 def self.customized_class
136 def self.customized_class
108 self.name =~ /^(.+)CustomField$/
137 self.name =~ /^(.+)CustomField$/
109 begin; $1.constantize; rescue nil; end
138 begin; $1.constantize; rescue nil; end
110 end
139 end
111
140
112 # to move in project_custom_field
141 # to move in project_custom_field
113 def self.for_all
142 def self.for_all
114 find(:all, :conditions => ["is_for_all=?", true], :order => 'position')
143 find(:all, :conditions => ["is_for_all=?", true], :order => 'position')
115 end
144 end
116
145
117 def type_name
146 def type_name
118 nil
147 nil
119 end
148 end
120 end
149 end
@@ -1,657 +1,660
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 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 QueryColumn
18 class QueryColumn
19 attr_accessor :name, :sortable, :groupable, :default_order
19 attr_accessor :name, :sortable, :groupable, :default_order
20 include Redmine::I18n
20 include Redmine::I18n
21
21
22 def initialize(name, options={})
22 def initialize(name, options={})
23 self.name = name
23 self.name = name
24 self.sortable = options[:sortable]
24 self.sortable = options[:sortable]
25 self.groupable = options[:groupable] || false
25 self.groupable = options[:groupable] || false
26 if groupable == true
26 if groupable == true
27 self.groupable = name.to_s
27 self.groupable = name.to_s
28 end
28 end
29 self.default_order = options[:default_order]
29 self.default_order = options[:default_order]
30 @caption_key = options[:caption] || "field_#{name}"
30 @caption_key = options[:caption] || "field_#{name}"
31 end
31 end
32
32
33 def caption
33 def caption
34 l(@caption_key)
34 l(@caption_key)
35 end
35 end
36
36
37 # Returns true if the column is sortable, otherwise false
37 # Returns true if the column is sortable, otherwise false
38 def sortable?
38 def sortable?
39 !sortable.nil?
39 !sortable.nil?
40 end
40 end
41
41
42 def value(issue)
42 def value(issue)
43 issue.send name
43 issue.send name
44 end
44 end
45 end
45 end
46
46
47 class QueryCustomFieldColumn < QueryColumn
47 class QueryCustomFieldColumn < QueryColumn
48
48
49 def initialize(custom_field)
49 def initialize(custom_field)
50 self.name = "cf_#{custom_field.id}".to_sym
50 self.name = "cf_#{custom_field.id}".to_sym
51 self.sortable = custom_field.order_statement || false
51 self.sortable = custom_field.order_statement || false
52 if %w(list date bool int).include?(custom_field.field_format)
52 if %w(list date bool int).include?(custom_field.field_format)
53 self.groupable = custom_field.order_statement
53 self.groupable = custom_field.order_statement
54 end
54 end
55 self.groupable ||= false
55 self.groupable ||= false
56 @cf = custom_field
56 @cf = custom_field
57 end
57 end
58
58
59 def caption
59 def caption
60 @cf.name
60 @cf.name
61 end
61 end
62
62
63 def custom_field
63 def custom_field
64 @cf
64 @cf
65 end
65 end
66
66
67 def value(issue)
67 def value(issue)
68 cv = issue.custom_values.detect {|v| v.custom_field_id == @cf.id}
68 cv = issue.custom_values.detect {|v| v.custom_field_id == @cf.id}
69 cv && @cf.cast_value(cv.value)
69 cv && @cf.cast_value(cv.value)
70 end
70 end
71 end
71 end
72
72
73 class Query < ActiveRecord::Base
73 class Query < ActiveRecord::Base
74 class StatementInvalid < ::ActiveRecord::StatementInvalid
74 class StatementInvalid < ::ActiveRecord::StatementInvalid
75 end
75 end
76
76
77 belongs_to :project
77 belongs_to :project
78 belongs_to :user
78 belongs_to :user
79 serialize :filters
79 serialize :filters
80 serialize :column_names
80 serialize :column_names
81 serialize :sort_criteria, Array
81 serialize :sort_criteria, Array
82
82
83 attr_protected :project_id, :user_id
83 attr_protected :project_id, :user_id
84
84
85 validates_presence_of :name, :on => :save
85 validates_presence_of :name, :on => :save
86 validates_length_of :name, :maximum => 255
86 validates_length_of :name, :maximum => 255
87
87
88 @@operators = { "=" => :label_equals,
88 @@operators = { "=" => :label_equals,
89 "!" => :label_not_equals,
89 "!" => :label_not_equals,
90 "o" => :label_open_issues,
90 "o" => :label_open_issues,
91 "c" => :label_closed_issues,
91 "c" => :label_closed_issues,
92 "!*" => :label_none,
92 "!*" => :label_none,
93 "*" => :label_all,
93 "*" => :label_all,
94 ">=" => :label_greater_or_equal,
94 ">=" => :label_greater_or_equal,
95 "<=" => :label_less_or_equal,
95 "<=" => :label_less_or_equal,
96 "<t+" => :label_in_less_than,
96 "<t+" => :label_in_less_than,
97 ">t+" => :label_in_more_than,
97 ">t+" => :label_in_more_than,
98 "t+" => :label_in,
98 "t+" => :label_in,
99 "t" => :label_today,
99 "t" => :label_today,
100 "w" => :label_this_week,
100 "w" => :label_this_week,
101 ">t-" => :label_less_than_ago,
101 ">t-" => :label_less_than_ago,
102 "<t-" => :label_more_than_ago,
102 "<t-" => :label_more_than_ago,
103 "t-" => :label_ago,
103 "t-" => :label_ago,
104 "~" => :label_contains,
104 "~" => :label_contains,
105 "!~" => :label_not_contains }
105 "!~" => :label_not_contains }
106
106
107 cattr_reader :operators
107 cattr_reader :operators
108
108
109 @@operators_by_filter_type = { :list => [ "=", "!" ],
109 @@operators_by_filter_type = { :list => [ "=", "!" ],
110 :list_status => [ "o", "=", "!", "c", "*" ],
110 :list_status => [ "o", "=", "!", "c", "*" ],
111 :list_optional => [ "=", "!", "!*", "*" ],
111 :list_optional => [ "=", "!", "!*", "*" ],
112 :list_subprojects => [ "*", "!*", "=" ],
112 :list_subprojects => [ "*", "!*", "=" ],
113 :date => [ "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-" ],
113 :date => [ "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-" ],
114 :date_past => [ ">t-", "<t-", "t-", "t", "w" ],
114 :date_past => [ ">t-", "<t-", "t-", "t", "w" ],
115 :string => [ "=", "~", "!", "!~" ],
115 :string => [ "=", "~", "!", "!~" ],
116 :text => [ "~", "!~" ],
116 :text => [ "~", "!~" ],
117 :integer => [ "=", ">=", "<=", "!*", "*" ] }
117 :integer => [ "=", ">=", "<=", "!*", "*" ] }
118
118
119 cattr_reader :operators_by_filter_type
119 cattr_reader :operators_by_filter_type
120
120
121 @@available_columns = [
121 @@available_columns = [
122 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
122 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
123 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
123 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
124 QueryColumn.new(:parent, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc', :caption => :field_parent_issue),
124 QueryColumn.new(:parent, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc', :caption => :field_parent_issue),
125 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
125 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
126 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
126 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
127 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
127 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
128 QueryColumn.new(:author),
128 QueryColumn.new(:author),
129 QueryColumn.new(:assigned_to, :sortable => ["#{User.table_name}.lastname", "#{User.table_name}.firstname", "#{User.table_name}.id"], :groupable => true),
129 QueryColumn.new(:assigned_to, :sortable => ["#{User.table_name}.lastname", "#{User.table_name}.firstname", "#{User.table_name}.id"], :groupable => true),
130 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
130 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
131 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
131 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
132 QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc', :groupable => true),
132 QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc', :groupable => true),
133 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
133 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
134 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
134 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
135 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
135 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
136 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
136 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
137 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
137 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
138 ]
138 ]
139 cattr_reader :available_columns
139 cattr_reader :available_columns
140
140
141 def initialize(attributes = nil)
141 def initialize(attributes = nil)
142 super attributes
142 super attributes
143 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
143 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
144 end
144 end
145
145
146 def after_initialize
146 def after_initialize
147 # Store the fact that project is nil (used in #editable_by?)
147 # Store the fact that project is nil (used in #editable_by?)
148 @is_for_all = project.nil?
148 @is_for_all = project.nil?
149 end
149 end
150
150
151 def validate
151 def validate
152 filters.each_key do |field|
152 filters.each_key do |field|
153 errors.add label_for(field), :blank unless
153 errors.add label_for(field), :blank unless
154 # filter requires one or more values
154 # filter requires one or more values
155 (values_for(field) and !values_for(field).first.blank?) or
155 (values_for(field) and !values_for(field).first.blank?) or
156 # filter doesn't require any value
156 # filter doesn't require any value
157 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
157 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
158 end if filters
158 end if filters
159 end
159 end
160
160
161 def editable_by?(user)
161 def editable_by?(user)
162 return false unless user
162 return false unless user
163 # Admin can edit them all and regular users can edit their private queries
163 # Admin can edit them all and regular users can edit their private queries
164 return true if user.admin? || (!is_public && self.user_id == user.id)
164 return true if user.admin? || (!is_public && self.user_id == user.id)
165 # Members can not edit public queries that are for all project (only admin is allowed to)
165 # Members can not edit public queries that are for all project (only admin is allowed to)
166 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
166 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
167 end
167 end
168
168
169 def available_filters
169 def available_filters
170 return @available_filters if @available_filters
170 return @available_filters if @available_filters
171
171
172 trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
172 trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
173
173
174 @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
174 @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
175 "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
175 "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
176 "priority_id" => { :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] } },
176 "priority_id" => { :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] } },
177 "subject" => { :type => :text, :order => 8 },
177 "subject" => { :type => :text, :order => 8 },
178 "created_on" => { :type => :date_past, :order => 9 },
178 "created_on" => { :type => :date_past, :order => 9 },
179 "updated_on" => { :type => :date_past, :order => 10 },
179 "updated_on" => { :type => :date_past, :order => 10 },
180 "start_date" => { :type => :date, :order => 11 },
180 "start_date" => { :type => :date, :order => 11 },
181 "due_date" => { :type => :date, :order => 12 },
181 "due_date" => { :type => :date, :order => 12 },
182 "estimated_hours" => { :type => :integer, :order => 13 },
182 "estimated_hours" => { :type => :integer, :order => 13 },
183 "done_ratio" => { :type => :integer, :order => 14 }}
183 "done_ratio" => { :type => :integer, :order => 14 }}
184
184
185 user_values = []
185 user_values = []
186 user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
186 user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
187 if project
187 if project
188 user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] }
188 user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] }
189 else
189 else
190 all_projects = Project.visible.all
190 all_projects = Project.visible.all
191 if all_projects.any?
191 if all_projects.any?
192 # members of visible projects
192 # members of visible projects
193 user_values += User.active.find(:all, :conditions => ["#{User.table_name}.id IN (SELECT DISTINCT user_id FROM members WHERE project_id IN (?))", all_projects.collect(&:id)]).sort.collect{|s| [s.name, s.id.to_s] }
193 user_values += User.active.find(:all, :conditions => ["#{User.table_name}.id IN (SELECT DISTINCT user_id FROM members WHERE project_id IN (?))", all_projects.collect(&:id)]).sort.collect{|s| [s.name, s.id.to_s] }
194
194
195 # project filter
195 # project filter
196 project_values = []
196 project_values = []
197 Project.project_tree(all_projects) do |p, level|
197 Project.project_tree(all_projects) do |p, level|
198 prefix = (level > 0 ? ('--' * level + ' ') : '')
198 prefix = (level > 0 ? ('--' * level + ' ') : '')
199 project_values << ["#{prefix}#{p.name}", p.id.to_s]
199 project_values << ["#{prefix}#{p.name}", p.id.to_s]
200 end
200 end
201 @available_filters["project_id"] = { :type => :list, :order => 1, :values => project_values} unless project_values.empty?
201 @available_filters["project_id"] = { :type => :list, :order => 1, :values => project_values} unless project_values.empty?
202 end
202 end
203 end
203 end
204 @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => user_values } unless user_values.empty?
204 @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => user_values } unless user_values.empty?
205 @available_filters["author_id"] = { :type => :list, :order => 5, :values => user_values } unless user_values.empty?
205 @available_filters["author_id"] = { :type => :list, :order => 5, :values => user_values } unless user_values.empty?
206
206
207 group_values = Group.all.collect {|g| [g.name, g.id.to_s] }
207 group_values = Group.all.collect {|g| [g.name, g.id.to_s] }
208 @available_filters["member_of_group"] = { :type => :list_optional, :order => 6, :values => group_values } unless group_values.empty?
208 @available_filters["member_of_group"] = { :type => :list_optional, :order => 6, :values => group_values } unless group_values.empty?
209
209
210 role_values = Role.givable.collect {|r| [r.name, r.id.to_s] }
210 role_values = Role.givable.collect {|r| [r.name, r.id.to_s] }
211 @available_filters["assigned_to_role"] = { :type => :list_optional, :order => 7, :values => role_values } unless role_values.empty?
211 @available_filters["assigned_to_role"] = { :type => :list_optional, :order => 7, :values => role_values } unless role_values.empty?
212
212
213 if User.current.logged?
213 if User.current.logged?
214 @available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] }
214 @available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] }
215 end
215 end
216
216
217 if project
217 if project
218 # project specific filters
218 # project specific filters
219 unless @project.issue_categories.empty?
219 unless @project.issue_categories.empty?
220 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => @project.issue_categories.collect{|s| [s.name, s.id.to_s] } }
220 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => @project.issue_categories.collect{|s| [s.name, s.id.to_s] } }
221 end
221 end
222 unless @project.shared_versions.empty?
222 unless @project.shared_versions.empty?
223 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => @project.shared_versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
223 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => @project.shared_versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
224 end
224 end
225 unless @project.descendants.active.empty?
225 unless @project.descendants.active.empty?
226 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.descendants.visible.collect{|s| [s.name, s.id.to_s] } }
226 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.descendants.visible.collect{|s| [s.name, s.id.to_s] } }
227 end
227 end
228 add_custom_fields_filters(@project.all_issue_custom_fields)
228 add_custom_fields_filters(@project.all_issue_custom_fields)
229 else
229 else
230 # global filters for cross project issue list
230 # global filters for cross project issue list
231 system_shared_versions = Version.visible.find_all_by_sharing('system')
231 system_shared_versions = Version.visible.find_all_by_sharing('system')
232 unless system_shared_versions.empty?
232 unless system_shared_versions.empty?
233 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => system_shared_versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
233 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => system_shared_versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
234 end
234 end
235 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
235 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
236 end
236 end
237 @available_filters
237 @available_filters
238 end
238 end
239
239
240 def add_filter(field, operator, values)
240 def add_filter(field, operator, values)
241 # values must be an array
241 # values must be an array
242 return unless values and values.is_a? Array # and !values.first.empty?
242 return unless values and values.is_a? Array # and !values.first.empty?
243 # check if field is defined as an available filter
243 # check if field is defined as an available filter
244 if available_filters.has_key? field
244 if available_filters.has_key? field
245 filter_options = available_filters[field]
245 filter_options = available_filters[field]
246 # check if operator is allowed for that filter
246 # check if operator is allowed for that filter
247 #if @@operators_by_filter_type[filter_options[:type]].include? operator
247 #if @@operators_by_filter_type[filter_options[:type]].include? operator
248 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
248 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
249 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
249 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
250 #end
250 #end
251 filters[field] = {:operator => operator, :values => values }
251 filters[field] = {:operator => operator, :values => values }
252 end
252 end
253 end
253 end
254
254
255 def add_short_filter(field, expression)
255 def add_short_filter(field, expression)
256 return unless expression
256 return unless expression
257 parms = expression.scan(/^(o|c|!\*|!|\*)?(.*)$/).first
257 parms = expression.scan(/^(o|c|!\*|!|\*)?(.*)$/).first
258 add_filter field, (parms[0] || "="), [parms[1] || ""]
258 add_filter field, (parms[0] || "="), [parms[1] || ""]
259 end
259 end
260
260
261 # Add multiple filters using +add_filter+
261 # Add multiple filters using +add_filter+
262 def add_filters(fields, operators, values)
262 def add_filters(fields, operators, values)
263 if fields.is_a?(Array) && operators.is_a?(Hash) && values.is_a?(Hash)
263 if fields.is_a?(Array) && operators.is_a?(Hash) && values.is_a?(Hash)
264 fields.each do |field|
264 fields.each do |field|
265 add_filter(field, operators[field], values[field])
265 add_filter(field, operators[field], values[field])
266 end
266 end
267 end
267 end
268 end
268 end
269
269
270 def has_filter?(field)
270 def has_filter?(field)
271 filters and filters[field]
271 filters and filters[field]
272 end
272 end
273
273
274 def operator_for(field)
274 def operator_for(field)
275 has_filter?(field) ? filters[field][:operator] : nil
275 has_filter?(field) ? filters[field][:operator] : nil
276 end
276 end
277
277
278 def values_for(field)
278 def values_for(field)
279 has_filter?(field) ? filters[field][:values] : nil
279 has_filter?(field) ? filters[field][:values] : nil
280 end
280 end
281
281
282 def label_for(field)
282 def label_for(field)
283 label = available_filters[field][:name] if available_filters.has_key?(field)
283 label = available_filters[field][:name] if available_filters.has_key?(field)
284 label ||= field.gsub(/\_id$/, "")
284 label ||= field.gsub(/\_id$/, "")
285 end
285 end
286
286
287 def available_columns
287 def available_columns
288 return @available_columns if @available_columns
288 return @available_columns if @available_columns
289 @available_columns = Query.available_columns
289 @available_columns = Query.available_columns
290 @available_columns += (project ?
290 @available_columns += (project ?
291 project.all_issue_custom_fields :
291 project.all_issue_custom_fields :
292 IssueCustomField.find(:all)
292 IssueCustomField.find(:all)
293 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
293 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
294 end
294 end
295
295
296 def self.available_columns=(v)
296 def self.available_columns=(v)
297 self.available_columns = (v)
297 self.available_columns = (v)
298 end
298 end
299
299
300 def self.add_available_column(column)
300 def self.add_available_column(column)
301 self.available_columns << (column) if column.is_a?(QueryColumn)
301 self.available_columns << (column) if column.is_a?(QueryColumn)
302 end
302 end
303
303
304 # Returns an array of columns that can be used to group the results
304 # Returns an array of columns that can be used to group the results
305 def groupable_columns
305 def groupable_columns
306 available_columns.select {|c| c.groupable}
306 available_columns.select {|c| c.groupable}
307 end
307 end
308
308
309 # Returns a Hash of columns and the key for sorting
309 # Returns a Hash of columns and the key for sorting
310 def sortable_columns
310 def sortable_columns
311 {'id' => "#{Issue.table_name}.id"}.merge(available_columns.inject({}) {|h, column|
311 {'id' => "#{Issue.table_name}.id"}.merge(available_columns.inject({}) {|h, column|
312 h[column.name.to_s] = column.sortable
312 h[column.name.to_s] = column.sortable
313 h
313 h
314 })
314 })
315 end
315 end
316
316
317 def columns
317 def columns
318 if has_default_columns?
318 if has_default_columns?
319 available_columns.select do |c|
319 available_columns.select do |c|
320 # Adds the project column by default for cross-project lists
320 # Adds the project column by default for cross-project lists
321 Setting.issue_list_default_columns.include?(c.name.to_s) || (c.name == :project && project.nil?)
321 Setting.issue_list_default_columns.include?(c.name.to_s) || (c.name == :project && project.nil?)
322 end
322 end
323 else
323 else
324 # preserve the column_names order
324 # preserve the column_names order
325 column_names.collect {|name| available_columns.find {|col| col.name == name}}.compact
325 column_names.collect {|name| available_columns.find {|col| col.name == name}}.compact
326 end
326 end
327 end
327 end
328
328
329 def column_names=(names)
329 def column_names=(names)
330 if names
330 if names
331 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
331 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
332 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
332 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
333 # Set column_names to nil if default columns
333 # Set column_names to nil if default columns
334 if names.map(&:to_s) == Setting.issue_list_default_columns
334 if names.map(&:to_s) == Setting.issue_list_default_columns
335 names = nil
335 names = nil
336 end
336 end
337 end
337 end
338 write_attribute(:column_names, names)
338 write_attribute(:column_names, names)
339 end
339 end
340
340
341 def has_column?(column)
341 def has_column?(column)
342 column_names && column_names.include?(column.name)
342 column_names && column_names.include?(column.name)
343 end
343 end
344
344
345 def has_default_columns?
345 def has_default_columns?
346 column_names.nil? || column_names.empty?
346 column_names.nil? || column_names.empty?
347 end
347 end
348
348
349 def sort_criteria=(arg)
349 def sort_criteria=(arg)
350 c = []
350 c = []
351 if arg.is_a?(Hash)
351 if arg.is_a?(Hash)
352 arg = arg.keys.sort.collect {|k| arg[k]}
352 arg = arg.keys.sort.collect {|k| arg[k]}
353 end
353 end
354 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']}
354 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']}
355 write_attribute(:sort_criteria, c)
355 write_attribute(:sort_criteria, c)
356 end
356 end
357
357
358 def sort_criteria
358 def sort_criteria
359 read_attribute(:sort_criteria) || []
359 read_attribute(:sort_criteria) || []
360 end
360 end
361
361
362 def sort_criteria_key(arg)
362 def sort_criteria_key(arg)
363 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
363 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
364 end
364 end
365
365
366 def sort_criteria_order(arg)
366 def sort_criteria_order(arg)
367 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
367 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
368 end
368 end
369
369
370 # Returns the SQL sort order that should be prepended for grouping
370 # Returns the SQL sort order that should be prepended for grouping
371 def group_by_sort_order
371 def group_by_sort_order
372 if grouped? && (column = group_by_column)
372 if grouped? && (column = group_by_column)
373 column.sortable.is_a?(Array) ?
373 column.sortable.is_a?(Array) ?
374 column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') :
374 column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') :
375 "#{column.sortable} #{column.default_order}"
375 "#{column.sortable} #{column.default_order}"
376 end
376 end
377 end
377 end
378
378
379 # Returns true if the query is a grouped query
379 # Returns true if the query is a grouped query
380 def grouped?
380 def grouped?
381 !group_by_column.nil?
381 !group_by_column.nil?
382 end
382 end
383
383
384 def group_by_column
384 def group_by_column
385 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
385 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
386 end
386 end
387
387
388 def group_by_statement
388 def group_by_statement
389 group_by_column.try(:groupable)
389 group_by_column.try(:groupable)
390 end
390 end
391
391
392 def project_statement
392 def project_statement
393 project_clauses = []
393 project_clauses = []
394 if project && !@project.descendants.active.empty?
394 if project && !@project.descendants.active.empty?
395 ids = [project.id]
395 ids = [project.id]
396 if has_filter?("subproject_id")
396 if has_filter?("subproject_id")
397 case operator_for("subproject_id")
397 case operator_for("subproject_id")
398 when '='
398 when '='
399 # include the selected subprojects
399 # include the selected subprojects
400 ids += values_for("subproject_id").each(&:to_i)
400 ids += values_for("subproject_id").each(&:to_i)
401 when '!*'
401 when '!*'
402 # main project only
402 # main project only
403 else
403 else
404 # all subprojects
404 # all subprojects
405 ids += project.descendants.collect(&:id)
405 ids += project.descendants.collect(&:id)
406 end
406 end
407 elsif Setting.display_subprojects_issues?
407 elsif Setting.display_subprojects_issues?
408 ids += project.descendants.collect(&:id)
408 ids += project.descendants.collect(&:id)
409 end
409 end
410 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
410 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
411 elsif project
411 elsif project
412 project_clauses << "#{Project.table_name}.id = %d" % project.id
412 project_clauses << "#{Project.table_name}.id = %d" % project.id
413 end
413 end
414 project_clauses << Issue.visible_condition(User.current)
414 project_clauses << Issue.visible_condition(User.current)
415 project_clauses.join(' AND ')
415 project_clauses.join(' AND ')
416 end
416 end
417
417
418 def statement
418 def statement
419 # filters clauses
419 # filters clauses
420 filters_clauses = []
420 filters_clauses = []
421 filters.each_key do |field|
421 filters.each_key do |field|
422 next if field == "subproject_id"
422 next if field == "subproject_id"
423 v = values_for(field).clone
423 v = values_for(field).clone
424 next unless v and !v.empty?
424 next unless v and !v.empty?
425 operator = operator_for(field)
425 operator = operator_for(field)
426
426
427 # "me" value subsitution
427 # "me" value subsitution
428 if %w(assigned_to_id author_id watcher_id).include?(field)
428 if %w(assigned_to_id author_id watcher_id).include?(field)
429 v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me")
429 v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me")
430 end
430 end
431
431
432 sql = ''
432 sql = ''
433 if field =~ /^cf_(\d+)$/
433 if field =~ /^cf_(\d+)$/
434 # custom field
434 # custom field
435 db_table = CustomValue.table_name
435 db_table = CustomValue.table_name
436 db_field = 'value'
436 db_field = 'value'
437 is_custom_filter = true
437 is_custom_filter = true
438 sql << "#{Issue.table_name}.id IN (SELECT #{Issue.table_name}.id FROM #{Issue.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='Issue' AND #{db_table}.customized_id=#{Issue.table_name}.id AND #{db_table}.custom_field_id=#{$1} WHERE "
438 sql << "#{Issue.table_name}.id IN (SELECT #{Issue.table_name}.id FROM #{Issue.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='Issue' AND #{db_table}.customized_id=#{Issue.table_name}.id AND #{db_table}.custom_field_id=#{$1} WHERE "
439 sql << sql_for_field(field, operator, v, db_table, db_field, true) + ')'
439 sql << sql_for_field(field, operator, v, db_table, db_field, true) + ')'
440 elsif field == 'watcher_id'
440 elsif field == 'watcher_id'
441 db_table = Watcher.table_name
441 db_table = Watcher.table_name
442 db_field = 'user_id'
442 db_field = 'user_id'
443 sql << "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND "
443 sql << "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND "
444 sql << sql_for_field(field, '=', v, db_table, db_field) + ')'
444 sql << sql_for_field(field, '=', v, db_table, db_field) + ')'
445 elsif field == "member_of_group" # named field
445 elsif field == "member_of_group" # named field
446 if operator == '*' # Any group
446 if operator == '*' # Any group
447 groups = Group.all
447 groups = Group.all
448 operator = '=' # Override the operator since we want to find by assigned_to
448 operator = '=' # Override the operator since we want to find by assigned_to
449 elsif operator == "!*"
449 elsif operator == "!*"
450 groups = Group.all
450 groups = Group.all
451 operator = '!' # Override the operator since we want to find by assigned_to
451 operator = '!' # Override the operator since we want to find by assigned_to
452 else
452 else
453 groups = Group.find_all_by_id(v)
453 groups = Group.find_all_by_id(v)
454 end
454 end
455 groups ||= []
455 groups ||= []
456
456
457 members_of_groups = groups.inject([]) {|user_ids, group|
457 members_of_groups = groups.inject([]) {|user_ids, group|
458 if group && group.user_ids.present?
458 if group && group.user_ids.present?
459 user_ids << group.user_ids
459 user_ids << group.user_ids
460 end
460 end
461 user_ids.flatten.uniq.compact
461 user_ids.flatten.uniq.compact
462 }.sort.collect(&:to_s)
462 }.sort.collect(&:to_s)
463
463
464 sql << '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
464 sql << '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
465
465
466 elsif field == "assigned_to_role" # named field
466 elsif field == "assigned_to_role" # named field
467 if operator == "*" # Any Role
467 if operator == "*" # Any Role
468 roles = Role.givable
468 roles = Role.givable
469 operator = '=' # Override the operator since we want to find by assigned_to
469 operator = '=' # Override the operator since we want to find by assigned_to
470 elsif operator == "!*" # No role
470 elsif operator == "!*" # No role
471 roles = Role.givable
471 roles = Role.givable
472 operator = '!' # Override the operator since we want to find by assigned_to
472 operator = '!' # Override the operator since we want to find by assigned_to
473 else
473 else
474 roles = Role.givable.find_all_by_id(v)
474 roles = Role.givable.find_all_by_id(v)
475 end
475 end
476 roles ||= []
476 roles ||= []
477
477
478 members_of_roles = roles.inject([]) {|user_ids, role|
478 members_of_roles = roles.inject([]) {|user_ids, role|
479 if role && role.members
479 if role && role.members
480 user_ids << role.members.collect(&:user_id)
480 user_ids << role.members.collect(&:user_id)
481 end
481 end
482 user_ids.flatten.uniq.compact
482 user_ids.flatten.uniq.compact
483 }.sort.collect(&:to_s)
483 }.sort.collect(&:to_s)
484
484
485 sql << '(' + sql_for_field("assigned_to_id", operator, members_of_roles, Issue.table_name, "assigned_to_id", false) + ')'
485 sql << '(' + sql_for_field("assigned_to_id", operator, members_of_roles, Issue.table_name, "assigned_to_id", false) + ')'
486 else
486 else
487 # regular field
487 # regular field
488 db_table = Issue.table_name
488 db_table = Issue.table_name
489 db_field = field
489 db_field = field
490 sql << '(' + sql_for_field(field, operator, v, db_table, db_field) + ')'
490 sql << '(' + sql_for_field(field, operator, v, db_table, db_field) + ')'
491 end
491 end
492 filters_clauses << sql
492 filters_clauses << sql
493
493
494 end if filters and valid?
494 end if filters and valid?
495
495
496 (filters_clauses << project_statement).join(' AND ')
496 (filters_clauses << project_statement).join(' AND ')
497 end
497 end
498
498
499 # Returns the issue count
499 # Returns the issue count
500 def issue_count
500 def issue_count
501 Issue.count(:include => [:status, :project], :conditions => statement)
501 Issue.count(:include => [:status, :project], :conditions => statement)
502 rescue ::ActiveRecord::StatementInvalid => e
502 rescue ::ActiveRecord::StatementInvalid => e
503 raise StatementInvalid.new(e.message)
503 raise StatementInvalid.new(e.message)
504 end
504 end
505
505
506 # Returns the issue count by group or nil if query is not grouped
506 # Returns the issue count by group or nil if query is not grouped
507 def issue_count_by_group
507 def issue_count_by_group
508 r = nil
508 r = nil
509 if grouped?
509 if grouped?
510 begin
510 begin
511 # Rails will raise an (unexpected) RecordNotFound if there's only a nil group value
511 # Rails will raise an (unexpected) RecordNotFound if there's only a nil group value
512 r = Issue.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement)
512 r = Issue.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement)
513 rescue ActiveRecord::RecordNotFound
513 rescue ActiveRecord::RecordNotFound
514 r = {nil => issue_count}
514 r = {nil => issue_count}
515 end
515 end
516 c = group_by_column
516 c = group_by_column
517 if c.is_a?(QueryCustomFieldColumn)
517 if c.is_a?(QueryCustomFieldColumn)
518 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
518 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
519 end
519 end
520 end
520 end
521 r
521 r
522 rescue ::ActiveRecord::StatementInvalid => e
522 rescue ::ActiveRecord::StatementInvalid => e
523 raise StatementInvalid.new(e.message)
523 raise StatementInvalid.new(e.message)
524 end
524 end
525
525
526 # Returns the issues
526 # Returns the issues
527 # Valid options are :order, :offset, :limit, :include, :conditions
527 # Valid options are :order, :offset, :limit, :include, :conditions
528 def issues(options={})
528 def issues(options={})
529 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
529 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
530 order_option = nil if order_option.blank?
530 order_option = nil if order_option.blank?
531
531
532 Issue.find :all, :include => ([:status, :project] + (options[:include] || [])).uniq,
532 Issue.find :all, :include => ([:status, :project] + (options[:include] || [])).uniq,
533 :conditions => Query.merge_conditions(statement, options[:conditions]),
533 :conditions => Query.merge_conditions(statement, options[:conditions]),
534 :order => order_option,
534 :order => order_option,
535 :limit => options[:limit],
535 :limit => options[:limit],
536 :offset => options[:offset]
536 :offset => options[:offset]
537 rescue ::ActiveRecord::StatementInvalid => e
537 rescue ::ActiveRecord::StatementInvalid => e
538 raise StatementInvalid.new(e.message)
538 raise StatementInvalid.new(e.message)
539 end
539 end
540
540
541 # Returns the journals
541 # Returns the journals
542 # Valid options are :order, :offset, :limit
542 # Valid options are :order, :offset, :limit
543 def journals(options={})
543 def journals(options={})
544 Journal.find :all, :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}],
544 Journal.find :all, :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}],
545 :conditions => statement,
545 :conditions => statement,
546 :order => options[:order],
546 :order => options[:order],
547 :limit => options[:limit],
547 :limit => options[:limit],
548 :offset => options[:offset]
548 :offset => options[:offset]
549 rescue ::ActiveRecord::StatementInvalid => e
549 rescue ::ActiveRecord::StatementInvalid => e
550 raise StatementInvalid.new(e.message)
550 raise StatementInvalid.new(e.message)
551 end
551 end
552
552
553 # Returns the versions
553 # Returns the versions
554 # Valid options are :conditions
554 # Valid options are :conditions
555 def versions(options={})
555 def versions(options={})
556 Version.find :all, :include => :project,
556 Version.find :all, :include => :project,
557 :conditions => Query.merge_conditions(project_statement, options[:conditions])
557 :conditions => Query.merge_conditions(project_statement, options[:conditions])
558 rescue ::ActiveRecord::StatementInvalid => e
558 rescue ::ActiveRecord::StatementInvalid => e
559 raise StatementInvalid.new(e.message)
559 raise StatementInvalid.new(e.message)
560 end
560 end
561
561
562 private
562 private
563
563
564 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
564 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
565 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
565 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
566 sql = ''
566 sql = ''
567 case operator
567 case operator
568 when "="
568 when "="
569 if value.any?
569 if value.any?
570 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
570 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
571 else
571 else
572 # IN an empty set
572 # IN an empty set
573 sql = "1=0"
573 sql = "1=0"
574 end
574 end
575 when "!"
575 when "!"
576 if value.any?
576 if value.any?
577 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
577 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
578 else
578 else
579 # NOT IN an empty set
579 # NOT IN an empty set
580 sql = "1=1"
580 sql = "1=1"
581 end
581 end
582 when "!*"
582 when "!*"
583 sql = "#{db_table}.#{db_field} IS NULL"
583 sql = "#{db_table}.#{db_field} IS NULL"
584 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
584 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
585 when "*"
585 when "*"
586 sql = "#{db_table}.#{db_field} IS NOT NULL"
586 sql = "#{db_table}.#{db_field} IS NOT NULL"
587 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
587 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
588 when ">="
588 when ">="
589 sql = "#{db_table}.#{db_field} >= #{value.first.to_i}"
589 sql = "#{db_table}.#{db_field} >= #{value.first.to_i}"
590 when "<="
590 when "<="
591 sql = "#{db_table}.#{db_field} <= #{value.first.to_i}"
591 sql = "#{db_table}.#{db_field} <= #{value.first.to_i}"
592 when "o"
592 when "o"
593 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
593 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
594 when "c"
594 when "c"
595 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
595 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
596 when ">t-"
596 when ">t-"
597 sql = date_range_clause(db_table, db_field, - value.first.to_i, 0)
597 sql = date_range_clause(db_table, db_field, - value.first.to_i, 0)
598 when "<t-"
598 when "<t-"
599 sql = date_range_clause(db_table, db_field, nil, - value.first.to_i)
599 sql = date_range_clause(db_table, db_field, nil, - value.first.to_i)
600 when "t-"
600 when "t-"
601 sql = date_range_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
601 sql = date_range_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
602 when ">t+"
602 when ">t+"
603 sql = date_range_clause(db_table, db_field, value.first.to_i, nil)
603 sql = date_range_clause(db_table, db_field, value.first.to_i, nil)
604 when "<t+"
604 when "<t+"
605 sql = date_range_clause(db_table, db_field, 0, value.first.to_i)
605 sql = date_range_clause(db_table, db_field, 0, value.first.to_i)
606 when "t+"
606 when "t+"
607 sql = date_range_clause(db_table, db_field, value.first.to_i, value.first.to_i)
607 sql = date_range_clause(db_table, db_field, value.first.to_i, value.first.to_i)
608 when "t"
608 when "t"
609 sql = date_range_clause(db_table, db_field, 0, 0)
609 sql = date_range_clause(db_table, db_field, 0, 0)
610 when "w"
610 when "w"
611 from = l(:general_first_day_of_week) == '7' ?
611 from = l(:general_first_day_of_week) == '7' ?
612 # week starts on sunday
612 # week starts on sunday
613 ((Date.today.cwday == 7) ? Time.now.at_beginning_of_day : Time.now.at_beginning_of_week - 1.day) :
613 ((Date.today.cwday == 7) ? Time.now.at_beginning_of_day : Time.now.at_beginning_of_week - 1.day) :
614 # week starts on monday (Rails default)
614 # week starts on monday (Rails default)
615 Time.now.at_beginning_of_week
615 Time.now.at_beginning_of_week
616 sql = "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(from), connection.quoted_date(from + 7.days)]
616 sql = "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(from), connection.quoted_date(from + 7.days)]
617 when "~"
617 when "~"
618 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
618 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
619 when "!~"
619 when "!~"
620 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
620 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
621 end
621 end
622
622
623 return sql
623 return sql
624 end
624 end
625
625
626 def add_custom_fields_filters(custom_fields)
626 def add_custom_fields_filters(custom_fields)
627 @available_filters ||= {}
627 @available_filters ||= {}
628
628
629 custom_fields.select(&:is_filter?).each do |field|
629 custom_fields.select(&:is_filter?).each do |field|
630 case field.field_format
630 case field.field_format
631 when "text"
631 when "text"
632 options = { :type => :text, :order => 20 }
632 options = { :type => :text, :order => 20 }
633 when "list"
633 when "list"
634 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
634 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
635 when "date"
635 when "date"
636 options = { :type => :date, :order => 20 }
636 options = { :type => :date, :order => 20 }
637 when "bool"
637 when "bool"
638 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
638 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
639 when "user", "version"
640 next unless project
641 options = { :type => :list_optional, :values => field.possible_values_options(project), :order => 20}
639 else
642 else
640 options = { :type => :string, :order => 20 }
643 options = { :type => :string, :order => 20 }
641 end
644 end
642 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
645 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
643 end
646 end
644 end
647 end
645
648
646 # Returns a SQL clause for a date or datetime field.
649 # Returns a SQL clause for a date or datetime field.
647 def date_range_clause(table, field, from, to)
650 def date_range_clause(table, field, from, to)
648 s = []
651 s = []
649 if from
652 if from
650 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date((Date.yesterday + from).to_time.end_of_day)])
653 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date((Date.yesterday + from).to_time.end_of_day)])
651 end
654 end
652 if to
655 if to
653 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date((Date.today + to).to_time.end_of_day)])
656 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date((Date.today + to).to_time.end_of_day)])
654 end
657 end
655 s.join(' AND ')
658 s.join(' AND ')
656 end
659 end
657 end
660 end
@@ -1,106 +1,114
1 <%= error_messages_for 'custom_field' %>
1 <%= error_messages_for 'custom_field' %>
2
2
3 <script type="text/javascript">
3 <script type="text/javascript">
4 //<![CDATA[
4 //<![CDATA[
5 function toggle_custom_field_format() {
5 function toggle_custom_field_format() {
6 format = $("custom_field_field_format");
6 format = $("custom_field_field_format");
7 p_length = $("custom_field_min_length");
7 p_length = $("custom_field_min_length");
8 p_regexp = $("custom_field_regexp");
8 p_regexp = $("custom_field_regexp");
9 p_values = $("custom_field_possible_values");
9 p_values = $("custom_field_possible_values");
10 p_searchable = $("custom_field_searchable");
10 p_searchable = $("custom_field_searchable");
11 p_default = $("custom_field_default_value");
11 p_default = $("custom_field_default_value");
12
12
13 p_default.setAttribute('type','text');
13 p_default.setAttribute('type','text');
14 Element.show(p_default.parentNode);
14 Element.show(p_default.parentNode);
15
15
16 switch (format.value) {
16 switch (format.value) {
17 case "list":
17 case "list":
18 Element.hide(p_length.parentNode);
18 Element.hide(p_length.parentNode);
19 Element.hide(p_regexp.parentNode);
19 Element.hide(p_regexp.parentNode);
20 if (p_searchable) Element.show(p_searchable.parentNode);
20 if (p_searchable) Element.show(p_searchable.parentNode);
21 Element.show(p_values.parentNode);
21 Element.show(p_values.parentNode);
22 break;
22 break;
23 case "bool":
23 case "bool":
24 p_default.setAttribute('type','checkbox');
24 p_default.setAttribute('type','checkbox');
25 Element.hide(p_length.parentNode);
25 Element.hide(p_length.parentNode);
26 Element.hide(p_regexp.parentNode);
26 Element.hide(p_regexp.parentNode);
27 if (p_searchable) Element.hide(p_searchable.parentNode);
27 if (p_searchable) Element.hide(p_searchable.parentNode);
28 Element.hide(p_values.parentNode);
28 Element.hide(p_values.parentNode);
29 break;
29 break;
30 case "date":
30 case "date":
31 Element.hide(p_length.parentNode);
31 Element.hide(p_length.parentNode);
32 Element.hide(p_regexp.parentNode);
32 Element.hide(p_regexp.parentNode);
33 if (p_searchable) Element.hide(p_searchable.parentNode);
33 if (p_searchable) Element.hide(p_searchable.parentNode);
34 Element.hide(p_values.parentNode);
34 Element.hide(p_values.parentNode);
35 break;
35 break;
36 case "float":
36 case "float":
37 case "int":
37 case "int":
38 Element.show(p_length.parentNode);
38 Element.show(p_length.parentNode);
39 Element.show(p_regexp.parentNode);
39 Element.show(p_regexp.parentNode);
40 if (p_searchable) Element.hide(p_searchable.parentNode);
40 if (p_searchable) Element.hide(p_searchable.parentNode);
41 Element.hide(p_values.parentNode);
41 Element.hide(p_values.parentNode);
42 break;
42 break;
43 case "user":
44 case "version":
45 Element.hide(p_length.parentNode);
46 Element.hide(p_regexp.parentNode);
47 if (p_searchable) Element.hide(p_searchable.parentNode);
48 Element.hide(p_values.parentNode);
49 Element.hide(p_default.parentNode);
50 break;
43 default:
51 default:
44 Element.show(p_length.parentNode);
52 Element.show(p_length.parentNode);
45 Element.show(p_regexp.parentNode);
53 Element.show(p_regexp.parentNode);
46 if (p_searchable) Element.show(p_searchable.parentNode);
54 if (p_searchable) Element.show(p_searchable.parentNode);
47 Element.hide(p_values.parentNode);
55 Element.hide(p_values.parentNode);
48 break;
56 break;
49 }
57 }
50 }
58 }
51
59
52 //]]>
60 //]]>
53 </script>
61 </script>
54
62
55 <div class="box">
63 <div class="box">
56 <p><%= f.text_field :name, :required => true %></p>
64 <p><%= f.text_field :name, :required => true %></p>
57 <p><%= f.select :field_format, custom_field_formats_for_select, {}, :onchange => "toggle_custom_field_format();",
65 <p><%= f.select :field_format, custom_field_formats_for_select(@custom_field), {}, :onchange => "toggle_custom_field_format();",
58 :disabled => !@custom_field.new_record? %></p>
66 :disabled => !@custom_field.new_record? %></p>
59 <p><label for="custom_field_min_length"><%=l(:label_min_max_length)%></label>
67 <p><label for="custom_field_min_length"><%=l(:label_min_max_length)%></label>
60 <%= f.text_field :min_length, :size => 5, :no_label => true %> -
68 <%= f.text_field :min_length, :size => 5, :no_label => true %> -
61 <%= f.text_field :max_length, :size => 5, :no_label => true %><br>(<%=l(:text_min_max_length_info)%>)</p>
69 <%= f.text_field :max_length, :size => 5, :no_label => true %><br>(<%=l(:text_min_max_length_info)%>)</p>
62 <p><%= f.text_field :regexp, :size => 50 %><br>(<%=l(:text_regexp_info)%>)</p>
70 <p><%= f.text_field :regexp, :size => 50 %><br>(<%=l(:text_regexp_info)%>)</p>
63 <p>
71 <p>
64 <%= f.text_area :possible_values, :value => @custom_field.possible_values.to_a.join("\n"), :rows => 15 %>
72 <%= f.text_area :possible_values, :value => @custom_field.possible_values.to_a.join("\n"), :rows => 15 %>
65 <br /><em><%= l(:text_custom_field_possible_values_info) %></em>
73 <br /><em><%= l(:text_custom_field_possible_values_info) %></em>
66 </p>
74 </p>
67 <p><%= @custom_field.field_format == 'bool' ? f.check_box(:default_value) : f.text_field(:default_value) %></p>
75 <p><%= @custom_field.field_format == 'bool' ? f.check_box(:default_value) : f.text_field(:default_value) %></p>
68 <%= call_hook(:view_custom_fields_form_upper_box, :custom_field => @custom_field, :form => f) %>
76 <%= call_hook(:view_custom_fields_form_upper_box, :custom_field => @custom_field, :form => f) %>
69 </div>
77 </div>
70
78
71 <div class="box">
79 <div class="box">
72 <% case @custom_field.class.name
80 <% case @custom_field.class.name
73 when "IssueCustomField" %>
81 when "IssueCustomField" %>
74
82
75 <fieldset><legend><%=l(:label_tracker_plural)%></legend>
83 <fieldset><legend><%=l(:label_tracker_plural)%></legend>
76 <% for tracker in @trackers %>
84 <% for tracker in @trackers %>
77 <%= check_box_tag "custom_field[tracker_ids][]", tracker.id, (@custom_field.trackers.include? tracker) %> <%= tracker.name %>
85 <%= check_box_tag "custom_field[tracker_ids][]", tracker.id, (@custom_field.trackers.include? tracker) %> <%= tracker.name %>
78 <% end %>
86 <% end %>
79 <%= hidden_field_tag "custom_field[tracker_ids][]", '' %>
87 <%= hidden_field_tag "custom_field[tracker_ids][]", '' %>
80 </fieldset>
88 </fieldset>
81 &nbsp;
89 &nbsp;
82 <p><%= f.check_box :is_required %></p>
90 <p><%= f.check_box :is_required %></p>
83 <p><%= f.check_box :is_for_all %></p>
91 <p><%= f.check_box :is_for_all %></p>
84 <p><%= f.check_box :is_filter %></p>
92 <p><%= f.check_box :is_filter %></p>
85 <p><%= f.check_box :searchable %></p>
93 <p><%= f.check_box :searchable %></p>
86
94
87 <% when "UserCustomField" %>
95 <% when "UserCustomField" %>
88 <p><%= f.check_box :is_required %></p>
96 <p><%= f.check_box :is_required %></p>
89 <p><%= f.check_box :visible %></p>
97 <p><%= f.check_box :visible %></p>
90 <p><%= f.check_box :editable %></p>
98 <p><%= f.check_box :editable %></p>
91
99
92 <% when "ProjectCustomField" %>
100 <% when "ProjectCustomField" %>
93 <p><%= f.check_box :is_required %></p>
101 <p><%= f.check_box :is_required %></p>
94 <p><%= f.check_box :visible %></p>
102 <p><%= f.check_box :visible %></p>
95 <p><%= f.check_box :searchable %></p>
103 <p><%= f.check_box :searchable %></p>
96
104
97 <% when "TimeEntryCustomField" %>
105 <% when "TimeEntryCustomField" %>
98 <p><%= f.check_box :is_required %></p>
106 <p><%= f.check_box :is_required %></p>
99
107
100 <% else %>
108 <% else %>
101 <p><%= f.check_box :is_required %></p>
109 <p><%= f.check_box :is_required %></p>
102
110
103 <% end %>
111 <% end %>
104 <%= call_hook(:"view_custom_fields_form_#{@custom_field.type.to_s.underscore}", :custom_field => @custom_field, :form => f) %>
112 <%= call_hook(:"view_custom_fields_form_#{@custom_field.type.to_s.underscore}", :custom_field => @custom_field, :form => f) %>
105 </div>
113 </div>
106 <%= javascript_tag "toggle_custom_field_format();" %>
114 <%= javascript_tag "toggle_custom_field_format();" %>
@@ -1,233 +1,235
1 require 'redmine/access_control'
1 require 'redmine/access_control'
2 require 'redmine/menu_manager'
2 require 'redmine/menu_manager'
3 require 'redmine/activity'
3 require 'redmine/activity'
4 require 'redmine/search'
4 require 'redmine/search'
5 require 'redmine/custom_field_format'
5 require 'redmine/custom_field_format'
6 require 'redmine/mime_type'
6 require 'redmine/mime_type'
7 require 'redmine/core_ext'
7 require 'redmine/core_ext'
8 require 'redmine/themes'
8 require 'redmine/themes'
9 require 'redmine/hook'
9 require 'redmine/hook'
10 require 'redmine/plugin'
10 require 'redmine/plugin'
11 require 'redmine/notifiable'
11 require 'redmine/notifiable'
12 require 'redmine/wiki_formatting'
12 require 'redmine/wiki_formatting'
13 require 'redmine/scm/base'
13 require 'redmine/scm/base'
14
14
15 begin
15 begin
16 require_library_or_gem 'RMagick' unless Object.const_defined?(:Magick)
16 require_library_or_gem 'RMagick' unless Object.const_defined?(:Magick)
17 rescue LoadError
17 rescue LoadError
18 # RMagick is not available
18 # RMagick is not available
19 end
19 end
20
20
21 if RUBY_VERSION < '1.9'
21 if RUBY_VERSION < '1.9'
22 require 'faster_csv'
22 require 'faster_csv'
23 else
23 else
24 require 'csv'
24 require 'csv'
25 FCSV = CSV
25 FCSV = CSV
26 end
26 end
27
27
28 Redmine::Scm::Base.add "Subversion"
28 Redmine::Scm::Base.add "Subversion"
29 Redmine::Scm::Base.add "Darcs"
29 Redmine::Scm::Base.add "Darcs"
30 Redmine::Scm::Base.add "Mercurial"
30 Redmine::Scm::Base.add "Mercurial"
31 Redmine::Scm::Base.add "Cvs"
31 Redmine::Scm::Base.add "Cvs"
32 Redmine::Scm::Base.add "Bazaar"
32 Redmine::Scm::Base.add "Bazaar"
33 Redmine::Scm::Base.add "Git"
33 Redmine::Scm::Base.add "Git"
34 Redmine::Scm::Base.add "Filesystem"
34 Redmine::Scm::Base.add "Filesystem"
35
35
36 Redmine::CustomFieldFormat.map do |fields|
36 Redmine::CustomFieldFormat.map do |fields|
37 fields.register Redmine::CustomFieldFormat.new('string', :label => :label_string, :order => 1)
37 fields.register Redmine::CustomFieldFormat.new('string', :label => :label_string, :order => 1)
38 fields.register Redmine::CustomFieldFormat.new('text', :label => :label_text, :order => 2)
38 fields.register Redmine::CustomFieldFormat.new('text', :label => :label_text, :order => 2)
39 fields.register Redmine::CustomFieldFormat.new('int', :label => :label_integer, :order => 3)
39 fields.register Redmine::CustomFieldFormat.new('int', :label => :label_integer, :order => 3)
40 fields.register Redmine::CustomFieldFormat.new('float', :label => :label_float, :order => 4)
40 fields.register Redmine::CustomFieldFormat.new('float', :label => :label_float, :order => 4)
41 fields.register Redmine::CustomFieldFormat.new('list', :label => :label_list, :order => 5)
41 fields.register Redmine::CustomFieldFormat.new('list', :label => :label_list, :order => 5)
42 fields.register Redmine::CustomFieldFormat.new('date', :label => :label_date, :order => 6)
42 fields.register Redmine::CustomFieldFormat.new('date', :label => :label_date, :order => 6)
43 fields.register Redmine::CustomFieldFormat.new('bool', :label => :label_boolean, :order => 7)
43 fields.register Redmine::CustomFieldFormat.new('bool', :label => :label_boolean, :order => 7)
44 fields.register Redmine::CustomFieldFormat.new('user', :label => :label_user, :only => %w(Issue TimeEntry Version Project), :edit_as => 'list', :order => 8)
45 fields.register Redmine::CustomFieldFormat.new('version', :label => :label_version, :only => %w(Issue TimeEntry Version Project), :edit_as => 'list', :order => 9)
44 end
46 end
45
47
46 # Permissions
48 # Permissions
47 Redmine::AccessControl.map do |map|
49 Redmine::AccessControl.map do |map|
48 map.permission :view_project, {:projects => [:show], :activities => [:index]}, :public => true
50 map.permission :view_project, {:projects => [:show], :activities => [:index]}, :public => true
49 map.permission :search_project, {:search => :index}, :public => true
51 map.permission :search_project, {:search => :index}, :public => true
50 map.permission :add_project, {:projects => [:new, :create]}, :require => :loggedin
52 map.permission :add_project, {:projects => [:new, :create]}, :require => :loggedin
51 map.permission :edit_project, {:projects => [:settings, :edit, :update]}, :require => :member
53 map.permission :edit_project, {:projects => [:settings, :edit, :update]}, :require => :member
52 map.permission :select_project_modules, {:projects => :modules}, :require => :member
54 map.permission :select_project_modules, {:projects => :modules}, :require => :member
53 map.permission :manage_members, {:projects => :settings, :members => [:new, :edit, :destroy, :autocomplete_for_member]}, :require => :member
55 map.permission :manage_members, {:projects => :settings, :members => [:new, :edit, :destroy, :autocomplete_for_member]}, :require => :member
54 map.permission :manage_versions, {:projects => :settings, :versions => [:new, :create, :edit, :update, :close_completed, :destroy]}, :require => :member
56 map.permission :manage_versions, {:projects => :settings, :versions => [:new, :create, :edit, :update, :close_completed, :destroy]}, :require => :member
55 map.permission :add_subprojects, {:projects => [:new, :create]}, :require => :member
57 map.permission :add_subprojects, {:projects => [:new, :create]}, :require => :member
56
58
57 map.project_module :issue_tracking do |map|
59 map.project_module :issue_tracking do |map|
58 # Issue categories
60 # Issue categories
59 map.permission :manage_categories, {:projects => :settings, :issue_categories => [:new, :edit, :destroy]}, :require => :member
61 map.permission :manage_categories, {:projects => :settings, :issue_categories => [:new, :edit, :destroy]}, :require => :member
60 # Issues
62 # Issues
61 map.permission :view_issues, {:issues => [:index, :show],
63 map.permission :view_issues, {:issues => [:index, :show],
62 :auto_complete => [:issues],
64 :auto_complete => [:issues],
63 :context_menus => [:issues],
65 :context_menus => [:issues],
64 :versions => [:index, :show, :status_by],
66 :versions => [:index, :show, :status_by],
65 :journals => [:index, :diff],
67 :journals => [:index, :diff],
66 :queries => :index,
68 :queries => :index,
67 :reports => [:issue_report, :issue_report_details]}
69 :reports => [:issue_report, :issue_report_details]}
68 map.permission :add_issues, {:issues => [:new, :create, :update_form]}
70 map.permission :add_issues, {:issues => [:new, :create, :update_form]}
69 map.permission :edit_issues, {:issues => [:edit, :update, :bulk_edit, :bulk_update, :update_form], :journals => [:new]}
71 map.permission :edit_issues, {:issues => [:edit, :update, :bulk_edit, :bulk_update, :update_form], :journals => [:new]}
70 map.permission :manage_issue_relations, {:issue_relations => [:new, :destroy]}
72 map.permission :manage_issue_relations, {:issue_relations => [:new, :destroy]}
71 map.permission :manage_subtasks, {}
73 map.permission :manage_subtasks, {}
72 map.permission :add_issue_notes, {:issues => [:edit, :update], :journals => [:new]}
74 map.permission :add_issue_notes, {:issues => [:edit, :update], :journals => [:new]}
73 map.permission :edit_issue_notes, {:journals => :edit}, :require => :loggedin
75 map.permission :edit_issue_notes, {:journals => :edit}, :require => :loggedin
74 map.permission :edit_own_issue_notes, {:journals => :edit}, :require => :loggedin
76 map.permission :edit_own_issue_notes, {:journals => :edit}, :require => :loggedin
75 map.permission :move_issues, {:issue_moves => [:new, :create]}, :require => :loggedin
77 map.permission :move_issues, {:issue_moves => [:new, :create]}, :require => :loggedin
76 map.permission :delete_issues, {:issues => :destroy}, :require => :member
78 map.permission :delete_issues, {:issues => :destroy}, :require => :member
77 # Queries
79 # Queries
78 map.permission :manage_public_queries, {:queries => [:new, :edit, :destroy]}, :require => :member
80 map.permission :manage_public_queries, {:queries => [:new, :edit, :destroy]}, :require => :member
79 map.permission :save_queries, {:queries => [:new, :edit, :destroy]}, :require => :loggedin
81 map.permission :save_queries, {:queries => [:new, :edit, :destroy]}, :require => :loggedin
80 # Watchers
82 # Watchers
81 map.permission :view_issue_watchers, {}
83 map.permission :view_issue_watchers, {}
82 map.permission :add_issue_watchers, {:watchers => :new}
84 map.permission :add_issue_watchers, {:watchers => :new}
83 map.permission :delete_issue_watchers, {:watchers => :destroy}
85 map.permission :delete_issue_watchers, {:watchers => :destroy}
84 end
86 end
85
87
86 map.project_module :time_tracking do |map|
88 map.project_module :time_tracking do |map|
87 map.permission :log_time, {:timelog => [:new, :create, :edit, :update]}, :require => :loggedin
89 map.permission :log_time, {:timelog => [:new, :create, :edit, :update]}, :require => :loggedin
88 map.permission :view_time_entries, :timelog => [:index, :show], :time_entry_reports => [:report]
90 map.permission :view_time_entries, :timelog => [:index, :show], :time_entry_reports => [:report]
89 map.permission :edit_time_entries, {:timelog => [:new, :create, :edit, :update, :destroy]}, :require => :member
91 map.permission :edit_time_entries, {:timelog => [:new, :create, :edit, :update, :destroy]}, :require => :member
90 map.permission :edit_own_time_entries, {:timelog => [:new, :create, :edit, :update, :destroy]}, :require => :loggedin
92 map.permission :edit_own_time_entries, {:timelog => [:new, :create, :edit, :update, :destroy]}, :require => :loggedin
91 map.permission :manage_project_activities, {:project_enumerations => [:update, :destroy]}, :require => :member
93 map.permission :manage_project_activities, {:project_enumerations => [:update, :destroy]}, :require => :member
92 end
94 end
93
95
94 map.project_module :news do |map|
96 map.project_module :news do |map|
95 map.permission :manage_news, {:news => [:new, :create, :edit, :update, :destroy], :comments => [:destroy]}, :require => :member
97 map.permission :manage_news, {:news => [:new, :create, :edit, :update, :destroy], :comments => [:destroy]}, :require => :member
96 map.permission :view_news, {:news => [:index, :show]}, :public => true
98 map.permission :view_news, {:news => [:index, :show]}, :public => true
97 map.permission :comment_news, {:comments => :create}
99 map.permission :comment_news, {:comments => :create}
98 end
100 end
99
101
100 map.project_module :documents do |map|
102 map.project_module :documents do |map|
101 map.permission :manage_documents, {:documents => [:new, :edit, :destroy, :add_attachment]}, :require => :loggedin
103 map.permission :manage_documents, {:documents => [:new, :edit, :destroy, :add_attachment]}, :require => :loggedin
102 map.permission :view_documents, :documents => [:index, :show, :download]
104 map.permission :view_documents, :documents => [:index, :show, :download]
103 end
105 end
104
106
105 map.project_module :files do |map|
107 map.project_module :files do |map|
106 map.permission :manage_files, {:files => [:new, :create]}, :require => :loggedin
108 map.permission :manage_files, {:files => [:new, :create]}, :require => :loggedin
107 map.permission :view_files, :files => :index, :versions => :download
109 map.permission :view_files, :files => :index, :versions => :download
108 end
110 end
109
111
110 map.project_module :wiki do |map|
112 map.project_module :wiki do |map|
111 map.permission :manage_wiki, {:wikis => [:edit, :destroy]}, :require => :member
113 map.permission :manage_wiki, {:wikis => [:edit, :destroy]}, :require => :member
112 map.permission :rename_wiki_pages, {:wiki => :rename}, :require => :member
114 map.permission :rename_wiki_pages, {:wiki => :rename}, :require => :member
113 map.permission :delete_wiki_pages, {:wiki => :destroy}, :require => :member
115 map.permission :delete_wiki_pages, {:wiki => :destroy}, :require => :member
114 map.permission :view_wiki_pages, :wiki => [:index, :show, :special, :date_index]
116 map.permission :view_wiki_pages, :wiki => [:index, :show, :special, :date_index]
115 map.permission :export_wiki_pages, :wiki => [:export]
117 map.permission :export_wiki_pages, :wiki => [:export]
116 map.permission :view_wiki_edits, :wiki => [:history, :diff, :annotate]
118 map.permission :view_wiki_edits, :wiki => [:history, :diff, :annotate]
117 map.permission :edit_wiki_pages, :wiki => [:edit, :update, :preview, :add_attachment]
119 map.permission :edit_wiki_pages, :wiki => [:edit, :update, :preview, :add_attachment]
118 map.permission :delete_wiki_pages_attachments, {}
120 map.permission :delete_wiki_pages_attachments, {}
119 map.permission :protect_wiki_pages, {:wiki => :protect}, :require => :member
121 map.permission :protect_wiki_pages, {:wiki => :protect}, :require => :member
120 end
122 end
121
123
122 map.project_module :repository do |map|
124 map.project_module :repository do |map|
123 map.permission :manage_repository, {:repositories => [:edit, :committers, :destroy]}, :require => :member
125 map.permission :manage_repository, {:repositories => [:edit, :committers, :destroy]}, :require => :member
124 map.permission :browse_repository, :repositories => [:show, :browse, :entry, :annotate, :changes, :diff, :stats, :graph]
126 map.permission :browse_repository, :repositories => [:show, :browse, :entry, :annotate, :changes, :diff, :stats, :graph]
125 map.permission :view_changesets, :repositories => [:show, :revisions, :revision]
127 map.permission :view_changesets, :repositories => [:show, :revisions, :revision]
126 map.permission :commit_access, {}
128 map.permission :commit_access, {}
127 end
129 end
128
130
129 map.project_module :boards do |map|
131 map.project_module :boards do |map|
130 map.permission :manage_boards, {:boards => [:new, :edit, :destroy]}, :require => :member
132 map.permission :manage_boards, {:boards => [:new, :edit, :destroy]}, :require => :member
131 map.permission :view_messages, {:boards => [:index, :show], :messages => [:show]}, :public => true
133 map.permission :view_messages, {:boards => [:index, :show], :messages => [:show]}, :public => true
132 map.permission :add_messages, {:messages => [:new, :reply, :quote]}
134 map.permission :add_messages, {:messages => [:new, :reply, :quote]}
133 map.permission :edit_messages, {:messages => :edit}, :require => :member
135 map.permission :edit_messages, {:messages => :edit}, :require => :member
134 map.permission :edit_own_messages, {:messages => :edit}, :require => :loggedin
136 map.permission :edit_own_messages, {:messages => :edit}, :require => :loggedin
135 map.permission :delete_messages, {:messages => :destroy}, :require => :member
137 map.permission :delete_messages, {:messages => :destroy}, :require => :member
136 map.permission :delete_own_messages, {:messages => :destroy}, :require => :loggedin
138 map.permission :delete_own_messages, {:messages => :destroy}, :require => :loggedin
137 end
139 end
138
140
139 map.project_module :calendar do |map|
141 map.project_module :calendar do |map|
140 map.permission :view_calendar, :calendars => [:show, :update]
142 map.permission :view_calendar, :calendars => [:show, :update]
141 end
143 end
142
144
143 map.project_module :gantt do |map|
145 map.project_module :gantt do |map|
144 map.permission :view_gantt, :gantts => [:show, :update]
146 map.permission :view_gantt, :gantts => [:show, :update]
145 end
147 end
146 end
148 end
147
149
148 Redmine::MenuManager.map :top_menu do |menu|
150 Redmine::MenuManager.map :top_menu do |menu|
149 menu.push :home, :home_path
151 menu.push :home, :home_path
150 menu.push :my_page, { :controller => 'my', :action => 'page' }, :if => Proc.new { User.current.logged? }
152 menu.push :my_page, { :controller => 'my', :action => 'page' }, :if => Proc.new { User.current.logged? }
151 menu.push :projects, { :controller => 'projects', :action => 'index' }, :caption => :label_project_plural
153 menu.push :projects, { :controller => 'projects', :action => 'index' }, :caption => :label_project_plural
152 menu.push :administration, { :controller => 'admin', :action => 'index' }, :if => Proc.new { User.current.admin? }, :last => true
154 menu.push :administration, { :controller => 'admin', :action => 'index' }, :if => Proc.new { User.current.admin? }, :last => true
153 menu.push :help, Redmine::Info.help_url, :last => true
155 menu.push :help, Redmine::Info.help_url, :last => true
154 end
156 end
155
157
156 Redmine::MenuManager.map :account_menu do |menu|
158 Redmine::MenuManager.map :account_menu do |menu|
157 menu.push :login, :signin_path, :if => Proc.new { !User.current.logged? }
159 menu.push :login, :signin_path, :if => Proc.new { !User.current.logged? }
158 menu.push :register, { :controller => 'account', :action => 'register' }, :if => Proc.new { !User.current.logged? && Setting.self_registration? }
160 menu.push :register, { :controller => 'account', :action => 'register' }, :if => Proc.new { !User.current.logged? && Setting.self_registration? }
159 menu.push :my_account, { :controller => 'my', :action => 'account' }, :if => Proc.new { User.current.logged? }
161 menu.push :my_account, { :controller => 'my', :action => 'account' }, :if => Proc.new { User.current.logged? }
160 menu.push :logout, :signout_path, :if => Proc.new { User.current.logged? }
162 menu.push :logout, :signout_path, :if => Proc.new { User.current.logged? }
161 end
163 end
162
164
163 Redmine::MenuManager.map :application_menu do |menu|
165 Redmine::MenuManager.map :application_menu do |menu|
164 # Empty
166 # Empty
165 end
167 end
166
168
167 Redmine::MenuManager.map :admin_menu do |menu|
169 Redmine::MenuManager.map :admin_menu do |menu|
168 menu.push :projects, {:controller => 'admin', :action => 'projects'}, :caption => :label_project_plural
170 menu.push :projects, {:controller => 'admin', :action => 'projects'}, :caption => :label_project_plural
169 menu.push :users, {:controller => 'users'}, :caption => :label_user_plural
171 menu.push :users, {:controller => 'users'}, :caption => :label_user_plural
170 menu.push :groups, {:controller => 'groups'}, :caption => :label_group_plural
172 menu.push :groups, {:controller => 'groups'}, :caption => :label_group_plural
171 menu.push :roles, {:controller => 'roles'}, :caption => :label_role_and_permissions
173 menu.push :roles, {:controller => 'roles'}, :caption => :label_role_and_permissions
172 menu.push :trackers, {:controller => 'trackers'}, :caption => :label_tracker_plural
174 menu.push :trackers, {:controller => 'trackers'}, :caption => :label_tracker_plural
173 menu.push :issue_statuses, {:controller => 'issue_statuses'}, :caption => :label_issue_status_plural,
175 menu.push :issue_statuses, {:controller => 'issue_statuses'}, :caption => :label_issue_status_plural,
174 :html => {:class => 'issue_statuses'}
176 :html => {:class => 'issue_statuses'}
175 menu.push :workflows, {:controller => 'workflows', :action => 'edit'}, :caption => :label_workflow
177 menu.push :workflows, {:controller => 'workflows', :action => 'edit'}, :caption => :label_workflow
176 menu.push :custom_fields, {:controller => 'custom_fields'}, :caption => :label_custom_field_plural,
178 menu.push :custom_fields, {:controller => 'custom_fields'}, :caption => :label_custom_field_plural,
177 :html => {:class => 'custom_fields'}
179 :html => {:class => 'custom_fields'}
178 menu.push :enumerations, {:controller => 'enumerations'}
180 menu.push :enumerations, {:controller => 'enumerations'}
179 menu.push :settings, {:controller => 'settings'}
181 menu.push :settings, {:controller => 'settings'}
180 menu.push :ldap_authentication, {:controller => 'ldap_auth_sources', :action => 'index'},
182 menu.push :ldap_authentication, {:controller => 'ldap_auth_sources', :action => 'index'},
181 :html => {:class => 'server_authentication'}
183 :html => {:class => 'server_authentication'}
182 menu.push :plugins, {:controller => 'admin', :action => 'plugins'}, :last => true
184 menu.push :plugins, {:controller => 'admin', :action => 'plugins'}, :last => true
183 menu.push :info, {:controller => 'admin', :action => 'info'}, :caption => :label_information_plural, :last => true
185 menu.push :info, {:controller => 'admin', :action => 'info'}, :caption => :label_information_plural, :last => true
184 end
186 end
185
187
186 Redmine::MenuManager.map :project_menu do |menu|
188 Redmine::MenuManager.map :project_menu do |menu|
187 menu.push :overview, { :controller => 'projects', :action => 'show' }
189 menu.push :overview, { :controller => 'projects', :action => 'show' }
188 menu.push :activity, { :controller => 'activities', :action => 'index' }
190 menu.push :activity, { :controller => 'activities', :action => 'index' }
189 menu.push :roadmap, { :controller => 'versions', :action => 'index' }, :param => :project_id,
191 menu.push :roadmap, { :controller => 'versions', :action => 'index' }, :param => :project_id,
190 :if => Proc.new { |p| p.shared_versions.any? }
192 :if => Proc.new { |p| p.shared_versions.any? }
191 menu.push :issues, { :controller => 'issues', :action => 'index' }, :param => :project_id, :caption => :label_issue_plural
193 menu.push :issues, { :controller => 'issues', :action => 'index' }, :param => :project_id, :caption => :label_issue_plural
192 menu.push :new_issue, { :controller => 'issues', :action => 'new' }, :param => :project_id, :caption => :label_issue_new,
194 menu.push :new_issue, { :controller => 'issues', :action => 'new' }, :param => :project_id, :caption => :label_issue_new,
193 :html => { :accesskey => Redmine::AccessKeys.key_for(:new_issue) }
195 :html => { :accesskey => Redmine::AccessKeys.key_for(:new_issue) }
194 menu.push :gantt, { :controller => 'gantts', :action => 'show' }, :param => :project_id, :caption => :label_gantt
196 menu.push :gantt, { :controller => 'gantts', :action => 'show' }, :param => :project_id, :caption => :label_gantt
195 menu.push :calendar, { :controller => 'calendars', :action => 'show' }, :param => :project_id, :caption => :label_calendar
197 menu.push :calendar, { :controller => 'calendars', :action => 'show' }, :param => :project_id, :caption => :label_calendar
196 menu.push :news, { :controller => 'news', :action => 'index' }, :param => :project_id, :caption => :label_news_plural
198 menu.push :news, { :controller => 'news', :action => 'index' }, :param => :project_id, :caption => :label_news_plural
197 menu.push :documents, { :controller => 'documents', :action => 'index' }, :param => :project_id, :caption => :label_document_plural
199 menu.push :documents, { :controller => 'documents', :action => 'index' }, :param => :project_id, :caption => :label_document_plural
198 menu.push :wiki, { :controller => 'wiki', :action => 'show', :id => nil }, :param => :project_id,
200 menu.push :wiki, { :controller => 'wiki', :action => 'show', :id => nil }, :param => :project_id,
199 :if => Proc.new { |p| p.wiki && !p.wiki.new_record? }
201 :if => Proc.new { |p| p.wiki && !p.wiki.new_record? }
200 menu.push :boards, { :controller => 'boards', :action => 'index', :id => nil }, :param => :project_id,
202 menu.push :boards, { :controller => 'boards', :action => 'index', :id => nil }, :param => :project_id,
201 :if => Proc.new { |p| p.boards.any? }, :caption => :label_board_plural
203 :if => Proc.new { |p| p.boards.any? }, :caption => :label_board_plural
202 menu.push :files, { :controller => 'files', :action => 'index' }, :caption => :label_file_plural, :param => :project_id
204 menu.push :files, { :controller => 'files', :action => 'index' }, :caption => :label_file_plural, :param => :project_id
203 menu.push :repository, { :controller => 'repositories', :action => 'show' },
205 menu.push :repository, { :controller => 'repositories', :action => 'show' },
204 :if => Proc.new { |p| p.repository && !p.repository.new_record? }
206 :if => Proc.new { |p| p.repository && !p.repository.new_record? }
205 menu.push :settings, { :controller => 'projects', :action => 'settings' }, :last => true
207 menu.push :settings, { :controller => 'projects', :action => 'settings' }, :last => true
206 end
208 end
207
209
208 Redmine::Activity.map do |activity|
210 Redmine::Activity.map do |activity|
209 activity.register :issues, :class_name => %w(Issue Journal)
211 activity.register :issues, :class_name => %w(Issue Journal)
210 activity.register :changesets
212 activity.register :changesets
211 activity.register :news
213 activity.register :news
212 activity.register :documents, :class_name => %w(Document Attachment)
214 activity.register :documents, :class_name => %w(Document Attachment)
213 activity.register :files, :class_name => 'Attachment'
215 activity.register :files, :class_name => 'Attachment'
214 activity.register :wiki_edits, :class_name => 'WikiContent::Version', :default => false
216 activity.register :wiki_edits, :class_name => 'WikiContent::Version', :default => false
215 activity.register :messages, :default => false
217 activity.register :messages, :default => false
216 activity.register :time_entries, :default => false
218 activity.register :time_entries, :default => false
217 end
219 end
218
220
219 Redmine::Search.map do |search|
221 Redmine::Search.map do |search|
220 search.register :issues
222 search.register :issues
221 search.register :news
223 search.register :news
222 search.register :documents
224 search.register :documents
223 search.register :changesets
225 search.register :changesets
224 search.register :wiki_pages
226 search.register :wiki_pages
225 search.register :messages
227 search.register :messages
226 search.register :projects
228 search.register :projects
227 end
229 end
228
230
229 Redmine::WikiFormatting.map do |format|
231 Redmine::WikiFormatting.map do |format|
230 format.register :textile, Redmine::WikiFormatting::Textile::Formatter, Redmine::WikiFormatting::Textile::Helper
232 format.register :textile, Redmine::WikiFormatting::Textile::Formatter, Redmine::WikiFormatting::Textile::Helper
231 end
233 end
232
234
233 ActionView::Template.register_template_handler :rsb, Redmine::Views::ApiTemplateHandler
235 ActionView::Template.register_template_handler :rsb, Redmine::Views::ApiTemplateHandler
@@ -1,101 +1,104
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2009 Jean-Philippe Lang
2 # Copyright (C) 2006-2009 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 class CustomFieldFormat
19 class CustomFieldFormat
20 include Redmine::I18n
20 include Redmine::I18n
21
21
22 cattr_accessor :available
22 cattr_accessor :available
23 @@available = {}
23 @@available = {}
24
24
25 attr_accessor :name, :order, :label
25 attr_accessor :name, :order, :label, :edit_as, :class_names
26
26
27 def initialize(name, options={})
27 def initialize(name, options={})
28 self.name = name
28 self.name = name
29 self.label = options[:label]
29 self.label = options[:label]
30 self.order = options[:order]
30 self.order = options[:order]
31 self.edit_as = options[:edit_as] || name
32 self.class_names = options[:only]
31 end
33 end
32
34
33 def format(value)
35 def format(value)
34 send "format_as_#{name}", value
36 send "format_as_#{name}", value
35 end
37 end
36
38
37 def format_as_date(value)
39 def format_as_date(value)
38 begin; format_date(value.to_date); rescue; value end
40 begin; format_date(value.to_date); rescue; value end
39 end
41 end
40
42
41 def format_as_bool(value)
43 def format_as_bool(value)
42 l(value == "1" ? :general_text_Yes : :general_text_No)
44 l(value == "1" ? :general_text_Yes : :general_text_No)
43 end
45 end
44
46
45 ['string','text','int','float','list'].each do |name|
47 ['string','text','int','float','list'].each do |name|
46 define_method("format_as_#{name}") {|value|
48 define_method("format_as_#{name}") {|value|
47 return value
49 return value
48 }
50 }
49 end
51 end
50
52
51 # Allow displaying the edit type of another field_format
53 ['user', 'version'].each do |name|
52 #
54 define_method("format_as_#{name}") {|value|
53 # Example: display a custom field as a list
55 return value.blank? ? "" : name.classify.constantize.find_by_id(value.to_i).to_s
54 def edit_as
56 }
55 name
56 end
57 end
57
58
58 class << self
59 class << self
59 def map(&block)
60 def map(&block)
60 yield self
61 yield self
61 end
62 end
62
63
63 # Registers a custom field format
64 # Registers a custom field format
64 def register(custom_field_format, options={})
65 def register(custom_field_format, options={})
65 @@available[custom_field_format.name] = custom_field_format unless @@available.keys.include?(custom_field_format.name)
66 @@available[custom_field_format.name] = custom_field_format unless @@available.keys.include?(custom_field_format.name)
66 end
67 end
67
68
68 def available_formats
69 def available_formats
69 @@available.keys
70 @@available.keys
70 end
71 end
71
72
72 def find_by_name(name)
73 def find_by_name(name)
73 @@available[name.to_s]
74 @@available[name.to_s]
74 end
75 end
75
76
76 def label_for(name)
77 def label_for(name)
77 format = @@available[name.to_s]
78 format = @@available[name.to_s]
78 format.label if format
79 format.label if format
79 end
80 end
80
81
81 # Return an array of custom field formats which can be used in select_tag
82 # Return an array of custom field formats which can be used in select_tag
82 def as_select
83 def as_select(class_name=nil)
83 @@available.values.sort {|a,b|
84 fields = @@available.values
85 fields = fields.select {|field| field.class_names.nil? || field.class_names.include?(class_name)}
86 fields.sort {|a,b|
84 a.order <=> b.order
87 a.order <=> b.order
85 }.collect {|custom_field_format|
88 }.collect {|custom_field_format|
86 [ l(custom_field_format.label), custom_field_format.name ]
89 [ l(custom_field_format.label), custom_field_format.name ]
87 }
90 }
88 end
91 end
89
92
90 def format_value(value, field_format)
93 def format_value(value, field_format)
91 return "" unless value && !value.empty?
94 return "" unless value && !value.empty?
92
95
93 if format_type = find_by_name(field_format)
96 if format_type = find_by_name(field_format)
94 format_type.format(value)
97 format_type.format(value)
95 else
98 else
96 value
99 value
97 end
100 end
98 end
101 end
99 end
102 end
100 end
103 end
101 end
104 end
@@ -1,61 +1,81
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2009 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 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 require 'custom_fields_controller'
19 require 'custom_fields_controller'
20
20
21 # Re-raise errors caught by the controller.
21 # Re-raise errors caught by the controller.
22 class CustomFieldsController; def rescue_action(e) raise e end; end
22 class CustomFieldsController; def rescue_action(e) raise e end; end
23
23
24 class CustomFieldsControllerTest < ActionController::TestCase
24 class CustomFieldsControllerTest < ActionController::TestCase
25 fixtures :custom_fields, :trackers, :users
25 fixtures :custom_fields, :trackers, :users
26
26
27 def setup
27 def setup
28 @controller = CustomFieldsController.new
28 @controller = CustomFieldsController.new
29 @request = ActionController::TestRequest.new
29 @request = ActionController::TestRequest.new
30 @response = ActionController::TestResponse.new
30 @response = ActionController::TestResponse.new
31 @request.session[:user_id] = 1
31 @request.session[:user_id] = 1
32 end
32 end
33
33
34 def test_get_new_issue_custom_field
35 get :new, :type => 'IssueCustomField'
36 assert_response :success
37 assert_template 'new'
38 assert_tag :select,
39 :attributes => {:name => 'custom_field[field_format]'},
40 :child => {
41 :tag => 'option',
42 :attributes => {:value => 'user'},
43 :content => 'User'
44 }
45 assert_tag :select,
46 :attributes => {:name => 'custom_field[field_format]'},
47 :child => {
48 :tag => 'option',
49 :attributes => {:value => 'version'},
50 :content => 'Version'
51 }
52 end
53
54 def test_get_new_with_invalid_custom_field_class_should_redirect_to_list
55 get :new, :type => 'UnknownCustomField'
56 assert_redirected_to '/custom_fields'
57 end
58
34 def test_post_new_list_custom_field
59 def test_post_new_list_custom_field
35 assert_difference 'CustomField.count' do
60 assert_difference 'CustomField.count' do
36 post :new, :type => "IssueCustomField",
61 post :new, :type => "IssueCustomField",
37 :custom_field => {:name => "test_post_new_list",
62 :custom_field => {:name => "test_post_new_list",
38 :default_value => "",
63 :default_value => "",
39 :min_length => "0",
64 :min_length => "0",
40 :searchable => "0",
65 :searchable => "0",
41 :regexp => "",
66 :regexp => "",
42 :is_for_all => "1",
67 :is_for_all => "1",
43 :possible_values => "0.1\n0.2\n",
68 :possible_values => "0.1\n0.2\n",
44 :max_length => "0",
69 :max_length => "0",
45 :is_filter => "0",
70 :is_filter => "0",
46 :is_required =>"0",
71 :is_required =>"0",
47 :field_format => "list",
72 :field_format => "list",
48 :tracker_ids => ["1", ""]}
73 :tracker_ids => ["1", ""]}
49 end
74 end
50 assert_redirected_to '/custom_fields?tab=IssueCustomField'
75 assert_redirected_to '/custom_fields?tab=IssueCustomField'
51 field = IssueCustomField.find_by_name('test_post_new_list')
76 field = IssueCustomField.find_by_name('test_post_new_list')
52 assert_not_nil field
77 assert_not_nil field
53 assert_equal ["0.1", "0.2"], field.possible_values
78 assert_equal ["0.1", "0.2"], field.possible_values
54 assert_equal 1, field.trackers.size
79 assert_equal 1, field.trackers.size
55 end
80 end
56
57 def test_invalid_custom_field_class_should_redirect_to_list
58 get :new, :type => 'UnknownCustomField'
59 assert_redirected_to '/custom_fields'
60 end
61 end
81 end
@@ -1,129 +1,200
1 # redMine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 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 IssuesTest < ActionController::IntegrationTest
20 class IssuesTest < ActionController::IntegrationTest
21 fixtures :projects,
21 fixtures :projects,
22 :users,
22 :users,
23 :roles,
23 :roles,
24 :members,
24 :members,
25 :trackers,
25 :trackers,
26 :projects_trackers,
26 :projects_trackers,
27 :enabled_modules,
27 :enabled_modules,
28 :issue_statuses,
28 :issue_statuses,
29 :issues,
29 :issues,
30 :enumerations,
30 :enumerations,
31 :custom_fields,
31 :custom_fields,
32 :custom_values,
32 :custom_values,
33 :custom_fields_trackers
33 :custom_fields_trackers
34
34
35 # create an issue
35 # create an issue
36 def test_add_issue
36 def test_add_issue
37 log_user('jsmith', 'jsmith')
37 log_user('jsmith', 'jsmith')
38 get 'projects/1/issues/new', :tracker_id => '1'
38 get 'projects/1/issues/new', :tracker_id => '1'
39 assert_response :success
39 assert_response :success
40 assert_template 'issues/new'
40 assert_template 'issues/new'
41
41
42 post 'projects/1/issues', :tracker_id => "1",
42 post 'projects/1/issues', :tracker_id => "1",
43 :issue => { :start_date => "2006-12-26",
43 :issue => { :start_date => "2006-12-26",
44 :priority_id => "4",
44 :priority_id => "4",
45 :subject => "new test issue",
45 :subject => "new test issue",
46 :category_id => "",
46 :category_id => "",
47 :description => "new issue",
47 :description => "new issue",
48 :done_ratio => "0",
48 :done_ratio => "0",
49 :due_date => "",
49 :due_date => "",
50 :assigned_to_id => "" },
50 :assigned_to_id => "" },
51 :custom_fields => {'2' => 'Value for field 2'}
51 :custom_fields => {'2' => 'Value for field 2'}
52 # find created issue
52 # find created issue
53 issue = Issue.find_by_subject("new test issue")
53 issue = Issue.find_by_subject("new test issue")
54 assert_kind_of Issue, issue
54 assert_kind_of Issue, issue
55
55
56 # check redirection
56 # check redirection
57 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
57 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
58 follow_redirect!
58 follow_redirect!
59 assert_equal issue, assigns(:issue)
59 assert_equal issue, assigns(:issue)
60
60
61 # check issue attributes
61 # check issue attributes
62 assert_equal 'jsmith', issue.author.login
62 assert_equal 'jsmith', issue.author.login
63 assert_equal 1, issue.project.id
63 assert_equal 1, issue.project.id
64 assert_equal 1, issue.status.id
64 assert_equal 1, issue.status.id
65 end
65 end
66
66
67 # add then remove 2 attachments to an issue
67 # add then remove 2 attachments to an issue
68 def test_issue_attachements
68 def test_issue_attachements
69 log_user('jsmith', 'jsmith')
69 log_user('jsmith', 'jsmith')
70 set_tmp_attachments_directory
70 set_tmp_attachments_directory
71
71
72 put 'issues/1',
72 put 'issues/1',
73 :notes => 'Some notes',
73 :notes => 'Some notes',
74 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'This is an attachment'}}
74 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'This is an attachment'}}
75 assert_redirected_to "/issues/1"
75 assert_redirected_to "/issues/1"
76
76
77 # make sure attachment was saved
77 # make sure attachment was saved
78 attachment = Issue.find(1).attachments.find_by_filename("testfile.txt")
78 attachment = Issue.find(1).attachments.find_by_filename("testfile.txt")
79 assert_kind_of Attachment, attachment
79 assert_kind_of Attachment, attachment
80 assert_equal Issue.find(1), attachment.container
80 assert_equal Issue.find(1), attachment.container
81 assert_equal 'This is an attachment', attachment.description
81 assert_equal 'This is an attachment', attachment.description
82 # verify the size of the attachment stored in db
82 # verify the size of the attachment stored in db
83 #assert_equal file_data_1.length, attachment.filesize
83 #assert_equal file_data_1.length, attachment.filesize
84 # verify that the attachment was written to disk
84 # verify that the attachment was written to disk
85 assert File.exist?(attachment.diskfile)
85 assert File.exist?(attachment.diskfile)
86
86
87 # remove the attachments
87 # remove the attachments
88 Issue.find(1).attachments.each(&:destroy)
88 Issue.find(1).attachments.each(&:destroy)
89 assert_equal 0, Issue.find(1).attachments.length
89 assert_equal 0, Issue.find(1).attachments.length
90 end
90 end
91
91
92 def test_other_formats_links_on_get_index
92 def test_other_formats_links_on_get_index
93 get '/projects/ecookbook/issues'
93 get '/projects/ecookbook/issues'
94
94
95 %w(Atom PDF CSV).each do |format|
95 %w(Atom PDF CSV).each do |format|
96 assert_tag :a, :content => format,
96 assert_tag :a, :content => format,
97 :attributes => { :href => "/projects/ecookbook/issues.#{format.downcase}",
97 :attributes => { :href => "/projects/ecookbook/issues.#{format.downcase}",
98 :rel => 'nofollow' }
98 :rel => 'nofollow' }
99 end
99 end
100 end
100 end
101
101
102 def test_other_formats_links_on_post_index_without_project_id_in_url
102 def test_other_formats_links_on_post_index_without_project_id_in_url
103 post '/issues', :project_id => 'ecookbook'
103 post '/issues', :project_id => 'ecookbook'
104
104
105 %w(Atom PDF CSV).each do |format|
105 %w(Atom PDF CSV).each do |format|
106 assert_tag :a, :content => format,
106 assert_tag :a, :content => format,
107 :attributes => { :href => "/projects/ecookbook/issues.#{format.downcase}",
107 :attributes => { :href => "/projects/ecookbook/issues.#{format.downcase}",
108 :rel => 'nofollow' }
108 :rel => 'nofollow' }
109 end
109 end
110 end
110 end
111
111
112 def test_pagination_links_on_get_index
112 def test_pagination_links_on_get_index
113 Setting.per_page_options = '2'
113 Setting.per_page_options = '2'
114 get '/projects/ecookbook/issues'
114 get '/projects/ecookbook/issues'
115
115
116 assert_tag :a, :content => '2',
116 assert_tag :a, :content => '2',
117 :attributes => { :href => '/projects/ecookbook/issues?page=2' }
117 :attributes => { :href => '/projects/ecookbook/issues?page=2' }
118
118
119 end
119 end
120
120
121 def test_pagination_links_on_post_index_without_project_id_in_url
121 def test_pagination_links_on_post_index_without_project_id_in_url
122 Setting.per_page_options = '2'
122 Setting.per_page_options = '2'
123 post '/issues', :project_id => 'ecookbook'
123 post '/issues', :project_id => 'ecookbook'
124
124
125 assert_tag :a, :content => '2',
125 assert_tag :a, :content => '2',
126 :attributes => { :href => '/projects/ecookbook/issues?page=2' }
126 :attributes => { :href => '/projects/ecookbook/issues?page=2' }
127
127
128 end
128 end
129
130 def test_issue_with_user_custom_field
131 @field = IssueCustomField.create!(:name => 'Tester', :field_format => 'user', :is_for_all => true, :trackers => Tracker.all)
132 Role.anonymous.add_permission! :add_issues, :edit_issues
133 users = Project.find(1).users
134 tester = users.first
135
136 # Issue form
137 get '/projects/ecookbook/issues/new'
138 assert_response :success
139 assert_tag :select,
140 :attributes => {:name => "issue[custom_field_values][#{@field.id}]"},
141 :children => {:count => (users.size + 1)}, # +1 for blank value
142 :child => {
143 :tag => 'option',
144 :attributes => {:value => tester.id.to_s},
145 :content => tester.name
146 }
147
148 # Create issue
149 assert_difference 'Issue.count' do
150 post '/projects/ecookbook/issues',
151 :issue => {
152 :tracker_id => '1',
153 :priority_id => '4',
154 :subject => 'Issue with user custom field',
155 :custom_field_values => {@field.id.to_s => users.first.id.to_s}
156 }
157 end
158 issue = Issue.first(:order => 'id DESC')
159 assert_response 302
160
161 # Issue view
162 follow_redirect!
163 assert_tag :th,
164 :content => /Tester/,
165 :sibling => {
166 :tag => 'td',
167 :content => tester.name
168 }
169 assert_tag :select,
170 :attributes => {:name => "issue[custom_field_values][#{@field.id}]"},
171 :children => {:count => (users.size + 1)}, # +1 for blank value
172 :child => {
173 :tag => 'option',
174 :attributes => {:value => tester.id.to_s, :selected => 'selected'},
175 :content => tester.name
176 }
177
178 # Update issue
179 new_tester = users[1]
180 assert_difference 'Journal.count' do
181 put "/issues/#{issue.id}",
182 :notes => 'Updating custom field',
183 :issue => {
184 :custom_field_values => {@field.id.to_s => new_tester.id.to_s}
185 }
186 end
187 assert_response 302
188
189 # Issue view
190 follow_redirect!
191 assert_tag :content => 'Tester',
192 :ancestor => {:tag => 'ul', :attributes => {:class => /details/}},
193 :sibling => {
194 :content => tester.name,
195 :sibling => {
196 :content => new_tester.name
197 }
198 }
199 end
129 end
200 end
@@ -1,112 +1,112
1 # redMine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 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, :as => :customized,
30 has_many :custom_values, :as => :customized,
31 :include => :custom_field,
31 :include => :custom_field,
32 :order => "#{CustomField.table_name}.position",
32 :order => "#{CustomField.table_name}.position",
33 :dependent => :delete_all
33 :dependent => :delete_all
34 before_validation_on_create { |customized| customized.custom_field_values }
34 before_validation_on_create { |customized| customized.custom_field_values }
35 # Trigger validation only if custom values were changed
35 # Trigger validation only if custom values were changed
36 validates_associated :custom_values, :on => :update, :if => Proc.new { |customized| customized.custom_field_values_changed? }
36 validates_associated :custom_values, :on => :update, :if => Proc.new { |customized| customized.custom_field_values_changed? }
37 send :include, Redmine::Acts::Customizable::InstanceMethods
37 send :include, Redmine::Acts::Customizable::InstanceMethods
38 # Save custom values when saving the customized object
38 # Save custom values when saving the customized object
39 after_save :save_custom_field_values
39 after_save :save_custom_field_values
40 end
40 end
41 end
41 end
42
42
43 module InstanceMethods
43 module InstanceMethods
44 def self.included(base)
44 def self.included(base)
45 base.extend ClassMethods
45 base.extend ClassMethods
46 end
46 end
47
47
48 def available_custom_fields
48 def available_custom_fields
49 CustomField.find(:all, :conditions => "type = '#{self.class.name}CustomField'",
49 CustomField.find(:all, :conditions => "type = '#{self.class.name}CustomField'",
50 :order => 'position')
50 :order => 'position')
51 end
51 end
52
52
53 # Sets the values of the object's custom fields
53 # Sets the values of the object's custom fields
54 # values is an array like [{'id' => 1, 'value' => 'foo'}, {'id' => 2, 'value' => 'bar'}]
54 # values is an array like [{'id' => 1, 'value' => 'foo'}, {'id' => 2, 'value' => 'bar'}]
55 def custom_fields=(values)
55 def custom_fields=(values)
56 values_to_hash = values.inject({}) do |hash, v|
56 values_to_hash = values.inject({}) do |hash, v|
57 v = v.stringify_keys
57 v = v.stringify_keys
58 if v['id'] && v.has_key?('value')
58 if v['id'] && v.has_key?('value')
59 hash[v['id']] = v['value']
59 hash[v['id']] = v['value']
60 end
60 end
61 hash
61 hash
62 end
62 end
63 self.custom_field_values = values_to_hash
63 self.custom_field_values = values_to_hash
64 end
64 end
65
65
66 # Sets the values of the object's custom fields
66 # Sets the values of the object's custom fields
67 # values is a hash like {'1' => 'foo', 2 => 'bar'}
67 # values is a hash like {'1' => 'foo', 2 => 'bar'}
68 def custom_field_values=(values)
68 def custom_field_values=(values)
69 @custom_field_values_changed = true
69 @custom_field_values_changed = true
70 values = values.stringify_keys
70 values = values.stringify_keys
71 custom_field_values.each do |custom_value|
71 custom_field_values.each do |custom_value|
72 custom_value.value = values[custom_value.custom_field_id.to_s] if values.has_key?(custom_value.custom_field_id.to_s)
72 custom_value.value = values[custom_value.custom_field_id.to_s] if values.has_key?(custom_value.custom_field_id.to_s)
73 end if values.is_a?(Hash)
73 end if values.is_a?(Hash)
74 self.custom_values = custom_field_values
74 self.custom_values = custom_field_values
75 end
75 end
76
76
77 def custom_field_values
77 def custom_field_values
78 @custom_field_values ||= available_custom_fields.collect { |x| custom_values.detect { |v| v.custom_field == x } || custom_values.build(:custom_field => x, :value => nil) }
78 @custom_field_values ||= available_custom_fields.collect { |x| custom_values.detect { |v| v.custom_field == x } || custom_values.build(:customized => self, :custom_field => x, :value => nil) }
79 end
79 end
80
80
81 def visible_custom_field_values
81 def visible_custom_field_values
82 custom_field_values.select(&:visible?)
82 custom_field_values.select(&:visible?)
83 end
83 end
84
84
85 def custom_field_values_changed?
85 def custom_field_values_changed?
86 @custom_field_values_changed == true
86 @custom_field_values_changed == true
87 end
87 end
88
88
89 def custom_value_for(c)
89 def custom_value_for(c)
90 field_id = (c.is_a?(CustomField) ? c.id : c.to_i)
90 field_id = (c.is_a?(CustomField) ? c.id : c.to_i)
91 custom_values.detect {|v| v.custom_field_id == field_id }
91 custom_values.detect {|v| v.custom_field_id == field_id }
92 end
92 end
93
93
94 def save_custom_field_values
94 def save_custom_field_values
95 custom_field_values.each(&:save)
95 custom_field_values.each(&:save)
96 @custom_field_values_changed = false
96 @custom_field_values_changed = false
97 @custom_field_values = nil
97 @custom_field_values = nil
98 end
98 end
99
99
100 def reset_custom_values!
100 def reset_custom_values!
101 @custom_field_values = nil
101 @custom_field_values = nil
102 @custom_field_values_changed = true
102 @custom_field_values_changed = true
103 values = custom_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
103 values = custom_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
104 custom_values.each {|cv| cv.destroy unless custom_field_values.include?(cv)}
104 custom_values.each {|cv| cv.destroy unless custom_field_values.include?(cv)}
105 end
105 end
106
106
107 module ClassMethods
107 module ClassMethods
108 end
108 end
109 end
109 end
110 end
110 end
111 end
111 end
112 end
112 end
General Comments 0
You need to be logged in to leave comments. Login now