##// END OF EJS Templates
Refactor: move method to model with compatibility wrapper...
Eric Davis -
r4168:0ca74df60403
parent child
Show More
@@ -1,845 +1,840
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require 'forwardable'
18 require 'forwardable'
19 require 'cgi'
19 require 'cgi'
20
20
21 module ApplicationHelper
21 module ApplicationHelper
22 include Redmine::WikiFormatting::Macros::Definitions
22 include Redmine::WikiFormatting::Macros::Definitions
23 include Redmine::I18n
23 include Redmine::I18n
24 include GravatarHelper::PublicMethods
24 include GravatarHelper::PublicMethods
25
25
26 extend Forwardable
26 extend Forwardable
27 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
27 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
28
28
29 # Return true if user is authorized for controller/action, otherwise false
29 # Return true if user is authorized for controller/action, otherwise false
30 def authorize_for(controller, action)
30 def authorize_for(controller, action)
31 User.current.allowed_to?({:controller => controller, :action => action}, @project)
31 User.current.allowed_to?({:controller => controller, :action => action}, @project)
32 end
32 end
33
33
34 # Display a link if user is authorized
34 # Display a link if user is authorized
35 #
35 #
36 # @param [String] name Anchor text (passed to link_to)
36 # @param [String] name Anchor text (passed to link_to)
37 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
37 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
38 # @param [optional, Hash] html_options Options passed to link_to
38 # @param [optional, Hash] html_options Options passed to link_to
39 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
39 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
40 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
40 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
41 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
41 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
42 end
42 end
43
43
44 # Display a link to remote if user is authorized
44 # Display a link to remote if user is authorized
45 def link_to_remote_if_authorized(name, options = {}, html_options = nil)
45 def link_to_remote_if_authorized(name, options = {}, html_options = nil)
46 url = options[:url] || {}
46 url = options[:url] || {}
47 link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action])
47 link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action])
48 end
48 end
49
49
50 # Displays a link to user's account page if active
50 # Displays a link to user's account page if active
51 def link_to_user(user, options={})
51 def link_to_user(user, options={})
52 if user.is_a?(User)
52 if user.is_a?(User)
53 name = h(user.name(options[:format]))
53 name = h(user.name(options[:format]))
54 if user.active?
54 if user.active?
55 link_to name, :controller => 'users', :action => 'show', :id => user
55 link_to name, :controller => 'users', :action => 'show', :id => user
56 else
56 else
57 name
57 name
58 end
58 end
59 else
59 else
60 h(user.to_s)
60 h(user.to_s)
61 end
61 end
62 end
62 end
63
63
64 # Displays a link to +issue+ with its subject.
64 # Displays a link to +issue+ with its subject.
65 # Examples:
65 # Examples:
66 #
66 #
67 # link_to_issue(issue) # => Defect #6: This is the subject
67 # link_to_issue(issue) # => Defect #6: This is the subject
68 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
68 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
69 # link_to_issue(issue, :subject => false) # => Defect #6
69 # link_to_issue(issue, :subject => false) # => Defect #6
70 # link_to_issue(issue, :project => true) # => Foo - Defect #6
70 # link_to_issue(issue, :project => true) # => Foo - Defect #6
71 #
71 #
72 def link_to_issue(issue, options={})
72 def link_to_issue(issue, options={})
73 title = nil
73 title = nil
74 subject = nil
74 subject = nil
75 if options[:subject] == false
75 if options[:subject] == false
76 title = truncate(issue.subject, :length => 60)
76 title = truncate(issue.subject, :length => 60)
77 else
77 else
78 subject = issue.subject
78 subject = issue.subject
79 if options[:truncate]
79 if options[:truncate]
80 subject = truncate(subject, :length => options[:truncate])
80 subject = truncate(subject, :length => options[:truncate])
81 end
81 end
82 end
82 end
83 s = link_to "#{issue.tracker} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue},
83 s = link_to "#{issue.tracker} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue},
84 :class => issue.css_classes,
84 :class => issue.css_classes,
85 :title => title
85 :title => title
86 s << ": #{h subject}" if subject
86 s << ": #{h subject}" if subject
87 s = "#{h issue.project} - " + s if options[:project]
87 s = "#{h issue.project} - " + s if options[:project]
88 s
88 s
89 end
89 end
90
90
91 # Generates a link to an attachment.
91 # Generates a link to an attachment.
92 # Options:
92 # Options:
93 # * :text - Link text (default to attachment filename)
93 # * :text - Link text (default to attachment filename)
94 # * :download - Force download (default: false)
94 # * :download - Force download (default: false)
95 def link_to_attachment(attachment, options={})
95 def link_to_attachment(attachment, options={})
96 text = options.delete(:text) || attachment.filename
96 text = options.delete(:text) || attachment.filename
97 action = options.delete(:download) ? 'download' : 'show'
97 action = options.delete(:download) ? 'download' : 'show'
98
98
99 link_to(h(text), {:controller => 'attachments', :action => action, :id => attachment, :filename => attachment.filename }, options)
99 link_to(h(text), {:controller => 'attachments', :action => action, :id => attachment, :filename => attachment.filename }, 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, project, options={})
105 def link_to_revision(revision, project, options={})
106 text = options.delete(:text) || format_revision(revision)
106 text = options.delete(:text) || format_revision(revision)
107
107
108 link_to(text, {:controller => 'repositories', :action => 'revision', :id => project, :rev => revision}, :title => l(:label_revision_id, revision))
108 link_to(text, {:controller => 'repositories', :action => 'revision', :id => project, :rev => revision}, :title => l(:label_revision_id, revision))
109 end
109 end
110
110
111 # Generates a link to a project if active
111 # Generates a link to a project if active
112 # Examples:
112 # Examples:
113 #
113 #
114 # link_to_project(project) # => link to the specified project overview
114 # link_to_project(project) # => link to the specified project overview
115 # link_to_project(project, :action=>'settings') # => link to project settings
115 # link_to_project(project, :action=>'settings') # => link to project settings
116 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
116 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
117 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
117 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
118 #
118 #
119 def link_to_project(project, options={}, html_options = nil)
119 def link_to_project(project, options={}, html_options = nil)
120 if project.active?
120 if project.active?
121 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
121 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
122 link_to(h(project), url, html_options)
122 link_to(h(project), url, html_options)
123 else
123 else
124 h(project)
124 h(project)
125 end
125 end
126 end
126 end
127
127
128 def toggle_link(name, id, options={})
128 def toggle_link(name, id, options={})
129 onclick = "Element.toggle('#{id}'); "
129 onclick = "Element.toggle('#{id}'); "
130 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
130 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
131 onclick << "return false;"
131 onclick << "return false;"
132 link_to(name, "#", :onclick => onclick)
132 link_to(name, "#", :onclick => onclick)
133 end
133 end
134
134
135 def image_to_function(name, function, html_options = {})
135 def image_to_function(name, function, html_options = {})
136 html_options.symbolize_keys!
136 html_options.symbolize_keys!
137 tag(:input, html_options.merge({
137 tag(:input, html_options.merge({
138 :type => "image", :src => image_path(name),
138 :type => "image", :src => image_path(name),
139 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
139 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
140 }))
140 }))
141 end
141 end
142
142
143 def prompt_to_remote(name, text, param, url, html_options = {})
143 def prompt_to_remote(name, text, param, url, html_options = {})
144 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
144 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
145 link_to name, {}, html_options
145 link_to name, {}, html_options
146 end
146 end
147
147
148 def format_activity_title(text)
148 def format_activity_title(text)
149 h(truncate_single_line(text, :length => 100))
149 h(truncate_single_line(text, :length => 100))
150 end
150 end
151
151
152 def format_activity_day(date)
152 def format_activity_day(date)
153 date == Date.today ? l(:label_today).titleize : format_date(date)
153 date == Date.today ? l(:label_today).titleize : format_date(date)
154 end
154 end
155
155
156 def format_activity_description(text)
156 def format_activity_description(text)
157 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')).gsub(/[\r\n]+/, "<br />")
157 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')).gsub(/[\r\n]+/, "<br />")
158 end
158 end
159
159
160 def format_version_name(version)
160 def format_version_name(version)
161 if version.project == @project
161 if version.project == @project
162 h(version)
162 h(version)
163 else
163 else
164 h("#{version.project} - #{version}")
164 h("#{version.project} - #{version}")
165 end
165 end
166 end
166 end
167
167
168 def due_date_distance_in_words(date)
168 def due_date_distance_in_words(date)
169 if date
169 if date
170 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
170 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
171 end
171 end
172 end
172 end
173
173
174 def render_page_hierarchy(pages, node=nil)
174 def render_page_hierarchy(pages, node=nil)
175 content = ''
175 content = ''
176 if pages[node]
176 if pages[node]
177 content << "<ul class=\"pages-hierarchy\">\n"
177 content << "<ul class=\"pages-hierarchy\">\n"
178 pages[node].each do |page|
178 pages[node].each do |page|
179 content << "<li>"
179 content << "<li>"
180 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :page => page.title},
180 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :page => page.title},
181 :title => (page.respond_to?(:updated_on) ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
181 :title => (page.respond_to?(:updated_on) ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
182 content << "\n" + render_page_hierarchy(pages, page.id) if pages[page.id]
182 content << "\n" + render_page_hierarchy(pages, page.id) if pages[page.id]
183 content << "</li>\n"
183 content << "</li>\n"
184 end
184 end
185 content << "</ul>\n"
185 content << "</ul>\n"
186 end
186 end
187 content
187 content
188 end
188 end
189
189
190 # Renders flash messages
190 # Renders flash messages
191 def render_flash_messages
191 def render_flash_messages
192 s = ''
192 s = ''
193 flash.each do |k,v|
193 flash.each do |k,v|
194 s << content_tag('div', v, :class => "flash #{k}")
194 s << content_tag('div', v, :class => "flash #{k}")
195 end
195 end
196 s
196 s
197 end
197 end
198
198
199 # Renders tabs and their content
199 # Renders tabs and their content
200 def render_tabs(tabs)
200 def render_tabs(tabs)
201 if tabs.any?
201 if tabs.any?
202 render :partial => 'common/tabs', :locals => {:tabs => tabs}
202 render :partial => 'common/tabs', :locals => {:tabs => tabs}
203 else
203 else
204 content_tag 'p', l(:label_no_data), :class => "nodata"
204 content_tag 'p', l(:label_no_data), :class => "nodata"
205 end
205 end
206 end
206 end
207
207
208 # Renders the project quick-jump box
208 # Renders the project quick-jump box
209 def render_project_jump_box
209 def render_project_jump_box
210 # Retrieve them now to avoid a COUNT query
210 # Retrieve them now to avoid a COUNT query
211 projects = User.current.projects.all
211 projects = User.current.projects.all
212 if projects.any?
212 if projects.any?
213 s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
213 s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
214 "<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
214 "<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
215 '<option value="" disabled="disabled">---</option>'
215 '<option value="" disabled="disabled">---</option>'
216 s << project_tree_options_for_select(projects, :selected => @project) do |p|
216 s << project_tree_options_for_select(projects, :selected => @project) do |p|
217 { :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
217 { :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
218 end
218 end
219 s << '</select>'
219 s << '</select>'
220 s
220 s
221 end
221 end
222 end
222 end
223
223
224 def project_tree_options_for_select(projects, options = {})
224 def project_tree_options_for_select(projects, options = {})
225 s = ''
225 s = ''
226 project_tree(projects) do |project, level|
226 project_tree(projects) do |project, level|
227 name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ') : '')
227 name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ') : '')
228 tag_options = {:value => project.id}
228 tag_options = {:value => project.id}
229 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
229 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
230 tag_options[:selected] = 'selected'
230 tag_options[:selected] = 'selected'
231 else
231 else
232 tag_options[:selected] = nil
232 tag_options[:selected] = nil
233 end
233 end
234 tag_options.merge!(yield(project)) if block_given?
234 tag_options.merge!(yield(project)) if block_given?
235 s << content_tag('option', name_prefix + h(project), tag_options)
235 s << content_tag('option', name_prefix + h(project), tag_options)
236 end
236 end
237 s
237 s
238 end
238 end
239
239
240 # Yields the given block for each project with its level in the tree
240 # Yields the given block for each project with its level in the tree
241 #
242 # Wrapper for Project#project_tree
241 def project_tree(projects, &block)
243 def project_tree(projects, &block)
242 ancestors = []
244 Project.project_tree(projects, &block)
243 projects.sort_by(&:lft).each do |project|
244 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
245 ancestors.pop
246 end
247 yield project, ancestors.size
248 ancestors << project
249 end
250 end
245 end
251
246
252 def project_nested_ul(projects, &block)
247 def project_nested_ul(projects, &block)
253 s = ''
248 s = ''
254 if projects.any?
249 if projects.any?
255 ancestors = []
250 ancestors = []
256 projects.sort_by(&:lft).each do |project|
251 projects.sort_by(&:lft).each do |project|
257 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
252 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
258 s << "<ul>\n"
253 s << "<ul>\n"
259 else
254 else
260 ancestors.pop
255 ancestors.pop
261 s << "</li>"
256 s << "</li>"
262 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
257 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
263 ancestors.pop
258 ancestors.pop
264 s << "</ul></li>\n"
259 s << "</ul></li>\n"
265 end
260 end
266 end
261 end
267 s << "<li>"
262 s << "<li>"
268 s << yield(project).to_s
263 s << yield(project).to_s
269 ancestors << project
264 ancestors << project
270 end
265 end
271 s << ("</li></ul>\n" * ancestors.size)
266 s << ("</li></ul>\n" * ancestors.size)
272 end
267 end
273 s
268 s
274 end
269 end
275
270
276 def principals_check_box_tags(name, principals)
271 def principals_check_box_tags(name, principals)
277 s = ''
272 s = ''
278 principals.sort.each do |principal|
273 principals.sort.each do |principal|
279 s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
274 s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
280 end
275 end
281 s
276 s
282 end
277 end
283
278
284 # Truncates and returns the string as a single line
279 # Truncates and returns the string as a single line
285 def truncate_single_line(string, *args)
280 def truncate_single_line(string, *args)
286 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
281 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
287 end
282 end
288
283
289 # Truncates at line break after 250 characters or options[:length]
284 # Truncates at line break after 250 characters or options[:length]
290 def truncate_lines(string, options={})
285 def truncate_lines(string, options={})
291 length = options[:length] || 250
286 length = options[:length] || 250
292 if string.to_s =~ /\A(.{#{length}}.*?)$/m
287 if string.to_s =~ /\A(.{#{length}}.*?)$/m
293 "#{$1}..."
288 "#{$1}..."
294 else
289 else
295 string
290 string
296 end
291 end
297 end
292 end
298
293
299 def html_hours(text)
294 def html_hours(text)
300 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>')
295 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>')
301 end
296 end
302
297
303 def authoring(created, author, options={})
298 def authoring(created, author, options={})
304 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created))
299 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created))
305 end
300 end
306
301
307 def time_tag(time)
302 def time_tag(time)
308 text = distance_of_time_in_words(Time.now, time)
303 text = distance_of_time_in_words(Time.now, time)
309 if @project
304 if @project
310 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => time.to_date}, :title => format_time(time))
305 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => time.to_date}, :title => format_time(time))
311 else
306 else
312 content_tag('acronym', text, :title => format_time(time))
307 content_tag('acronym', text, :title => format_time(time))
313 end
308 end
314 end
309 end
315
310
316 def syntax_highlight(name, content)
311 def syntax_highlight(name, content)
317 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
312 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
318 end
313 end
319
314
320 def to_path_param(path)
315 def to_path_param(path)
321 path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
316 path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
322 end
317 end
323
318
324 def pagination_links_full(paginator, count=nil, options={})
319 def pagination_links_full(paginator, count=nil, options={})
325 page_param = options.delete(:page_param) || :page
320 page_param = options.delete(:page_param) || :page
326 per_page_links = options.delete(:per_page_links)
321 per_page_links = options.delete(:per_page_links)
327 url_param = params.dup
322 url_param = params.dup
328 # don't reuse query params if filters are present
323 # don't reuse query params if filters are present
329 url_param.merge!(:fields => nil, :values => nil, :operators => nil) if url_param.delete(:set_filter)
324 url_param.merge!(:fields => nil, :values => nil, :operators => nil) if url_param.delete(:set_filter)
330
325
331 html = ''
326 html = ''
332 if paginator.current.previous
327 if paginator.current.previous
333 html << link_to_remote_content_update('&#171; ' + l(:label_previous), url_param.merge(page_param => paginator.current.previous)) + ' '
328 html << link_to_remote_content_update('&#171; ' + l(:label_previous), url_param.merge(page_param => paginator.current.previous)) + ' '
334 end
329 end
335
330
336 html << (pagination_links_each(paginator, options) do |n|
331 html << (pagination_links_each(paginator, options) do |n|
337 link_to_remote_content_update(n.to_s, url_param.merge(page_param => n))
332 link_to_remote_content_update(n.to_s, url_param.merge(page_param => n))
338 end || '')
333 end || '')
339
334
340 if paginator.current.next
335 if paginator.current.next
341 html << ' ' + link_to_remote_content_update((l(:label_next) + ' &#187;'), url_param.merge(page_param => paginator.current.next))
336 html << ' ' + link_to_remote_content_update((l(:label_next) + ' &#187;'), url_param.merge(page_param => paginator.current.next))
342 end
337 end
343
338
344 unless count.nil?
339 unless count.nil?
345 html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
340 html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
346 if per_page_links != false && links = per_page_links(paginator.items_per_page)
341 if per_page_links != false && links = per_page_links(paginator.items_per_page)
347 html << " | #{links}"
342 html << " | #{links}"
348 end
343 end
349 end
344 end
350
345
351 html
346 html
352 end
347 end
353
348
354 def per_page_links(selected=nil)
349 def per_page_links(selected=nil)
355 url_param = params.dup
350 url_param = params.dup
356 url_param.clear if url_param.has_key?(:set_filter)
351 url_param.clear if url_param.has_key?(:set_filter)
357
352
358 links = Setting.per_page_options_array.collect do |n|
353 links = Setting.per_page_options_array.collect do |n|
359 n == selected ? n : link_to_remote(n, {:update => "content",
354 n == selected ? n : link_to_remote(n, {:update => "content",
360 :url => params.dup.merge(:per_page => n),
355 :url => params.dup.merge(:per_page => n),
361 :method => :get},
356 :method => :get},
362 {:href => url_for(url_param.merge(:per_page => n))})
357 {:href => url_for(url_param.merge(:per_page => n))})
363 end
358 end
364 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
359 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
365 end
360 end
366
361
367 def reorder_links(name, url)
362 def reorder_links(name, url)
368 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)), url.merge({"#{name}[move_to]" => 'highest'}), :method => :post, :title => l(:label_sort_highest)) +
363 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)), url.merge({"#{name}[move_to]" => 'highest'}), :method => :post, :title => l(:label_sort_highest)) +
369 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)), url.merge({"#{name}[move_to]" => 'higher'}), :method => :post, :title => l(:label_sort_higher)) +
364 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)), url.merge({"#{name}[move_to]" => 'higher'}), :method => :post, :title => l(:label_sort_higher)) +
370 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)), url.merge({"#{name}[move_to]" => 'lower'}), :method => :post, :title => l(:label_sort_lower)) +
365 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)), url.merge({"#{name}[move_to]" => 'lower'}), :method => :post, :title => l(:label_sort_lower)) +
371 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), url.merge({"#{name}[move_to]" => 'lowest'}), :method => :post, :title => l(:label_sort_lowest))
366 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), url.merge({"#{name}[move_to]" => 'lowest'}), :method => :post, :title => l(:label_sort_lowest))
372 end
367 end
373
368
374 def breadcrumb(*args)
369 def breadcrumb(*args)
375 elements = args.flatten
370 elements = args.flatten
376 elements.any? ? content_tag('p', args.join(' &#187; ') + ' &#187; ', :class => 'breadcrumb') : nil
371 elements.any? ? content_tag('p', args.join(' &#187; ') + ' &#187; ', :class => 'breadcrumb') : nil
377 end
372 end
378
373
379 def other_formats_links(&block)
374 def other_formats_links(&block)
380 concat('<p class="other-formats">' + l(:label_export_to))
375 concat('<p class="other-formats">' + l(:label_export_to))
381 yield Redmine::Views::OtherFormatsBuilder.new(self)
376 yield Redmine::Views::OtherFormatsBuilder.new(self)
382 concat('</p>')
377 concat('</p>')
383 end
378 end
384
379
385 def page_header_title
380 def page_header_title
386 if @project.nil? || @project.new_record?
381 if @project.nil? || @project.new_record?
387 h(Setting.app_title)
382 h(Setting.app_title)
388 else
383 else
389 b = []
384 b = []
390 ancestors = (@project.root? ? [] : @project.ancestors.visible)
385 ancestors = (@project.root? ? [] : @project.ancestors.visible)
391 if ancestors.any?
386 if ancestors.any?
392 root = ancestors.shift
387 root = ancestors.shift
393 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
388 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
394 if ancestors.size > 2
389 if ancestors.size > 2
395 b << '&#8230;'
390 b << '&#8230;'
396 ancestors = ancestors[-2, 2]
391 ancestors = ancestors[-2, 2]
397 end
392 end
398 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
393 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
399 end
394 end
400 b << h(@project)
395 b << h(@project)
401 b.join(' &#187; ')
396 b.join(' &#187; ')
402 end
397 end
403 end
398 end
404
399
405 def html_title(*args)
400 def html_title(*args)
406 if args.empty?
401 if args.empty?
407 title = []
402 title = []
408 title << @project.name if @project
403 title << @project.name if @project
409 title += @html_title if @html_title
404 title += @html_title if @html_title
410 title << Setting.app_title
405 title << Setting.app_title
411 title.select {|t| !t.blank? }.join(' - ')
406 title.select {|t| !t.blank? }.join(' - ')
412 else
407 else
413 @html_title ||= []
408 @html_title ||= []
414 @html_title += args
409 @html_title += args
415 end
410 end
416 end
411 end
417
412
418 # Returns the theme, controller name, and action as css classes for the
413 # Returns the theme, controller name, and action as css classes for the
419 # HTML body.
414 # HTML body.
420 def body_css_classes
415 def body_css_classes
421 css = []
416 css = []
422 if theme = Redmine::Themes.theme(Setting.ui_theme)
417 if theme = Redmine::Themes.theme(Setting.ui_theme)
423 css << 'theme-' + theme.name
418 css << 'theme-' + theme.name
424 end
419 end
425
420
426 css << 'controller-' + params[:controller]
421 css << 'controller-' + params[:controller]
427 css << 'action-' + params[:action]
422 css << 'action-' + params[:action]
428 css.join(' ')
423 css.join(' ')
429 end
424 end
430
425
431 def accesskey(s)
426 def accesskey(s)
432 Redmine::AccessKeys.key_for s
427 Redmine::AccessKeys.key_for s
433 end
428 end
434
429
435 # Formats text according to system settings.
430 # Formats text according to system settings.
436 # 2 ways to call this method:
431 # 2 ways to call this method:
437 # * with a String: textilizable(text, options)
432 # * with a String: textilizable(text, options)
438 # * with an object and one of its attribute: textilizable(issue, :description, options)
433 # * with an object and one of its attribute: textilizable(issue, :description, options)
439 def textilizable(*args)
434 def textilizable(*args)
440 options = args.last.is_a?(Hash) ? args.pop : {}
435 options = args.last.is_a?(Hash) ? args.pop : {}
441 case args.size
436 case args.size
442 when 1
437 when 1
443 obj = options[:object]
438 obj = options[:object]
444 text = args.shift
439 text = args.shift
445 when 2
440 when 2
446 obj = args.shift
441 obj = args.shift
447 attr = args.shift
442 attr = args.shift
448 text = obj.send(attr).to_s
443 text = obj.send(attr).to_s
449 else
444 else
450 raise ArgumentError, 'invalid arguments to textilizable'
445 raise ArgumentError, 'invalid arguments to textilizable'
451 end
446 end
452 return '' if text.blank?
447 return '' if text.blank?
453 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
448 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
454 only_path = options.delete(:only_path) == false ? false : true
449 only_path = options.delete(:only_path) == false ? false : true
455
450
456 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr) { |macro, args| exec_macro(macro, obj, args) }
451 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr) { |macro, args| exec_macro(macro, obj, args) }
457
452
458 parse_non_pre_blocks(text) do |text|
453 parse_non_pre_blocks(text) do |text|
459 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
454 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
460 send method_name, text, project, obj, attr, only_path, options
455 send method_name, text, project, obj, attr, only_path, options
461 end
456 end
462 end
457 end
463 end
458 end
464
459
465 def parse_non_pre_blocks(text)
460 def parse_non_pre_blocks(text)
466 s = StringScanner.new(text)
461 s = StringScanner.new(text)
467 tags = []
462 tags = []
468 parsed = ''
463 parsed = ''
469 while !s.eos?
464 while !s.eos?
470 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
465 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
471 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
466 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
472 if tags.empty?
467 if tags.empty?
473 yield text
468 yield text
474 end
469 end
475 parsed << text
470 parsed << text
476 if tag
471 if tag
477 if closing
472 if closing
478 if tags.last == tag.downcase
473 if tags.last == tag.downcase
479 tags.pop
474 tags.pop
480 end
475 end
481 else
476 else
482 tags << tag.downcase
477 tags << tag.downcase
483 end
478 end
484 parsed << full_tag
479 parsed << full_tag
485 end
480 end
486 end
481 end
487 # Close any non closing tags
482 # Close any non closing tags
488 while tag = tags.pop
483 while tag = tags.pop
489 parsed << "</#{tag}>"
484 parsed << "</#{tag}>"
490 end
485 end
491 parsed
486 parsed
492 end
487 end
493
488
494 def parse_inline_attachments(text, project, obj, attr, only_path, options)
489 def parse_inline_attachments(text, project, obj, attr, only_path, options)
495 # when using an image link, try to use an attachment, if possible
490 # when using an image link, try to use an attachment, if possible
496 if options[:attachments] || (obj && obj.respond_to?(:attachments))
491 if options[:attachments] || (obj && obj.respond_to?(:attachments))
497 attachments = nil
492 attachments = nil
498 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
493 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
499 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
494 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
500 attachments ||= (options[:attachments] || obj.attachments).sort_by(&:created_on).reverse
495 attachments ||= (options[:attachments] || obj.attachments).sort_by(&:created_on).reverse
501 # search for the picture in attachments
496 # search for the picture in attachments
502 if found = attachments.detect { |att| att.filename.downcase == filename }
497 if found = attachments.detect { |att| att.filename.downcase == filename }
503 image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found
498 image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found
504 desc = found.description.to_s.gsub('"', '')
499 desc = found.description.to_s.gsub('"', '')
505 if !desc.blank? && alttext.blank?
500 if !desc.blank? && alttext.blank?
506 alt = " title=\"#{desc}\" alt=\"#{desc}\""
501 alt = " title=\"#{desc}\" alt=\"#{desc}\""
507 end
502 end
508 "src=\"#{image_url}\"#{alt}"
503 "src=\"#{image_url}\"#{alt}"
509 else
504 else
510 m
505 m
511 end
506 end
512 end
507 end
513 end
508 end
514 end
509 end
515
510
516 # Wiki links
511 # Wiki links
517 #
512 #
518 # Examples:
513 # Examples:
519 # [[mypage]]
514 # [[mypage]]
520 # [[mypage|mytext]]
515 # [[mypage|mytext]]
521 # wiki links can refer other project wikis, using project name or identifier:
516 # wiki links can refer other project wikis, using project name or identifier:
522 # [[project:]] -> wiki starting page
517 # [[project:]] -> wiki starting page
523 # [[project:|mytext]]
518 # [[project:|mytext]]
524 # [[project:mypage]]
519 # [[project:mypage]]
525 # [[project:mypage|mytext]]
520 # [[project:mypage|mytext]]
526 def parse_wiki_links(text, project, obj, attr, only_path, options)
521 def parse_wiki_links(text, project, obj, attr, only_path, options)
527 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
522 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
528 link_project = project
523 link_project = project
529 esc, all, page, title = $1, $2, $3, $5
524 esc, all, page, title = $1, $2, $3, $5
530 if esc.nil?
525 if esc.nil?
531 if page =~ /^([^\:]+)\:(.*)$/
526 if page =~ /^([^\:]+)\:(.*)$/
532 link_project = Project.find_by_name($1) || Project.find_by_identifier($1)
527 link_project = Project.find_by_name($1) || Project.find_by_identifier($1)
533 page = $2
528 page = $2
534 title ||= $1 if page.blank?
529 title ||= $1 if page.blank?
535 end
530 end
536
531
537 if link_project && link_project.wiki
532 if link_project && link_project.wiki
538 # extract anchor
533 # extract anchor
539 anchor = nil
534 anchor = nil
540 if page =~ /^(.+?)\#(.+)$/
535 if page =~ /^(.+?)\#(.+)$/
541 page, anchor = $1, $2
536 page, anchor = $1, $2
542 end
537 end
543 # check if page exists
538 # check if page exists
544 wiki_page = link_project.wiki.find_page(page)
539 wiki_page = link_project.wiki.find_page(page)
545 url = case options[:wiki_links]
540 url = case options[:wiki_links]
546 when :local; "#{title}.html"
541 when :local; "#{title}.html"
547 when :anchor; "##{title}" # used for single-file wiki export
542 when :anchor; "##{title}" # used for single-file wiki export
548 else
543 else
549 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project, :page => Wiki.titleize(page), :anchor => anchor)
544 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project, :page => Wiki.titleize(page), :anchor => anchor)
550 end
545 end
551 link_to((title || page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
546 link_to((title || page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
552 else
547 else
553 # project or wiki doesn't exist
548 # project or wiki doesn't exist
554 all
549 all
555 end
550 end
556 else
551 else
557 all
552 all
558 end
553 end
559 end
554 end
560 end
555 end
561
556
562 # Redmine links
557 # Redmine links
563 #
558 #
564 # Examples:
559 # Examples:
565 # Issues:
560 # Issues:
566 # #52 -> Link to issue #52
561 # #52 -> Link to issue #52
567 # Changesets:
562 # Changesets:
568 # r52 -> Link to revision 52
563 # r52 -> Link to revision 52
569 # commit:a85130f -> Link to scmid starting with a85130f
564 # commit:a85130f -> Link to scmid starting with a85130f
570 # Documents:
565 # Documents:
571 # document#17 -> Link to document with id 17
566 # document#17 -> Link to document with id 17
572 # document:Greetings -> Link to the document with title "Greetings"
567 # document:Greetings -> Link to the document with title "Greetings"
573 # document:"Some document" -> Link to the document with title "Some document"
568 # document:"Some document" -> Link to the document with title "Some document"
574 # Versions:
569 # Versions:
575 # version#3 -> Link to version with id 3
570 # version#3 -> Link to version with id 3
576 # version:1.0.0 -> Link to version named "1.0.0"
571 # version:1.0.0 -> Link to version named "1.0.0"
577 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
572 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
578 # Attachments:
573 # Attachments:
579 # attachment:file.zip -> Link to the attachment of the current object named file.zip
574 # attachment:file.zip -> Link to the attachment of the current object named file.zip
580 # Source files:
575 # Source files:
581 # source:some/file -> Link to the file located at /some/file in the project's repository
576 # source:some/file -> Link to the file located at /some/file in the project's repository
582 # source:some/file@52 -> Link to the file's revision 52
577 # source:some/file@52 -> Link to the file's revision 52
583 # source:some/file#L120 -> Link to line 120 of the file
578 # source:some/file#L120 -> Link to line 120 of the file
584 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
579 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
585 # export:some/file -> Force the download of the file
580 # export:some/file -> Force the download of the file
586 # Forum messages:
581 # Forum messages:
587 # message#1218 -> Link to message with id 1218
582 # message#1218 -> Link to message with id 1218
588 def parse_redmine_links(text, project, obj, attr, only_path, options)
583 def parse_redmine_links(text, project, obj, attr, only_path, options)
589 text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(attachment|document|version|commit|source|export|message|project)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|,|\s|\]|<|$)}) do |m|
584 text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(attachment|document|version|commit|source|export|message|project)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|,|\s|\]|<|$)}) do |m|
590 leading, esc, prefix, sep, identifier = $1, $2, $3, $5 || $7, $6 || $8
585 leading, esc, prefix, sep, identifier = $1, $2, $3, $5 || $7, $6 || $8
591 link = nil
586 link = nil
592 if esc.nil?
587 if esc.nil?
593 if prefix.nil? && sep == 'r'
588 if prefix.nil? && sep == 'r'
594 if project && (changeset = project.changesets.find_by_revision(identifier))
589 if project && (changeset = project.changesets.find_by_revision(identifier))
595 link = link_to("r#{identifier}", {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
590 link = link_to("r#{identifier}", {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
596 :class => 'changeset',
591 :class => 'changeset',
597 :title => truncate_single_line(changeset.comments, :length => 100))
592 :title => truncate_single_line(changeset.comments, :length => 100))
598 end
593 end
599 elsif sep == '#'
594 elsif sep == '#'
600 oid = identifier.to_i
595 oid = identifier.to_i
601 case prefix
596 case prefix
602 when nil
597 when nil
603 if issue = Issue.visible.find_by_id(oid, :include => :status)
598 if issue = Issue.visible.find_by_id(oid, :include => :status)
604 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
599 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
605 :class => issue.css_classes,
600 :class => issue.css_classes,
606 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
601 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
607 end
602 end
608 when 'document'
603 when 'document'
609 if document = Document.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
604 if document = Document.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
610 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
605 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
611 :class => 'document'
606 :class => 'document'
612 end
607 end
613 when 'version'
608 when 'version'
614 if version = Version.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
609 if version = Version.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
615 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
610 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
616 :class => 'version'
611 :class => 'version'
617 end
612 end
618 when 'message'
613 when 'message'
619 if message = Message.find_by_id(oid, :include => [:parent, {:board => :project}], :conditions => Project.visible_by(User.current))
614 if message = Message.find_by_id(oid, :include => [:parent, {:board => :project}], :conditions => Project.visible_by(User.current))
620 link = link_to h(truncate(message.subject, :length => 60)), {:only_path => only_path,
615 link = link_to h(truncate(message.subject, :length => 60)), {:only_path => only_path,
621 :controller => 'messages',
616 :controller => 'messages',
622 :action => 'show',
617 :action => 'show',
623 :board_id => message.board,
618 :board_id => message.board,
624 :id => message.root,
619 :id => message.root,
625 :anchor => (message.parent ? "message-#{message.id}" : nil)},
620 :anchor => (message.parent ? "message-#{message.id}" : nil)},
626 :class => 'message'
621 :class => 'message'
627 end
622 end
628 when 'project'
623 when 'project'
629 if p = Project.visible.find_by_id(oid)
624 if p = Project.visible.find_by_id(oid)
630 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
625 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
631 end
626 end
632 end
627 end
633 elsif sep == ':'
628 elsif sep == ':'
634 # removes the double quotes if any
629 # removes the double quotes if any
635 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
630 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
636 case prefix
631 case prefix
637 when 'document'
632 when 'document'
638 if project && document = project.documents.find_by_title(name)
633 if project && document = project.documents.find_by_title(name)
639 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
634 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
640 :class => 'document'
635 :class => 'document'
641 end
636 end
642 when 'version'
637 when 'version'
643 if project && version = project.versions.find_by_name(name)
638 if project && version = project.versions.find_by_name(name)
644 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
639 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
645 :class => 'version'
640 :class => 'version'
646 end
641 end
647 when 'commit'
642 when 'commit'
648 if project && (changeset = project.changesets.find(:first, :conditions => ["scmid LIKE ?", "#{name}%"]))
643 if project && (changeset = project.changesets.find(:first, :conditions => ["scmid LIKE ?", "#{name}%"]))
649 link = link_to h("#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
644 link = link_to h("#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
650 :class => 'changeset',
645 :class => 'changeset',
651 :title => truncate_single_line(changeset.comments, :length => 100)
646 :title => truncate_single_line(changeset.comments, :length => 100)
652 end
647 end
653 when 'source', 'export'
648 when 'source', 'export'
654 if project && project.repository
649 if project && project.repository
655 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
650 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
656 path, rev, anchor = $1, $3, $5
651 path, rev, anchor = $1, $3, $5
657 link = link_to h("#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project,
652 link = link_to h("#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project,
658 :path => to_path_param(path),
653 :path => to_path_param(path),
659 :rev => rev,
654 :rev => rev,
660 :anchor => anchor,
655 :anchor => anchor,
661 :format => (prefix == 'export' ? 'raw' : nil)},
656 :format => (prefix == 'export' ? 'raw' : nil)},
662 :class => (prefix == 'export' ? 'source download' : 'source')
657 :class => (prefix == 'export' ? 'source download' : 'source')
663 end
658 end
664 when 'attachment'
659 when 'attachment'
665 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
660 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
666 if attachments && attachment = attachments.detect {|a| a.filename == name }
661 if attachments && attachment = attachments.detect {|a| a.filename == name }
667 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
662 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
668 :class => 'attachment'
663 :class => 'attachment'
669 end
664 end
670 when 'project'
665 when 'project'
671 if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
666 if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
672 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
667 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
673 end
668 end
674 end
669 end
675 end
670 end
676 end
671 end
677 leading + (link || "#{prefix}#{sep}#{identifier}")
672 leading + (link || "#{prefix}#{sep}#{identifier}")
678 end
673 end
679 end
674 end
680
675
681 # Same as Rails' simple_format helper without using paragraphs
676 # Same as Rails' simple_format helper without using paragraphs
682 def simple_format_without_paragraph(text)
677 def simple_format_without_paragraph(text)
683 text.to_s.
678 text.to_s.
684 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
679 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
685 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
680 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
686 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
681 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
687 end
682 end
688
683
689 def lang_options_for_select(blank=true)
684 def lang_options_for_select(blank=true)
690 (blank ? [["(auto)", ""]] : []) +
685 (blank ? [["(auto)", ""]] : []) +
691 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
686 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
692 end
687 end
693
688
694 def label_tag_for(name, option_tags = nil, options = {})
689 def label_tag_for(name, option_tags = nil, options = {})
695 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
690 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
696 content_tag("label", label_text)
691 content_tag("label", label_text)
697 end
692 end
698
693
699 def labelled_tabular_form_for(name, object, options, &proc)
694 def labelled_tabular_form_for(name, object, options, &proc)
700 options[:html] ||= {}
695 options[:html] ||= {}
701 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
696 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
702 form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
697 form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
703 end
698 end
704
699
705 def back_url_hidden_field_tag
700 def back_url_hidden_field_tag
706 back_url = params[:back_url] || request.env['HTTP_REFERER']
701 back_url = params[:back_url] || request.env['HTTP_REFERER']
707 back_url = CGI.unescape(back_url.to_s)
702 back_url = CGI.unescape(back_url.to_s)
708 hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
703 hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
709 end
704 end
710
705
711 def check_all_links(form_name)
706 def check_all_links(form_name)
712 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
707 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
713 " | " +
708 " | " +
714 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
709 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
715 end
710 end
716
711
717 def progress_bar(pcts, options={})
712 def progress_bar(pcts, options={})
718 pcts = [pcts, pcts] unless pcts.is_a?(Array)
713 pcts = [pcts, pcts] unless pcts.is_a?(Array)
719 pcts = pcts.collect(&:round)
714 pcts = pcts.collect(&:round)
720 pcts[1] = pcts[1] - pcts[0]
715 pcts[1] = pcts[1] - pcts[0]
721 pcts << (100 - pcts[1] - pcts[0])
716 pcts << (100 - pcts[1] - pcts[0])
722 width = options[:width] || '100px;'
717 width = options[:width] || '100px;'
723 legend = options[:legend] || ''
718 legend = options[:legend] || ''
724 content_tag('table',
719 content_tag('table',
725 content_tag('tr',
720 content_tag('tr',
726 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : '') +
721 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : '') +
727 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : '') +
722 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : '') +
728 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : '')
723 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : '')
729 ), :class => 'progress', :style => "width: #{width};") +
724 ), :class => 'progress', :style => "width: #{width};") +
730 content_tag('p', legend, :class => 'pourcent')
725 content_tag('p', legend, :class => 'pourcent')
731 end
726 end
732
727
733 def checked_image(checked=true)
728 def checked_image(checked=true)
734 if checked
729 if checked
735 image_tag 'toggle_check.png'
730 image_tag 'toggle_check.png'
736 end
731 end
737 end
732 end
738
733
739 def context_menu(url)
734 def context_menu(url)
740 unless @context_menu_included
735 unless @context_menu_included
741 content_for :header_tags do
736 content_for :header_tags do
742 javascript_include_tag('context_menu') +
737 javascript_include_tag('context_menu') +
743 stylesheet_link_tag('context_menu')
738 stylesheet_link_tag('context_menu')
744 end
739 end
745 if l(:direction) == 'rtl'
740 if l(:direction) == 'rtl'
746 content_for :header_tags do
741 content_for :header_tags do
747 stylesheet_link_tag('context_menu_rtl')
742 stylesheet_link_tag('context_menu_rtl')
748 end
743 end
749 end
744 end
750 @context_menu_included = true
745 @context_menu_included = true
751 end
746 end
752 javascript_tag "new ContextMenu('#{ url_for(url) }')"
747 javascript_tag "new ContextMenu('#{ url_for(url) }')"
753 end
748 end
754
749
755 def context_menu_link(name, url, options={})
750 def context_menu_link(name, url, options={})
756 options[:class] ||= ''
751 options[:class] ||= ''
757 if options.delete(:selected)
752 if options.delete(:selected)
758 options[:class] << ' icon-checked disabled'
753 options[:class] << ' icon-checked disabled'
759 options[:disabled] = true
754 options[:disabled] = true
760 end
755 end
761 if options.delete(:disabled)
756 if options.delete(:disabled)
762 options.delete(:method)
757 options.delete(:method)
763 options.delete(:confirm)
758 options.delete(:confirm)
764 options.delete(:onclick)
759 options.delete(:onclick)
765 options[:class] << ' disabled'
760 options[:class] << ' disabled'
766 url = '#'
761 url = '#'
767 end
762 end
768 link_to name, url, options
763 link_to name, url, options
769 end
764 end
770
765
771 def calendar_for(field_id)
766 def calendar_for(field_id)
772 include_calendar_headers_tags
767 include_calendar_headers_tags
773 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
768 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
774 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
769 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
775 end
770 end
776
771
777 def include_calendar_headers_tags
772 def include_calendar_headers_tags
778 unless @calendar_headers_tags_included
773 unless @calendar_headers_tags_included
779 @calendar_headers_tags_included = true
774 @calendar_headers_tags_included = true
780 content_for :header_tags do
775 content_for :header_tags do
781 start_of_week = case Setting.start_of_week.to_i
776 start_of_week = case Setting.start_of_week.to_i
782 when 1
777 when 1
783 'Calendar._FD = 1;' # Monday
778 'Calendar._FD = 1;' # Monday
784 when 7
779 when 7
785 'Calendar._FD = 0;' # Sunday
780 'Calendar._FD = 0;' # Sunday
786 else
781 else
787 '' # use language
782 '' # use language
788 end
783 end
789
784
790 javascript_include_tag('calendar/calendar') +
785 javascript_include_tag('calendar/calendar') +
791 javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
786 javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
792 javascript_tag(start_of_week) +
787 javascript_tag(start_of_week) +
793 javascript_include_tag('calendar/calendar-setup') +
788 javascript_include_tag('calendar/calendar-setup') +
794 stylesheet_link_tag('calendar')
789 stylesheet_link_tag('calendar')
795 end
790 end
796 end
791 end
797 end
792 end
798
793
799 def content_for(name, content = nil, &block)
794 def content_for(name, content = nil, &block)
800 @has_content ||= {}
795 @has_content ||= {}
801 @has_content[name] = true
796 @has_content[name] = true
802 super(name, content, &block)
797 super(name, content, &block)
803 end
798 end
804
799
805 def has_content?(name)
800 def has_content?(name)
806 (@has_content && @has_content[name]) || false
801 (@has_content && @has_content[name]) || false
807 end
802 end
808
803
809 # Returns the avatar image tag for the given +user+ if avatars are enabled
804 # Returns the avatar image tag for the given +user+ if avatars are enabled
810 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
805 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
811 def avatar(user, options = { })
806 def avatar(user, options = { })
812 if Setting.gravatar_enabled?
807 if Setting.gravatar_enabled?
813 options.merge!({:ssl => (defined?(request) && request.ssl?), :default => Setting.gravatar_default})
808 options.merge!({:ssl => (defined?(request) && request.ssl?), :default => Setting.gravatar_default})
814 email = nil
809 email = nil
815 if user.respond_to?(:mail)
810 if user.respond_to?(:mail)
816 email = user.mail
811 email = user.mail
817 elsif user.to_s =~ %r{<(.+?)>}
812 elsif user.to_s =~ %r{<(.+?)>}
818 email = $1
813 email = $1
819 end
814 end
820 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
815 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
821 else
816 else
822 ''
817 ''
823 end
818 end
824 end
819 end
825
820
826 def favicon
821 def favicon
827 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />"
822 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />"
828 end
823 end
829
824
830 private
825 private
831
826
832 def wiki_helper
827 def wiki_helper
833 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
828 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
834 extend helper
829 extend helper
835 return self
830 return self
836 end
831 end
837
832
838 def link_to_remote_content_update(text, url_params)
833 def link_to_remote_content_update(text, url_params)
839 link_to_remote(text,
834 link_to_remote(text,
840 {:url => url_params, :method => :get, :update => 'content', :complete => 'window.scrollTo(0,0)'},
835 {:url => url_params, :method => :get, :update => 'content', :complete => 'window.scrollTo(0,0)'},
841 {:href => url_for(:params => url_params)}
836 {:href => url_for(:params => url_params)}
842 )
837 )
843 end
838 end
844
839
845 end
840 end
@@ -1,775 +1,787
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
2 # Copyright (C) 2006 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 Project < ActiveRecord::Base
18 class Project < ActiveRecord::Base
19 # Project statuses
19 # Project statuses
20 STATUS_ACTIVE = 1
20 STATUS_ACTIVE = 1
21 STATUS_ARCHIVED = 9
21 STATUS_ARCHIVED = 9
22
22
23 # Specific overidden Activities
23 # Specific overidden Activities
24 has_many :time_entry_activities
24 has_many :time_entry_activities
25 has_many :members, :include => [:user, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
25 has_many :members, :include => [:user, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
26 has_many :memberships, :class_name => 'Member'
26 has_many :memberships, :class_name => 'Member'
27 has_many :member_principals, :class_name => 'Member',
27 has_many :member_principals, :class_name => 'Member',
28 :include => :principal,
28 :include => :principal,
29 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})"
29 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})"
30 has_many :users, :through => :members
30 has_many :users, :through => :members
31 has_many :principals, :through => :member_principals, :source => :principal
31 has_many :principals, :through => :member_principals, :source => :principal
32
32
33 has_many :enabled_modules, :dependent => :delete_all
33 has_many :enabled_modules, :dependent => :delete_all
34 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
34 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
35 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
35 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
36 has_many :issue_changes, :through => :issues, :source => :journals
36 has_many :issue_changes, :through => :issues, :source => :journals
37 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
37 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
38 has_many :time_entries, :dependent => :delete_all
38 has_many :time_entries, :dependent => :delete_all
39 has_many :queries, :dependent => :delete_all
39 has_many :queries, :dependent => :delete_all
40 has_many :documents, :dependent => :destroy
40 has_many :documents, :dependent => :destroy
41 has_many :news, :dependent => :delete_all, :include => :author
41 has_many :news, :dependent => :delete_all, :include => :author
42 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
42 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
43 has_many :boards, :dependent => :destroy, :order => "position ASC"
43 has_many :boards, :dependent => :destroy, :order => "position ASC"
44 has_one :repository, :dependent => :destroy
44 has_one :repository, :dependent => :destroy
45 has_many :changesets, :through => :repository
45 has_many :changesets, :through => :repository
46 has_one :wiki, :dependent => :destroy
46 has_one :wiki, :dependent => :destroy
47 # Custom field for the project issues
47 # Custom field for the project issues
48 has_and_belongs_to_many :issue_custom_fields,
48 has_and_belongs_to_many :issue_custom_fields,
49 :class_name => 'IssueCustomField',
49 :class_name => 'IssueCustomField',
50 :order => "#{CustomField.table_name}.position",
50 :order => "#{CustomField.table_name}.position",
51 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
51 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
52 :association_foreign_key => 'custom_field_id'
52 :association_foreign_key => 'custom_field_id'
53
53
54 acts_as_nested_set :order => 'name'
54 acts_as_nested_set :order => 'name'
55 acts_as_attachable :view_permission => :view_files,
55 acts_as_attachable :view_permission => :view_files,
56 :delete_permission => :manage_files
56 :delete_permission => :manage_files
57
57
58 acts_as_customizable
58 acts_as_customizable
59 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
59 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
60 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
60 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
61 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
61 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
62 :author => nil
62 :author => nil
63
63
64 attr_protected :status, :enabled_module_names
64 attr_protected :status, :enabled_module_names
65
65
66 validates_presence_of :name, :identifier
66 validates_presence_of :name, :identifier
67 validates_uniqueness_of :name, :identifier
67 validates_uniqueness_of :name, :identifier
68 validates_associated :repository, :wiki
68 validates_associated :repository, :wiki
69 validates_length_of :name, :maximum => 30
69 validates_length_of :name, :maximum => 30
70 validates_length_of :homepage, :maximum => 255
70 validates_length_of :homepage, :maximum => 255
71 validates_length_of :identifier, :in => 1..20
71 validates_length_of :identifier, :in => 1..20
72 # donwcase letters, digits, dashes but not digits only
72 # donwcase letters, digits, dashes but not digits only
73 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :if => Proc.new { |p| p.identifier_changed? }
73 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :if => Proc.new { |p| p.identifier_changed? }
74 # reserved words
74 # reserved words
75 validates_exclusion_of :identifier, :in => %w( new )
75 validates_exclusion_of :identifier, :in => %w( new )
76
76
77 before_destroy :delete_all_members, :destroy_children
77 before_destroy :delete_all_members, :destroy_children
78
78
79 named_scope :has_module, lambda { |mod| { :conditions => ["#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s] } }
79 named_scope :has_module, lambda { |mod| { :conditions => ["#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s] } }
80 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
80 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
81 named_scope :all_public, { :conditions => { :is_public => true } }
81 named_scope :all_public, { :conditions => { :is_public => true } }
82 named_scope :visible, lambda { { :conditions => Project.visible_by(User.current) } }
82 named_scope :visible, lambda { { :conditions => Project.visible_by(User.current) } }
83
83
84 def identifier=(identifier)
84 def identifier=(identifier)
85 super unless identifier_frozen?
85 super unless identifier_frozen?
86 end
86 end
87
87
88 def identifier_frozen?
88 def identifier_frozen?
89 errors[:identifier].nil? && !(new_record? || identifier.blank?)
89 errors[:identifier].nil? && !(new_record? || identifier.blank?)
90 end
90 end
91
91
92 # returns latest created projects
92 # returns latest created projects
93 # non public projects will be returned only if user is a member of those
93 # non public projects will be returned only if user is a member of those
94 def self.latest(user=nil, count=5)
94 def self.latest(user=nil, count=5)
95 find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC")
95 find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC")
96 end
96 end
97
97
98 # Returns a SQL :conditions string used to find all active projects for the specified user.
98 # Returns a SQL :conditions string used to find all active projects for the specified user.
99 #
99 #
100 # Examples:
100 # Examples:
101 # Projects.visible_by(admin) => "projects.status = 1"
101 # Projects.visible_by(admin) => "projects.status = 1"
102 # Projects.visible_by(normal_user) => "projects.status = 1 AND projects.is_public = 1"
102 # Projects.visible_by(normal_user) => "projects.status = 1 AND projects.is_public = 1"
103 def self.visible_by(user=nil)
103 def self.visible_by(user=nil)
104 user ||= User.current
104 user ||= User.current
105 if user && user.admin?
105 if user && user.admin?
106 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
106 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
107 elsif user && user.memberships.any?
107 elsif user && user.memberships.any?
108 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND (#{Project.table_name}.is_public = #{connection.quoted_true} or #{Project.table_name}.id IN (#{user.memberships.collect{|m| m.project_id}.join(',')}))"
108 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND (#{Project.table_name}.is_public = #{connection.quoted_true} or #{Project.table_name}.id IN (#{user.memberships.collect{|m| m.project_id}.join(',')}))"
109 else
109 else
110 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
110 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
111 end
111 end
112 end
112 end
113
113
114 def self.allowed_to_condition(user, permission, options={})
114 def self.allowed_to_condition(user, permission, options={})
115 statements = []
115 statements = []
116 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
116 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
117 if perm = Redmine::AccessControl.permission(permission)
117 if perm = Redmine::AccessControl.permission(permission)
118 unless perm.project_module.nil?
118 unless perm.project_module.nil?
119 # If the permission belongs to a project module, make sure the module is enabled
119 # If the permission belongs to a project module, make sure the module is enabled
120 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
120 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
121 end
121 end
122 end
122 end
123 if options[:project]
123 if options[:project]
124 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
124 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
125 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
125 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
126 base_statement = "(#{project_statement}) AND (#{base_statement})"
126 base_statement = "(#{project_statement}) AND (#{base_statement})"
127 end
127 end
128 if user.admin?
128 if user.admin?
129 # no restriction
129 # no restriction
130 else
130 else
131 statements << "1=0"
131 statements << "1=0"
132 if user.logged?
132 if user.logged?
133 if Role.non_member.allowed_to?(permission) && !options[:member]
133 if Role.non_member.allowed_to?(permission) && !options[:member]
134 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
134 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
135 end
135 end
136 allowed_project_ids = user.memberships.select {|m| m.roles.detect {|role| role.allowed_to?(permission)}}.collect {|m| m.project_id}
136 allowed_project_ids = user.memberships.select {|m| m.roles.detect {|role| role.allowed_to?(permission)}}.collect {|m| m.project_id}
137 statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any?
137 statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any?
138 else
138 else
139 if Role.anonymous.allowed_to?(permission) && !options[:member]
139 if Role.anonymous.allowed_to?(permission) && !options[:member]
140 # anonymous user allowed on public project
140 # anonymous user allowed on public project
141 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
141 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
142 end
142 end
143 end
143 end
144 end
144 end
145 statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))"
145 statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))"
146 end
146 end
147
147
148 # Returns the Systemwide and project specific activities
148 # Returns the Systemwide and project specific activities
149 def activities(include_inactive=false)
149 def activities(include_inactive=false)
150 if include_inactive
150 if include_inactive
151 return all_activities
151 return all_activities
152 else
152 else
153 return active_activities
153 return active_activities
154 end
154 end
155 end
155 end
156
156
157 # Will create a new Project specific Activity or update an existing one
157 # Will create a new Project specific Activity or update an existing one
158 #
158 #
159 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
159 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
160 # does not successfully save.
160 # does not successfully save.
161 def update_or_create_time_entry_activity(id, activity_hash)
161 def update_or_create_time_entry_activity(id, activity_hash)
162 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
162 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
163 self.create_time_entry_activity_if_needed(activity_hash)
163 self.create_time_entry_activity_if_needed(activity_hash)
164 else
164 else
165 activity = project.time_entry_activities.find_by_id(id.to_i)
165 activity = project.time_entry_activities.find_by_id(id.to_i)
166 activity.update_attributes(activity_hash) if activity
166 activity.update_attributes(activity_hash) if activity
167 end
167 end
168 end
168 end
169
169
170 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
170 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
171 #
171 #
172 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
172 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
173 # does not successfully save.
173 # does not successfully save.
174 def create_time_entry_activity_if_needed(activity)
174 def create_time_entry_activity_if_needed(activity)
175 if activity['parent_id']
175 if activity['parent_id']
176
176
177 parent_activity = TimeEntryActivity.find(activity['parent_id'])
177 parent_activity = TimeEntryActivity.find(activity['parent_id'])
178 activity['name'] = parent_activity.name
178 activity['name'] = parent_activity.name
179 activity['position'] = parent_activity.position
179 activity['position'] = parent_activity.position
180
180
181 if Enumeration.overridding_change?(activity, parent_activity)
181 if Enumeration.overridding_change?(activity, parent_activity)
182 project_activity = self.time_entry_activities.create(activity)
182 project_activity = self.time_entry_activities.create(activity)
183
183
184 if project_activity.new_record?
184 if project_activity.new_record?
185 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
185 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
186 else
186 else
187 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
187 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
188 end
188 end
189 end
189 end
190 end
190 end
191 end
191 end
192
192
193 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
193 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
194 #
194 #
195 # Examples:
195 # Examples:
196 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
196 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
197 # project.project_condition(false) => "projects.id = 1"
197 # project.project_condition(false) => "projects.id = 1"
198 def project_condition(with_subprojects)
198 def project_condition(with_subprojects)
199 cond = "#{Project.table_name}.id = #{id}"
199 cond = "#{Project.table_name}.id = #{id}"
200 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
200 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
201 cond
201 cond
202 end
202 end
203
203
204 def self.find(*args)
204 def self.find(*args)
205 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
205 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
206 project = find_by_identifier(*args)
206 project = find_by_identifier(*args)
207 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
207 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
208 project
208 project
209 else
209 else
210 super
210 super
211 end
211 end
212 end
212 end
213
213
214 def to_param
214 def to_param
215 # id is used for projects with a numeric identifier (compatibility)
215 # id is used for projects with a numeric identifier (compatibility)
216 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
216 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
217 end
217 end
218
218
219 def active?
219 def active?
220 self.status == STATUS_ACTIVE
220 self.status == STATUS_ACTIVE
221 end
221 end
222
222
223 # Archives the project and its descendants
223 # Archives the project and its descendants
224 def archive
224 def archive
225 # Check that there is no issue of a non descendant project that is assigned
225 # Check that there is no issue of a non descendant project that is assigned
226 # to one of the project or descendant versions
226 # to one of the project or descendant versions
227 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
227 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
228 if v_ids.any? && Issue.find(:first, :include => :project,
228 if v_ids.any? && Issue.find(:first, :include => :project,
229 :conditions => ["(#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?)" +
229 :conditions => ["(#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?)" +
230 " AND #{Issue.table_name}.fixed_version_id IN (?)", lft, rgt, v_ids])
230 " AND #{Issue.table_name}.fixed_version_id IN (?)", lft, rgt, v_ids])
231 return false
231 return false
232 end
232 end
233 Project.transaction do
233 Project.transaction do
234 archive!
234 archive!
235 end
235 end
236 true
236 true
237 end
237 end
238
238
239 # Unarchives the project
239 # Unarchives the project
240 # All its ancestors must be active
240 # All its ancestors must be active
241 def unarchive
241 def unarchive
242 return false if ancestors.detect {|a| !a.active?}
242 return false if ancestors.detect {|a| !a.active?}
243 update_attribute :status, STATUS_ACTIVE
243 update_attribute :status, STATUS_ACTIVE
244 end
244 end
245
245
246 # Returns an array of projects the project can be moved to
246 # Returns an array of projects the project can be moved to
247 # by the current user
247 # by the current user
248 def allowed_parents
248 def allowed_parents
249 return @allowed_parents if @allowed_parents
249 return @allowed_parents if @allowed_parents
250 @allowed_parents = Project.find(:all, :conditions => Project.allowed_to_condition(User.current, :add_subprojects))
250 @allowed_parents = Project.find(:all, :conditions => Project.allowed_to_condition(User.current, :add_subprojects))
251 @allowed_parents = @allowed_parents - self_and_descendants
251 @allowed_parents = @allowed_parents - self_and_descendants
252 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
252 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
253 @allowed_parents << nil
253 @allowed_parents << nil
254 end
254 end
255 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
255 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
256 @allowed_parents << parent
256 @allowed_parents << parent
257 end
257 end
258 @allowed_parents
258 @allowed_parents
259 end
259 end
260
260
261 # Sets the parent of the project with authorization check
261 # Sets the parent of the project with authorization check
262 def set_allowed_parent!(p)
262 def set_allowed_parent!(p)
263 unless p.nil? || p.is_a?(Project)
263 unless p.nil? || p.is_a?(Project)
264 if p.to_s.blank?
264 if p.to_s.blank?
265 p = nil
265 p = nil
266 else
266 else
267 p = Project.find_by_id(p)
267 p = Project.find_by_id(p)
268 return false unless p
268 return false unless p
269 end
269 end
270 end
270 end
271 if p.nil?
271 if p.nil?
272 if !new_record? && allowed_parents.empty?
272 if !new_record? && allowed_parents.empty?
273 return false
273 return false
274 end
274 end
275 elsif !allowed_parents.include?(p)
275 elsif !allowed_parents.include?(p)
276 return false
276 return false
277 end
277 end
278 set_parent!(p)
278 set_parent!(p)
279 end
279 end
280
280
281 # Sets the parent of the project
281 # Sets the parent of the project
282 # Argument can be either a Project, a String, a Fixnum or nil
282 # Argument can be either a Project, a String, a Fixnum or nil
283 def set_parent!(p)
283 def set_parent!(p)
284 unless p.nil? || p.is_a?(Project)
284 unless p.nil? || p.is_a?(Project)
285 if p.to_s.blank?
285 if p.to_s.blank?
286 p = nil
286 p = nil
287 else
287 else
288 p = Project.find_by_id(p)
288 p = Project.find_by_id(p)
289 return false unless p
289 return false unless p
290 end
290 end
291 end
291 end
292 if p == parent && !p.nil?
292 if p == parent && !p.nil?
293 # Nothing to do
293 # Nothing to do
294 true
294 true
295 elsif p.nil? || (p.active? && move_possible?(p))
295 elsif p.nil? || (p.active? && move_possible?(p))
296 # Insert the project so that target's children or root projects stay alphabetically sorted
296 # Insert the project so that target's children or root projects stay alphabetically sorted
297 sibs = (p.nil? ? self.class.roots : p.children)
297 sibs = (p.nil? ? self.class.roots : p.children)
298 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
298 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
299 if to_be_inserted_before
299 if to_be_inserted_before
300 move_to_left_of(to_be_inserted_before)
300 move_to_left_of(to_be_inserted_before)
301 elsif p.nil?
301 elsif p.nil?
302 if sibs.empty?
302 if sibs.empty?
303 # move_to_root adds the project in first (ie. left) position
303 # move_to_root adds the project in first (ie. left) position
304 move_to_root
304 move_to_root
305 else
305 else
306 move_to_right_of(sibs.last) unless self == sibs.last
306 move_to_right_of(sibs.last) unless self == sibs.last
307 end
307 end
308 else
308 else
309 # move_to_child_of adds the project in last (ie.right) position
309 # move_to_child_of adds the project in last (ie.right) position
310 move_to_child_of(p)
310 move_to_child_of(p)
311 end
311 end
312 Issue.update_versions_from_hierarchy_change(self)
312 Issue.update_versions_from_hierarchy_change(self)
313 true
313 true
314 else
314 else
315 # Can not move to the given target
315 # Can not move to the given target
316 false
316 false
317 end
317 end
318 end
318 end
319
319
320 # Returns an array of the trackers used by the project and its active sub projects
320 # Returns an array of the trackers used by the project and its active sub projects
321 def rolled_up_trackers
321 def rolled_up_trackers
322 @rolled_up_trackers ||=
322 @rolled_up_trackers ||=
323 Tracker.find(:all, :include => :projects,
323 Tracker.find(:all, :include => :projects,
324 :select => "DISTINCT #{Tracker.table_name}.*",
324 :select => "DISTINCT #{Tracker.table_name}.*",
325 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
325 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
326 :order => "#{Tracker.table_name}.position")
326 :order => "#{Tracker.table_name}.position")
327 end
327 end
328
328
329 # Closes open and locked project versions that are completed
329 # Closes open and locked project versions that are completed
330 def close_completed_versions
330 def close_completed_versions
331 Version.transaction do
331 Version.transaction do
332 versions.find(:all, :conditions => {:status => %w(open locked)}).each do |version|
332 versions.find(:all, :conditions => {:status => %w(open locked)}).each do |version|
333 if version.completed?
333 if version.completed?
334 version.update_attribute(:status, 'closed')
334 version.update_attribute(:status, 'closed')
335 end
335 end
336 end
336 end
337 end
337 end
338 end
338 end
339
339
340 # Returns a scope of the Versions on subprojects
340 # Returns a scope of the Versions on subprojects
341 def rolled_up_versions
341 def rolled_up_versions
342 @rolled_up_versions ||=
342 @rolled_up_versions ||=
343 Version.scoped(:include => :project,
343 Version.scoped(:include => :project,
344 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt])
344 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt])
345 end
345 end
346
346
347 # Returns a scope of the Versions used by the project
347 # Returns a scope of the Versions used by the project
348 def shared_versions
348 def shared_versions
349 @shared_versions ||=
349 @shared_versions ||=
350 Version.scoped(:include => :project,
350 Version.scoped(:include => :project,
351 :conditions => "#{Project.table_name}.id = #{id}" +
351 :conditions => "#{Project.table_name}.id = #{id}" +
352 " OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" +
352 " OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" +
353 " #{Version.table_name}.sharing = 'system'" +
353 " #{Version.table_name}.sharing = 'system'" +
354 " OR (#{Project.table_name}.lft >= #{root.lft} AND #{Project.table_name}.rgt <= #{root.rgt} AND #{Version.table_name}.sharing = 'tree')" +
354 " OR (#{Project.table_name}.lft >= #{root.lft} AND #{Project.table_name}.rgt <= #{root.rgt} AND #{Version.table_name}.sharing = 'tree')" +
355 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
355 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
356 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
356 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
357 "))")
357 "))")
358 end
358 end
359
359
360 # Returns a hash of project users grouped by role
360 # Returns a hash of project users grouped by role
361 def users_by_role
361 def users_by_role
362 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
362 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
363 m.roles.each do |r|
363 m.roles.each do |r|
364 h[r] ||= []
364 h[r] ||= []
365 h[r] << m.user
365 h[r] << m.user
366 end
366 end
367 h
367 h
368 end
368 end
369 end
369 end
370
370
371 # Deletes all project's members
371 # Deletes all project's members
372 def delete_all_members
372 def delete_all_members
373 me, mr = Member.table_name, MemberRole.table_name
373 me, mr = Member.table_name, MemberRole.table_name
374 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
374 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
375 Member.delete_all(['project_id = ?', id])
375 Member.delete_all(['project_id = ?', id])
376 end
376 end
377
377
378 # Users issues can be assigned to
378 # Users issues can be assigned to
379 def assignable_users
379 def assignable_users
380 members.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.user}.sort
380 members.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.user}.sort
381 end
381 end
382
382
383 # Returns the mail adresses of users that should be always notified on project events
383 # Returns the mail adresses of users that should be always notified on project events
384 def recipients
384 def recipients
385 notified_users.collect {|user| user.mail}
385 notified_users.collect {|user| user.mail}
386 end
386 end
387
387
388 # Returns the users that should be notified on project events
388 # Returns the users that should be notified on project events
389 def notified_users
389 def notified_users
390 # TODO: User part should be extracted to User#notify_about?
390 # TODO: User part should be extracted to User#notify_about?
391 members.select {|m| m.mail_notification? || m.user.mail_notification == 'all'}.collect {|m| m.user}
391 members.select {|m| m.mail_notification? || m.user.mail_notification == 'all'}.collect {|m| m.user}
392 end
392 end
393
393
394 # Returns an array of all custom fields enabled for project issues
394 # Returns an array of all custom fields enabled for project issues
395 # (explictly associated custom fields and custom fields enabled for all projects)
395 # (explictly associated custom fields and custom fields enabled for all projects)
396 def all_issue_custom_fields
396 def all_issue_custom_fields
397 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
397 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
398 end
398 end
399
399
400 def project
400 def project
401 self
401 self
402 end
402 end
403
403
404 def <=>(project)
404 def <=>(project)
405 name.downcase <=> project.name.downcase
405 name.downcase <=> project.name.downcase
406 end
406 end
407
407
408 def to_s
408 def to_s
409 name
409 name
410 end
410 end
411
411
412 # Returns a short description of the projects (first lines)
412 # Returns a short description of the projects (first lines)
413 def short_description(length = 255)
413 def short_description(length = 255)
414 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
414 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
415 end
415 end
416
416
417 def css_classes
417 def css_classes
418 s = 'project'
418 s = 'project'
419 s << ' root' if root?
419 s << ' root' if root?
420 s << ' child' if child?
420 s << ' child' if child?
421 s << (leaf? ? ' leaf' : ' parent')
421 s << (leaf? ? ' leaf' : ' parent')
422 s
422 s
423 end
423 end
424
424
425 # The earliest start date of a project, based on it's issues and versions
425 # The earliest start date of a project, based on it's issues and versions
426 def start_date
426 def start_date
427 if module_enabled?(:issue_tracking)
427 if module_enabled?(:issue_tracking)
428 [
428 [
429 issues.minimum('start_date'),
429 issues.minimum('start_date'),
430 shared_versions.collect(&:effective_date),
430 shared_versions.collect(&:effective_date),
431 shared_versions.collect {|v| v.fixed_issues.minimum('start_date')}
431 shared_versions.collect {|v| v.fixed_issues.minimum('start_date')}
432 ].flatten.compact.min
432 ].flatten.compact.min
433 end
433 end
434 end
434 end
435
435
436 # The latest due date of an issue or version
436 # The latest due date of an issue or version
437 def due_date
437 def due_date
438 if module_enabled?(:issue_tracking)
438 if module_enabled?(:issue_tracking)
439 [
439 [
440 issues.maximum('due_date'),
440 issues.maximum('due_date'),
441 shared_versions.collect(&:effective_date),
441 shared_versions.collect(&:effective_date),
442 shared_versions.collect {|v| v.fixed_issues.maximum('due_date')}
442 shared_versions.collect {|v| v.fixed_issues.maximum('due_date')}
443 ].flatten.compact.max
443 ].flatten.compact.max
444 end
444 end
445 end
445 end
446
446
447 def overdue?
447 def overdue?
448 active? && !due_date.nil? && (due_date < Date.today)
448 active? && !due_date.nil? && (due_date < Date.today)
449 end
449 end
450
450
451 # Returns the percent completed for this project, based on the
451 # Returns the percent completed for this project, based on the
452 # progress on it's versions.
452 # progress on it's versions.
453 def completed_percent(options={:include_subprojects => false})
453 def completed_percent(options={:include_subprojects => false})
454 if options.delete(:include_subprojects)
454 if options.delete(:include_subprojects)
455 total = self_and_descendants.collect(&:completed_percent).sum
455 total = self_and_descendants.collect(&:completed_percent).sum
456
456
457 total / self_and_descendants.count
457 total / self_and_descendants.count
458 else
458 else
459 if versions.count > 0
459 if versions.count > 0
460 total = versions.collect(&:completed_pourcent).sum
460 total = versions.collect(&:completed_pourcent).sum
461
461
462 total / versions.count
462 total / versions.count
463 else
463 else
464 100
464 100
465 end
465 end
466 end
466 end
467 end
467 end
468
468
469 # Return true if this project is allowed to do the specified action.
469 # Return true if this project is allowed to do the specified action.
470 # action can be:
470 # action can be:
471 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
471 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
472 # * a permission Symbol (eg. :edit_project)
472 # * a permission Symbol (eg. :edit_project)
473 def allows_to?(action)
473 def allows_to?(action)
474 if action.is_a? Hash
474 if action.is_a? Hash
475 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
475 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
476 else
476 else
477 allowed_permissions.include? action
477 allowed_permissions.include? action
478 end
478 end
479 end
479 end
480
480
481 def module_enabled?(module_name)
481 def module_enabled?(module_name)
482 module_name = module_name.to_s
482 module_name = module_name.to_s
483 enabled_modules.detect {|m| m.name == module_name}
483 enabled_modules.detect {|m| m.name == module_name}
484 end
484 end
485
485
486 def enabled_module_names=(module_names)
486 def enabled_module_names=(module_names)
487 if module_names && module_names.is_a?(Array)
487 if module_names && module_names.is_a?(Array)
488 module_names = module_names.collect(&:to_s)
488 module_names = module_names.collect(&:to_s)
489 # remove disabled modules
489 # remove disabled modules
490 enabled_modules.each {|mod| mod.destroy unless module_names.include?(mod.name)}
490 enabled_modules.each {|mod| mod.destroy unless module_names.include?(mod.name)}
491 # add new modules
491 # add new modules
492 module_names.reject {|name| module_enabled?(name)}.each {|name| enabled_modules << EnabledModule.new(:name => name)}
492 module_names.reject {|name| module_enabled?(name)}.each {|name| enabled_modules << EnabledModule.new(:name => name)}
493 else
493 else
494 enabled_modules.clear
494 enabled_modules.clear
495 end
495 end
496 end
496 end
497
497
498 # Returns an array of projects that are in this project's hierarchy
498 # Returns an array of projects that are in this project's hierarchy
499 #
499 #
500 # Example: parents, children, siblings
500 # Example: parents, children, siblings
501 def hierarchy
501 def hierarchy
502 parents = project.self_and_ancestors || []
502 parents = project.self_and_ancestors || []
503 descendants = project.descendants || []
503 descendants = project.descendants || []
504 project_hierarchy = parents | descendants # Set union
504 project_hierarchy = parents | descendants # Set union
505 end
505 end
506
506
507 # Returns an auto-generated project identifier based on the last identifier used
507 # Returns an auto-generated project identifier based on the last identifier used
508 def self.next_identifier
508 def self.next_identifier
509 p = Project.find(:first, :order => 'created_on DESC')
509 p = Project.find(:first, :order => 'created_on DESC')
510 p.nil? ? nil : p.identifier.to_s.succ
510 p.nil? ? nil : p.identifier.to_s.succ
511 end
511 end
512
512
513 # Copies and saves the Project instance based on the +project+.
513 # Copies and saves the Project instance based on the +project+.
514 # Duplicates the source project's:
514 # Duplicates the source project's:
515 # * Wiki
515 # * Wiki
516 # * Versions
516 # * Versions
517 # * Categories
517 # * Categories
518 # * Issues
518 # * Issues
519 # * Members
519 # * Members
520 # * Queries
520 # * Queries
521 #
521 #
522 # Accepts an +options+ argument to specify what to copy
522 # Accepts an +options+ argument to specify what to copy
523 #
523 #
524 # Examples:
524 # Examples:
525 # project.copy(1) # => copies everything
525 # project.copy(1) # => copies everything
526 # project.copy(1, :only => 'members') # => copies members only
526 # project.copy(1, :only => 'members') # => copies members only
527 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
527 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
528 def copy(project, options={})
528 def copy(project, options={})
529 project = project.is_a?(Project) ? project : Project.find(project)
529 project = project.is_a?(Project) ? project : Project.find(project)
530
530
531 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
531 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
532 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
532 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
533
533
534 Project.transaction do
534 Project.transaction do
535 if save
535 if save
536 reload
536 reload
537 to_be_copied.each do |name|
537 to_be_copied.each do |name|
538 send "copy_#{name}", project
538 send "copy_#{name}", project
539 end
539 end
540 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
540 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
541 save
541 save
542 end
542 end
543 end
543 end
544 end
544 end
545
545
546
546
547 # Copies +project+ and returns the new instance. This will not save
547 # Copies +project+ and returns the new instance. This will not save
548 # the copy
548 # the copy
549 def self.copy_from(project)
549 def self.copy_from(project)
550 begin
550 begin
551 project = project.is_a?(Project) ? project : Project.find(project)
551 project = project.is_a?(Project) ? project : Project.find(project)
552 if project
552 if project
553 # clear unique attributes
553 # clear unique attributes
554 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
554 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
555 copy = Project.new(attributes)
555 copy = Project.new(attributes)
556 copy.enabled_modules = project.enabled_modules
556 copy.enabled_modules = project.enabled_modules
557 copy.trackers = project.trackers
557 copy.trackers = project.trackers
558 copy.custom_values = project.custom_values.collect {|v| v.clone}
558 copy.custom_values = project.custom_values.collect {|v| v.clone}
559 copy.issue_custom_fields = project.issue_custom_fields
559 copy.issue_custom_fields = project.issue_custom_fields
560 return copy
560 return copy
561 else
561 else
562 return nil
562 return nil
563 end
563 end
564 rescue ActiveRecord::RecordNotFound
564 rescue ActiveRecord::RecordNotFound
565 return nil
565 return nil
566 end
566 end
567 end
567 end
568
568
569 # Yields the given block for each project with its level in the tree
570 def self.project_tree(projects, &block)
571 ancestors = []
572 projects.sort_by(&:lft).each do |project|
573 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
574 ancestors.pop
575 end
576 yield project, ancestors.size
577 ancestors << project
578 end
579 end
580
569 private
581 private
570
582
571 # Destroys children before destroying self
583 # Destroys children before destroying self
572 def destroy_children
584 def destroy_children
573 children.each do |child|
585 children.each do |child|
574 child.destroy
586 child.destroy
575 end
587 end
576 end
588 end
577
589
578 # Copies wiki from +project+
590 # Copies wiki from +project+
579 def copy_wiki(project)
591 def copy_wiki(project)
580 # Check that the source project has a wiki first
592 # Check that the source project has a wiki first
581 unless project.wiki.nil?
593 unless project.wiki.nil?
582 self.wiki ||= Wiki.new
594 self.wiki ||= Wiki.new
583 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
595 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
584 wiki_pages_map = {}
596 wiki_pages_map = {}
585 project.wiki.pages.each do |page|
597 project.wiki.pages.each do |page|
586 # Skip pages without content
598 # Skip pages without content
587 next if page.content.nil?
599 next if page.content.nil?
588 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
600 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
589 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
601 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
590 new_wiki_page.content = new_wiki_content
602 new_wiki_page.content = new_wiki_content
591 wiki.pages << new_wiki_page
603 wiki.pages << new_wiki_page
592 wiki_pages_map[page.id] = new_wiki_page
604 wiki_pages_map[page.id] = new_wiki_page
593 end
605 end
594 wiki.save
606 wiki.save
595 # Reproduce page hierarchy
607 # Reproduce page hierarchy
596 project.wiki.pages.each do |page|
608 project.wiki.pages.each do |page|
597 if page.parent_id && wiki_pages_map[page.id]
609 if page.parent_id && wiki_pages_map[page.id]
598 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
610 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
599 wiki_pages_map[page.id].save
611 wiki_pages_map[page.id].save
600 end
612 end
601 end
613 end
602 end
614 end
603 end
615 end
604
616
605 # Copies versions from +project+
617 # Copies versions from +project+
606 def copy_versions(project)
618 def copy_versions(project)
607 project.versions.each do |version|
619 project.versions.each do |version|
608 new_version = Version.new
620 new_version = Version.new
609 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
621 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
610 self.versions << new_version
622 self.versions << new_version
611 end
623 end
612 end
624 end
613
625
614 # Copies issue categories from +project+
626 # Copies issue categories from +project+
615 def copy_issue_categories(project)
627 def copy_issue_categories(project)
616 project.issue_categories.each do |issue_category|
628 project.issue_categories.each do |issue_category|
617 new_issue_category = IssueCategory.new
629 new_issue_category = IssueCategory.new
618 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
630 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
619 self.issue_categories << new_issue_category
631 self.issue_categories << new_issue_category
620 end
632 end
621 end
633 end
622
634
623 # Copies issues from +project+
635 # Copies issues from +project+
624 def copy_issues(project)
636 def copy_issues(project)
625 # Stores the source issue id as a key and the copied issues as the
637 # Stores the source issue id as a key and the copied issues as the
626 # value. Used to map the two togeather for issue relations.
638 # value. Used to map the two togeather for issue relations.
627 issues_map = {}
639 issues_map = {}
628
640
629 # Get issues sorted by root_id, lft so that parent issues
641 # Get issues sorted by root_id, lft so that parent issues
630 # get copied before their children
642 # get copied before their children
631 project.issues.find(:all, :order => 'root_id, lft').each do |issue|
643 project.issues.find(:all, :order => 'root_id, lft').each do |issue|
632 new_issue = Issue.new
644 new_issue = Issue.new
633 new_issue.copy_from(issue)
645 new_issue.copy_from(issue)
634 new_issue.project = self
646 new_issue.project = self
635 # Reassign fixed_versions by name, since names are unique per
647 # Reassign fixed_versions by name, since names are unique per
636 # project and the versions for self are not yet saved
648 # project and the versions for self are not yet saved
637 if issue.fixed_version
649 if issue.fixed_version
638 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
650 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
639 end
651 end
640 # Reassign the category by name, since names are unique per
652 # Reassign the category by name, since names are unique per
641 # project and the categories for self are not yet saved
653 # project and the categories for self are not yet saved
642 if issue.category
654 if issue.category
643 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
655 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
644 end
656 end
645 # Parent issue
657 # Parent issue
646 if issue.parent_id
658 if issue.parent_id
647 if copied_parent = issues_map[issue.parent_id]
659 if copied_parent = issues_map[issue.parent_id]
648 new_issue.parent_issue_id = copied_parent.id
660 new_issue.parent_issue_id = copied_parent.id
649 end
661 end
650 end
662 end
651
663
652 self.issues << new_issue
664 self.issues << new_issue
653 issues_map[issue.id] = new_issue
665 issues_map[issue.id] = new_issue
654 end
666 end
655
667
656 # Relations after in case issues related each other
668 # Relations after in case issues related each other
657 project.issues.each do |issue|
669 project.issues.each do |issue|
658 new_issue = issues_map[issue.id]
670 new_issue = issues_map[issue.id]
659
671
660 # Relations
672 # Relations
661 issue.relations_from.each do |source_relation|
673 issue.relations_from.each do |source_relation|
662 new_issue_relation = IssueRelation.new
674 new_issue_relation = IssueRelation.new
663 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
675 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
664 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
676 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
665 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
677 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
666 new_issue_relation.issue_to = source_relation.issue_to
678 new_issue_relation.issue_to = source_relation.issue_to
667 end
679 end
668 new_issue.relations_from << new_issue_relation
680 new_issue.relations_from << new_issue_relation
669 end
681 end
670
682
671 issue.relations_to.each do |source_relation|
683 issue.relations_to.each do |source_relation|
672 new_issue_relation = IssueRelation.new
684 new_issue_relation = IssueRelation.new
673 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
685 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
674 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
686 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
675 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
687 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
676 new_issue_relation.issue_from = source_relation.issue_from
688 new_issue_relation.issue_from = source_relation.issue_from
677 end
689 end
678 new_issue.relations_to << new_issue_relation
690 new_issue.relations_to << new_issue_relation
679 end
691 end
680 end
692 end
681 end
693 end
682
694
683 # Copies members from +project+
695 # Copies members from +project+
684 def copy_members(project)
696 def copy_members(project)
685 project.memberships.each do |member|
697 project.memberships.each do |member|
686 new_member = Member.new
698 new_member = Member.new
687 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
699 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
688 # only copy non inherited roles
700 # only copy non inherited roles
689 # inherited roles will be added when copying the group membership
701 # inherited roles will be added when copying the group membership
690 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
702 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
691 next if role_ids.empty?
703 next if role_ids.empty?
692 new_member.role_ids = role_ids
704 new_member.role_ids = role_ids
693 new_member.project = self
705 new_member.project = self
694 self.members << new_member
706 self.members << new_member
695 end
707 end
696 end
708 end
697
709
698 # Copies queries from +project+
710 # Copies queries from +project+
699 def copy_queries(project)
711 def copy_queries(project)
700 project.queries.each do |query|
712 project.queries.each do |query|
701 new_query = Query.new
713 new_query = Query.new
702 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
714 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
703 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
715 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
704 new_query.project = self
716 new_query.project = self
705 self.queries << new_query
717 self.queries << new_query
706 end
718 end
707 end
719 end
708
720
709 # Copies boards from +project+
721 # Copies boards from +project+
710 def copy_boards(project)
722 def copy_boards(project)
711 project.boards.each do |board|
723 project.boards.each do |board|
712 new_board = Board.new
724 new_board = Board.new
713 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
725 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
714 new_board.project = self
726 new_board.project = self
715 self.boards << new_board
727 self.boards << new_board
716 end
728 end
717 end
729 end
718
730
719 def allowed_permissions
731 def allowed_permissions
720 @allowed_permissions ||= begin
732 @allowed_permissions ||= begin
721 module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
733 module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
722 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
734 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
723 end
735 end
724 end
736 end
725
737
726 def allowed_actions
738 def allowed_actions
727 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
739 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
728 end
740 end
729
741
730 # Returns all the active Systemwide and project specific activities
742 # Returns all the active Systemwide and project specific activities
731 def active_activities
743 def active_activities
732 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
744 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
733
745
734 if overridden_activity_ids.empty?
746 if overridden_activity_ids.empty?
735 return TimeEntryActivity.shared.active
747 return TimeEntryActivity.shared.active
736 else
748 else
737 return system_activities_and_project_overrides
749 return system_activities_and_project_overrides
738 end
750 end
739 end
751 end
740
752
741 # Returns all the Systemwide and project specific activities
753 # Returns all the Systemwide and project specific activities
742 # (inactive and active)
754 # (inactive and active)
743 def all_activities
755 def all_activities
744 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
756 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
745
757
746 if overridden_activity_ids.empty?
758 if overridden_activity_ids.empty?
747 return TimeEntryActivity.shared
759 return TimeEntryActivity.shared
748 else
760 else
749 return system_activities_and_project_overrides(true)
761 return system_activities_and_project_overrides(true)
750 end
762 end
751 end
763 end
752
764
753 # Returns the systemwide active activities merged with the project specific overrides
765 # Returns the systemwide active activities merged with the project specific overrides
754 def system_activities_and_project_overrides(include_inactive=false)
766 def system_activities_and_project_overrides(include_inactive=false)
755 if include_inactive
767 if include_inactive
756 return TimeEntryActivity.shared.
768 return TimeEntryActivity.shared.
757 find(:all,
769 find(:all,
758 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
770 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
759 self.time_entry_activities
771 self.time_entry_activities
760 else
772 else
761 return TimeEntryActivity.shared.active.
773 return TimeEntryActivity.shared.active.
762 find(:all,
774 find(:all,
763 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
775 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
764 self.time_entry_activities.active
776 self.time_entry_activities.active
765 end
777 end
766 end
778 end
767
779
768 # Archives subprojects recursively
780 # Archives subprojects recursively
769 def archive!
781 def archive!
770 children.each do |subproject|
782 children.each do |subproject|
771 subproject.send :archive!
783 subproject.send :archive!
772 end
784 end
773 update_attribute :status, STATUS_ARCHIVED
785 update_attribute :status, STATUS_ARCHIVED
774 end
786 end
775 end
787 end
General Comments 0
You need to be logged in to leave comments. Login now