##// END OF EJS Templates
Fixed that Link custom fields are not displayed as links on the issue list (#16496)....
Jean-Philippe Lang -
r12779:22b2a1f69927
parent child
Show More
@@ -1,1349 +1,1352
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2014 Jean-Philippe Lang
4 # Copyright (C) 2006-2014 Jean-Philippe Lang
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
9 # of the License, or (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
20 require 'forwardable'
20 require 'forwardable'
21 require 'cgi'
21 require 'cgi'
22
22
23 module ApplicationHelper
23 module ApplicationHelper
24 include Redmine::WikiFormatting::Macros::Definitions
24 include Redmine::WikiFormatting::Macros::Definitions
25 include Redmine::I18n
25 include Redmine::I18n
26 include GravatarHelper::PublicMethods
26 include GravatarHelper::PublicMethods
27 include Redmine::Pagination::Helper
27 include Redmine::Pagination::Helper
28
28
29 extend Forwardable
29 extend Forwardable
30 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
30 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
31
31
32 # Return true if user is authorized for controller/action, otherwise false
32 # Return true if user is authorized for controller/action, otherwise false
33 def authorize_for(controller, action)
33 def authorize_for(controller, action)
34 User.current.allowed_to?({:controller => controller, :action => action}, @project)
34 User.current.allowed_to?({:controller => controller, :action => action}, @project)
35 end
35 end
36
36
37 # Display a link if user is authorized
37 # Display a link if user is authorized
38 #
38 #
39 # @param [String] name Anchor text (passed to link_to)
39 # @param [String] name Anchor text (passed to link_to)
40 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
40 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
41 # @param [optional, Hash] html_options Options passed to link_to
41 # @param [optional, Hash] html_options Options passed to link_to
42 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
42 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
43 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
43 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
44 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
44 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
45 end
45 end
46
46
47 # Displays a link to user's account page if active
47 # Displays a link to user's account page if active
48 def link_to_user(user, options={})
48 def link_to_user(user, options={})
49 if user.is_a?(User)
49 if user.is_a?(User)
50 name = h(user.name(options[:format]))
50 name = h(user.name(options[:format]))
51 if user.active? || (User.current.admin? && user.logged?)
51 if user.active? || (User.current.admin? && user.logged?)
52 link_to name, user_path(user), :class => user.css_classes
52 link_to name, user_path(user), :class => user.css_classes
53 else
53 else
54 name
54 name
55 end
55 end
56 else
56 else
57 h(user.to_s)
57 h(user.to_s)
58 end
58 end
59 end
59 end
60
60
61 # Displays a link to +issue+ with its subject.
61 # Displays a link to +issue+ with its subject.
62 # Examples:
62 # Examples:
63 #
63 #
64 # link_to_issue(issue) # => Defect #6: This is the subject
64 # link_to_issue(issue) # => Defect #6: This is the subject
65 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
65 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
66 # link_to_issue(issue, :subject => false) # => Defect #6
66 # link_to_issue(issue, :subject => false) # => Defect #6
67 # link_to_issue(issue, :project => true) # => Foo - Defect #6
67 # link_to_issue(issue, :project => true) # => Foo - Defect #6
68 # link_to_issue(issue, :subject => false, :tracker => false) # => #6
68 # link_to_issue(issue, :subject => false, :tracker => false) # => #6
69 #
69 #
70 def link_to_issue(issue, options={})
70 def link_to_issue(issue, options={})
71 title = nil
71 title = nil
72 subject = nil
72 subject = nil
73 text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}"
73 text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}"
74 if options[:subject] == false
74 if options[:subject] == false
75 title = issue.subject.truncate(60)
75 title = issue.subject.truncate(60)
76 else
76 else
77 subject = issue.subject
77 subject = issue.subject
78 if truncate_length = options[:truncate]
78 if truncate_length = options[:truncate]
79 subject = subject.truncate(truncate_length)
79 subject = subject.truncate(truncate_length)
80 end
80 end
81 end
81 end
82 only_path = options[:only_path].nil? ? true : options[:only_path]
82 only_path = options[:only_path].nil? ? true : options[:only_path]
83 s = link_to(text, issue_path(issue, :only_path => only_path),
83 s = link_to(text, issue_path(issue, :only_path => only_path),
84 :class => issue.css_classes, :title => title)
84 :class => issue.css_classes, :title => title)
85 s << h(": #{subject}") if subject
85 s << h(": #{subject}") if subject
86 s = h("#{issue.project} - ") + s if options[:project]
86 s = h("#{issue.project} - ") + s if options[:project]
87 s
87 s
88 end
88 end
89
89
90 # Generates a link to an attachment.
90 # Generates a link to an attachment.
91 # Options:
91 # Options:
92 # * :text - Link text (default to attachment filename)
92 # * :text - Link text (default to attachment filename)
93 # * :download - Force download (default: false)
93 # * :download - Force download (default: false)
94 def link_to_attachment(attachment, options={})
94 def link_to_attachment(attachment, options={})
95 text = options.delete(:text) || attachment.filename
95 text = options.delete(:text) || attachment.filename
96 route_method = options.delete(:download) ? :download_named_attachment_path : :named_attachment_path
96 route_method = options.delete(:download) ? :download_named_attachment_path : :named_attachment_path
97 html_options = options.slice!(:only_path)
97 html_options = options.slice!(:only_path)
98 url = send(route_method, attachment, attachment.filename, options)
98 url = send(route_method, attachment, attachment.filename, options)
99 link_to text, url, html_options
99 link_to text, url, html_options
100 end
100 end
101
101
102 # Generates a link to a SCM revision
102 # Generates a link to a SCM revision
103 # Options:
103 # Options:
104 # * :text - Link text (default to the formatted revision)
104 # * :text - Link text (default to the formatted revision)
105 def link_to_revision(revision, repository, options={})
105 def link_to_revision(revision, repository, options={})
106 if repository.is_a?(Project)
106 if repository.is_a?(Project)
107 repository = repository.repository
107 repository = repository.repository
108 end
108 end
109 text = options.delete(:text) || format_revision(revision)
109 text = options.delete(:text) || format_revision(revision)
110 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
110 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
111 link_to(
111 link_to(
112 h(text),
112 h(text),
113 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
113 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
114 :title => l(:label_revision_id, format_revision(revision))
114 :title => l(:label_revision_id, format_revision(revision))
115 )
115 )
116 end
116 end
117
117
118 # Generates a link to a message
118 # Generates a link to a message
119 def link_to_message(message, options={}, html_options = nil)
119 def link_to_message(message, options={}, html_options = nil)
120 link_to(
120 link_to(
121 message.subject.truncate(60),
121 message.subject.truncate(60),
122 board_message_path(message.board_id, message.parent_id || message.id, {
122 board_message_path(message.board_id, message.parent_id || message.id, {
123 :r => (message.parent_id && message.id),
123 :r => (message.parent_id && message.id),
124 :anchor => (message.parent_id ? "message-#{message.id}" : nil)
124 :anchor => (message.parent_id ? "message-#{message.id}" : nil)
125 }.merge(options)),
125 }.merge(options)),
126 html_options
126 html_options
127 )
127 )
128 end
128 end
129
129
130 # Generates a link to a project if active
130 # Generates a link to a project if active
131 # Examples:
131 # Examples:
132 #
132 #
133 # link_to_project(project) # => link to the specified project overview
133 # link_to_project(project) # => link to the specified project overview
134 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
134 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
135 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
135 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
136 #
136 #
137 def link_to_project(project, options={}, html_options = nil)
137 def link_to_project(project, options={}, html_options = nil)
138 if project.archived?
138 if project.archived?
139 h(project.name)
139 h(project.name)
140 elsif options.key?(:action)
140 elsif options.key?(:action)
141 ActiveSupport::Deprecation.warn "#link_to_project with :action option is deprecated and will be removed in Redmine 3.0."
141 ActiveSupport::Deprecation.warn "#link_to_project with :action option is deprecated and will be removed in Redmine 3.0."
142 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
142 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
143 link_to project.name, url, html_options
143 link_to project.name, url, html_options
144 else
144 else
145 link_to project.name, project_path(project, options), html_options
145 link_to project.name, project_path(project, options), html_options
146 end
146 end
147 end
147 end
148
148
149 # Generates a link to a project settings if active
149 # Generates a link to a project settings if active
150 def link_to_project_settings(project, options={}, html_options=nil)
150 def link_to_project_settings(project, options={}, html_options=nil)
151 if project.active?
151 if project.active?
152 link_to project.name, settings_project_path(project, options), html_options
152 link_to project.name, settings_project_path(project, options), html_options
153 elsif project.archived?
153 elsif project.archived?
154 h(project.name)
154 h(project.name)
155 else
155 else
156 link_to project.name, project_path(project, options), html_options
156 link_to project.name, project_path(project, options), html_options
157 end
157 end
158 end
158 end
159
159
160 # Helper that formats object for html or text rendering
160 # Helper that formats object for html or text rendering
161 def format_object(object, html=true)
161 def format_object(object, html=true, &block)
162 if block_given?
163 object = yield object
164 end
162 case object.class.name
165 case object.class.name
163 when 'Array'
166 when 'Array'
164 object.map {|o| format_object(o, html)}.join(', ').html_safe
167 object.map {|o| format_object(o, html)}.join(', ').html_safe
165 when 'Time'
168 when 'Time'
166 format_time(object)
169 format_time(object)
167 when 'Date'
170 when 'Date'
168 format_date(object)
171 format_date(object)
169 when 'Fixnum'
172 when 'Fixnum'
170 object.to_s
173 object.to_s
171 when 'Float'
174 when 'Float'
172 sprintf "%.2f", object
175 sprintf "%.2f", object
173 when 'User'
176 when 'User'
174 html ? link_to_user(object) : object.to_s
177 html ? link_to_user(object) : object.to_s
175 when 'Project'
178 when 'Project'
176 html ? link_to_project(object) : object.to_s
179 html ? link_to_project(object) : object.to_s
177 when 'Version'
180 when 'Version'
178 html ? link_to(object.name, version_path(object)) : object.to_s
181 html ? link_to(object.name, version_path(object)) : object.to_s
179 when 'TrueClass'
182 when 'TrueClass'
180 l(:general_text_Yes)
183 l(:general_text_Yes)
181 when 'FalseClass'
184 when 'FalseClass'
182 l(:general_text_No)
185 l(:general_text_No)
183 when 'Issue'
186 when 'Issue'
184 object.visible? && html ? link_to_issue(object) : "##{object.id}"
187 object.visible? && html ? link_to_issue(object) : "##{object.id}"
185 when 'CustomValue', 'CustomFieldValue'
188 when 'CustomValue', 'CustomFieldValue'
186 if object.custom_field
189 if object.custom_field
187 f = object.custom_field.format.formatted_custom_value(self, object, html)
190 f = object.custom_field.format.formatted_custom_value(self, object, html)
188 if f.nil? || f.is_a?(String)
191 if f.nil? || f.is_a?(String)
189 f
192 f
190 else
193 else
191 format_object(f, html)
194 format_object(f, html, &block)
192 end
195 end
193 else
196 else
194 object.value.to_s
197 object.value.to_s
195 end
198 end
196 else
199 else
197 html ? h(object) : object.to_s
200 html ? h(object) : object.to_s
198 end
201 end
199 end
202 end
200
203
201 def wiki_page_path(page, options={})
204 def wiki_page_path(page, options={})
202 url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
205 url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
203 end
206 end
204
207
205 def thumbnail_tag(attachment)
208 def thumbnail_tag(attachment)
206 link_to image_tag(thumbnail_path(attachment)),
209 link_to image_tag(thumbnail_path(attachment)),
207 named_attachment_path(attachment, attachment.filename),
210 named_attachment_path(attachment, attachment.filename),
208 :title => attachment.filename
211 :title => attachment.filename
209 end
212 end
210
213
211 def toggle_link(name, id, options={})
214 def toggle_link(name, id, options={})
212 onclick = "$('##{id}').toggle(); "
215 onclick = "$('##{id}').toggle(); "
213 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
216 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
214 onclick << "return false;"
217 onclick << "return false;"
215 link_to(name, "#", :onclick => onclick)
218 link_to(name, "#", :onclick => onclick)
216 end
219 end
217
220
218 def image_to_function(name, function, html_options = {})
221 def image_to_function(name, function, html_options = {})
219 html_options.symbolize_keys!
222 html_options.symbolize_keys!
220 tag(:input, html_options.merge({
223 tag(:input, html_options.merge({
221 :type => "image", :src => image_path(name),
224 :type => "image", :src => image_path(name),
222 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
225 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
223 }))
226 }))
224 end
227 end
225
228
226 def format_activity_title(text)
229 def format_activity_title(text)
227 h(truncate_single_line_raw(text, 100))
230 h(truncate_single_line_raw(text, 100))
228 end
231 end
229
232
230 def format_activity_day(date)
233 def format_activity_day(date)
231 date == User.current.today ? l(:label_today).titleize : format_date(date)
234 date == User.current.today ? l(:label_today).titleize : format_date(date)
232 end
235 end
233
236
234 def format_activity_description(text)
237 def format_activity_description(text)
235 h(text.to_s.truncate(120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
238 h(text.to_s.truncate(120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
236 ).gsub(/[\r\n]+/, "<br />").html_safe
239 ).gsub(/[\r\n]+/, "<br />").html_safe
237 end
240 end
238
241
239 def format_version_name(version)
242 def format_version_name(version)
240 if version.project == @project
243 if version.project == @project
241 h(version)
244 h(version)
242 else
245 else
243 h("#{version.project} - #{version}")
246 h("#{version.project} - #{version}")
244 end
247 end
245 end
248 end
246
249
247 def due_date_distance_in_words(date)
250 def due_date_distance_in_words(date)
248 if date
251 if date
249 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
252 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
250 end
253 end
251 end
254 end
252
255
253 # Renders a tree of projects as a nested set of unordered lists
256 # Renders a tree of projects as a nested set of unordered lists
254 # The given collection may be a subset of the whole project tree
257 # The given collection may be a subset of the whole project tree
255 # (eg. some intermediate nodes are private and can not be seen)
258 # (eg. some intermediate nodes are private and can not be seen)
256 def render_project_nested_lists(projects)
259 def render_project_nested_lists(projects)
257 s = ''
260 s = ''
258 if projects.any?
261 if projects.any?
259 ancestors = []
262 ancestors = []
260 original_project = @project
263 original_project = @project
261 projects.sort_by(&:lft).each do |project|
264 projects.sort_by(&:lft).each do |project|
262 # set the project environment to please macros.
265 # set the project environment to please macros.
263 @project = project
266 @project = project
264 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
267 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
265 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
268 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
266 else
269 else
267 ancestors.pop
270 ancestors.pop
268 s << "</li>"
271 s << "</li>"
269 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
272 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
270 ancestors.pop
273 ancestors.pop
271 s << "</ul></li>\n"
274 s << "</ul></li>\n"
272 end
275 end
273 end
276 end
274 classes = (ancestors.empty? ? 'root' : 'child')
277 classes = (ancestors.empty? ? 'root' : 'child')
275 s << "<li class='#{classes}'><div class='#{classes}'>"
278 s << "<li class='#{classes}'><div class='#{classes}'>"
276 s << h(block_given? ? yield(project) : project.name)
279 s << h(block_given? ? yield(project) : project.name)
277 s << "</div>\n"
280 s << "</div>\n"
278 ancestors << project
281 ancestors << project
279 end
282 end
280 s << ("</li></ul>\n" * ancestors.size)
283 s << ("</li></ul>\n" * ancestors.size)
281 @project = original_project
284 @project = original_project
282 end
285 end
283 s.html_safe
286 s.html_safe
284 end
287 end
285
288
286 def render_page_hierarchy(pages, node=nil, options={})
289 def render_page_hierarchy(pages, node=nil, options={})
287 content = ''
290 content = ''
288 if pages[node]
291 if pages[node]
289 content << "<ul class=\"pages-hierarchy\">\n"
292 content << "<ul class=\"pages-hierarchy\">\n"
290 pages[node].each do |page|
293 pages[node].each do |page|
291 content << "<li>"
294 content << "<li>"
292 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
295 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
293 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
296 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
294 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
297 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
295 content << "</li>\n"
298 content << "</li>\n"
296 end
299 end
297 content << "</ul>\n"
300 content << "</ul>\n"
298 end
301 end
299 content.html_safe
302 content.html_safe
300 end
303 end
301
304
302 # Renders flash messages
305 # Renders flash messages
303 def render_flash_messages
306 def render_flash_messages
304 s = ''
307 s = ''
305 flash.each do |k,v|
308 flash.each do |k,v|
306 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
309 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
307 end
310 end
308 s.html_safe
311 s.html_safe
309 end
312 end
310
313
311 # Renders tabs and their content
314 # Renders tabs and their content
312 def render_tabs(tabs, selected=params[:tab])
315 def render_tabs(tabs, selected=params[:tab])
313 if tabs.any?
316 if tabs.any?
314 unless tabs.detect {|tab| tab[:name] == selected}
317 unless tabs.detect {|tab| tab[:name] == selected}
315 selected = nil
318 selected = nil
316 end
319 end
317 selected ||= tabs.first[:name]
320 selected ||= tabs.first[:name]
318 render :partial => 'common/tabs', :locals => {:tabs => tabs, :selected_tab => selected}
321 render :partial => 'common/tabs', :locals => {:tabs => tabs, :selected_tab => selected}
319 else
322 else
320 content_tag 'p', l(:label_no_data), :class => "nodata"
323 content_tag 'p', l(:label_no_data), :class => "nodata"
321 end
324 end
322 end
325 end
323
326
324 # Renders the project quick-jump box
327 # Renders the project quick-jump box
325 def render_project_jump_box
328 def render_project_jump_box
326 return unless User.current.logged?
329 return unless User.current.logged?
327 projects = User.current.memberships.collect(&:project).compact.select(&:active?).uniq
330 projects = User.current.memberships.collect(&:project).compact.select(&:active?).uniq
328 if projects.any?
331 if projects.any?
329 options =
332 options =
330 ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
333 ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
331 '<option value="" disabled="disabled">---</option>').html_safe
334 '<option value="" disabled="disabled">---</option>').html_safe
332
335
333 options << project_tree_options_for_select(projects, :selected => @project) do |p|
336 options << project_tree_options_for_select(projects, :selected => @project) do |p|
334 { :value => project_path(:id => p, :jump => current_menu_item) }
337 { :value => project_path(:id => p, :jump => current_menu_item) }
335 end
338 end
336
339
337 select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
340 select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
338 end
341 end
339 end
342 end
340
343
341 def project_tree_options_for_select(projects, options = {})
344 def project_tree_options_for_select(projects, options = {})
342 s = ''
345 s = ''
343 project_tree(projects) do |project, level|
346 project_tree(projects) do |project, level|
344 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
347 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
345 tag_options = {:value => project.id}
348 tag_options = {:value => project.id}
346 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
349 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
347 tag_options[:selected] = 'selected'
350 tag_options[:selected] = 'selected'
348 else
351 else
349 tag_options[:selected] = nil
352 tag_options[:selected] = nil
350 end
353 end
351 tag_options.merge!(yield(project)) if block_given?
354 tag_options.merge!(yield(project)) if block_given?
352 s << content_tag('option', name_prefix + h(project), tag_options)
355 s << content_tag('option', name_prefix + h(project), tag_options)
353 end
356 end
354 s.html_safe
357 s.html_safe
355 end
358 end
356
359
357 # Yields the given block for each project with its level in the tree
360 # Yields the given block for each project with its level in the tree
358 #
361 #
359 # Wrapper for Project#project_tree
362 # Wrapper for Project#project_tree
360 def project_tree(projects, &block)
363 def project_tree(projects, &block)
361 Project.project_tree(projects, &block)
364 Project.project_tree(projects, &block)
362 end
365 end
363
366
364 def principals_check_box_tags(name, principals)
367 def principals_check_box_tags(name, principals)
365 s = ''
368 s = ''
366 principals.each do |principal|
369 principals.each do |principal|
367 s << "<label>#{ check_box_tag name, principal.id, false, :id => nil } #{h principal}</label>\n"
370 s << "<label>#{ check_box_tag name, principal.id, false, :id => nil } #{h principal}</label>\n"
368 end
371 end
369 s.html_safe
372 s.html_safe
370 end
373 end
371
374
372 # Returns a string for users/groups option tags
375 # Returns a string for users/groups option tags
373 def principals_options_for_select(collection, selected=nil)
376 def principals_options_for_select(collection, selected=nil)
374 s = ''
377 s = ''
375 if collection.include?(User.current)
378 if collection.include?(User.current)
376 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
379 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
377 end
380 end
378 groups = ''
381 groups = ''
379 collection.sort.each do |element|
382 collection.sort.each do |element|
380 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) || element.id.to_s == selected
383 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) || element.id.to_s == selected
381 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
384 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
382 end
385 end
383 unless groups.empty?
386 unless groups.empty?
384 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
387 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
385 end
388 end
386 s.html_safe
389 s.html_safe
387 end
390 end
388
391
389 # Options for the new membership projects combo-box
392 # Options for the new membership projects combo-box
390 def options_for_membership_project_select(principal, projects)
393 def options_for_membership_project_select(principal, projects)
391 options = content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---")
394 options = content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---")
392 options << project_tree_options_for_select(projects) do |p|
395 options << project_tree_options_for_select(projects) do |p|
393 {:disabled => principal.projects.to_a.include?(p)}
396 {:disabled => principal.projects.to_a.include?(p)}
394 end
397 end
395 options
398 options
396 end
399 end
397
400
398 def option_tag(name, text, value, selected=nil, options={})
401 def option_tag(name, text, value, selected=nil, options={})
399 content_tag 'option', value, options.merge(:value => value, :selected => (value == selected))
402 content_tag 'option', value, options.merge(:value => value, :selected => (value == selected))
400 end
403 end
401
404
402 # Truncates and returns the string as a single line
405 # Truncates and returns the string as a single line
403 def truncate_single_line(string, *args)
406 def truncate_single_line(string, *args)
404 ActiveSupport::Deprecation.warn(
407 ActiveSupport::Deprecation.warn(
405 "ApplicationHelper#truncate_single_line is deprecated and will be removed in Rails 4 poring")
408 "ApplicationHelper#truncate_single_line is deprecated and will be removed in Rails 4 poring")
406 # Rails 4 ActionView::Helpers::TextHelper#truncate escapes.
409 # Rails 4 ActionView::Helpers::TextHelper#truncate escapes.
407 # So, result is broken.
410 # So, result is broken.
408 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
411 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
409 end
412 end
410
413
411 def truncate_single_line_raw(string, length)
414 def truncate_single_line_raw(string, length)
412 string.truncate(length).gsub(%r{[\r\n]+}m, ' ')
415 string.truncate(length).gsub(%r{[\r\n]+}m, ' ')
413 end
416 end
414
417
415 # Truncates at line break after 250 characters or options[:length]
418 # Truncates at line break after 250 characters or options[:length]
416 def truncate_lines(string, options={})
419 def truncate_lines(string, options={})
417 length = options[:length] || 250
420 length = options[:length] || 250
418 if string.to_s =~ /\A(.{#{length}}.*?)$/m
421 if string.to_s =~ /\A(.{#{length}}.*?)$/m
419 "#{$1}..."
422 "#{$1}..."
420 else
423 else
421 string
424 string
422 end
425 end
423 end
426 end
424
427
425 def anchor(text)
428 def anchor(text)
426 text.to_s.gsub(' ', '_')
429 text.to_s.gsub(' ', '_')
427 end
430 end
428
431
429 def html_hours(text)
432 def html_hours(text)
430 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
433 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
431 end
434 end
432
435
433 def authoring(created, author, options={})
436 def authoring(created, author, options={})
434 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
437 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
435 end
438 end
436
439
437 def time_tag(time)
440 def time_tag(time)
438 text = distance_of_time_in_words(Time.now, time)
441 text = distance_of_time_in_words(Time.now, time)
439 if @project
442 if @project
440 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => User.current.time_to_date(time)}, :title => format_time(time))
443 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => User.current.time_to_date(time)}, :title => format_time(time))
441 else
444 else
442 content_tag('abbr', text, :title => format_time(time))
445 content_tag('abbr', text, :title => format_time(time))
443 end
446 end
444 end
447 end
445
448
446 def syntax_highlight_lines(name, content)
449 def syntax_highlight_lines(name, content)
447 lines = []
450 lines = []
448 syntax_highlight(name, content).each_line { |line| lines << line }
451 syntax_highlight(name, content).each_line { |line| lines << line }
449 lines
452 lines
450 end
453 end
451
454
452 def syntax_highlight(name, content)
455 def syntax_highlight(name, content)
453 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
456 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
454 end
457 end
455
458
456 def to_path_param(path)
459 def to_path_param(path)
457 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
460 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
458 str.blank? ? nil : str
461 str.blank? ? nil : str
459 end
462 end
460
463
461 def reorder_links(name, url, method = :post)
464 def reorder_links(name, url, method = :post)
462 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
465 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
463 url.merge({"#{name}[move_to]" => 'highest'}),
466 url.merge({"#{name}[move_to]" => 'highest'}),
464 :method => method, :title => l(:label_sort_highest)) +
467 :method => method, :title => l(:label_sort_highest)) +
465 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
468 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
466 url.merge({"#{name}[move_to]" => 'higher'}),
469 url.merge({"#{name}[move_to]" => 'higher'}),
467 :method => method, :title => l(:label_sort_higher)) +
470 :method => method, :title => l(:label_sort_higher)) +
468 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
471 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
469 url.merge({"#{name}[move_to]" => 'lower'}),
472 url.merge({"#{name}[move_to]" => 'lower'}),
470 :method => method, :title => l(:label_sort_lower)) +
473 :method => method, :title => l(:label_sort_lower)) +
471 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
474 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
472 url.merge({"#{name}[move_to]" => 'lowest'}),
475 url.merge({"#{name}[move_to]" => 'lowest'}),
473 :method => method, :title => l(:label_sort_lowest))
476 :method => method, :title => l(:label_sort_lowest))
474 end
477 end
475
478
476 def breadcrumb(*args)
479 def breadcrumb(*args)
477 elements = args.flatten
480 elements = args.flatten
478 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
481 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
479 end
482 end
480
483
481 def other_formats_links(&block)
484 def other_formats_links(&block)
482 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
485 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
483 yield Redmine::Views::OtherFormatsBuilder.new(self)
486 yield Redmine::Views::OtherFormatsBuilder.new(self)
484 concat('</p>'.html_safe)
487 concat('</p>'.html_safe)
485 end
488 end
486
489
487 def page_header_title
490 def page_header_title
488 if @project.nil? || @project.new_record?
491 if @project.nil? || @project.new_record?
489 h(Setting.app_title)
492 h(Setting.app_title)
490 else
493 else
491 b = []
494 b = []
492 ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
495 ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
493 if ancestors.any?
496 if ancestors.any?
494 root = ancestors.shift
497 root = ancestors.shift
495 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
498 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
496 if ancestors.size > 2
499 if ancestors.size > 2
497 b << "\xe2\x80\xa6"
500 b << "\xe2\x80\xa6"
498 ancestors = ancestors[-2, 2]
501 ancestors = ancestors[-2, 2]
499 end
502 end
500 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
503 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
501 end
504 end
502 b << h(@project)
505 b << h(@project)
503 b.join(" \xc2\xbb ").html_safe
506 b.join(" \xc2\xbb ").html_safe
504 end
507 end
505 end
508 end
506
509
507 # Returns a h2 tag and sets the html title with the given arguments
510 # Returns a h2 tag and sets the html title with the given arguments
508 def title(*args)
511 def title(*args)
509 strings = args.map do |arg|
512 strings = args.map do |arg|
510 if arg.is_a?(Array) && arg.size >= 2
513 if arg.is_a?(Array) && arg.size >= 2
511 link_to(*arg)
514 link_to(*arg)
512 else
515 else
513 h(arg.to_s)
516 h(arg.to_s)
514 end
517 end
515 end
518 end
516 html_title args.reverse.map {|s| (s.is_a?(Array) ? s.first : s).to_s}
519 html_title args.reverse.map {|s| (s.is_a?(Array) ? s.first : s).to_s}
517 content_tag('h2', strings.join(' &#187; ').html_safe)
520 content_tag('h2', strings.join(' &#187; ').html_safe)
518 end
521 end
519
522
520 # Sets the html title
523 # Sets the html title
521 # Returns the html title when called without arguments
524 # Returns the html title when called without arguments
522 # Current project name and app_title and automatically appended
525 # Current project name and app_title and automatically appended
523 # Exemples:
526 # Exemples:
524 # html_title 'Foo', 'Bar'
527 # html_title 'Foo', 'Bar'
525 # html_title # => 'Foo - Bar - My Project - Redmine'
528 # html_title # => 'Foo - Bar - My Project - Redmine'
526 def html_title(*args)
529 def html_title(*args)
527 if args.empty?
530 if args.empty?
528 title = @html_title || []
531 title = @html_title || []
529 title << @project.name if @project
532 title << @project.name if @project
530 title << Setting.app_title unless Setting.app_title == title.last
533 title << Setting.app_title unless Setting.app_title == title.last
531 title.reject(&:blank?).join(' - ')
534 title.reject(&:blank?).join(' - ')
532 else
535 else
533 @html_title ||= []
536 @html_title ||= []
534 @html_title += args
537 @html_title += args
535 end
538 end
536 end
539 end
537
540
538 # Returns the theme, controller name, and action as css classes for the
541 # Returns the theme, controller name, and action as css classes for the
539 # HTML body.
542 # HTML body.
540 def body_css_classes
543 def body_css_classes
541 css = []
544 css = []
542 if theme = Redmine::Themes.theme(Setting.ui_theme)
545 if theme = Redmine::Themes.theme(Setting.ui_theme)
543 css << 'theme-' + theme.name
546 css << 'theme-' + theme.name
544 end
547 end
545
548
546 css << 'project-' + @project.identifier if @project && @project.identifier.present?
549 css << 'project-' + @project.identifier if @project && @project.identifier.present?
547 css << 'controller-' + controller_name
550 css << 'controller-' + controller_name
548 css << 'action-' + action_name
551 css << 'action-' + action_name
549 css.join(' ')
552 css.join(' ')
550 end
553 end
551
554
552 def accesskey(s)
555 def accesskey(s)
553 @used_accesskeys ||= []
556 @used_accesskeys ||= []
554 key = Redmine::AccessKeys.key_for(s)
557 key = Redmine::AccessKeys.key_for(s)
555 return nil if @used_accesskeys.include?(key)
558 return nil if @used_accesskeys.include?(key)
556 @used_accesskeys << key
559 @used_accesskeys << key
557 key
560 key
558 end
561 end
559
562
560 # Formats text according to system settings.
563 # Formats text according to system settings.
561 # 2 ways to call this method:
564 # 2 ways to call this method:
562 # * with a String: textilizable(text, options)
565 # * with a String: textilizable(text, options)
563 # * with an object and one of its attribute: textilizable(issue, :description, options)
566 # * with an object and one of its attribute: textilizable(issue, :description, options)
564 def textilizable(*args)
567 def textilizable(*args)
565 options = args.last.is_a?(Hash) ? args.pop : {}
568 options = args.last.is_a?(Hash) ? args.pop : {}
566 case args.size
569 case args.size
567 when 1
570 when 1
568 obj = options[:object]
571 obj = options[:object]
569 text = args.shift
572 text = args.shift
570 when 2
573 when 2
571 obj = args.shift
574 obj = args.shift
572 attr = args.shift
575 attr = args.shift
573 text = obj.send(attr).to_s
576 text = obj.send(attr).to_s
574 else
577 else
575 raise ArgumentError, 'invalid arguments to textilizable'
578 raise ArgumentError, 'invalid arguments to textilizable'
576 end
579 end
577 return '' if text.blank?
580 return '' if text.blank?
578 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
581 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
579 only_path = options.delete(:only_path) == false ? false : true
582 only_path = options.delete(:only_path) == false ? false : true
580
583
581 text = text.dup
584 text = text.dup
582 macros = catch_macros(text)
585 macros = catch_macros(text)
583 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
586 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
584
587
585 @parsed_headings = []
588 @parsed_headings = []
586 @heading_anchors = {}
589 @heading_anchors = {}
587 @current_section = 0 if options[:edit_section_links]
590 @current_section = 0 if options[:edit_section_links]
588
591
589 parse_sections(text, project, obj, attr, only_path, options)
592 parse_sections(text, project, obj, attr, only_path, options)
590 text = parse_non_pre_blocks(text, obj, macros) do |text|
593 text = parse_non_pre_blocks(text, obj, macros) do |text|
591 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
594 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
592 send method_name, text, project, obj, attr, only_path, options
595 send method_name, text, project, obj, attr, only_path, options
593 end
596 end
594 end
597 end
595 parse_headings(text, project, obj, attr, only_path, options)
598 parse_headings(text, project, obj, attr, only_path, options)
596
599
597 if @parsed_headings.any?
600 if @parsed_headings.any?
598 replace_toc(text, @parsed_headings)
601 replace_toc(text, @parsed_headings)
599 end
602 end
600
603
601 text.html_safe
604 text.html_safe
602 end
605 end
603
606
604 def parse_non_pre_blocks(text, obj, macros)
607 def parse_non_pre_blocks(text, obj, macros)
605 s = StringScanner.new(text)
608 s = StringScanner.new(text)
606 tags = []
609 tags = []
607 parsed = ''
610 parsed = ''
608 while !s.eos?
611 while !s.eos?
609 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
612 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
610 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
613 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
611 if tags.empty?
614 if tags.empty?
612 yield text
615 yield text
613 inject_macros(text, obj, macros) if macros.any?
616 inject_macros(text, obj, macros) if macros.any?
614 else
617 else
615 inject_macros(text, obj, macros, false) if macros.any?
618 inject_macros(text, obj, macros, false) if macros.any?
616 end
619 end
617 parsed << text
620 parsed << text
618 if tag
621 if tag
619 if closing
622 if closing
620 if tags.last == tag.downcase
623 if tags.last == tag.downcase
621 tags.pop
624 tags.pop
622 end
625 end
623 else
626 else
624 tags << tag.downcase
627 tags << tag.downcase
625 end
628 end
626 parsed << full_tag
629 parsed << full_tag
627 end
630 end
628 end
631 end
629 # Close any non closing tags
632 # Close any non closing tags
630 while tag = tags.pop
633 while tag = tags.pop
631 parsed << "</#{tag}>"
634 parsed << "</#{tag}>"
632 end
635 end
633 parsed
636 parsed
634 end
637 end
635
638
636 def parse_inline_attachments(text, project, obj, attr, only_path, options)
639 def parse_inline_attachments(text, project, obj, attr, only_path, options)
637 # when using an image link, try to use an attachment, if possible
640 # when using an image link, try to use an attachment, if possible
638 attachments = options[:attachments] || []
641 attachments = options[:attachments] || []
639 attachments += obj.attachments if obj.respond_to?(:attachments)
642 attachments += obj.attachments if obj.respond_to?(:attachments)
640 if attachments.present?
643 if attachments.present?
641 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
644 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
642 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
645 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
643 # search for the picture in attachments
646 # search for the picture in attachments
644 if found = Attachment.latest_attach(attachments, filename)
647 if found = Attachment.latest_attach(attachments, filename)
645 image_url = download_named_attachment_path(found, found.filename, :only_path => only_path)
648 image_url = download_named_attachment_path(found, found.filename, :only_path => only_path)
646 desc = found.description.to_s.gsub('"', '')
649 desc = found.description.to_s.gsub('"', '')
647 if !desc.blank? && alttext.blank?
650 if !desc.blank? && alttext.blank?
648 alt = " title=\"#{desc}\" alt=\"#{desc}\""
651 alt = " title=\"#{desc}\" alt=\"#{desc}\""
649 end
652 end
650 "src=\"#{image_url}\"#{alt}"
653 "src=\"#{image_url}\"#{alt}"
651 else
654 else
652 m
655 m
653 end
656 end
654 end
657 end
655 end
658 end
656 end
659 end
657
660
658 # Wiki links
661 # Wiki links
659 #
662 #
660 # Examples:
663 # Examples:
661 # [[mypage]]
664 # [[mypage]]
662 # [[mypage|mytext]]
665 # [[mypage|mytext]]
663 # wiki links can refer other project wikis, using project name or identifier:
666 # wiki links can refer other project wikis, using project name or identifier:
664 # [[project:]] -> wiki starting page
667 # [[project:]] -> wiki starting page
665 # [[project:|mytext]]
668 # [[project:|mytext]]
666 # [[project:mypage]]
669 # [[project:mypage]]
667 # [[project:mypage|mytext]]
670 # [[project:mypage|mytext]]
668 def parse_wiki_links(text, project, obj, attr, only_path, options)
671 def parse_wiki_links(text, project, obj, attr, only_path, options)
669 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
672 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
670 link_project = project
673 link_project = project
671 esc, all, page, title = $1, $2, $3, $5
674 esc, all, page, title = $1, $2, $3, $5
672 if esc.nil?
675 if esc.nil?
673 if page =~ /^([^\:]+)\:(.*)$/
676 if page =~ /^([^\:]+)\:(.*)$/
674 identifier, page = $1, $2
677 identifier, page = $1, $2
675 link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
678 link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
676 title ||= identifier if page.blank?
679 title ||= identifier if page.blank?
677 end
680 end
678
681
679 if link_project && link_project.wiki
682 if link_project && link_project.wiki
680 # extract anchor
683 # extract anchor
681 anchor = nil
684 anchor = nil
682 if page =~ /^(.+?)\#(.+)$/
685 if page =~ /^(.+?)\#(.+)$/
683 page, anchor = $1, $2
686 page, anchor = $1, $2
684 end
687 end
685 anchor = sanitize_anchor_name(anchor) if anchor.present?
688 anchor = sanitize_anchor_name(anchor) if anchor.present?
686 # check if page exists
689 # check if page exists
687 wiki_page = link_project.wiki.find_page(page)
690 wiki_page = link_project.wiki.find_page(page)
688 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
691 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
689 "##{anchor}"
692 "##{anchor}"
690 else
693 else
691 case options[:wiki_links]
694 case options[:wiki_links]
692 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
695 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
693 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
696 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
694 else
697 else
695 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
698 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
696 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
699 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
697 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
700 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
698 :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
701 :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
699 end
702 end
700 end
703 end
701 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
704 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
702 else
705 else
703 # project or wiki doesn't exist
706 # project or wiki doesn't exist
704 all
707 all
705 end
708 end
706 else
709 else
707 all
710 all
708 end
711 end
709 end
712 end
710 end
713 end
711
714
712 # Redmine links
715 # Redmine links
713 #
716 #
714 # Examples:
717 # Examples:
715 # Issues:
718 # Issues:
716 # #52 -> Link to issue #52
719 # #52 -> Link to issue #52
717 # Changesets:
720 # Changesets:
718 # r52 -> Link to revision 52
721 # r52 -> Link to revision 52
719 # commit:a85130f -> Link to scmid starting with a85130f
722 # commit:a85130f -> Link to scmid starting with a85130f
720 # Documents:
723 # Documents:
721 # document#17 -> Link to document with id 17
724 # document#17 -> Link to document with id 17
722 # document:Greetings -> Link to the document with title "Greetings"
725 # document:Greetings -> Link to the document with title "Greetings"
723 # document:"Some document" -> Link to the document with title "Some document"
726 # document:"Some document" -> Link to the document with title "Some document"
724 # Versions:
727 # Versions:
725 # version#3 -> Link to version with id 3
728 # version#3 -> Link to version with id 3
726 # version:1.0.0 -> Link to version named "1.0.0"
729 # version:1.0.0 -> Link to version named "1.0.0"
727 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
730 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
728 # Attachments:
731 # Attachments:
729 # attachment:file.zip -> Link to the attachment of the current object named file.zip
732 # attachment:file.zip -> Link to the attachment of the current object named file.zip
730 # Source files:
733 # Source files:
731 # source:some/file -> Link to the file located at /some/file in the project's repository
734 # source:some/file -> Link to the file located at /some/file in the project's repository
732 # source:some/file@52 -> Link to the file's revision 52
735 # source:some/file@52 -> Link to the file's revision 52
733 # source:some/file#L120 -> Link to line 120 of the file
736 # source:some/file#L120 -> Link to line 120 of the file
734 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
737 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
735 # export:some/file -> Force the download of the file
738 # export:some/file -> Force the download of the file
736 # Forum messages:
739 # Forum messages:
737 # message#1218 -> Link to message with id 1218
740 # message#1218 -> Link to message with id 1218
738 # Projects:
741 # Projects:
739 # project:someproject -> Link to project named "someproject"
742 # project:someproject -> Link to project named "someproject"
740 # project#3 -> Link to project with id 3
743 # project#3 -> Link to project with id 3
741 #
744 #
742 # Links can refer other objects from other projects, using project identifier:
745 # Links can refer other objects from other projects, using project identifier:
743 # identifier:r52
746 # identifier:r52
744 # identifier:document:"Some document"
747 # identifier:document:"Some document"
745 # identifier:version:1.0.0
748 # identifier:version:1.0.0
746 # identifier:source:some/file
749 # identifier:source:some/file
747 def parse_redmine_links(text, default_project, obj, attr, only_path, options)
750 def parse_redmine_links(text, default_project, obj, attr, only_path, options)
748 text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-_]+):)?(attachment|document|version|forum|news|message|project|commit|source|export)?(((#)|((([a-z0-9\-_]+)\|)?(r)))((\d+)((#note)?-(\d+))?)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]][^A-Za-z0-9_/])|,|\s|\]|<|$)}) do |m|
751 text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-_]+):)?(attachment|document|version|forum|news|message|project|commit|source|export)?(((#)|((([a-z0-9\-_]+)\|)?(r)))((\d+)((#note)?-(\d+))?)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]][^A-Za-z0-9_/])|,|\s|\]|<|$)}) do |m|
749 leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $1, $2, $3, $4, $5, $10, $11, $8 || $12 || $18, $14 || $19, $15, $17
752 leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $1, $2, $3, $4, $5, $10, $11, $8 || $12 || $18, $14 || $19, $15, $17
750 link = nil
753 link = nil
751 project = default_project
754 project = default_project
752 if project_identifier
755 if project_identifier
753 project = Project.visible.find_by_identifier(project_identifier)
756 project = Project.visible.find_by_identifier(project_identifier)
754 end
757 end
755 if esc.nil?
758 if esc.nil?
756 if prefix.nil? && sep == 'r'
759 if prefix.nil? && sep == 'r'
757 if project
760 if project
758 repository = nil
761 repository = nil
759 if repo_identifier
762 if repo_identifier
760 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
763 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
761 else
764 else
762 repository = project.repository
765 repository = project.repository
763 end
766 end
764 # project.changesets.visible raises an SQL error because of a double join on repositories
767 # project.changesets.visible raises an SQL error because of a double join on repositories
765 if repository &&
768 if repository &&
766 (changeset = Changeset.visible.
769 (changeset = Changeset.visible.
767 find_by_repository_id_and_revision(repository.id, identifier))
770 find_by_repository_id_and_revision(repository.id, identifier))
768 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"),
771 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"),
769 {:only_path => only_path, :controller => 'repositories',
772 {:only_path => only_path, :controller => 'repositories',
770 :action => 'revision', :id => project,
773 :action => 'revision', :id => project,
771 :repository_id => repository.identifier_param,
774 :repository_id => repository.identifier_param,
772 :rev => changeset.revision},
775 :rev => changeset.revision},
773 :class => 'changeset',
776 :class => 'changeset',
774 :title => truncate_single_line_raw(changeset.comments, 100))
777 :title => truncate_single_line_raw(changeset.comments, 100))
775 end
778 end
776 end
779 end
777 elsif sep == '#'
780 elsif sep == '#'
778 oid = identifier.to_i
781 oid = identifier.to_i
779 case prefix
782 case prefix
780 when nil
783 when nil
781 if oid.to_s == identifier &&
784 if oid.to_s == identifier &&
782 issue = Issue.visible.includes(:status).find_by_id(oid)
785 issue = Issue.visible.includes(:status).find_by_id(oid)
783 anchor = comment_id ? "note-#{comment_id}" : nil
786 anchor = comment_id ? "note-#{comment_id}" : nil
784 link = link_to(h("##{oid}#{comment_suffix}"),
787 link = link_to(h("##{oid}#{comment_suffix}"),
785 {:only_path => only_path, :controller => 'issues',
788 {:only_path => only_path, :controller => 'issues',
786 :action => 'show', :id => oid, :anchor => anchor},
789 :action => 'show', :id => oid, :anchor => anchor},
787 :class => issue.css_classes,
790 :class => issue.css_classes,
788 :title => "#{issue.subject.truncate(100)} (#{issue.status.name})")
791 :title => "#{issue.subject.truncate(100)} (#{issue.status.name})")
789 end
792 end
790 when 'document'
793 when 'document'
791 if document = Document.visible.find_by_id(oid)
794 if document = Document.visible.find_by_id(oid)
792 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
795 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
793 :class => 'document'
796 :class => 'document'
794 end
797 end
795 when 'version'
798 when 'version'
796 if version = Version.visible.find_by_id(oid)
799 if version = Version.visible.find_by_id(oid)
797 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
800 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
798 :class => 'version'
801 :class => 'version'
799 end
802 end
800 when 'message'
803 when 'message'
801 if message = Message.visible.includes(:parent).find_by_id(oid)
804 if message = Message.visible.includes(:parent).find_by_id(oid)
802 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
805 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
803 end
806 end
804 when 'forum'
807 when 'forum'
805 if board = Board.visible.find_by_id(oid)
808 if board = Board.visible.find_by_id(oid)
806 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
809 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
807 :class => 'board'
810 :class => 'board'
808 end
811 end
809 when 'news'
812 when 'news'
810 if news = News.visible.find_by_id(oid)
813 if news = News.visible.find_by_id(oid)
811 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
814 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
812 :class => 'news'
815 :class => 'news'
813 end
816 end
814 when 'project'
817 when 'project'
815 if p = Project.visible.find_by_id(oid)
818 if p = Project.visible.find_by_id(oid)
816 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
819 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
817 end
820 end
818 end
821 end
819 elsif sep == ':'
822 elsif sep == ':'
820 # removes the double quotes if any
823 # removes the double quotes if any
821 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
824 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
822 case prefix
825 case prefix
823 when 'document'
826 when 'document'
824 if project && document = project.documents.visible.find_by_title(name)
827 if project && document = project.documents.visible.find_by_title(name)
825 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
828 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
826 :class => 'document'
829 :class => 'document'
827 end
830 end
828 when 'version'
831 when 'version'
829 if project && version = project.versions.visible.find_by_name(name)
832 if project && version = project.versions.visible.find_by_name(name)
830 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
833 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
831 :class => 'version'
834 :class => 'version'
832 end
835 end
833 when 'forum'
836 when 'forum'
834 if project && board = project.boards.visible.find_by_name(name)
837 if project && board = project.boards.visible.find_by_name(name)
835 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
838 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
836 :class => 'board'
839 :class => 'board'
837 end
840 end
838 when 'news'
841 when 'news'
839 if project && news = project.news.visible.find_by_title(name)
842 if project && news = project.news.visible.find_by_title(name)
840 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
843 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
841 :class => 'news'
844 :class => 'news'
842 end
845 end
843 when 'commit', 'source', 'export'
846 when 'commit', 'source', 'export'
844 if project
847 if project
845 repository = nil
848 repository = nil
846 if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$}
849 if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$}
847 repo_prefix, repo_identifier, name = $1, $2, $3
850 repo_prefix, repo_identifier, name = $1, $2, $3
848 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
851 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
849 else
852 else
850 repository = project.repository
853 repository = project.repository
851 end
854 end
852 if prefix == 'commit'
855 if prefix == 'commit'
853 if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
856 if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
854 link = link_to h("#{project_prefix}#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.identifier},
857 link = link_to h("#{project_prefix}#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.identifier},
855 :class => 'changeset',
858 :class => 'changeset',
856 :title => truncate_single_line_raw(changeset.comments, 100)
859 :title => truncate_single_line_raw(changeset.comments, 100)
857 end
860 end
858 else
861 else
859 if repository && User.current.allowed_to?(:browse_repository, project)
862 if repository && User.current.allowed_to?(:browse_repository, project)
860 name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
863 name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
861 path, rev, anchor = $1, $3, $5
864 path, rev, anchor = $1, $3, $5
862 link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:controller => 'repositories', :action => (prefix == 'export' ? 'raw' : 'entry'), :id => project, :repository_id => repository.identifier_param,
865 link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:controller => 'repositories', :action => (prefix == 'export' ? 'raw' : 'entry'), :id => project, :repository_id => repository.identifier_param,
863 :path => to_path_param(path),
866 :path => to_path_param(path),
864 :rev => rev,
867 :rev => rev,
865 :anchor => anchor},
868 :anchor => anchor},
866 :class => (prefix == 'export' ? 'source download' : 'source')
869 :class => (prefix == 'export' ? 'source download' : 'source')
867 end
870 end
868 end
871 end
869 repo_prefix = nil
872 repo_prefix = nil
870 end
873 end
871 when 'attachment'
874 when 'attachment'
872 attachments = options[:attachments] || []
875 attachments = options[:attachments] || []
873 attachments += obj.attachments if obj.respond_to?(:attachments)
876 attachments += obj.attachments if obj.respond_to?(:attachments)
874 if attachments && attachment = Attachment.latest_attach(attachments, name)
877 if attachments && attachment = Attachment.latest_attach(attachments, name)
875 link = link_to_attachment(attachment, :only_path => only_path, :download => true, :class => 'attachment')
878 link = link_to_attachment(attachment, :only_path => only_path, :download => true, :class => 'attachment')
876 end
879 end
877 when 'project'
880 when 'project'
878 if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
881 if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
879 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
882 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
880 end
883 end
881 end
884 end
882 end
885 end
883 end
886 end
884 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
887 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
885 end
888 end
886 end
889 end
887
890
888 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
891 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
889
892
890 def parse_sections(text, project, obj, attr, only_path, options)
893 def parse_sections(text, project, obj, attr, only_path, options)
891 return unless options[:edit_section_links]
894 return unless options[:edit_section_links]
892 text.gsub!(HEADING_RE) do
895 text.gsub!(HEADING_RE) do
893 heading = $1
896 heading = $1
894 @current_section += 1
897 @current_section += 1
895 if @current_section > 1
898 if @current_section > 1
896 content_tag('div',
899 content_tag('div',
897 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
900 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
898 :class => 'contextual',
901 :class => 'contextual',
899 :title => l(:button_edit_section),
902 :title => l(:button_edit_section),
900 :id => "section-#{@current_section}") + heading.html_safe
903 :id => "section-#{@current_section}") + heading.html_safe
901 else
904 else
902 heading
905 heading
903 end
906 end
904 end
907 end
905 end
908 end
906
909
907 # Headings and TOC
910 # Headings and TOC
908 # Adds ids and links to headings unless options[:headings] is set to false
911 # Adds ids and links to headings unless options[:headings] is set to false
909 def parse_headings(text, project, obj, attr, only_path, options)
912 def parse_headings(text, project, obj, attr, only_path, options)
910 return if options[:headings] == false
913 return if options[:headings] == false
911
914
912 text.gsub!(HEADING_RE) do
915 text.gsub!(HEADING_RE) do
913 level, attrs, content = $2.to_i, $3, $4
916 level, attrs, content = $2.to_i, $3, $4
914 item = strip_tags(content).strip
917 item = strip_tags(content).strip
915 anchor = sanitize_anchor_name(item)
918 anchor = sanitize_anchor_name(item)
916 # used for single-file wiki export
919 # used for single-file wiki export
917 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
920 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
918 @heading_anchors[anchor] ||= 0
921 @heading_anchors[anchor] ||= 0
919 idx = (@heading_anchors[anchor] += 1)
922 idx = (@heading_anchors[anchor] += 1)
920 if idx > 1
923 if idx > 1
921 anchor = "#{anchor}-#{idx}"
924 anchor = "#{anchor}-#{idx}"
922 end
925 end
923 @parsed_headings << [level, anchor, item]
926 @parsed_headings << [level, anchor, item]
924 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
927 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
925 end
928 end
926 end
929 end
927
930
928 MACROS_RE = /(
931 MACROS_RE = /(
929 (!)? # escaping
932 (!)? # escaping
930 (
933 (
931 \{\{ # opening tag
934 \{\{ # opening tag
932 ([\w]+) # macro name
935 ([\w]+) # macro name
933 (\(([^\n\r]*?)\))? # optional arguments
936 (\(([^\n\r]*?)\))? # optional arguments
934 ([\n\r].*?[\n\r])? # optional block of text
937 ([\n\r].*?[\n\r])? # optional block of text
935 \}\} # closing tag
938 \}\} # closing tag
936 )
939 )
937 )/mx unless const_defined?(:MACROS_RE)
940 )/mx unless const_defined?(:MACROS_RE)
938
941
939 MACRO_SUB_RE = /(
942 MACRO_SUB_RE = /(
940 \{\{
943 \{\{
941 macro\((\d+)\)
944 macro\((\d+)\)
942 \}\}
945 \}\}
943 )/x unless const_defined?(:MACRO_SUB_RE)
946 )/x unless const_defined?(:MACRO_SUB_RE)
944
947
945 # Extracts macros from text
948 # Extracts macros from text
946 def catch_macros(text)
949 def catch_macros(text)
947 macros = {}
950 macros = {}
948 text.gsub!(MACROS_RE) do
951 text.gsub!(MACROS_RE) do
949 all, macro = $1, $4.downcase
952 all, macro = $1, $4.downcase
950 if macro_exists?(macro) || all =~ MACRO_SUB_RE
953 if macro_exists?(macro) || all =~ MACRO_SUB_RE
951 index = macros.size
954 index = macros.size
952 macros[index] = all
955 macros[index] = all
953 "{{macro(#{index})}}"
956 "{{macro(#{index})}}"
954 else
957 else
955 all
958 all
956 end
959 end
957 end
960 end
958 macros
961 macros
959 end
962 end
960
963
961 # Executes and replaces macros in text
964 # Executes and replaces macros in text
962 def inject_macros(text, obj, macros, execute=true)
965 def inject_macros(text, obj, macros, execute=true)
963 text.gsub!(MACRO_SUB_RE) do
966 text.gsub!(MACRO_SUB_RE) do
964 all, index = $1, $2.to_i
967 all, index = $1, $2.to_i
965 orig = macros.delete(index)
968 orig = macros.delete(index)
966 if execute && orig && orig =~ MACROS_RE
969 if execute && orig && orig =~ MACROS_RE
967 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
970 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
968 if esc.nil?
971 if esc.nil?
969 h(exec_macro(macro, obj, args, block) || all)
972 h(exec_macro(macro, obj, args, block) || all)
970 else
973 else
971 h(all)
974 h(all)
972 end
975 end
973 elsif orig
976 elsif orig
974 h(orig)
977 h(orig)
975 else
978 else
976 h(all)
979 h(all)
977 end
980 end
978 end
981 end
979 end
982 end
980
983
981 TOC_RE = /<p>\{\{((<|&lt;)|(>|&gt;))?toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
984 TOC_RE = /<p>\{\{((<|&lt;)|(>|&gt;))?toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
982
985
983 # Renders the TOC with given headings
986 # Renders the TOC with given headings
984 def replace_toc(text, headings)
987 def replace_toc(text, headings)
985 text.gsub!(TOC_RE) do
988 text.gsub!(TOC_RE) do
986 left_align, right_align = $2, $3
989 left_align, right_align = $2, $3
987 # Keep only the 4 first levels
990 # Keep only the 4 first levels
988 headings = headings.select{|level, anchor, item| level <= 4}
991 headings = headings.select{|level, anchor, item| level <= 4}
989 if headings.empty?
992 if headings.empty?
990 ''
993 ''
991 else
994 else
992 div_class = 'toc'
995 div_class = 'toc'
993 div_class << ' right' if right_align
996 div_class << ' right' if right_align
994 div_class << ' left' if left_align
997 div_class << ' left' if left_align
995 out = "<ul class=\"#{div_class}\"><li>"
998 out = "<ul class=\"#{div_class}\"><li>"
996 root = headings.map(&:first).min
999 root = headings.map(&:first).min
997 current = root
1000 current = root
998 started = false
1001 started = false
999 headings.each do |level, anchor, item|
1002 headings.each do |level, anchor, item|
1000 if level > current
1003 if level > current
1001 out << '<ul><li>' * (level - current)
1004 out << '<ul><li>' * (level - current)
1002 elsif level < current
1005 elsif level < current
1003 out << "</li></ul>\n" * (current - level) + "</li><li>"
1006 out << "</li></ul>\n" * (current - level) + "</li><li>"
1004 elsif started
1007 elsif started
1005 out << '</li><li>'
1008 out << '</li><li>'
1006 end
1009 end
1007 out << "<a href=\"##{anchor}\">#{item}</a>"
1010 out << "<a href=\"##{anchor}\">#{item}</a>"
1008 current = level
1011 current = level
1009 started = true
1012 started = true
1010 end
1013 end
1011 out << '</li></ul>' * (current - root)
1014 out << '</li></ul>' * (current - root)
1012 out << '</li></ul>'
1015 out << '</li></ul>'
1013 end
1016 end
1014 end
1017 end
1015 end
1018 end
1016
1019
1017 # Same as Rails' simple_format helper without using paragraphs
1020 # Same as Rails' simple_format helper without using paragraphs
1018 def simple_format_without_paragraph(text)
1021 def simple_format_without_paragraph(text)
1019 text.to_s.
1022 text.to_s.
1020 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
1023 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
1021 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
1024 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
1022 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
1025 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
1023 html_safe
1026 html_safe
1024 end
1027 end
1025
1028
1026 def lang_options_for_select(blank=true)
1029 def lang_options_for_select(blank=true)
1027 (blank ? [["(auto)", ""]] : []) + languages_options
1030 (blank ? [["(auto)", ""]] : []) + languages_options
1028 end
1031 end
1029
1032
1030 def label_tag_for(name, option_tags = nil, options = {})
1033 def label_tag_for(name, option_tags = nil, options = {})
1031 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
1034 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
1032 content_tag("label", label_text)
1035 content_tag("label", label_text)
1033 end
1036 end
1034
1037
1035 def labelled_form_for(*args, &proc)
1038 def labelled_form_for(*args, &proc)
1036 args << {} unless args.last.is_a?(Hash)
1039 args << {} unless args.last.is_a?(Hash)
1037 options = args.last
1040 options = args.last
1038 if args.first.is_a?(Symbol)
1041 if args.first.is_a?(Symbol)
1039 options.merge!(:as => args.shift)
1042 options.merge!(:as => args.shift)
1040 end
1043 end
1041 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1044 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1042 form_for(*args, &proc)
1045 form_for(*args, &proc)
1043 end
1046 end
1044
1047
1045 def labelled_fields_for(*args, &proc)
1048 def labelled_fields_for(*args, &proc)
1046 args << {} unless args.last.is_a?(Hash)
1049 args << {} unless args.last.is_a?(Hash)
1047 options = args.last
1050 options = args.last
1048 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1051 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1049 fields_for(*args, &proc)
1052 fields_for(*args, &proc)
1050 end
1053 end
1051
1054
1052 def labelled_remote_form_for(*args, &proc)
1055 def labelled_remote_form_for(*args, &proc)
1053 ActiveSupport::Deprecation.warn "ApplicationHelper#labelled_remote_form_for is deprecated and will be removed in Redmine 2.2."
1056 ActiveSupport::Deprecation.warn "ApplicationHelper#labelled_remote_form_for is deprecated and will be removed in Redmine 2.2."
1054 args << {} unless args.last.is_a?(Hash)
1057 args << {} unless args.last.is_a?(Hash)
1055 options = args.last
1058 options = args.last
1056 options.merge!({:builder => Redmine::Views::LabelledFormBuilder, :remote => true})
1059 options.merge!({:builder => Redmine::Views::LabelledFormBuilder, :remote => true})
1057 form_for(*args, &proc)
1060 form_for(*args, &proc)
1058 end
1061 end
1059
1062
1060 def error_messages_for(*objects)
1063 def error_messages_for(*objects)
1061 html = ""
1064 html = ""
1062 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
1065 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
1063 errors = objects.map {|o| o.errors.full_messages}.flatten
1066 errors = objects.map {|o| o.errors.full_messages}.flatten
1064 if errors.any?
1067 if errors.any?
1065 html << "<div id='errorExplanation'><ul>\n"
1068 html << "<div id='errorExplanation'><ul>\n"
1066 errors.each do |error|
1069 errors.each do |error|
1067 html << "<li>#{h error}</li>\n"
1070 html << "<li>#{h error}</li>\n"
1068 end
1071 end
1069 html << "</ul></div>\n"
1072 html << "</ul></div>\n"
1070 end
1073 end
1071 html.html_safe
1074 html.html_safe
1072 end
1075 end
1073
1076
1074 def delete_link(url, options={})
1077 def delete_link(url, options={})
1075 options = {
1078 options = {
1076 :method => :delete,
1079 :method => :delete,
1077 :data => {:confirm => l(:text_are_you_sure)},
1080 :data => {:confirm => l(:text_are_you_sure)},
1078 :class => 'icon icon-del'
1081 :class => 'icon icon-del'
1079 }.merge(options)
1082 }.merge(options)
1080
1083
1081 link_to l(:button_delete), url, options
1084 link_to l(:button_delete), url, options
1082 end
1085 end
1083
1086
1084 def preview_link(url, form, target='preview', options={})
1087 def preview_link(url, form, target='preview', options={})
1085 content_tag 'a', l(:label_preview), {
1088 content_tag 'a', l(:label_preview), {
1086 :href => "#",
1089 :href => "#",
1087 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
1090 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
1088 :accesskey => accesskey(:preview)
1091 :accesskey => accesskey(:preview)
1089 }.merge(options)
1092 }.merge(options)
1090 end
1093 end
1091
1094
1092 def link_to_function(name, function, html_options={})
1095 def link_to_function(name, function, html_options={})
1093 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1096 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1094 end
1097 end
1095
1098
1096 # Helper to render JSON in views
1099 # Helper to render JSON in views
1097 def raw_json(arg)
1100 def raw_json(arg)
1098 arg.to_json.to_s.gsub('/', '\/').html_safe
1101 arg.to_json.to_s.gsub('/', '\/').html_safe
1099 end
1102 end
1100
1103
1101 def back_url
1104 def back_url
1102 url = params[:back_url]
1105 url = params[:back_url]
1103 if url.nil? && referer = request.env['HTTP_REFERER']
1106 if url.nil? && referer = request.env['HTTP_REFERER']
1104 url = CGI.unescape(referer.to_s)
1107 url = CGI.unescape(referer.to_s)
1105 end
1108 end
1106 url
1109 url
1107 end
1110 end
1108
1111
1109 def back_url_hidden_field_tag
1112 def back_url_hidden_field_tag
1110 url = back_url
1113 url = back_url
1111 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1114 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1112 end
1115 end
1113
1116
1114 def check_all_links(form_name)
1117 def check_all_links(form_name)
1115 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1118 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1116 " | ".html_safe +
1119 " | ".html_safe +
1117 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1120 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1118 end
1121 end
1119
1122
1120 def progress_bar(pcts, options={})
1123 def progress_bar(pcts, options={})
1121 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1124 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1122 pcts = pcts.collect(&:round)
1125 pcts = pcts.collect(&:round)
1123 pcts[1] = pcts[1] - pcts[0]
1126 pcts[1] = pcts[1] - pcts[0]
1124 pcts << (100 - pcts[1] - pcts[0])
1127 pcts << (100 - pcts[1] - pcts[0])
1125 width = options[:width] || '100px;'
1128 width = options[:width] || '100px;'
1126 legend = options[:legend] || ''
1129 legend = options[:legend] || ''
1127 content_tag('table',
1130 content_tag('table',
1128 content_tag('tr',
1131 content_tag('tr',
1129 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
1132 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
1130 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
1133 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
1131 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
1134 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
1132 ), :class => "progress progress-#{pcts[0]}", :style => "width: #{width};").html_safe +
1135 ), :class => "progress progress-#{pcts[0]}", :style => "width: #{width};").html_safe +
1133 content_tag('p', legend, :class => 'percent').html_safe
1136 content_tag('p', legend, :class => 'percent').html_safe
1134 end
1137 end
1135
1138
1136 def checked_image(checked=true)
1139 def checked_image(checked=true)
1137 if checked
1140 if checked
1138 image_tag 'toggle_check.png'
1141 image_tag 'toggle_check.png'
1139 end
1142 end
1140 end
1143 end
1141
1144
1142 def context_menu(url)
1145 def context_menu(url)
1143 unless @context_menu_included
1146 unless @context_menu_included
1144 content_for :header_tags do
1147 content_for :header_tags do
1145 javascript_include_tag('context_menu') +
1148 javascript_include_tag('context_menu') +
1146 stylesheet_link_tag('context_menu')
1149 stylesheet_link_tag('context_menu')
1147 end
1150 end
1148 if l(:direction) == 'rtl'
1151 if l(:direction) == 'rtl'
1149 content_for :header_tags do
1152 content_for :header_tags do
1150 stylesheet_link_tag('context_menu_rtl')
1153 stylesheet_link_tag('context_menu_rtl')
1151 end
1154 end
1152 end
1155 end
1153 @context_menu_included = true
1156 @context_menu_included = true
1154 end
1157 end
1155 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1158 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1156 end
1159 end
1157
1160
1158 def calendar_for(field_id)
1161 def calendar_for(field_id)
1159 include_calendar_headers_tags
1162 include_calendar_headers_tags
1160 javascript_tag("$(function() { $('##{field_id}').datepicker(datepickerOptions); });")
1163 javascript_tag("$(function() { $('##{field_id}').datepicker(datepickerOptions); });")
1161 end
1164 end
1162
1165
1163 def include_calendar_headers_tags
1166 def include_calendar_headers_tags
1164 unless @calendar_headers_tags_included
1167 unless @calendar_headers_tags_included
1165 tags = javascript_include_tag("datepicker")
1168 tags = javascript_include_tag("datepicker")
1166 @calendar_headers_tags_included = true
1169 @calendar_headers_tags_included = true
1167 content_for :header_tags do
1170 content_for :header_tags do
1168 start_of_week = Setting.start_of_week
1171 start_of_week = Setting.start_of_week
1169 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1172 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1170 # Redmine uses 1..7 (monday..sunday) in settings and locales
1173 # Redmine uses 1..7 (monday..sunday) in settings and locales
1171 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1174 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1172 start_of_week = start_of_week.to_i % 7
1175 start_of_week = start_of_week.to_i % 7
1173 tags << javascript_tag(
1176 tags << javascript_tag(
1174 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1177 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1175 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1178 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1176 path_to_image('/images/calendar.png') +
1179 path_to_image('/images/calendar.png') +
1177 "', showButtonPanel: true, showWeek: true, showOtherMonths: true, " +
1180 "', showButtonPanel: true, showWeek: true, showOtherMonths: true, " +
1178 "selectOtherMonths: true, changeMonth: true, changeYear: true, " +
1181 "selectOtherMonths: true, changeMonth: true, changeYear: true, " +
1179 "beforeShow: beforeShowDatePicker};")
1182 "beforeShow: beforeShowDatePicker};")
1180 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1183 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1181 unless jquery_locale == 'en'
1184 unless jquery_locale == 'en'
1182 tags << javascript_include_tag("i18n/jquery.ui.datepicker-#{jquery_locale}.js")
1185 tags << javascript_include_tag("i18n/jquery.ui.datepicker-#{jquery_locale}.js")
1183 end
1186 end
1184 tags
1187 tags
1185 end
1188 end
1186 end
1189 end
1187 end
1190 end
1188
1191
1189 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1192 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1190 # Examples:
1193 # Examples:
1191 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1194 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1192 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1195 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1193 #
1196 #
1194 def stylesheet_link_tag(*sources)
1197 def stylesheet_link_tag(*sources)
1195 options = sources.last.is_a?(Hash) ? sources.pop : {}
1198 options = sources.last.is_a?(Hash) ? sources.pop : {}
1196 plugin = options.delete(:plugin)
1199 plugin = options.delete(:plugin)
1197 sources = sources.map do |source|
1200 sources = sources.map do |source|
1198 if plugin
1201 if plugin
1199 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1202 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1200 elsif current_theme && current_theme.stylesheets.include?(source)
1203 elsif current_theme && current_theme.stylesheets.include?(source)
1201 current_theme.stylesheet_path(source)
1204 current_theme.stylesheet_path(source)
1202 else
1205 else
1203 source
1206 source
1204 end
1207 end
1205 end
1208 end
1206 super sources, options
1209 super sources, options
1207 end
1210 end
1208
1211
1209 # Overrides Rails' image_tag with themes and plugins support.
1212 # Overrides Rails' image_tag with themes and plugins support.
1210 # Examples:
1213 # Examples:
1211 # image_tag('image.png') # => picks image.png from the current theme or defaults
1214 # image_tag('image.png') # => picks image.png from the current theme or defaults
1212 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1215 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1213 #
1216 #
1214 def image_tag(source, options={})
1217 def image_tag(source, options={})
1215 if plugin = options.delete(:plugin)
1218 if plugin = options.delete(:plugin)
1216 source = "/plugin_assets/#{plugin}/images/#{source}"
1219 source = "/plugin_assets/#{plugin}/images/#{source}"
1217 elsif current_theme && current_theme.images.include?(source)
1220 elsif current_theme && current_theme.images.include?(source)
1218 source = current_theme.image_path(source)
1221 source = current_theme.image_path(source)
1219 end
1222 end
1220 super source, options
1223 super source, options
1221 end
1224 end
1222
1225
1223 # Overrides Rails' javascript_include_tag with plugins support
1226 # Overrides Rails' javascript_include_tag with plugins support
1224 # Examples:
1227 # Examples:
1225 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1228 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1226 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1229 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1227 #
1230 #
1228 def javascript_include_tag(*sources)
1231 def javascript_include_tag(*sources)
1229 options = sources.last.is_a?(Hash) ? sources.pop : {}
1232 options = sources.last.is_a?(Hash) ? sources.pop : {}
1230 if plugin = options.delete(:plugin)
1233 if plugin = options.delete(:plugin)
1231 sources = sources.map do |source|
1234 sources = sources.map do |source|
1232 if plugin
1235 if plugin
1233 "/plugin_assets/#{plugin}/javascripts/#{source}"
1236 "/plugin_assets/#{plugin}/javascripts/#{source}"
1234 else
1237 else
1235 source
1238 source
1236 end
1239 end
1237 end
1240 end
1238 end
1241 end
1239 super sources, options
1242 super sources, options
1240 end
1243 end
1241
1244
1242 # TODO: remove this in 2.5.0
1245 # TODO: remove this in 2.5.0
1243 def has_content?(name)
1246 def has_content?(name)
1244 content_for?(name)
1247 content_for?(name)
1245 end
1248 end
1246
1249
1247 def sidebar_content?
1250 def sidebar_content?
1248 content_for?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1251 content_for?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1249 end
1252 end
1250
1253
1251 def view_layouts_base_sidebar_hook_response
1254 def view_layouts_base_sidebar_hook_response
1252 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1255 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1253 end
1256 end
1254
1257
1255 def email_delivery_enabled?
1258 def email_delivery_enabled?
1256 !!ActionMailer::Base.perform_deliveries
1259 !!ActionMailer::Base.perform_deliveries
1257 end
1260 end
1258
1261
1259 # Returns the avatar image tag for the given +user+ if avatars are enabled
1262 # Returns the avatar image tag for the given +user+ if avatars are enabled
1260 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1263 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1261 def avatar(user, options = { })
1264 def avatar(user, options = { })
1262 if Setting.gravatar_enabled?
1265 if Setting.gravatar_enabled?
1263 options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
1266 options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
1264 email = nil
1267 email = nil
1265 if user.respond_to?(:mail)
1268 if user.respond_to?(:mail)
1266 email = user.mail
1269 email = user.mail
1267 elsif user.to_s =~ %r{<(.+?)>}
1270 elsif user.to_s =~ %r{<(.+?)>}
1268 email = $1
1271 email = $1
1269 end
1272 end
1270 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1273 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1271 else
1274 else
1272 ''
1275 ''
1273 end
1276 end
1274 end
1277 end
1275
1278
1276 def sanitize_anchor_name(anchor)
1279 def sanitize_anchor_name(anchor)
1277 if ''.respond_to?(:encoding) || RUBY_PLATFORM == 'java'
1280 if ''.respond_to?(:encoding) || RUBY_PLATFORM == 'java'
1278 anchor.gsub(%r{[^\s\-\p{Word}]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1281 anchor.gsub(%r{[^\s\-\p{Word}]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1279 else
1282 else
1280 # TODO: remove when ruby1.8 is no longer supported
1283 # TODO: remove when ruby1.8 is no longer supported
1281 anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1284 anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1282 end
1285 end
1283 end
1286 end
1284
1287
1285 # Returns the javascript tags that are included in the html layout head
1288 # Returns the javascript tags that are included in the html layout head
1286 def javascript_heads
1289 def javascript_heads
1287 tags = javascript_include_tag('jquery-1.8.3-ui-1.9.2-ujs-2.0.3', 'application')
1290 tags = javascript_include_tag('jquery-1.8.3-ui-1.9.2-ujs-2.0.3', 'application')
1288 unless User.current.pref.warn_on_leaving_unsaved == '0'
1291 unless User.current.pref.warn_on_leaving_unsaved == '0'
1289 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1292 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1290 end
1293 end
1291 tags
1294 tags
1292 end
1295 end
1293
1296
1294 def favicon
1297 def favicon
1295 "<link rel='shortcut icon' href='#{favicon_path}' />".html_safe
1298 "<link rel='shortcut icon' href='#{favicon_path}' />".html_safe
1296 end
1299 end
1297
1300
1298 # Returns the path to the favicon
1301 # Returns the path to the favicon
1299 def favicon_path
1302 def favicon_path
1300 icon = (current_theme && current_theme.favicon?) ? current_theme.favicon_path : '/favicon.ico'
1303 icon = (current_theme && current_theme.favicon?) ? current_theme.favicon_path : '/favicon.ico'
1301 image_path(icon)
1304 image_path(icon)
1302 end
1305 end
1303
1306
1304 # Returns the full URL to the favicon
1307 # Returns the full URL to the favicon
1305 def favicon_url
1308 def favicon_url
1306 # TODO: use #image_url introduced in Rails4
1309 # TODO: use #image_url introduced in Rails4
1307 path = favicon_path
1310 path = favicon_path
1308 base = url_for(:controller => 'welcome', :action => 'index', :only_path => false)
1311 base = url_for(:controller => 'welcome', :action => 'index', :only_path => false)
1309 base.sub(%r{/+$},'') + '/' + path.sub(%r{^/+},'')
1312 base.sub(%r{/+$},'') + '/' + path.sub(%r{^/+},'')
1310 end
1313 end
1311
1314
1312 def robot_exclusion_tag
1315 def robot_exclusion_tag
1313 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1316 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1314 end
1317 end
1315
1318
1316 # Returns true if arg is expected in the API response
1319 # Returns true if arg is expected in the API response
1317 def include_in_api_response?(arg)
1320 def include_in_api_response?(arg)
1318 unless @included_in_api_response
1321 unless @included_in_api_response
1319 param = params[:include]
1322 param = params[:include]
1320 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1323 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1321 @included_in_api_response.collect!(&:strip)
1324 @included_in_api_response.collect!(&:strip)
1322 end
1325 end
1323 @included_in_api_response.include?(arg.to_s)
1326 @included_in_api_response.include?(arg.to_s)
1324 end
1327 end
1325
1328
1326 # Returns options or nil if nometa param or X-Redmine-Nometa header
1329 # Returns options or nil if nometa param or X-Redmine-Nometa header
1327 # was set in the request
1330 # was set in the request
1328 def api_meta(options)
1331 def api_meta(options)
1329 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1332 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1330 # compatibility mode for activeresource clients that raise
1333 # compatibility mode for activeresource clients that raise
1331 # an error when unserializing an array with attributes
1334 # an error when unserializing an array with attributes
1332 nil
1335 nil
1333 else
1336 else
1334 options
1337 options
1335 end
1338 end
1336 end
1339 end
1337
1340
1338 private
1341 private
1339
1342
1340 def wiki_helper
1343 def wiki_helper
1341 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1344 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1342 extend helper
1345 extend helper
1343 return self
1346 return self
1344 end
1347 end
1345
1348
1346 def link_to_content_update(text, url_params = {}, html_options = {})
1349 def link_to_content_update(text, url_params = {}, html_options = {})
1347 link_to(text, url_params, html_options)
1350 link_to(text, url_params, html_options)
1348 end
1351 end
1349 end
1352 end
@@ -1,202 +1,198
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2014 Jean-Philippe Lang
4 # Copyright (C) 2006-2014 Jean-Philippe Lang
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
9 # of the License, or (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
20 module QueriesHelper
20 module QueriesHelper
21 include ApplicationHelper
22
21 def filters_options_for_select(query)
23 def filters_options_for_select(query)
22 options_for_select(filters_options(query))
24 options_for_select(filters_options(query))
23 end
25 end
24
26
25 def filters_options(query)
27 def filters_options(query)
26 options = [[]]
28 options = [[]]
27 options += query.available_filters.map do |field, field_options|
29 options += query.available_filters.map do |field, field_options|
28 [field_options[:name], field]
30 [field_options[:name], field]
29 end
31 end
30 end
32 end
31
33
32 def query_filters_hidden_tags(query)
34 def query_filters_hidden_tags(query)
33 tags = ''.html_safe
35 tags = ''.html_safe
34 query.filters.each do |field, options|
36 query.filters.each do |field, options|
35 tags << hidden_field_tag("f[]", field, :id => nil)
37 tags << hidden_field_tag("f[]", field, :id => nil)
36 tags << hidden_field_tag("op[#{field}]", options[:operator], :id => nil)
38 tags << hidden_field_tag("op[#{field}]", options[:operator], :id => nil)
37 options[:values].each do |value|
39 options[:values].each do |value|
38 tags << hidden_field_tag("v[#{field}][]", value, :id => nil)
40 tags << hidden_field_tag("v[#{field}][]", value, :id => nil)
39 end
41 end
40 end
42 end
41 tags
43 tags
42 end
44 end
43
45
44 def query_columns_hidden_tags(query)
46 def query_columns_hidden_tags(query)
45 tags = ''.html_safe
47 tags = ''.html_safe
46 query.columns.each do |column|
48 query.columns.each do |column|
47 tags << hidden_field_tag("c[]", column.name, :id => nil)
49 tags << hidden_field_tag("c[]", column.name, :id => nil)
48 end
50 end
49 tags
51 tags
50 end
52 end
51
53
52 def query_hidden_tags(query)
54 def query_hidden_tags(query)
53 query_filters_hidden_tags(query) + query_columns_hidden_tags(query)
55 query_filters_hidden_tags(query) + query_columns_hidden_tags(query)
54 end
56 end
55
57
56 def available_block_columns_tags(query)
58 def available_block_columns_tags(query)
57 tags = ''.html_safe
59 tags = ''.html_safe
58 query.available_block_columns.each do |column|
60 query.available_block_columns.each do |column|
59 tags << content_tag('label', check_box_tag('c[]', column.name.to_s, query.has_column?(column), :id => nil) + " #{column.caption}", :class => 'inline')
61 tags << content_tag('label', check_box_tag('c[]', column.name.to_s, query.has_column?(column), :id => nil) + " #{column.caption}", :class => 'inline')
60 end
62 end
61 tags
63 tags
62 end
64 end
63
65
64 def query_available_inline_columns_options(query)
66 def query_available_inline_columns_options(query)
65 (query.available_inline_columns - query.columns).reject(&:frozen?).collect {|column| [column.caption, column.name]}
67 (query.available_inline_columns - query.columns).reject(&:frozen?).collect {|column| [column.caption, column.name]}
66 end
68 end
67
69
68 def query_selected_inline_columns_options(query)
70 def query_selected_inline_columns_options(query)
69 (query.inline_columns & query.available_inline_columns).reject(&:frozen?).collect {|column| [column.caption, column.name]}
71 (query.inline_columns & query.available_inline_columns).reject(&:frozen?).collect {|column| [column.caption, column.name]}
70 end
72 end
71
73
72 def render_query_columns_selection(query, options={})
74 def render_query_columns_selection(query, options={})
73 tag_name = (options[:name] || 'c') + '[]'
75 tag_name = (options[:name] || 'c') + '[]'
74 render :partial => 'queries/columns', :locals => {:query => query, :tag_name => tag_name}
76 render :partial => 'queries/columns', :locals => {:query => query, :tag_name => tag_name}
75 end
77 end
76
78
77 def column_header(column)
79 def column_header(column)
78 column.sortable ? sort_header_tag(column.name.to_s, :caption => column.caption,
80 column.sortable ? sort_header_tag(column.name.to_s, :caption => column.caption,
79 :default_order => column.default_order) :
81 :default_order => column.default_order) :
80 content_tag('th', h(column.caption))
82 content_tag('th', h(column.caption))
81 end
83 end
82
84
83 def column_content(column, issue)
85 def column_content(column, issue)
84 value = column.value(issue)
86 value = column.value_object(issue)
85 if value.is_a?(Array)
87 if value.is_a?(Array)
86 value.collect {|v| column_value(column, issue, v)}.compact.join(', ').html_safe
88 value.collect {|v| column_value(column, issue, v)}.compact.join(', ').html_safe
87 else
89 else
88 column_value(column, issue, value)
90 column_value(column, issue, value)
89 end
91 end
90 end
92 end
91
93
92 def column_value(column, issue, value)
94 def column_value(column, issue, value)
93 case column.name
95 case column.name
94 when :id
96 when :id
95 link_to value, issue_path(issue)
97 link_to value, issue_path(issue)
96 when :subject
98 when :subject
97 link_to value, issue_path(issue)
99 link_to value, issue_path(issue)
98 when :description
100 when :description
99 issue.description? ? content_tag('div', textilizable(issue, :description), :class => "wiki") : ''
101 issue.description? ? content_tag('div', textilizable(issue, :description), :class => "wiki") : ''
100 when :done_ratio
102 when :done_ratio
101 progress_bar(value, :width => '80px')
103 progress_bar(value, :width => '80px')
102 when :relations
104 when :relations
103 other = value.other_issue(issue)
105 other = value.other_issue(issue)
104 content_tag('span',
106 content_tag('span',
105 (l(value.label_for(issue)) + " " + link_to_issue(other, :subject => false, :tracker => false)).html_safe,
107 (l(value.label_for(issue)) + " " + link_to_issue(other, :subject => false, :tracker => false)).html_safe,
106 :class => value.css_classes_for(issue))
108 :class => value.css_classes_for(issue))
107 else
109 else
108 format_object(value)
110 format_object(value)
109 end
111 end
110 end
112 end
111
113
112 def csv_content(column, issue)
114 def csv_content(column, issue)
113 value = column.value(issue)
115 value = column.value_object(issue)
114 if value.is_a?(Array)
116 if value.is_a?(Array)
115 value.collect {|v| csv_value(column, issue, v)}.compact.join(', ')
117 value.collect {|v| csv_value(column, issue, v)}.compact.join(', ')
116 else
118 else
117 csv_value(column, issue, value)
119 csv_value(column, issue, value)
118 end
120 end
119 end
121 end
120
122
121 def csv_value(column, issue, value)
123 def csv_value(column, issue, value)
124 format_object(value, false) do |value|
122 case value.class.name
125 case value.class.name
123 when 'Time'
124 format_time(value)
125 when 'Date'
126 format_date(value)
127 when 'Float'
126 when 'Float'
128 sprintf("%.2f", value).gsub('.', l(:general_csv_decimal_separator))
127 sprintf("%.2f", value).gsub('.', l(:general_csv_decimal_separator))
129 when 'IssueRelation'
128 when 'IssueRelation'
130 other = value.other_issue(issue)
129 other = value.other_issue(issue)
131 l(value.label_for(issue)) + " ##{other.id}"
130 l(value.label_for(issue)) + " ##{other.id}"
132 when 'TrueClass'
133 l(:general_text_Yes)
134 when 'FalseClass'
135 l(:general_text_No)
136 else
131 else
137 value.to_s
132 value
133 end
138 end
134 end
139 end
135 end
140
136
141 def query_to_csv(items, query, options={})
137 def query_to_csv(items, query, options={})
142 encoding = l(:general_csv_encoding)
138 encoding = l(:general_csv_encoding)
143 columns = (options[:columns] == 'all' ? query.available_inline_columns : query.inline_columns)
139 columns = (options[:columns] == 'all' ? query.available_inline_columns : query.inline_columns)
144 query.available_block_columns.each do |column|
140 query.available_block_columns.each do |column|
145 if options[column.name].present?
141 if options[column.name].present?
146 columns << column
142 columns << column
147 end
143 end
148 end
144 end
149
145
150 export = FCSV.generate(:col_sep => l(:general_csv_separator)) do |csv|
146 export = FCSV.generate(:col_sep => l(:general_csv_separator)) do |csv|
151 # csv header fields
147 # csv header fields
152 csv << columns.collect {|c| Redmine::CodesetUtil.from_utf8(c.caption.to_s, encoding) }
148 csv << columns.collect {|c| Redmine::CodesetUtil.from_utf8(c.caption.to_s, encoding) }
153 # csv lines
149 # csv lines
154 items.each do |item|
150 items.each do |item|
155 csv << columns.collect {|c| Redmine::CodesetUtil.from_utf8(csv_content(c, item), encoding) }
151 csv << columns.collect {|c| Redmine::CodesetUtil.from_utf8(csv_content(c, item), encoding) }
156 end
152 end
157 end
153 end
158 export
154 export
159 end
155 end
160
156
161 # Retrieve query from session or build a new query
157 # Retrieve query from session or build a new query
162 def retrieve_query
158 def retrieve_query
163 if !params[:query_id].blank?
159 if !params[:query_id].blank?
164 cond = "project_id IS NULL"
160 cond = "project_id IS NULL"
165 cond << " OR project_id = #{@project.id}" if @project
161 cond << " OR project_id = #{@project.id}" if @project
166 @query = IssueQuery.where(cond).find(params[:query_id])
162 @query = IssueQuery.where(cond).find(params[:query_id])
167 raise ::Unauthorized unless @query.visible?
163 raise ::Unauthorized unless @query.visible?
168 @query.project = @project
164 @query.project = @project
169 session[:query] = {:id => @query.id, :project_id => @query.project_id}
165 session[:query] = {:id => @query.id, :project_id => @query.project_id}
170 sort_clear
166 sort_clear
171 elsif api_request? || params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
167 elsif api_request? || params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
172 # Give it a name, required to be valid
168 # Give it a name, required to be valid
173 @query = IssueQuery.new(:name => "_")
169 @query = IssueQuery.new(:name => "_")
174 @query.project = @project
170 @query.project = @project
175 @query.build_from_params(params)
171 @query.build_from_params(params)
176 session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names}
172 session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names}
177 else
173 else
178 # retrieve from session
174 # retrieve from session
179 @query = nil
175 @query = nil
180 @query = IssueQuery.find_by_id(session[:query][:id]) if session[:query][:id]
176 @query = IssueQuery.find_by_id(session[:query][:id]) if session[:query][:id]
181 @query ||= IssueQuery.new(:name => "_", :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
177 @query ||= IssueQuery.new(:name => "_", :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
182 @query.project = @project
178 @query.project = @project
183 end
179 end
184 end
180 end
185
181
186 def retrieve_query_from_session
182 def retrieve_query_from_session
187 if session[:query]
183 if session[:query]
188 if session[:query][:id]
184 if session[:query][:id]
189 @query = IssueQuery.find_by_id(session[:query][:id])
185 @query = IssueQuery.find_by_id(session[:query][:id])
190 return unless @query
186 return unless @query
191 else
187 else
192 @query = IssueQuery.new(:name => "_", :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
188 @query = IssueQuery.new(:name => "_", :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
193 end
189 end
194 if session[:query].has_key?(:project_id)
190 if session[:query].has_key?(:project_id)
195 @query.project_id = session[:query][:project_id]
191 @query.project_id = session[:query][:project_id]
196 else
192 else
197 @query.project = @project
193 @query.project = @project
198 end
194 end
199 @query
195 @query
200 end
196 end
201 end
197 end
202 end
198 end
@@ -1,869 +1,884
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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 @inline = options.key?(:inline) ? options[:inline] : true
30 @inline = options.key?(:inline) ? options[:inline] : true
31 @caption_key = options[:caption] || "field_#{name}".to_sym
31 @caption_key = options[:caption] || "field_#{name}".to_sym
32 @frozen = options[:frozen]
32 @frozen = options[:frozen]
33 end
33 end
34
34
35 def caption
35 def caption
36 @caption_key.is_a?(Symbol) ? l(@caption_key) : @caption_key
36 @caption_key.is_a?(Symbol) ? l(@caption_key) : @caption_key
37 end
37 end
38
38
39 # Returns true if the column is sortable, otherwise false
39 # Returns true if the column is sortable, otherwise false
40 def sortable?
40 def sortable?
41 !@sortable.nil?
41 !@sortable.nil?
42 end
42 end
43
43
44 def sortable
44 def sortable
45 @sortable.is_a?(Proc) ? @sortable.call : @sortable
45 @sortable.is_a?(Proc) ? @sortable.call : @sortable
46 end
46 end
47
47
48 def inline?
48 def inline?
49 @inline
49 @inline
50 end
50 end
51
51
52 def frozen?
52 def frozen?
53 @frozen
53 @frozen
54 end
54 end
55
55
56 def value(object)
56 def value(object)
57 object.send name
57 object.send name
58 end
58 end
59
59
60 def value_object(object)
61 object.send name
62 end
63
60 def css_classes
64 def css_classes
61 name
65 name
62 end
66 end
63 end
67 end
64
68
65 class QueryCustomFieldColumn < QueryColumn
69 class QueryCustomFieldColumn < QueryColumn
66
70
67 def initialize(custom_field)
71 def initialize(custom_field)
68 self.name = "cf_#{custom_field.id}".to_sym
72 self.name = "cf_#{custom_field.id}".to_sym
69 self.sortable = custom_field.order_statement || false
73 self.sortable = custom_field.order_statement || false
70 self.groupable = custom_field.group_statement || false
74 self.groupable = custom_field.group_statement || false
71 @inline = true
75 @inline = true
72 @cf = custom_field
76 @cf = custom_field
73 end
77 end
74
78
75 def caption
79 def caption
76 @cf.name
80 @cf.name
77 end
81 end
78
82
79 def custom_field
83 def custom_field
80 @cf
84 @cf
81 end
85 end
82
86
83 def value(object)
87 def value_object(object)
84 if custom_field.visible_by?(object.project, User.current)
88 if custom_field.visible_by?(object.project, User.current)
85 cv = object.custom_values.select {|v| v.custom_field_id == @cf.id}.collect {|v| @cf.cast_value(v.value)}
89 cv = object.custom_values.select {|v| v.custom_field_id == @cf.id}
86 cv.size > 1 ? cv.sort {|a,b| a.to_s <=> b.to_s} : cv.first
90 cv.size > 1 ? cv.sort {|a,b| a.value.to_s <=> b.value.to_s} : cv.first
91 else
92 nil
93 end
94 end
95
96 def value(object)
97 raw = value_object(object)
98 if raw.is_a?(Array)
99 raw.map {|r| @cf.cast_value(r.value)}
100 elsif raw
101 @cf.cast_value(raw.value)
87 else
102 else
88 nil
103 nil
89 end
104 end
90 end
105 end
91
106
92 def css_classes
107 def css_classes
93 @css_classes ||= "#{name} #{@cf.field_format}"
108 @css_classes ||= "#{name} #{@cf.field_format}"
94 end
109 end
95 end
110 end
96
111
97 class QueryAssociationCustomFieldColumn < QueryCustomFieldColumn
112 class QueryAssociationCustomFieldColumn < QueryCustomFieldColumn
98
113
99 def initialize(association, custom_field)
114 def initialize(association, custom_field)
100 super(custom_field)
115 super(custom_field)
101 self.name = "#{association}.cf_#{custom_field.id}".to_sym
116 self.name = "#{association}.cf_#{custom_field.id}".to_sym
102 # TODO: support sorting/grouping by association custom field
117 # TODO: support sorting/grouping by association custom field
103 self.sortable = false
118 self.sortable = false
104 self.groupable = false
119 self.groupable = false
105 @association = association
120 @association = association
106 end
121 end
107
122
108 def value(object)
123 def value_object(object)
109 if assoc = object.send(@association)
124 if assoc = object.send(@association)
110 super(assoc)
125 super(assoc)
111 end
126 end
112 end
127 end
113
128
114 def css_classes
129 def css_classes
115 @css_classes ||= "#{@association}_cf_#{@cf.id} #{@cf.field_format}"
130 @css_classes ||= "#{@association}_cf_#{@cf.id} #{@cf.field_format}"
116 end
131 end
117 end
132 end
118
133
119 class Query < ActiveRecord::Base
134 class Query < ActiveRecord::Base
120 class StatementInvalid < ::ActiveRecord::StatementInvalid
135 class StatementInvalid < ::ActiveRecord::StatementInvalid
121 end
136 end
122
137
123 VISIBILITY_PRIVATE = 0
138 VISIBILITY_PRIVATE = 0
124 VISIBILITY_ROLES = 1
139 VISIBILITY_ROLES = 1
125 VISIBILITY_PUBLIC = 2
140 VISIBILITY_PUBLIC = 2
126
141
127 belongs_to :project
142 belongs_to :project
128 belongs_to :user
143 belongs_to :user
129 has_and_belongs_to_many :roles, :join_table => "#{table_name_prefix}queries_roles#{table_name_suffix}", :foreign_key => "query_id"
144 has_and_belongs_to_many :roles, :join_table => "#{table_name_prefix}queries_roles#{table_name_suffix}", :foreign_key => "query_id"
130 serialize :filters
145 serialize :filters
131 serialize :column_names
146 serialize :column_names
132 serialize :sort_criteria, Array
147 serialize :sort_criteria, Array
133 serialize :options, Hash
148 serialize :options, Hash
134
149
135 attr_protected :project_id, :user_id
150 attr_protected :project_id, :user_id
136
151
137 validates_presence_of :name
152 validates_presence_of :name
138 validates_length_of :name, :maximum => 255
153 validates_length_of :name, :maximum => 255
139 validates :visibility, :inclusion => { :in => [VISIBILITY_PUBLIC, VISIBILITY_ROLES, VISIBILITY_PRIVATE] }
154 validates :visibility, :inclusion => { :in => [VISIBILITY_PUBLIC, VISIBILITY_ROLES, VISIBILITY_PRIVATE] }
140 validate :validate_query_filters
155 validate :validate_query_filters
141 validate do |query|
156 validate do |query|
142 errors.add(:base, l(:label_role_plural) + ' ' + l('activerecord.errors.messages.blank')) if query.visibility == VISIBILITY_ROLES && roles.blank?
157 errors.add(:base, l(:label_role_plural) + ' ' + l('activerecord.errors.messages.blank')) if query.visibility == VISIBILITY_ROLES && roles.blank?
143 end
158 end
144
159
145 after_save do |query|
160 after_save do |query|
146 if query.visibility_changed? && query.visibility != VISIBILITY_ROLES
161 if query.visibility_changed? && query.visibility != VISIBILITY_ROLES
147 query.roles.clear
162 query.roles.clear
148 end
163 end
149 end
164 end
150
165
151 class_attribute :operators
166 class_attribute :operators
152 self.operators = {
167 self.operators = {
153 "=" => :label_equals,
168 "=" => :label_equals,
154 "!" => :label_not_equals,
169 "!" => :label_not_equals,
155 "o" => :label_open_issues,
170 "o" => :label_open_issues,
156 "c" => :label_closed_issues,
171 "c" => :label_closed_issues,
157 "!*" => :label_none,
172 "!*" => :label_none,
158 "*" => :label_any,
173 "*" => :label_any,
159 ">=" => :label_greater_or_equal,
174 ">=" => :label_greater_or_equal,
160 "<=" => :label_less_or_equal,
175 "<=" => :label_less_or_equal,
161 "><" => :label_between,
176 "><" => :label_between,
162 "<t+" => :label_in_less_than,
177 "<t+" => :label_in_less_than,
163 ">t+" => :label_in_more_than,
178 ">t+" => :label_in_more_than,
164 "><t+"=> :label_in_the_next_days,
179 "><t+"=> :label_in_the_next_days,
165 "t+" => :label_in,
180 "t+" => :label_in,
166 "t" => :label_today,
181 "t" => :label_today,
167 "ld" => :label_yesterday,
182 "ld" => :label_yesterday,
168 "w" => :label_this_week,
183 "w" => :label_this_week,
169 "lw" => :label_last_week,
184 "lw" => :label_last_week,
170 "l2w" => [:label_last_n_weeks, {:count => 2}],
185 "l2w" => [:label_last_n_weeks, {:count => 2}],
171 "m" => :label_this_month,
186 "m" => :label_this_month,
172 "lm" => :label_last_month,
187 "lm" => :label_last_month,
173 "y" => :label_this_year,
188 "y" => :label_this_year,
174 ">t-" => :label_less_than_ago,
189 ">t-" => :label_less_than_ago,
175 "<t-" => :label_more_than_ago,
190 "<t-" => :label_more_than_ago,
176 "><t-"=> :label_in_the_past_days,
191 "><t-"=> :label_in_the_past_days,
177 "t-" => :label_ago,
192 "t-" => :label_ago,
178 "~" => :label_contains,
193 "~" => :label_contains,
179 "!~" => :label_not_contains,
194 "!~" => :label_not_contains,
180 "=p" => :label_any_issues_in_project,
195 "=p" => :label_any_issues_in_project,
181 "=!p" => :label_any_issues_not_in_project,
196 "=!p" => :label_any_issues_not_in_project,
182 "!p" => :label_no_issues_in_project
197 "!p" => :label_no_issues_in_project
183 }
198 }
184
199
185 class_attribute :operators_by_filter_type
200 class_attribute :operators_by_filter_type
186 self.operators_by_filter_type = {
201 self.operators_by_filter_type = {
187 :list => [ "=", "!" ],
202 :list => [ "=", "!" ],
188 :list_status => [ "o", "=", "!", "c", "*" ],
203 :list_status => [ "o", "=", "!", "c", "*" ],
189 :list_optional => [ "=", "!", "!*", "*" ],
204 :list_optional => [ "=", "!", "!*", "*" ],
190 :list_subprojects => [ "*", "!*", "=" ],
205 :list_subprojects => [ "*", "!*", "=" ],
191 :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "><t+", "t+", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", ">t-", "<t-", "><t-", "t-", "!*", "*" ],
206 :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "><t+", "t+", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", ">t-", "<t-", "><t-", "t-", "!*", "*" ],
192 :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "><t-", "t-", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", "!*", "*" ],
207 :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "><t-", "t-", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", "!*", "*" ],
193 :string => [ "=", "~", "!", "!~", "!*", "*" ],
208 :string => [ "=", "~", "!", "!~", "!*", "*" ],
194 :text => [ "~", "!~", "!*", "*" ],
209 :text => [ "~", "!~", "!*", "*" ],
195 :integer => [ "=", ">=", "<=", "><", "!*", "*" ],
210 :integer => [ "=", ">=", "<=", "><", "!*", "*" ],
196 :float => [ "=", ">=", "<=", "><", "!*", "*" ],
211 :float => [ "=", ">=", "<=", "><", "!*", "*" ],
197 :relation => ["=", "=p", "=!p", "!p", "!*", "*"]
212 :relation => ["=", "=p", "=!p", "!p", "!*", "*"]
198 }
213 }
199
214
200 class_attribute :available_columns
215 class_attribute :available_columns
201 self.available_columns = []
216 self.available_columns = []
202
217
203 class_attribute :queried_class
218 class_attribute :queried_class
204
219
205 def queried_table_name
220 def queried_table_name
206 @queried_table_name ||= self.class.queried_class.table_name
221 @queried_table_name ||= self.class.queried_class.table_name
207 end
222 end
208
223
209 def initialize(attributes=nil, *args)
224 def initialize(attributes=nil, *args)
210 super attributes
225 super attributes
211 @is_for_all = project.nil?
226 @is_for_all = project.nil?
212 end
227 end
213
228
214 # Builds the query from the given params
229 # Builds the query from the given params
215 def build_from_params(params)
230 def build_from_params(params)
216 if params[:fields] || params[:f]
231 if params[:fields] || params[:f]
217 self.filters = {}
232 self.filters = {}
218 add_filters(params[:fields] || params[:f], params[:operators] || params[:op], params[:values] || params[:v])
233 add_filters(params[:fields] || params[:f], params[:operators] || params[:op], params[:values] || params[:v])
219 else
234 else
220 available_filters.keys.each do |field|
235 available_filters.keys.each do |field|
221 add_short_filter(field, params[field]) if params[field]
236 add_short_filter(field, params[field]) if params[field]
222 end
237 end
223 end
238 end
224 self.group_by = params[:group_by] || (params[:query] && params[:query][:group_by])
239 self.group_by = params[:group_by] || (params[:query] && params[:query][:group_by])
225 self.column_names = params[:c] || (params[:query] && params[:query][:column_names])
240 self.column_names = params[:c] || (params[:query] && params[:query][:column_names])
226 self
241 self
227 end
242 end
228
243
229 # Builds a new query from the given params and attributes
244 # Builds a new query from the given params and attributes
230 def self.build_from_params(params, attributes={})
245 def self.build_from_params(params, attributes={})
231 new(attributes).build_from_params(params)
246 new(attributes).build_from_params(params)
232 end
247 end
233
248
234 def validate_query_filters
249 def validate_query_filters
235 filters.each_key do |field|
250 filters.each_key do |field|
236 if values_for(field)
251 if values_for(field)
237 case type_for(field)
252 case type_for(field)
238 when :integer
253 when :integer
239 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+$/) }
254 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+$/) }
240 when :float
255 when :float
241 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+(\.\d*)?$/) }
256 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+(\.\d*)?$/) }
242 when :date, :date_past
257 when :date, :date_past
243 case operator_for(field)
258 case operator_for(field)
244 when "=", ">=", "<=", "><"
259 when "=", ">=", "<=", "><"
245 add_filter_error(field, :invalid) if values_for(field).detect {|v|
260 add_filter_error(field, :invalid) if values_for(field).detect {|v|
246 v.present? && (!v.match(/\A\d{4}-\d{2}-\d{2}(T\d{2}((:)?\d{2}){0,2}(Z|\d{2}:?\d{2})?)?\z/) || parse_date(v).nil?)
261 v.present? && (!v.match(/\A\d{4}-\d{2}-\d{2}(T\d{2}((:)?\d{2}){0,2}(Z|\d{2}:?\d{2})?)?\z/) || parse_date(v).nil?)
247 }
262 }
248 when ">t-", "<t-", "t-", ">t+", "<t+", "t+", "><t+", "><t-"
263 when ">t-", "<t-", "t-", ">t+", "<t+", "t+", "><t+", "><t-"
249 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
264 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
250 end
265 end
251 end
266 end
252 end
267 end
253
268
254 add_filter_error(field, :blank) unless
269 add_filter_error(field, :blank) unless
255 # filter requires one or more values
270 # filter requires one or more values
256 (values_for(field) and !values_for(field).first.blank?) or
271 (values_for(field) and !values_for(field).first.blank?) or
257 # filter doesn't require any value
272 # filter doesn't require any value
258 ["o", "c", "!*", "*", "t", "ld", "w", "lw", "l2w", "m", "lm", "y"].include? operator_for(field)
273 ["o", "c", "!*", "*", "t", "ld", "w", "lw", "l2w", "m", "lm", "y"].include? operator_for(field)
259 end if filters
274 end if filters
260 end
275 end
261
276
262 def add_filter_error(field, message)
277 def add_filter_error(field, message)
263 m = label_for(field) + " " + l(message, :scope => 'activerecord.errors.messages')
278 m = label_for(field) + " " + l(message, :scope => 'activerecord.errors.messages')
264 errors.add(:base, m)
279 errors.add(:base, m)
265 end
280 end
266
281
267 def editable_by?(user)
282 def editable_by?(user)
268 return false unless user
283 return false unless user
269 # Admin can edit them all and regular users can edit their private queries
284 # Admin can edit them all and regular users can edit their private queries
270 return true if user.admin? || (is_private? && self.user_id == user.id)
285 return true if user.admin? || (is_private? && self.user_id == user.id)
271 # Members can not edit public queries that are for all project (only admin is allowed to)
286 # Members can not edit public queries that are for all project (only admin is allowed to)
272 is_public? && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
287 is_public? && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
273 end
288 end
274
289
275 def trackers
290 def trackers
276 @trackers ||= project.nil? ? Tracker.sorted.all : project.rolled_up_trackers
291 @trackers ||= project.nil? ? Tracker.sorted.all : project.rolled_up_trackers
277 end
292 end
278
293
279 # Returns a hash of localized labels for all filter operators
294 # Returns a hash of localized labels for all filter operators
280 def self.operators_labels
295 def self.operators_labels
281 operators.inject({}) {|h, operator| h[operator.first] = l(*operator.last); h}
296 operators.inject({}) {|h, operator| h[operator.first] = l(*operator.last); h}
282 end
297 end
283
298
284 # Returns a representation of the available filters for JSON serialization
299 # Returns a representation of the available filters for JSON serialization
285 def available_filters_as_json
300 def available_filters_as_json
286 json = {}
301 json = {}
287 available_filters.each do |field, options|
302 available_filters.each do |field, options|
288 json[field] = options.slice(:type, :name, :values).stringify_keys
303 json[field] = options.slice(:type, :name, :values).stringify_keys
289 end
304 end
290 json
305 json
291 end
306 end
292
307
293 def all_projects
308 def all_projects
294 @all_projects ||= Project.visible.all
309 @all_projects ||= Project.visible.all
295 end
310 end
296
311
297 def all_projects_values
312 def all_projects_values
298 return @all_projects_values if @all_projects_values
313 return @all_projects_values if @all_projects_values
299
314
300 values = []
315 values = []
301 Project.project_tree(all_projects) do |p, level|
316 Project.project_tree(all_projects) do |p, level|
302 prefix = (level > 0 ? ('--' * level + ' ') : '')
317 prefix = (level > 0 ? ('--' * level + ' ') : '')
303 values << ["#{prefix}#{p.name}", p.id.to_s]
318 values << ["#{prefix}#{p.name}", p.id.to_s]
304 end
319 end
305 @all_projects_values = values
320 @all_projects_values = values
306 end
321 end
307
322
308 # Adds available filters
323 # Adds available filters
309 def initialize_available_filters
324 def initialize_available_filters
310 # implemented by sub-classes
325 # implemented by sub-classes
311 end
326 end
312 protected :initialize_available_filters
327 protected :initialize_available_filters
313
328
314 # Adds an available filter
329 # Adds an available filter
315 def add_available_filter(field, options)
330 def add_available_filter(field, options)
316 @available_filters ||= ActiveSupport::OrderedHash.new
331 @available_filters ||= ActiveSupport::OrderedHash.new
317 @available_filters[field] = options
332 @available_filters[field] = options
318 @available_filters
333 @available_filters
319 end
334 end
320
335
321 # Removes an available filter
336 # Removes an available filter
322 def delete_available_filter(field)
337 def delete_available_filter(field)
323 if @available_filters
338 if @available_filters
324 @available_filters.delete(field)
339 @available_filters.delete(field)
325 end
340 end
326 end
341 end
327
342
328 # Return a hash of available filters
343 # Return a hash of available filters
329 def available_filters
344 def available_filters
330 unless @available_filters
345 unless @available_filters
331 initialize_available_filters
346 initialize_available_filters
332 @available_filters.each do |field, options|
347 @available_filters.each do |field, options|
333 options[:name] ||= l(options[:label] || "field_#{field}".gsub(/_id$/, ''))
348 options[:name] ||= l(options[:label] || "field_#{field}".gsub(/_id$/, ''))
334 end
349 end
335 end
350 end
336 @available_filters
351 @available_filters
337 end
352 end
338
353
339 def add_filter(field, operator, values=nil)
354 def add_filter(field, operator, values=nil)
340 # values must be an array
355 # values must be an array
341 return unless values.nil? || values.is_a?(Array)
356 return unless values.nil? || values.is_a?(Array)
342 # check if field is defined as an available filter
357 # check if field is defined as an available filter
343 if available_filters.has_key? field
358 if available_filters.has_key? field
344 filter_options = available_filters[field]
359 filter_options = available_filters[field]
345 filters[field] = {:operator => operator, :values => (values || [''])}
360 filters[field] = {:operator => operator, :values => (values || [''])}
346 end
361 end
347 end
362 end
348
363
349 def add_short_filter(field, expression)
364 def add_short_filter(field, expression)
350 return unless expression && available_filters.has_key?(field)
365 return unless expression && available_filters.has_key?(field)
351 field_type = available_filters[field][:type]
366 field_type = available_filters[field][:type]
352 operators_by_filter_type[field_type].sort.reverse.detect do |operator|
367 operators_by_filter_type[field_type].sort.reverse.detect do |operator|
353 next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/
368 next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/
354 values = $1
369 values = $1
355 add_filter field, operator, values.present? ? values.split('|') : ['']
370 add_filter field, operator, values.present? ? values.split('|') : ['']
356 end || add_filter(field, '=', expression.split('|'))
371 end || add_filter(field, '=', expression.split('|'))
357 end
372 end
358
373
359 # Add multiple filters using +add_filter+
374 # Add multiple filters using +add_filter+
360 def add_filters(fields, operators, values)
375 def add_filters(fields, operators, values)
361 if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash))
376 if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash))
362 fields.each do |field|
377 fields.each do |field|
363 add_filter(field, operators[field], values && values[field])
378 add_filter(field, operators[field], values && values[field])
364 end
379 end
365 end
380 end
366 end
381 end
367
382
368 def has_filter?(field)
383 def has_filter?(field)
369 filters and filters[field]
384 filters and filters[field]
370 end
385 end
371
386
372 def type_for(field)
387 def type_for(field)
373 available_filters[field][:type] if available_filters.has_key?(field)
388 available_filters[field][:type] if available_filters.has_key?(field)
374 end
389 end
375
390
376 def operator_for(field)
391 def operator_for(field)
377 has_filter?(field) ? filters[field][:operator] : nil
392 has_filter?(field) ? filters[field][:operator] : nil
378 end
393 end
379
394
380 def values_for(field)
395 def values_for(field)
381 has_filter?(field) ? filters[field][:values] : nil
396 has_filter?(field) ? filters[field][:values] : nil
382 end
397 end
383
398
384 def value_for(field, index=0)
399 def value_for(field, index=0)
385 (values_for(field) || [])[index]
400 (values_for(field) || [])[index]
386 end
401 end
387
402
388 def label_for(field)
403 def label_for(field)
389 label = available_filters[field][:name] if available_filters.has_key?(field)
404 label = available_filters[field][:name] if available_filters.has_key?(field)
390 label ||= l("field_#{field.to_s.gsub(/_id$/, '')}", :default => field)
405 label ||= l("field_#{field.to_s.gsub(/_id$/, '')}", :default => field)
391 end
406 end
392
407
393 def self.add_available_column(column)
408 def self.add_available_column(column)
394 self.available_columns << (column) if column.is_a?(QueryColumn)
409 self.available_columns << (column) if column.is_a?(QueryColumn)
395 end
410 end
396
411
397 # Returns an array of columns that can be used to group the results
412 # Returns an array of columns that can be used to group the results
398 def groupable_columns
413 def groupable_columns
399 available_columns.select {|c| c.groupable}
414 available_columns.select {|c| c.groupable}
400 end
415 end
401
416
402 # Returns a Hash of columns and the key for sorting
417 # Returns a Hash of columns and the key for sorting
403 def sortable_columns
418 def sortable_columns
404 available_columns.inject({}) {|h, column|
419 available_columns.inject({}) {|h, column|
405 h[column.name.to_s] = column.sortable
420 h[column.name.to_s] = column.sortable
406 h
421 h
407 }
422 }
408 end
423 end
409
424
410 def columns
425 def columns
411 # preserve the column_names order
426 # preserve the column_names order
412 cols = (has_default_columns? ? default_columns_names : column_names).collect do |name|
427 cols = (has_default_columns? ? default_columns_names : column_names).collect do |name|
413 available_columns.find { |col| col.name == name }
428 available_columns.find { |col| col.name == name }
414 end.compact
429 end.compact
415 available_columns.select(&:frozen?) | cols
430 available_columns.select(&:frozen?) | cols
416 end
431 end
417
432
418 def inline_columns
433 def inline_columns
419 columns.select(&:inline?)
434 columns.select(&:inline?)
420 end
435 end
421
436
422 def block_columns
437 def block_columns
423 columns.reject(&:inline?)
438 columns.reject(&:inline?)
424 end
439 end
425
440
426 def available_inline_columns
441 def available_inline_columns
427 available_columns.select(&:inline?)
442 available_columns.select(&:inline?)
428 end
443 end
429
444
430 def available_block_columns
445 def available_block_columns
431 available_columns.reject(&:inline?)
446 available_columns.reject(&:inline?)
432 end
447 end
433
448
434 def default_columns_names
449 def default_columns_names
435 []
450 []
436 end
451 end
437
452
438 def column_names=(names)
453 def column_names=(names)
439 if names
454 if names
440 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
455 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
441 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
456 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
442 # Set column_names to nil if default columns
457 # Set column_names to nil if default columns
443 if names == default_columns_names
458 if names == default_columns_names
444 names = nil
459 names = nil
445 end
460 end
446 end
461 end
447 write_attribute(:column_names, names)
462 write_attribute(:column_names, names)
448 end
463 end
449
464
450 def has_column?(column)
465 def has_column?(column)
451 column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column)
466 column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column)
452 end
467 end
453
468
454 def has_custom_field_column?
469 def has_custom_field_column?
455 columns.any? {|column| column.is_a? QueryCustomFieldColumn}
470 columns.any? {|column| column.is_a? QueryCustomFieldColumn}
456 end
471 end
457
472
458 def has_default_columns?
473 def has_default_columns?
459 column_names.nil? || column_names.empty?
474 column_names.nil? || column_names.empty?
460 end
475 end
461
476
462 def sort_criteria=(arg)
477 def sort_criteria=(arg)
463 c = []
478 c = []
464 if arg.is_a?(Hash)
479 if arg.is_a?(Hash)
465 arg = arg.keys.sort.collect {|k| arg[k]}
480 arg = arg.keys.sort.collect {|k| arg[k]}
466 end
481 end
467 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, (o == 'desc' || o == false) ? 'desc' : 'asc']}
482 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, (o == 'desc' || o == false) ? 'desc' : 'asc']}
468 write_attribute(:sort_criteria, c)
483 write_attribute(:sort_criteria, c)
469 end
484 end
470
485
471 def sort_criteria
486 def sort_criteria
472 read_attribute(:sort_criteria) || []
487 read_attribute(:sort_criteria) || []
473 end
488 end
474
489
475 def sort_criteria_key(arg)
490 def sort_criteria_key(arg)
476 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
491 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
477 end
492 end
478
493
479 def sort_criteria_order(arg)
494 def sort_criteria_order(arg)
480 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
495 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
481 end
496 end
482
497
483 def sort_criteria_order_for(key)
498 def sort_criteria_order_for(key)
484 sort_criteria.detect {|k, order| key.to_s == k}.try(:last)
499 sort_criteria.detect {|k, order| key.to_s == k}.try(:last)
485 end
500 end
486
501
487 # Returns the SQL sort order that should be prepended for grouping
502 # Returns the SQL sort order that should be prepended for grouping
488 def group_by_sort_order
503 def group_by_sort_order
489 if grouped? && (column = group_by_column)
504 if grouped? && (column = group_by_column)
490 order = sort_criteria_order_for(column.name) || column.default_order
505 order = sort_criteria_order_for(column.name) || column.default_order
491 column.sortable.is_a?(Array) ?
506 column.sortable.is_a?(Array) ?
492 column.sortable.collect {|s| "#{s} #{order}"}.join(',') :
507 column.sortable.collect {|s| "#{s} #{order}"}.join(',') :
493 "#{column.sortable} #{order}"
508 "#{column.sortable} #{order}"
494 end
509 end
495 end
510 end
496
511
497 # Returns true if the query is a grouped query
512 # Returns true if the query is a grouped query
498 def grouped?
513 def grouped?
499 !group_by_column.nil?
514 !group_by_column.nil?
500 end
515 end
501
516
502 def group_by_column
517 def group_by_column
503 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
518 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
504 end
519 end
505
520
506 def group_by_statement
521 def group_by_statement
507 group_by_column.try(:groupable)
522 group_by_column.try(:groupable)
508 end
523 end
509
524
510 def project_statement
525 def project_statement
511 project_clauses = []
526 project_clauses = []
512 if project && !project.descendants.active.empty?
527 if project && !project.descendants.active.empty?
513 ids = [project.id]
528 ids = [project.id]
514 if has_filter?("subproject_id")
529 if has_filter?("subproject_id")
515 case operator_for("subproject_id")
530 case operator_for("subproject_id")
516 when '='
531 when '='
517 # include the selected subprojects
532 # include the selected subprojects
518 ids += values_for("subproject_id").each(&:to_i)
533 ids += values_for("subproject_id").each(&:to_i)
519 when '!*'
534 when '!*'
520 # main project only
535 # main project only
521 else
536 else
522 # all subprojects
537 # all subprojects
523 ids += project.descendants.collect(&:id)
538 ids += project.descendants.collect(&:id)
524 end
539 end
525 elsif Setting.display_subprojects_issues?
540 elsif Setting.display_subprojects_issues?
526 ids += project.descendants.collect(&:id)
541 ids += project.descendants.collect(&:id)
527 end
542 end
528 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
543 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
529 elsif project
544 elsif project
530 project_clauses << "#{Project.table_name}.id = %d" % project.id
545 project_clauses << "#{Project.table_name}.id = %d" % project.id
531 end
546 end
532 project_clauses.any? ? project_clauses.join(' AND ') : nil
547 project_clauses.any? ? project_clauses.join(' AND ') : nil
533 end
548 end
534
549
535 def statement
550 def statement
536 # filters clauses
551 # filters clauses
537 filters_clauses = []
552 filters_clauses = []
538 filters.each_key do |field|
553 filters.each_key do |field|
539 next if field == "subproject_id"
554 next if field == "subproject_id"
540 v = values_for(field).clone
555 v = values_for(field).clone
541 next unless v and !v.empty?
556 next unless v and !v.empty?
542 operator = operator_for(field)
557 operator = operator_for(field)
543
558
544 # "me" value subsitution
559 # "me" value subsitution
545 if %w(assigned_to_id author_id user_id watcher_id).include?(field)
560 if %w(assigned_to_id author_id user_id watcher_id).include?(field)
546 if v.delete("me")
561 if v.delete("me")
547 if User.current.logged?
562 if User.current.logged?
548 v.push(User.current.id.to_s)
563 v.push(User.current.id.to_s)
549 v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id'
564 v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id'
550 else
565 else
551 v.push("0")
566 v.push("0")
552 end
567 end
553 end
568 end
554 end
569 end
555
570
556 if field == 'project_id'
571 if field == 'project_id'
557 if v.delete('mine')
572 if v.delete('mine')
558 v += User.current.memberships.map(&:project_id).map(&:to_s)
573 v += User.current.memberships.map(&:project_id).map(&:to_s)
559 end
574 end
560 end
575 end
561
576
562 if field =~ /cf_(\d+)$/
577 if field =~ /cf_(\d+)$/
563 # custom field
578 # custom field
564 filters_clauses << sql_for_custom_field(field, operator, v, $1)
579 filters_clauses << sql_for_custom_field(field, operator, v, $1)
565 elsif respond_to?("sql_for_#{field}_field")
580 elsif respond_to?("sql_for_#{field}_field")
566 # specific statement
581 # specific statement
567 filters_clauses << send("sql_for_#{field}_field", field, operator, v)
582 filters_clauses << send("sql_for_#{field}_field", field, operator, v)
568 else
583 else
569 # regular field
584 # regular field
570 filters_clauses << '(' + sql_for_field(field, operator, v, queried_table_name, field) + ')'
585 filters_clauses << '(' + sql_for_field(field, operator, v, queried_table_name, field) + ')'
571 end
586 end
572 end if filters and valid?
587 end if filters and valid?
573
588
574 if (c = group_by_column) && c.is_a?(QueryCustomFieldColumn)
589 if (c = group_by_column) && c.is_a?(QueryCustomFieldColumn)
575 # Excludes results for which the grouped custom field is not visible
590 # Excludes results for which the grouped custom field is not visible
576 filters_clauses << c.custom_field.visibility_by_project_condition
591 filters_clauses << c.custom_field.visibility_by_project_condition
577 end
592 end
578
593
579 filters_clauses << project_statement
594 filters_clauses << project_statement
580 filters_clauses.reject!(&:blank?)
595 filters_clauses.reject!(&:blank?)
581
596
582 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
597 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
583 end
598 end
584
599
585 private
600 private
586
601
587 def sql_for_custom_field(field, operator, value, custom_field_id)
602 def sql_for_custom_field(field, operator, value, custom_field_id)
588 db_table = CustomValue.table_name
603 db_table = CustomValue.table_name
589 db_field = 'value'
604 db_field = 'value'
590 filter = @available_filters[field]
605 filter = @available_filters[field]
591 return nil unless filter
606 return nil unless filter
592 if filter[:field].format.target_class && filter[:field].format.target_class <= User
607 if filter[:field].format.target_class && filter[:field].format.target_class <= User
593 if value.delete('me')
608 if value.delete('me')
594 value.push User.current.id.to_s
609 value.push User.current.id.to_s
595 end
610 end
596 end
611 end
597 not_in = nil
612 not_in = nil
598 if operator == '!'
613 if operator == '!'
599 # Makes ! operator work for custom fields with multiple values
614 # Makes ! operator work for custom fields with multiple values
600 operator = '='
615 operator = '='
601 not_in = 'NOT'
616 not_in = 'NOT'
602 end
617 end
603 customized_key = "id"
618 customized_key = "id"
604 customized_class = queried_class
619 customized_class = queried_class
605 if field =~ /^(.+)\.cf_/
620 if field =~ /^(.+)\.cf_/
606 assoc = $1
621 assoc = $1
607 customized_key = "#{assoc}_id"
622 customized_key = "#{assoc}_id"
608 customized_class = queried_class.reflect_on_association(assoc.to_sym).klass.base_class rescue nil
623 customized_class = queried_class.reflect_on_association(assoc.to_sym).klass.base_class rescue nil
609 raise "Unknown #{queried_class.name} association #{assoc}" unless customized_class
624 raise "Unknown #{queried_class.name} association #{assoc}" unless customized_class
610 end
625 end
611 where = sql_for_field(field, operator, value, db_table, db_field, true)
626 where = sql_for_field(field, operator, value, db_table, db_field, true)
612 if operator =~ /[<>]/
627 if operator =~ /[<>]/
613 where = "(#{where}) AND #{db_table}.#{db_field} <> ''"
628 where = "(#{where}) AND #{db_table}.#{db_field} <> ''"
614 end
629 end
615 "#{queried_table_name}.#{customized_key} #{not_in} IN (" +
630 "#{queried_table_name}.#{customized_key} #{not_in} IN (" +
616 "SELECT #{customized_class.table_name}.id FROM #{customized_class.table_name}" +
631 "SELECT #{customized_class.table_name}.id FROM #{customized_class.table_name}" +
617 " LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='#{customized_class}' AND #{db_table}.customized_id=#{customized_class.table_name}.id AND #{db_table}.custom_field_id=#{custom_field_id}" +
632 " LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='#{customized_class}' AND #{db_table}.customized_id=#{customized_class.table_name}.id AND #{db_table}.custom_field_id=#{custom_field_id}" +
618 " WHERE (#{where}) AND (#{filter[:field].visibility_by_project_condition}))"
633 " WHERE (#{where}) AND (#{filter[:field].visibility_by_project_condition}))"
619 end
634 end
620
635
621 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
636 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
622 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
637 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
623 sql = ''
638 sql = ''
624 case operator
639 case operator
625 when "="
640 when "="
626 if value.any?
641 if value.any?
627 case type_for(field)
642 case type_for(field)
628 when :date, :date_past
643 when :date, :date_past
629 sql = date_clause(db_table, db_field, parse_date(value.first), parse_date(value.first))
644 sql = date_clause(db_table, db_field, parse_date(value.first), parse_date(value.first))
630 when :integer
645 when :integer
631 if is_custom_filter
646 if is_custom_filter
632 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) = #{value.first.to_i})"
647 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) = #{value.first.to_i})"
633 else
648 else
634 sql = "#{db_table}.#{db_field} = #{value.first.to_i}"
649 sql = "#{db_table}.#{db_field} = #{value.first.to_i}"
635 end
650 end
636 when :float
651 when :float
637 if is_custom_filter
652 if is_custom_filter
638 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5})"
653 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5})"
639 else
654 else
640 sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}"
655 sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}"
641 end
656 end
642 else
657 else
643 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
658 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
644 end
659 end
645 else
660 else
646 # IN an empty set
661 # IN an empty set
647 sql = "1=0"
662 sql = "1=0"
648 end
663 end
649 when "!"
664 when "!"
650 if value.any?
665 if value.any?
651 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
666 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
652 else
667 else
653 # NOT IN an empty set
668 # NOT IN an empty set
654 sql = "1=1"
669 sql = "1=1"
655 end
670 end
656 when "!*"
671 when "!*"
657 sql = "#{db_table}.#{db_field} IS NULL"
672 sql = "#{db_table}.#{db_field} IS NULL"
658 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
673 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
659 when "*"
674 when "*"
660 sql = "#{db_table}.#{db_field} IS NOT NULL"
675 sql = "#{db_table}.#{db_field} IS NOT NULL"
661 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
676 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
662 when ">="
677 when ">="
663 if [:date, :date_past].include?(type_for(field))
678 if [:date, :date_past].include?(type_for(field))
664 sql = date_clause(db_table, db_field, parse_date(value.first), nil)
679 sql = date_clause(db_table, db_field, parse_date(value.first), nil)
665 else
680 else
666 if is_custom_filter
681 if is_custom_filter
667 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) >= #{value.first.to_f})"
682 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) >= #{value.first.to_f})"
668 else
683 else
669 sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
684 sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
670 end
685 end
671 end
686 end
672 when "<="
687 when "<="
673 if [:date, :date_past].include?(type_for(field))
688 if [:date, :date_past].include?(type_for(field))
674 sql = date_clause(db_table, db_field, nil, parse_date(value.first))
689 sql = date_clause(db_table, db_field, nil, parse_date(value.first))
675 else
690 else
676 if is_custom_filter
691 if is_custom_filter
677 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) <= #{value.first.to_f})"
692 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) <= #{value.first.to_f})"
678 else
693 else
679 sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
694 sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
680 end
695 end
681 end
696 end
682 when "><"
697 when "><"
683 if [:date, :date_past].include?(type_for(field))
698 if [:date, :date_past].include?(type_for(field))
684 sql = date_clause(db_table, db_field, parse_date(value[0]), parse_date(value[1]))
699 sql = date_clause(db_table, db_field, parse_date(value[0]), parse_date(value[1]))
685 else
700 else
686 if is_custom_filter
701 if is_custom_filter
687 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) BETWEEN #{value[0].to_f} AND #{value[1].to_f})"
702 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) BETWEEN #{value[0].to_f} AND #{value[1].to_f})"
688 else
703 else
689 sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
704 sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
690 end
705 end
691 end
706 end
692 when "o"
707 when "o"
693 sql = "#{queried_table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_false})" if field == "status_id"
708 sql = "#{queried_table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_false})" if field == "status_id"
694 when "c"
709 when "c"
695 sql = "#{queried_table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_true})" if field == "status_id"
710 sql = "#{queried_table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_true})" if field == "status_id"
696 when "><t-"
711 when "><t-"
697 # between today - n days and today
712 # between today - n days and today
698 sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0)
713 sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0)
699 when ">t-"
714 when ">t-"
700 # >= today - n days
715 # >= today - n days
701 sql = relative_date_clause(db_table, db_field, - value.first.to_i, nil)
716 sql = relative_date_clause(db_table, db_field, - value.first.to_i, nil)
702 when "<t-"
717 when "<t-"
703 # <= today - n days
718 # <= today - n days
704 sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i)
719 sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i)
705 when "t-"
720 when "t-"
706 # = n days in past
721 # = n days in past
707 sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
722 sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
708 when "><t+"
723 when "><t+"
709 # between today and today + n days
724 # between today and today + n days
710 sql = relative_date_clause(db_table, db_field, 0, value.first.to_i)
725 sql = relative_date_clause(db_table, db_field, 0, value.first.to_i)
711 when ">t+"
726 when ">t+"
712 # >= today + n days
727 # >= today + n days
713 sql = relative_date_clause(db_table, db_field, value.first.to_i, nil)
728 sql = relative_date_clause(db_table, db_field, value.first.to_i, nil)
714 when "<t+"
729 when "<t+"
715 # <= today + n days
730 # <= today + n days
716 sql = relative_date_clause(db_table, db_field, nil, value.first.to_i)
731 sql = relative_date_clause(db_table, db_field, nil, value.first.to_i)
717 when "t+"
732 when "t+"
718 # = today + n days
733 # = today + n days
719 sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i)
734 sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i)
720 when "t"
735 when "t"
721 # = today
736 # = today
722 sql = relative_date_clause(db_table, db_field, 0, 0)
737 sql = relative_date_clause(db_table, db_field, 0, 0)
723 when "ld"
738 when "ld"
724 # = yesterday
739 # = yesterday
725 sql = relative_date_clause(db_table, db_field, -1, -1)
740 sql = relative_date_clause(db_table, db_field, -1, -1)
726 when "w"
741 when "w"
727 # = this week
742 # = this week
728 first_day_of_week = l(:general_first_day_of_week).to_i
743 first_day_of_week = l(:general_first_day_of_week).to_i
729 day_of_week = Date.today.cwday
744 day_of_week = Date.today.cwday
730 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
745 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
731 sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6)
746 sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6)
732 when "lw"
747 when "lw"
733 # = last week
748 # = last week
734 first_day_of_week = l(:general_first_day_of_week).to_i
749 first_day_of_week = l(:general_first_day_of_week).to_i
735 day_of_week = Date.today.cwday
750 day_of_week = Date.today.cwday
736 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
751 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
737 sql = relative_date_clause(db_table, db_field, - days_ago - 7, - days_ago - 1)
752 sql = relative_date_clause(db_table, db_field, - days_ago - 7, - days_ago - 1)
738 when "l2w"
753 when "l2w"
739 # = last 2 weeks
754 # = last 2 weeks
740 first_day_of_week = l(:general_first_day_of_week).to_i
755 first_day_of_week = l(:general_first_day_of_week).to_i
741 day_of_week = Date.today.cwday
756 day_of_week = Date.today.cwday
742 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
757 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
743 sql = relative_date_clause(db_table, db_field, - days_ago - 14, - days_ago - 1)
758 sql = relative_date_clause(db_table, db_field, - days_ago - 14, - days_ago - 1)
744 when "m"
759 when "m"
745 # = this month
760 # = this month
746 date = Date.today
761 date = Date.today
747 sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month)
762 sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month)
748 when "lm"
763 when "lm"
749 # = last month
764 # = last month
750 date = Date.today.prev_month
765 date = Date.today.prev_month
751 sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month)
766 sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month)
752 when "y"
767 when "y"
753 # = this year
768 # = this year
754 date = Date.today
769 date = Date.today
755 sql = date_clause(db_table, db_field, date.beginning_of_year, date.end_of_year)
770 sql = date_clause(db_table, db_field, date.beginning_of_year, date.end_of_year)
756 when "~"
771 when "~"
757 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
772 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
758 when "!~"
773 when "!~"
759 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
774 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
760 else
775 else
761 raise "Unknown query operator #{operator}"
776 raise "Unknown query operator #{operator}"
762 end
777 end
763
778
764 return sql
779 return sql
765 end
780 end
766
781
767 # Adds a filter for the given custom field
782 # Adds a filter for the given custom field
768 def add_custom_field_filter(field, assoc=nil)
783 def add_custom_field_filter(field, assoc=nil)
769 options = field.format.query_filter_options(field, self)
784 options = field.format.query_filter_options(field, self)
770 if field.format.target_class && field.format.target_class <= User
785 if field.format.target_class && field.format.target_class <= User
771 if options[:values].is_a?(Array) && User.current.logged?
786 if options[:values].is_a?(Array) && User.current.logged?
772 options[:values].unshift ["<< #{l(:label_me)} >>", "me"]
787 options[:values].unshift ["<< #{l(:label_me)} >>", "me"]
773 end
788 end
774 end
789 end
775
790
776 filter_id = "cf_#{field.id}"
791 filter_id = "cf_#{field.id}"
777 filter_name = field.name
792 filter_name = field.name
778 if assoc.present?
793 if assoc.present?
779 filter_id = "#{assoc}.#{filter_id}"
794 filter_id = "#{assoc}.#{filter_id}"
780 filter_name = l("label_attribute_of_#{assoc}", :name => filter_name)
795 filter_name = l("label_attribute_of_#{assoc}", :name => filter_name)
781 end
796 end
782 add_available_filter filter_id, options.merge({
797 add_available_filter filter_id, options.merge({
783 :name => filter_name,
798 :name => filter_name,
784 :field => field
799 :field => field
785 })
800 })
786 end
801 end
787
802
788 # Adds filters for the given custom fields scope
803 # Adds filters for the given custom fields scope
789 def add_custom_fields_filters(scope, assoc=nil)
804 def add_custom_fields_filters(scope, assoc=nil)
790 scope.visible.where(:is_filter => true).sorted.each do |field|
805 scope.visible.where(:is_filter => true).sorted.each do |field|
791 add_custom_field_filter(field, assoc)
806 add_custom_field_filter(field, assoc)
792 end
807 end
793 end
808 end
794
809
795 # Adds filters for the given associations custom fields
810 # Adds filters for the given associations custom fields
796 def add_associations_custom_fields_filters(*associations)
811 def add_associations_custom_fields_filters(*associations)
797 fields_by_class = CustomField.visible.where(:is_filter => true).group_by(&:class)
812 fields_by_class = CustomField.visible.where(:is_filter => true).group_by(&:class)
798 associations.each do |assoc|
813 associations.each do |assoc|
799 association_klass = queried_class.reflect_on_association(assoc).klass
814 association_klass = queried_class.reflect_on_association(assoc).klass
800 fields_by_class.each do |field_class, fields|
815 fields_by_class.each do |field_class, fields|
801 if field_class.customized_class <= association_klass
816 if field_class.customized_class <= association_klass
802 fields.sort.each do |field|
817 fields.sort.each do |field|
803 add_custom_field_filter(field, assoc)
818 add_custom_field_filter(field, assoc)
804 end
819 end
805 end
820 end
806 end
821 end
807 end
822 end
808 end
823 end
809
824
810 # Returns a SQL clause for a date or datetime field.
825 # Returns a SQL clause for a date or datetime field.
811 def date_clause(table, field, from, to)
826 def date_clause(table, field, from, to)
812 s = []
827 s = []
813 if from
828 if from
814 if from.is_a?(Date)
829 if from.is_a?(Date)
815 from = Time.local(from.year, from.month, from.day).yesterday.end_of_day
830 from = Time.local(from.year, from.month, from.day).yesterday.end_of_day
816 else
831 else
817 from = from - 1 # second
832 from = from - 1 # second
818 end
833 end
819 if self.class.default_timezone == :utc
834 if self.class.default_timezone == :utc
820 from = from.utc
835 from = from.utc
821 end
836 end
822 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date(from)])
837 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date(from)])
823 end
838 end
824 if to
839 if to
825 if to.is_a?(Date)
840 if to.is_a?(Date)
826 to = Time.local(to.year, to.month, to.day).end_of_day
841 to = Time.local(to.year, to.month, to.day).end_of_day
827 end
842 end
828 if self.class.default_timezone == :utc
843 if self.class.default_timezone == :utc
829 to = to.utc
844 to = to.utc
830 end
845 end
831 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date(to)])
846 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date(to)])
832 end
847 end
833 s.join(' AND ')
848 s.join(' AND ')
834 end
849 end
835
850
836 # Returns a SQL clause for a date or datetime field using relative dates.
851 # Returns a SQL clause for a date or datetime field using relative dates.
837 def relative_date_clause(table, field, days_from, days_to)
852 def relative_date_clause(table, field, days_from, days_to)
838 date_clause(table, field, (days_from ? Date.today + days_from : nil), (days_to ? Date.today + days_to : nil))
853 date_clause(table, field, (days_from ? Date.today + days_from : nil), (days_to ? Date.today + days_to : nil))
839 end
854 end
840
855
841 # Returns a Date or Time from the given filter value
856 # Returns a Date or Time from the given filter value
842 def parse_date(arg)
857 def parse_date(arg)
843 if arg.to_s =~ /\A\d{4}-\d{2}-\d{2}T/
858 if arg.to_s =~ /\A\d{4}-\d{2}-\d{2}T/
844 Time.parse(arg) rescue nil
859 Time.parse(arg) rescue nil
845 else
860 else
846 Date.parse(arg) rescue nil
861 Date.parse(arg) rescue nil
847 end
862 end
848 end
863 end
849
864
850 # Additional joins required for the given sort options
865 # Additional joins required for the given sort options
851 def joins_for_order_statement(order_options)
866 def joins_for_order_statement(order_options)
852 joins = []
867 joins = []
853
868
854 if order_options
869 if order_options
855 if order_options.include?('authors')
870 if order_options.include?('authors')
856 joins << "LEFT OUTER JOIN #{User.table_name} authors ON authors.id = #{queried_table_name}.author_id"
871 joins << "LEFT OUTER JOIN #{User.table_name} authors ON authors.id = #{queried_table_name}.author_id"
857 end
872 end
858 order_options.scan(/cf_\d+/).uniq.each do |name|
873 order_options.scan(/cf_\d+/).uniq.each do |name|
859 column = available_columns.detect {|c| c.name.to_s == name}
874 column = available_columns.detect {|c| c.name.to_s == name}
860 join = column && column.custom_field.join_for_order_statement
875 join = column && column.custom_field.join_for_order_statement
861 if join
876 if join
862 joins << join
877 joins << join
863 end
878 end
864 end
879 end
865 end
880 end
866
881
867 joins.any? ? joins.join(' ') : nil
882 joins.any? ? joins.join(' ') : nil
868 end
883 end
869 end
884 end
General Comments 0
You need to be logged in to leave comments. Login now