##// END OF EJS Templates
Use IssueRelation#to_s....
Jean-Philippe Lang -
r13183:4d6c52a82b44
parent child
Show More
@@ -1,206 +1,204
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 4 # Copyright (C) 2006-2014 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module QueriesHelper
21 21 include ApplicationHelper
22 22
23 23 def filters_options_for_select(query)
24 24 options_for_select(filters_options(query))
25 25 end
26 26
27 27 def filters_options(query)
28 28 options = [[]]
29 29 options += query.available_filters.map do |field, field_options|
30 30 [field_options[:name], field]
31 31 end
32 32 end
33 33
34 34 def query_filters_hidden_tags(query)
35 35 tags = ''.html_safe
36 36 query.filters.each do |field, options|
37 37 tags << hidden_field_tag("f[]", field, :id => nil)
38 38 tags << hidden_field_tag("op[#{field}]", options[:operator], :id => nil)
39 39 options[:values].each do |value|
40 40 tags << hidden_field_tag("v[#{field}][]", value, :id => nil)
41 41 end
42 42 end
43 43 tags
44 44 end
45 45
46 46 def query_columns_hidden_tags(query)
47 47 tags = ''.html_safe
48 48 query.columns.each do |column|
49 49 tags << hidden_field_tag("c[]", column.name, :id => nil)
50 50 end
51 51 tags
52 52 end
53 53
54 54 def query_hidden_tags(query)
55 55 query_filters_hidden_tags(query) + query_columns_hidden_tags(query)
56 56 end
57 57
58 58 def available_block_columns_tags(query)
59 59 tags = ''.html_safe
60 60 query.available_block_columns.each do |column|
61 61 tags << content_tag('label', check_box_tag('c[]', column.name.to_s, query.has_column?(column), :id => nil) + " #{column.caption}", :class => 'inline')
62 62 end
63 63 tags
64 64 end
65 65
66 66 def query_available_inline_columns_options(query)
67 67 (query.available_inline_columns - query.columns).reject(&:frozen?).collect {|column| [column.caption, column.name]}
68 68 end
69 69
70 70 def query_selected_inline_columns_options(query)
71 71 (query.inline_columns & query.available_inline_columns).reject(&:frozen?).collect {|column| [column.caption, column.name]}
72 72 end
73 73
74 74 def render_query_columns_selection(query, options={})
75 75 tag_name = (options[:name] || 'c') + '[]'
76 76 render :partial => 'queries/columns', :locals => {:query => query, :tag_name => tag_name}
77 77 end
78 78
79 79 def column_header(column)
80 80 column.sortable ? sort_header_tag(column.name.to_s, :caption => column.caption,
81 81 :default_order => column.default_order) :
82 82 content_tag('th', h(column.caption))
83 83 end
84 84
85 85 def column_content(column, issue)
86 86 value = column.value_object(issue)
87 87 if value.is_a?(Array)
88 88 value.collect {|v| column_value(column, issue, v)}.compact.join(', ').html_safe
89 89 else
90 90 column_value(column, issue, value)
91 91 end
92 92 end
93 93
94 94 def column_value(column, issue, value)
95 95 case column.name
96 96 when :id
97 97 link_to value, issue_path(issue)
98 98 when :subject
99 99 link_to value, issue_path(issue)
100 100 when :parent
101 101 value ? (value.visible? ? link_to_issue(value, :subject => false) : "##{value.id}") : ''
102 102 when :description
103 103 issue.description? ? content_tag('div', textilizable(issue, :description), :class => "wiki") : ''
104 104 when :done_ratio
105 105 progress_bar(value, :width => '80px')
106 106 when :relations
107 other = value.other_issue(issue)
108 107 content_tag('span',
109 (l(value.label_for(issue)) + " " + link_to_issue(other, :subject => false, :tracker => false)).html_safe,
108 value.to_s(issue) {|other| link_to_issue(other, :subject => false, :tracker => false)}.html_safe,
110 109 :class => value.css_classes_for(issue))
111 110 else
112 111 format_object(value)
113 112 end
114 113 end
115 114
116 115 def csv_content(column, issue)
117 116 value = column.value_object(issue)
118 117 if value.is_a?(Array)
119 118 value.collect {|v| csv_value(column, issue, v)}.compact.join(', ')
120 119 else
121 120 csv_value(column, issue, value)
122 121 end
123 122 end
124 123
125 124 def csv_value(column, object, value)
126 125 format_object(value, false) do |value|
127 126 case value.class.name
128 127 when 'Float'
129 128 sprintf("%.2f", value).gsub('.', l(:general_csv_decimal_separator))
130 129 when 'IssueRelation'
131 other = value.other_issue(object)
132 l(value.label_for(object)) + " ##{other.id}"
130 value.to_s(object)
133 131 when 'Issue'
134 132 if object.is_a?(TimeEntry)
135 133 "#{value.tracker} ##{value.id}: #{value.subject}"
136 134 else
137 135 value.id
138 136 end
139 137 else
140 138 value
141 139 end
142 140 end
143 141 end
144 142
145 143 def query_to_csv(items, query, options={})
146 144 encoding = l(:general_csv_encoding)
147 145 columns = (options[:columns] == 'all' ? query.available_inline_columns : query.inline_columns)
148 146 query.available_block_columns.each do |column|
149 147 if options[column.name].present?
150 148 columns << column
151 149 end
152 150 end
153 151
154 152 export = CSV.generate(:col_sep => l(:general_csv_separator)) do |csv|
155 153 # csv header fields
156 154 csv << columns.collect {|c| Redmine::CodesetUtil.from_utf8(c.caption.to_s, encoding) }
157 155 # csv lines
158 156 items.each do |item|
159 157 csv << columns.collect {|c| Redmine::CodesetUtil.from_utf8(csv_content(c, item), encoding) }
160 158 end
161 159 end
162 160 export
163 161 end
164 162
165 163 # Retrieve query from session or build a new query
166 164 def retrieve_query
167 165 if !params[:query_id].blank?
168 166 cond = "project_id IS NULL"
169 167 cond << " OR project_id = #{@project.id}" if @project
170 168 @query = IssueQuery.where(cond).find(params[:query_id])
171 169 raise ::Unauthorized unless @query.visible?
172 170 @query.project = @project
173 171 session[:query] = {:id => @query.id, :project_id => @query.project_id}
174 172 sort_clear
175 173 elsif api_request? || params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
176 174 # Give it a name, required to be valid
177 175 @query = IssueQuery.new(:name => "_")
178 176 @query.project = @project
179 177 @query.build_from_params(params)
180 178 session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names}
181 179 else
182 180 # retrieve from session
183 181 @query = nil
184 182 @query = IssueQuery.find_by_id(session[:query][:id]) if session[:query][:id]
185 183 @query ||= IssueQuery.new(:name => "_", :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
186 184 @query.project = @project
187 185 end
188 186 end
189 187
190 188 def retrieve_query_from_session
191 189 if session[:query]
192 190 if session[:query][:id]
193 191 @query = IssueQuery.find_by_id(session[:query][:id])
194 192 return unless @query
195 193 else
196 194 @query = IssueQuery.new(:name => "_", :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
197 195 end
198 196 if session[:query].has_key?(:project_id)
199 197 @query.project_id = session[:query][:project_id]
200 198 else
201 199 @query.project = @project
202 200 end
203 201 @query
204 202 end
205 203 end
206 204 end
@@ -1,215 +1,215
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2014 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class IssueRelation < ActiveRecord::Base
19 19 # Class used to represent the relations of an issue
20 20 class Relations < Array
21 21 include Redmine::I18n
22 22
23 23 def initialize(issue, *args)
24 24 @issue = issue
25 25 super(*args)
26 26 end
27 27
28 28 def to_s(*args)
29 map {|relation| "#{l(relation.label_for(@issue))} ##{relation.other_issue(@issue).id}"}.join(', ')
29 map {|relation| relation.to_s(@issue)}.join(', ')
30 30 end
31 31 end
32 32
33 33 belongs_to :issue_from, :class_name => 'Issue'
34 34 belongs_to :issue_to, :class_name => 'Issue'
35 35
36 36 TYPE_RELATES = "relates"
37 37 TYPE_DUPLICATES = "duplicates"
38 38 TYPE_DUPLICATED = "duplicated"
39 39 TYPE_BLOCKS = "blocks"
40 40 TYPE_BLOCKED = "blocked"
41 41 TYPE_PRECEDES = "precedes"
42 42 TYPE_FOLLOWS = "follows"
43 43 TYPE_COPIED_TO = "copied_to"
44 44 TYPE_COPIED_FROM = "copied_from"
45 45
46 46 TYPES = {
47 47 TYPE_RELATES => { :name => :label_relates_to, :sym_name => :label_relates_to,
48 48 :order => 1, :sym => TYPE_RELATES },
49 49 TYPE_DUPLICATES => { :name => :label_duplicates, :sym_name => :label_duplicated_by,
50 50 :order => 2, :sym => TYPE_DUPLICATED },
51 51 TYPE_DUPLICATED => { :name => :label_duplicated_by, :sym_name => :label_duplicates,
52 52 :order => 3, :sym => TYPE_DUPLICATES, :reverse => TYPE_DUPLICATES },
53 53 TYPE_BLOCKS => { :name => :label_blocks, :sym_name => :label_blocked_by,
54 54 :order => 4, :sym => TYPE_BLOCKED },
55 55 TYPE_BLOCKED => { :name => :label_blocked_by, :sym_name => :label_blocks,
56 56 :order => 5, :sym => TYPE_BLOCKS, :reverse => TYPE_BLOCKS },
57 57 TYPE_PRECEDES => { :name => :label_precedes, :sym_name => :label_follows,
58 58 :order => 6, :sym => TYPE_FOLLOWS },
59 59 TYPE_FOLLOWS => { :name => :label_follows, :sym_name => :label_precedes,
60 60 :order => 7, :sym => TYPE_PRECEDES, :reverse => TYPE_PRECEDES },
61 61 TYPE_COPIED_TO => { :name => :label_copied_to, :sym_name => :label_copied_from,
62 62 :order => 8, :sym => TYPE_COPIED_FROM },
63 63 TYPE_COPIED_FROM => { :name => :label_copied_from, :sym_name => :label_copied_to,
64 64 :order => 9, :sym => TYPE_COPIED_TO, :reverse => TYPE_COPIED_TO }
65 65 }.freeze
66 66
67 67 validates_presence_of :issue_from, :issue_to, :relation_type
68 68 validates_inclusion_of :relation_type, :in => TYPES.keys
69 69 validates_numericality_of :delay, :allow_nil => true
70 70 validates_uniqueness_of :issue_to_id, :scope => :issue_from_id
71 71 validate :validate_issue_relation
72 72
73 73 attr_protected :issue_from_id, :issue_to_id
74 74 before_save :handle_issue_order
75 75 after_create :call_issues_relation_added_callback
76 76 after_destroy :call_issues_relation_removed_callback
77 77
78 78 def visible?(user=User.current)
79 79 (issue_from.nil? || issue_from.visible?(user)) && (issue_to.nil? || issue_to.visible?(user))
80 80 end
81 81
82 82 def deletable?(user=User.current)
83 83 visible?(user) &&
84 84 ((issue_from.nil? || user.allowed_to?(:manage_issue_relations, issue_from.project)) ||
85 85 (issue_to.nil? || user.allowed_to?(:manage_issue_relations, issue_to.project)))
86 86 end
87 87
88 88 def initialize(attributes=nil, *args)
89 89 super
90 90 if new_record?
91 91 if relation_type.blank?
92 92 self.relation_type = IssueRelation::TYPE_RELATES
93 93 end
94 94 end
95 95 end
96 96
97 97 def validate_issue_relation
98 98 if issue_from && issue_to
99 99 errors.add :issue_to_id, :invalid if issue_from_id == issue_to_id
100 100 unless issue_from.project_id == issue_to.project_id ||
101 101 Setting.cross_project_issue_relations?
102 102 errors.add :issue_to_id, :not_same_project
103 103 end
104 104 # detect circular dependencies depending wether the relation should be reversed
105 105 if TYPES.has_key?(relation_type) && TYPES[relation_type][:reverse]
106 106 errors.add :base, :circular_dependency if issue_from.all_dependent_issues.include? issue_to
107 107 else
108 108 errors.add :base, :circular_dependency if issue_to.all_dependent_issues.include? issue_from
109 109 end
110 110 if issue_from.is_descendant_of?(issue_to) || issue_from.is_ancestor_of?(issue_to)
111 111 errors.add :base, :cant_link_an_issue_with_a_descendant
112 112 end
113 113 end
114 114 end
115 115
116 116 def other_issue(issue)
117 117 (self.issue_from_id == issue.id) ? issue_to : issue_from
118 118 end
119 119
120 120 # Returns the relation type for +issue+
121 121 def relation_type_for(issue)
122 122 if TYPES[relation_type]
123 123 if self.issue_from_id == issue.id
124 124 relation_type
125 125 else
126 126 TYPES[relation_type][:sym]
127 127 end
128 128 end
129 129 end
130 130
131 131 def label_for(issue)
132 132 TYPES[relation_type] ?
133 133 TYPES[relation_type][(self.issue_from_id == issue.id) ? :name : :sym_name] :
134 134 :unknow
135 135 end
136 136
137 137 def to_s(issue=nil)
138 138 issue ||= issue_from
139 139 issue_text = block_given? ? yield(other_issue(issue)) : "##{other_issue(issue).try(:id)}"
140 140 s = []
141 141 s << l(label_for(issue))
142 142 s << "(#{l('datetime.distance_in_words.x_days', :count => delay)})" if delay && delay != 0
143 143 s << issue_text
144 144 s.join(' ')
145 145 end
146 146
147 147 def css_classes_for(issue)
148 148 "rel-#{relation_type_for(issue)}"
149 149 end
150 150
151 151 def handle_issue_order
152 152 reverse_if_needed
153 153
154 154 if TYPE_PRECEDES == relation_type
155 155 self.delay ||= 0
156 156 else
157 157 self.delay = nil
158 158 end
159 159 set_issue_to_dates
160 160 end
161 161
162 162 def set_issue_to_dates
163 163 soonest_start = self.successor_soonest_start
164 164 if soonest_start && issue_to
165 165 issue_to.reschedule_on!(soonest_start)
166 166 end
167 167 end
168 168
169 169 def successor_soonest_start
170 170 if (TYPE_PRECEDES == self.relation_type) && delay && issue_from &&
171 171 (issue_from.start_date || issue_from.due_date)
172 172 (issue_from.due_date || issue_from.start_date) + 1 + delay
173 173 end
174 174 end
175 175
176 176 def <=>(relation)
177 177 r = TYPES[self.relation_type][:order] <=> TYPES[relation.relation_type][:order]
178 178 r == 0 ? id <=> relation.id : r
179 179 end
180 180
181 181 def init_journals(user)
182 182 issue_from.init_journal(user) if issue_from
183 183 issue_to.init_journal(user) if issue_to
184 184 end
185 185
186 186 private
187 187
188 188 # Reverses the relation if needed so that it gets stored in the proper way
189 189 # Should not be reversed before validation so that it can be displayed back
190 190 # as entered on new relation form
191 191 def reverse_if_needed
192 192 if TYPES.has_key?(relation_type) && TYPES[relation_type][:reverse]
193 193 issue_tmp = issue_to
194 194 self.issue_to = issue_from
195 195 self.issue_from = issue_tmp
196 196 self.relation_type = TYPES[relation_type][:reverse]
197 197 end
198 198 end
199 199
200 200 def call_issues_relation_added_callback
201 201 call_issues_callback :relation_added
202 202 end
203 203
204 204 def call_issues_relation_removed_callback
205 205 call_issues_callback :relation_removed
206 206 end
207 207
208 208 def call_issues_callback(name)
209 209 [issue_from, issue_to].each do |issue|
210 210 if issue
211 211 issue.send name, self
212 212 end
213 213 end
214 214 end
215 215 end
General Comments 0
You need to be logged in to leave comments. Login now