##// END OF EJS Templates
Adds visible scope to redmine links queries....
Jean-Philippe Lang -
r4639:b20210e83c42
parent child
Show More
@@ -1,930 +1,931
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2010 Jean-Philippe Lang
2 # Copyright (C) 2006-2010 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 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
107 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
108
108
109 link_to(text, {:controller => 'repositories', :action => 'revision', :id => project, :rev => rev},
109 link_to(text, {:controller => 'repositories', :action => 'revision', :id => project, :rev => rev},
110 :title => l(:label_revision_id, format_revision(revision)))
110 :title => l(:label_revision_id, format_revision(revision)))
111 end
111 end
112
112
113 # Generates a link to a project if active
113 # Generates a link to a project if active
114 # Examples:
114 # Examples:
115 #
115 #
116 # link_to_project(project) # => link to the specified project overview
116 # link_to_project(project) # => link to the specified project overview
117 # link_to_project(project, :action=>'settings') # => link to project settings
117 # link_to_project(project, :action=>'settings') # => link to project settings
118 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
118 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
119 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
119 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
120 #
120 #
121 def link_to_project(project, options={}, html_options = nil)
121 def link_to_project(project, options={}, html_options = nil)
122 if project.active?
122 if project.active?
123 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
123 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
124 link_to(h(project), url, html_options)
124 link_to(h(project), url, html_options)
125 else
125 else
126 h(project)
126 h(project)
127 end
127 end
128 end
128 end
129
129
130 def toggle_link(name, id, options={})
130 def toggle_link(name, id, options={})
131 onclick = "Element.toggle('#{id}'); "
131 onclick = "Element.toggle('#{id}'); "
132 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
132 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
133 onclick << "return false;"
133 onclick << "return false;"
134 link_to(name, "#", :onclick => onclick)
134 link_to(name, "#", :onclick => onclick)
135 end
135 end
136
136
137 def image_to_function(name, function, html_options = {})
137 def image_to_function(name, function, html_options = {})
138 html_options.symbolize_keys!
138 html_options.symbolize_keys!
139 tag(:input, html_options.merge({
139 tag(:input, html_options.merge({
140 :type => "image", :src => image_path(name),
140 :type => "image", :src => image_path(name),
141 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
141 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
142 }))
142 }))
143 end
143 end
144
144
145 def prompt_to_remote(name, text, param, url, html_options = {})
145 def prompt_to_remote(name, text, param, url, html_options = {})
146 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
146 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
147 link_to name, {}, html_options
147 link_to name, {}, html_options
148 end
148 end
149
149
150 def format_activity_title(text)
150 def format_activity_title(text)
151 h(truncate_single_line(text, :length => 100))
151 h(truncate_single_line(text, :length => 100))
152 end
152 end
153
153
154 def format_activity_day(date)
154 def format_activity_day(date)
155 date == Date.today ? l(:label_today).titleize : format_date(date)
155 date == Date.today ? l(:label_today).titleize : format_date(date)
156 end
156 end
157
157
158 def format_activity_description(text)
158 def format_activity_description(text)
159 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')).gsub(/[\r\n]+/, "<br />")
159 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')).gsub(/[\r\n]+/, "<br />")
160 end
160 end
161
161
162 def format_version_name(version)
162 def format_version_name(version)
163 if version.project == @project
163 if version.project == @project
164 h(version)
164 h(version)
165 else
165 else
166 h("#{version.project} - #{version}")
166 h("#{version.project} - #{version}")
167 end
167 end
168 end
168 end
169
169
170 def due_date_distance_in_words(date)
170 def due_date_distance_in_words(date)
171 if date
171 if date
172 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
172 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
173 end
173 end
174 end
174 end
175
175
176 def render_page_hierarchy(pages, node=nil)
176 def render_page_hierarchy(pages, node=nil)
177 content = ''
177 content = ''
178 if pages[node]
178 if pages[node]
179 content << "<ul class=\"pages-hierarchy\">\n"
179 content << "<ul class=\"pages-hierarchy\">\n"
180 pages[node].each do |page|
180 pages[node].each do |page|
181 content << "<li>"
181 content << "<li>"
182 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title},
182 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title},
183 :title => (page.respond_to?(:updated_on) ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
183 :title => (page.respond_to?(:updated_on) ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
184 content << "\n" + render_page_hierarchy(pages, page.id) if pages[page.id]
184 content << "\n" + render_page_hierarchy(pages, page.id) if pages[page.id]
185 content << "</li>\n"
185 content << "</li>\n"
186 end
186 end
187 content << "</ul>\n"
187 content << "</ul>\n"
188 end
188 end
189 content
189 content
190 end
190 end
191
191
192 # Renders flash messages
192 # Renders flash messages
193 def render_flash_messages
193 def render_flash_messages
194 s = ''
194 s = ''
195 flash.each do |k,v|
195 flash.each do |k,v|
196 s << content_tag('div', v, :class => "flash #{k}")
196 s << content_tag('div', v, :class => "flash #{k}")
197 end
197 end
198 s
198 s
199 end
199 end
200
200
201 # Renders tabs and their content
201 # Renders tabs and their content
202 def render_tabs(tabs)
202 def render_tabs(tabs)
203 if tabs.any?
203 if tabs.any?
204 render :partial => 'common/tabs', :locals => {:tabs => tabs}
204 render :partial => 'common/tabs', :locals => {:tabs => tabs}
205 else
205 else
206 content_tag 'p', l(:label_no_data), :class => "nodata"
206 content_tag 'p', l(:label_no_data), :class => "nodata"
207 end
207 end
208 end
208 end
209
209
210 # Renders the project quick-jump box
210 # Renders the project quick-jump box
211 def render_project_jump_box
211 def render_project_jump_box
212 # Retrieve them now to avoid a COUNT query
212 # Retrieve them now to avoid a COUNT query
213 projects = User.current.projects.all
213 projects = User.current.projects.all
214 if projects.any?
214 if projects.any?
215 s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
215 s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
216 "<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
216 "<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
217 '<option value="" disabled="disabled">---</option>'
217 '<option value="" disabled="disabled">---</option>'
218 s << project_tree_options_for_select(projects, :selected => @project) do |p|
218 s << project_tree_options_for_select(projects, :selected => @project) do |p|
219 { :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
219 { :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
220 end
220 end
221 s << '</select>'
221 s << '</select>'
222 s
222 s
223 end
223 end
224 end
224 end
225
225
226 def project_tree_options_for_select(projects, options = {})
226 def project_tree_options_for_select(projects, options = {})
227 s = ''
227 s = ''
228 project_tree(projects) do |project, level|
228 project_tree(projects) do |project, level|
229 name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ') : '')
229 name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ') : '')
230 tag_options = {:value => project.id}
230 tag_options = {:value => project.id}
231 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
231 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
232 tag_options[:selected] = 'selected'
232 tag_options[:selected] = 'selected'
233 else
233 else
234 tag_options[:selected] = nil
234 tag_options[:selected] = nil
235 end
235 end
236 tag_options.merge!(yield(project)) if block_given?
236 tag_options.merge!(yield(project)) if block_given?
237 s << content_tag('option', name_prefix + h(project), tag_options)
237 s << content_tag('option', name_prefix + h(project), tag_options)
238 end
238 end
239 s
239 s
240 end
240 end
241
241
242 # Yields the given block for each project with its level in the tree
242 # Yields the given block for each project with its level in the tree
243 #
243 #
244 # Wrapper for Project#project_tree
244 # Wrapper for Project#project_tree
245 def project_tree(projects, &block)
245 def project_tree(projects, &block)
246 Project.project_tree(projects, &block)
246 Project.project_tree(projects, &block)
247 end
247 end
248
248
249 def project_nested_ul(projects, &block)
249 def project_nested_ul(projects, &block)
250 s = ''
250 s = ''
251 if projects.any?
251 if projects.any?
252 ancestors = []
252 ancestors = []
253 projects.sort_by(&:lft).each do |project|
253 projects.sort_by(&:lft).each do |project|
254 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
254 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
255 s << "<ul>\n"
255 s << "<ul>\n"
256 else
256 else
257 ancestors.pop
257 ancestors.pop
258 s << "</li>"
258 s << "</li>"
259 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
259 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
260 ancestors.pop
260 ancestors.pop
261 s << "</ul></li>\n"
261 s << "</ul></li>\n"
262 end
262 end
263 end
263 end
264 s << "<li>"
264 s << "<li>"
265 s << yield(project).to_s
265 s << yield(project).to_s
266 ancestors << project
266 ancestors << project
267 end
267 end
268 s << ("</li></ul>\n" * ancestors.size)
268 s << ("</li></ul>\n" * ancestors.size)
269 end
269 end
270 s
270 s
271 end
271 end
272
272
273 def principals_check_box_tags(name, principals)
273 def principals_check_box_tags(name, principals)
274 s = ''
274 s = ''
275 principals.sort.each do |principal|
275 principals.sort.each do |principal|
276 s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
276 s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
277 end
277 end
278 s
278 s
279 end
279 end
280
280
281 # Truncates and returns the string as a single line
281 # Truncates and returns the string as a single line
282 def truncate_single_line(string, *args)
282 def truncate_single_line(string, *args)
283 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
283 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
284 end
284 end
285
285
286 # Truncates at line break after 250 characters or options[:length]
286 # Truncates at line break after 250 characters or options[:length]
287 def truncate_lines(string, options={})
287 def truncate_lines(string, options={})
288 length = options[:length] || 250
288 length = options[:length] || 250
289 if string.to_s =~ /\A(.{#{length}}.*?)$/m
289 if string.to_s =~ /\A(.{#{length}}.*?)$/m
290 "#{$1}..."
290 "#{$1}..."
291 else
291 else
292 string
292 string
293 end
293 end
294 end
294 end
295
295
296 def html_hours(text)
296 def html_hours(text)
297 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>')
297 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>')
298 end
298 end
299
299
300 def authoring(created, author, options={})
300 def authoring(created, author, options={})
301 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created))
301 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created))
302 end
302 end
303
303
304 def time_tag(time)
304 def time_tag(time)
305 text = distance_of_time_in_words(Time.now, time)
305 text = distance_of_time_in_words(Time.now, time)
306 if @project
306 if @project
307 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => time.to_date}, :title => format_time(time))
307 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => time.to_date}, :title => format_time(time))
308 else
308 else
309 content_tag('acronym', text, :title => format_time(time))
309 content_tag('acronym', text, :title => format_time(time))
310 end
310 end
311 end
311 end
312
312
313 def syntax_highlight(name, content)
313 def syntax_highlight(name, content)
314 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
314 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
315 end
315 end
316
316
317 def to_path_param(path)
317 def to_path_param(path)
318 path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
318 path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
319 end
319 end
320
320
321 def pagination_links_full(paginator, count=nil, options={})
321 def pagination_links_full(paginator, count=nil, options={})
322 page_param = options.delete(:page_param) || :page
322 page_param = options.delete(:page_param) || :page
323 per_page_links = options.delete(:per_page_links)
323 per_page_links = options.delete(:per_page_links)
324 url_param = params.dup
324 url_param = params.dup
325 # don't reuse query params if filters are present
325 # don't reuse query params if filters are present
326 url_param.merge!(:fields => nil, :values => nil, :operators => nil) if url_param.delete(:set_filter)
326 url_param.merge!(:fields => nil, :values => nil, :operators => nil) if url_param.delete(:set_filter)
327
327
328 html = ''
328 html = ''
329 if paginator.current.previous
329 if paginator.current.previous
330 html << link_to_remote_content_update('&#171; ' + l(:label_previous), url_param.merge(page_param => paginator.current.previous)) + ' '
330 html << link_to_remote_content_update('&#171; ' + l(:label_previous), url_param.merge(page_param => paginator.current.previous)) + ' '
331 end
331 end
332
332
333 html << (pagination_links_each(paginator, options) do |n|
333 html << (pagination_links_each(paginator, options) do |n|
334 link_to_remote_content_update(n.to_s, url_param.merge(page_param => n))
334 link_to_remote_content_update(n.to_s, url_param.merge(page_param => n))
335 end || '')
335 end || '')
336
336
337 if paginator.current.next
337 if paginator.current.next
338 html << ' ' + link_to_remote_content_update((l(:label_next) + ' &#187;'), url_param.merge(page_param => paginator.current.next))
338 html << ' ' + link_to_remote_content_update((l(:label_next) + ' &#187;'), url_param.merge(page_param => paginator.current.next))
339 end
339 end
340
340
341 unless count.nil?
341 unless count.nil?
342 html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
342 html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
343 if per_page_links != false && links = per_page_links(paginator.items_per_page)
343 if per_page_links != false && links = per_page_links(paginator.items_per_page)
344 html << " | #{links}"
344 html << " | #{links}"
345 end
345 end
346 end
346 end
347
347
348 html
348 html
349 end
349 end
350
350
351 def per_page_links(selected=nil)
351 def per_page_links(selected=nil)
352 url_param = params.dup
352 url_param = params.dup
353 url_param.clear if url_param.has_key?(:set_filter)
353 url_param.clear if url_param.has_key?(:set_filter)
354
354
355 links = Setting.per_page_options_array.collect do |n|
355 links = Setting.per_page_options_array.collect do |n|
356 n == selected ? n : link_to_remote(n, {:update => "content",
356 n == selected ? n : link_to_remote(n, {:update => "content",
357 :url => params.dup.merge(:per_page => n),
357 :url => params.dup.merge(:per_page => n),
358 :method => :get},
358 :method => :get},
359 {:href => url_for(url_param.merge(:per_page => n))})
359 {:href => url_for(url_param.merge(:per_page => n))})
360 end
360 end
361 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
361 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
362 end
362 end
363
363
364 def reorder_links(name, url)
364 def reorder_links(name, url)
365 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)), url.merge({"#{name}[move_to]" => 'highest'}), :method => :post, :title => l(:label_sort_highest)) +
365 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)), url.merge({"#{name}[move_to]" => 'highest'}), :method => :post, :title => l(:label_sort_highest)) +
366 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)), url.merge({"#{name}[move_to]" => 'higher'}), :method => :post, :title => l(:label_sort_higher)) +
366 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)), url.merge({"#{name}[move_to]" => 'higher'}), :method => :post, :title => l(:label_sort_higher)) +
367 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)), url.merge({"#{name}[move_to]" => 'lower'}), :method => :post, :title => l(:label_sort_lower)) +
367 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)), url.merge({"#{name}[move_to]" => 'lower'}), :method => :post, :title => l(:label_sort_lower)) +
368 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), url.merge({"#{name}[move_to]" => 'lowest'}), :method => :post, :title => l(:label_sort_lowest))
368 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), url.merge({"#{name}[move_to]" => 'lowest'}), :method => :post, :title => l(:label_sort_lowest))
369 end
369 end
370
370
371 def breadcrumb(*args)
371 def breadcrumb(*args)
372 elements = args.flatten
372 elements = args.flatten
373 elements.any? ? content_tag('p', args.join(' &#187; ') + ' &#187; ', :class => 'breadcrumb') : nil
373 elements.any? ? content_tag('p', args.join(' &#187; ') + ' &#187; ', :class => 'breadcrumb') : nil
374 end
374 end
375
375
376 def other_formats_links(&block)
376 def other_formats_links(&block)
377 concat('<p class="other-formats">' + l(:label_export_to))
377 concat('<p class="other-formats">' + l(:label_export_to))
378 yield Redmine::Views::OtherFormatsBuilder.new(self)
378 yield Redmine::Views::OtherFormatsBuilder.new(self)
379 concat('</p>')
379 concat('</p>')
380 end
380 end
381
381
382 def page_header_title
382 def page_header_title
383 if @project.nil? || @project.new_record?
383 if @project.nil? || @project.new_record?
384 h(Setting.app_title)
384 h(Setting.app_title)
385 else
385 else
386 b = []
386 b = []
387 ancestors = (@project.root? ? [] : @project.ancestors.visible)
387 ancestors = (@project.root? ? [] : @project.ancestors.visible)
388 if ancestors.any?
388 if ancestors.any?
389 root = ancestors.shift
389 root = ancestors.shift
390 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
390 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
391 if ancestors.size > 2
391 if ancestors.size > 2
392 b << '&#8230;'
392 b << '&#8230;'
393 ancestors = ancestors[-2, 2]
393 ancestors = ancestors[-2, 2]
394 end
394 end
395 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
395 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
396 end
396 end
397 b << h(@project)
397 b << h(@project)
398 b.join(' &#187; ')
398 b.join(' &#187; ')
399 end
399 end
400 end
400 end
401
401
402 def html_title(*args)
402 def html_title(*args)
403 if args.empty?
403 if args.empty?
404 title = []
404 title = []
405 title << @project.name if @project
405 title << @project.name if @project
406 title += @html_title if @html_title
406 title += @html_title if @html_title
407 title << Setting.app_title
407 title << Setting.app_title
408 title.select {|t| !t.blank? }.join(' - ')
408 title.select {|t| !t.blank? }.join(' - ')
409 else
409 else
410 @html_title ||= []
410 @html_title ||= []
411 @html_title += args
411 @html_title += args
412 end
412 end
413 end
413 end
414
414
415 # Returns the theme, controller name, and action as css classes for the
415 # Returns the theme, controller name, and action as css classes for the
416 # HTML body.
416 # HTML body.
417 def body_css_classes
417 def body_css_classes
418 css = []
418 css = []
419 if theme = Redmine::Themes.theme(Setting.ui_theme)
419 if theme = Redmine::Themes.theme(Setting.ui_theme)
420 css << 'theme-' + theme.name
420 css << 'theme-' + theme.name
421 end
421 end
422
422
423 css << 'controller-' + params[:controller]
423 css << 'controller-' + params[:controller]
424 css << 'action-' + params[:action]
424 css << 'action-' + params[:action]
425 css.join(' ')
425 css.join(' ')
426 end
426 end
427
427
428 def accesskey(s)
428 def accesskey(s)
429 Redmine::AccessKeys.key_for s
429 Redmine::AccessKeys.key_for s
430 end
430 end
431
431
432 # Formats text according to system settings.
432 # Formats text according to system settings.
433 # 2 ways to call this method:
433 # 2 ways to call this method:
434 # * with a String: textilizable(text, options)
434 # * with a String: textilizable(text, options)
435 # * with an object and one of its attribute: textilizable(issue, :description, options)
435 # * with an object and one of its attribute: textilizable(issue, :description, options)
436 def textilizable(*args)
436 def textilizable(*args)
437 options = args.last.is_a?(Hash) ? args.pop : {}
437 options = args.last.is_a?(Hash) ? args.pop : {}
438 case args.size
438 case args.size
439 when 1
439 when 1
440 obj = options[:object]
440 obj = options[:object]
441 text = args.shift
441 text = args.shift
442 when 2
442 when 2
443 obj = args.shift
443 obj = args.shift
444 attr = args.shift
444 attr = args.shift
445 text = obj.send(attr).to_s
445 text = obj.send(attr).to_s
446 else
446 else
447 raise ArgumentError, 'invalid arguments to textilizable'
447 raise ArgumentError, 'invalid arguments to textilizable'
448 end
448 end
449 return '' if text.blank?
449 return '' if text.blank?
450 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
450 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
451 only_path = options.delete(:only_path) == false ? false : true
451 only_path = options.delete(:only_path) == false ? false : true
452
452
453 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr) { |macro, args| exec_macro(macro, obj, args) }
453 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr) { |macro, args| exec_macro(macro, obj, args) }
454
454
455 @parsed_headings = []
455 @parsed_headings = []
456 text = parse_non_pre_blocks(text) do |text|
456 text = parse_non_pre_blocks(text) do |text|
457 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links, :parse_headings].each do |method_name|
457 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links, :parse_headings].each do |method_name|
458 send method_name, text, project, obj, attr, only_path, options
458 send method_name, text, project, obj, attr, only_path, options
459 end
459 end
460 end
460 end
461
461
462 if @parsed_headings.any?
462 if @parsed_headings.any?
463 replace_toc(text, @parsed_headings)
463 replace_toc(text, @parsed_headings)
464 end
464 end
465
465
466 text
466 text
467 end
467 end
468
468
469 def parse_non_pre_blocks(text)
469 def parse_non_pre_blocks(text)
470 s = StringScanner.new(text)
470 s = StringScanner.new(text)
471 tags = []
471 tags = []
472 parsed = ''
472 parsed = ''
473 while !s.eos?
473 while !s.eos?
474 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
474 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
475 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
475 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
476 if tags.empty?
476 if tags.empty?
477 yield text
477 yield text
478 end
478 end
479 parsed << text
479 parsed << text
480 if tag
480 if tag
481 if closing
481 if closing
482 if tags.last == tag.downcase
482 if tags.last == tag.downcase
483 tags.pop
483 tags.pop
484 end
484 end
485 else
485 else
486 tags << tag.downcase
486 tags << tag.downcase
487 end
487 end
488 parsed << full_tag
488 parsed << full_tag
489 end
489 end
490 end
490 end
491 # Close any non closing tags
491 # Close any non closing tags
492 while tag = tags.pop
492 while tag = tags.pop
493 parsed << "</#{tag}>"
493 parsed << "</#{tag}>"
494 end
494 end
495 parsed
495 parsed
496 end
496 end
497
497
498 def parse_inline_attachments(text, project, obj, attr, only_path, options)
498 def parse_inline_attachments(text, project, obj, attr, only_path, options)
499 # when using an image link, try to use an attachment, if possible
499 # when using an image link, try to use an attachment, if possible
500 if options[:attachments] || (obj && obj.respond_to?(:attachments))
500 if options[:attachments] || (obj && obj.respond_to?(:attachments))
501 attachments = nil
501 attachments = nil
502 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
502 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
503 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
503 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
504 attachments ||= (options[:attachments] || obj.attachments).sort_by(&:created_on).reverse
504 attachments ||= (options[:attachments] || obj.attachments).sort_by(&:created_on).reverse
505 # search for the picture in attachments
505 # search for the picture in attachments
506 if found = attachments.detect { |att| att.filename.downcase == filename }
506 if found = attachments.detect { |att| att.filename.downcase == filename }
507 image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found
507 image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found
508 desc = found.description.to_s.gsub('"', '')
508 desc = found.description.to_s.gsub('"', '')
509 if !desc.blank? && alttext.blank?
509 if !desc.blank? && alttext.blank?
510 alt = " title=\"#{desc}\" alt=\"#{desc}\""
510 alt = " title=\"#{desc}\" alt=\"#{desc}\""
511 end
511 end
512 "src=\"#{image_url}\"#{alt}"
512 "src=\"#{image_url}\"#{alt}"
513 else
513 else
514 m
514 m
515 end
515 end
516 end
516 end
517 end
517 end
518 end
518 end
519
519
520 # Wiki links
520 # Wiki links
521 #
521 #
522 # Examples:
522 # Examples:
523 # [[mypage]]
523 # [[mypage]]
524 # [[mypage|mytext]]
524 # [[mypage|mytext]]
525 # wiki links can refer other project wikis, using project name or identifier:
525 # wiki links can refer other project wikis, using project name or identifier:
526 # [[project:]] -> wiki starting page
526 # [[project:]] -> wiki starting page
527 # [[project:|mytext]]
527 # [[project:|mytext]]
528 # [[project:mypage]]
528 # [[project:mypage]]
529 # [[project:mypage|mytext]]
529 # [[project:mypage|mytext]]
530 def parse_wiki_links(text, project, obj, attr, only_path, options)
530 def parse_wiki_links(text, project, obj, attr, only_path, options)
531 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
531 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
532 link_project = project
532 link_project = project
533 esc, all, page, title = $1, $2, $3, $5
533 esc, all, page, title = $1, $2, $3, $5
534 if esc.nil?
534 if esc.nil?
535 if page =~ /^([^\:]+)\:(.*)$/
535 if page =~ /^([^\:]+)\:(.*)$/
536 link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
536 link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
537 page = $2
537 page = $2
538 title ||= $1 if page.blank?
538 title ||= $1 if page.blank?
539 end
539 end
540
540
541 if link_project && link_project.wiki
541 if link_project && link_project.wiki
542 # extract anchor
542 # extract anchor
543 anchor = nil
543 anchor = nil
544 if page =~ /^(.+?)\#(.+)$/
544 if page =~ /^(.+?)\#(.+)$/
545 page, anchor = $1, $2
545 page, anchor = $1, $2
546 end
546 end
547 # check if page exists
547 # check if page exists
548 wiki_page = link_project.wiki.find_page(page)
548 wiki_page = link_project.wiki.find_page(page)
549 url = case options[:wiki_links]
549 url = case options[:wiki_links]
550 when :local; "#{title}.html"
550 when :local; "#{title}.html"
551 when :anchor; "##{title}" # used for single-file wiki export
551 when :anchor; "##{title}" # used for single-file wiki export
552 else
552 else
553 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
553 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
554 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project, :id => wiki_page_id, :anchor => anchor)
554 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project, :id => wiki_page_id, :anchor => anchor)
555 end
555 end
556 link_to((title || page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
556 link_to((title || page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
557 else
557 else
558 # project or wiki doesn't exist
558 # project or wiki doesn't exist
559 all
559 all
560 end
560 end
561 else
561 else
562 all
562 all
563 end
563 end
564 end
564 end
565 end
565 end
566
566
567 # Redmine links
567 # Redmine links
568 #
568 #
569 # Examples:
569 # Examples:
570 # Issues:
570 # Issues:
571 # #52 -> Link to issue #52
571 # #52 -> Link to issue #52
572 # Changesets:
572 # Changesets:
573 # r52 -> Link to revision 52
573 # r52 -> Link to revision 52
574 # commit:a85130f -> Link to scmid starting with a85130f
574 # commit:a85130f -> Link to scmid starting with a85130f
575 # Documents:
575 # Documents:
576 # document#17 -> Link to document with id 17
576 # document#17 -> Link to document with id 17
577 # document:Greetings -> Link to the document with title "Greetings"
577 # document:Greetings -> Link to the document with title "Greetings"
578 # document:"Some document" -> Link to the document with title "Some document"
578 # document:"Some document" -> Link to the document with title "Some document"
579 # Versions:
579 # Versions:
580 # version#3 -> Link to version with id 3
580 # version#3 -> Link to version with id 3
581 # version:1.0.0 -> Link to version named "1.0.0"
581 # version:1.0.0 -> Link to version named "1.0.0"
582 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
582 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
583 # Attachments:
583 # Attachments:
584 # attachment:file.zip -> Link to the attachment of the current object named file.zip
584 # attachment:file.zip -> Link to the attachment of the current object named file.zip
585 # Source files:
585 # Source files:
586 # source:some/file -> Link to the file located at /some/file in the project's repository
586 # source:some/file -> Link to the file located at /some/file in the project's repository
587 # source:some/file@52 -> Link to the file's revision 52
587 # source:some/file@52 -> Link to the file's revision 52
588 # source:some/file#L120 -> Link to line 120 of the file
588 # source:some/file#L120 -> Link to line 120 of the file
589 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
589 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
590 # export:some/file -> Force the download of the file
590 # export:some/file -> Force the download of the file
591 # Forum messages:
591 # Forum messages:
592 # message#1218 -> Link to message with id 1218
592 # message#1218 -> Link to message with id 1218
593 #
593 #
594 # Links can refer other objects from other projects, using project identifier:
594 # Links can refer other objects from other projects, using project identifier:
595 # identifier:r52
595 # identifier:r52
596 # identifier:document:"Some document"
596 # identifier:document:"Some document"
597 # identifier:version:1.0.0
597 # identifier:version:1.0.0
598 # identifier:source:some/file
598 # identifier:source:some/file
599 def parse_redmine_links(text, project, obj, attr, only_path, options)
599 def parse_redmine_links(text, project, obj, attr, only_path, options)
600 text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-]+):)?(attachment|document|version|commit|source|export|message|project)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|,|\s|\]|<|$)}) do |m|
600 text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-]+):)?(attachment|document|version|commit|source|export|message|project)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|,|\s|\]|<|$)}) do |m|
601 leading, esc, project_prefix, project_identifier, prefix, sep, identifier = $1, $2, $3, $4, $5, $7 || $9, $8 || $10
601 leading, esc, project_prefix, project_identifier, prefix, sep, identifier = $1, $2, $3, $4, $5, $7 || $9, $8 || $10
602 link = nil
602 link = nil
603 if project_identifier
603 if project_identifier
604 project = Project.visible.find_by_identifier(project_identifier)
604 project = Project.visible.find_by_identifier(project_identifier)
605 end
605 end
606 if esc.nil?
606 if esc.nil?
607 if prefix.nil? && sep == 'r'
607 if prefix.nil? && sep == 'r'
608 if project && (changeset = project.changesets.find_by_revision(identifier))
608 # project.changesets.visible raises an SQL error because of a double join on repositories
609 if project && project.repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(project.repository.id, identifier))
609 link = link_to("#{project_prefix}r#{identifier}", {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
610 link = link_to("#{project_prefix}r#{identifier}", {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
610 :class => 'changeset',
611 :class => 'changeset',
611 :title => truncate_single_line(changeset.comments, :length => 100))
612 :title => truncate_single_line(changeset.comments, :length => 100))
612 end
613 end
613 elsif sep == '#'
614 elsif sep == '#'
614 oid = identifier.to_i
615 oid = identifier.to_i
615 case prefix
616 case prefix
616 when nil
617 when nil
617 if issue = Issue.visible.find_by_id(oid, :include => :status)
618 if issue = Issue.visible.find_by_id(oid, :include => :status)
618 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
619 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
619 :class => issue.css_classes,
620 :class => issue.css_classes,
620 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
621 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
621 end
622 end
622 when 'document'
623 when 'document'
623 if document = Document.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
624 if document = Document.visible.find_by_id(oid)
624 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
625 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
625 :class => 'document'
626 :class => 'document'
626 end
627 end
627 when 'version'
628 when 'version'
628 if version = Version.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
629 if version = Version.visible.find_by_id(oid)
629 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
630 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
630 :class => 'version'
631 :class => 'version'
631 end
632 end
632 when 'message'
633 when 'message'
633 if message = Message.find_by_id(oid, :include => [:parent, {:board => :project}], :conditions => Project.visible_by(User.current))
634 if message = Message.visible.find_by_id(oid, :include => :parent)
634 link = link_to h(truncate(message.subject, :length => 60)), {:only_path => only_path,
635 link = link_to h(truncate(message.subject, :length => 60)), {:only_path => only_path,
635 :controller => 'messages',
636 :controller => 'messages',
636 :action => 'show',
637 :action => 'show',
637 :board_id => message.board,
638 :board_id => message.board,
638 :id => message.root,
639 :id => message.root,
639 :anchor => (message.parent ? "message-#{message.id}" : nil)},
640 :anchor => (message.parent ? "message-#{message.id}" : nil)},
640 :class => 'message'
641 :class => 'message'
641 end
642 end
642 when 'project'
643 when 'project'
643 if p = Project.visible.find_by_id(oid)
644 if p = Project.visible.find_by_id(oid)
644 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
645 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
645 end
646 end
646 end
647 end
647 elsif sep == ':'
648 elsif sep == ':'
648 # removes the double quotes if any
649 # removes the double quotes if any
649 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
650 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
650 case prefix
651 case prefix
651 when 'document'
652 when 'document'
652 if project && document = project.documents.find_by_title(name)
653 if project && document = project.documents.visible.find_by_title(name)
653 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
654 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
654 :class => 'document'
655 :class => 'document'
655 end
656 end
656 when 'version'
657 when 'version'
657 if project && version = project.versions.find_by_name(name)
658 if project && version = project.versions.visible.find_by_name(name)
658 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
659 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
659 :class => 'version'
660 :class => 'version'
660 end
661 end
661 when 'commit'
662 when 'commit'
662 if project && (changeset = project.changesets.find(:first, :conditions => ["scmid LIKE ?", "#{name}%"]))
663 if project && project.repository && (changeset = Changeset.visible.find(:first, :conditions => ["repository_id = ? AND scmid LIKE ?", project.repository.id, "#{name}%"]))
663 link = link_to h("#{project_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.identifier},
664 link = link_to h("#{project_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.identifier},
664 :class => 'changeset',
665 :class => 'changeset',
665 :title => truncate_single_line(changeset.comments, :length => 100)
666 :title => truncate_single_line(changeset.comments, :length => 100)
666 end
667 end
667 when 'source', 'export'
668 when 'source', 'export'
668 if project && project.repository
669 if project && project.repository && User.current.allowed_to?(:browse_repository, project)
669 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
670 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
670 path, rev, anchor = $1, $3, $5
671 path, rev, anchor = $1, $3, $5
671 link = link_to h("#{project_prefix}#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project,
672 link = link_to h("#{project_prefix}#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project,
672 :path => to_path_param(path),
673 :path => to_path_param(path),
673 :rev => rev,
674 :rev => rev,
674 :anchor => anchor,
675 :anchor => anchor,
675 :format => (prefix == 'export' ? 'raw' : nil)},
676 :format => (prefix == 'export' ? 'raw' : nil)},
676 :class => (prefix == 'export' ? 'source download' : 'source')
677 :class => (prefix == 'export' ? 'source download' : 'source')
677 end
678 end
678 when 'attachment'
679 when 'attachment'
679 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
680 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
680 if attachments && attachment = attachments.detect {|a| a.filename == name }
681 if attachments && attachment = attachments.detect {|a| a.filename == name }
681 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
682 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
682 :class => 'attachment'
683 :class => 'attachment'
683 end
684 end
684 when 'project'
685 when 'project'
685 if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
686 if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
686 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
687 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
687 end
688 end
688 end
689 end
689 end
690 end
690 end
691 end
691 leading + (link || "#{project_prefix}#{prefix}#{sep}#{identifier}")
692 leading + (link || "#{project_prefix}#{prefix}#{sep}#{identifier}")
692 end
693 end
693 end
694 end
694
695
695 HEADING_RE = /<h(1|2|3|4)( [^>]+)?>(.+?)<\/h(1|2|3|4)>/i unless const_defined?(:HEADING_RE)
696 HEADING_RE = /<h(1|2|3|4)( [^>]+)?>(.+?)<\/h(1|2|3|4)>/i unless const_defined?(:HEADING_RE)
696
697
697 # Headings and TOC
698 # Headings and TOC
698 # Adds ids and links to headings unless options[:headings] is set to false
699 # Adds ids and links to headings unless options[:headings] is set to false
699 def parse_headings(text, project, obj, attr, only_path, options)
700 def parse_headings(text, project, obj, attr, only_path, options)
700 return if options[:headings] == false
701 return if options[:headings] == false
701
702
702 text.gsub!(HEADING_RE) do
703 text.gsub!(HEADING_RE) do
703 level, attrs, content = $1.to_i, $2, $3
704 level, attrs, content = $1.to_i, $2, $3
704 item = strip_tags(content).strip
705 item = strip_tags(content).strip
705 anchor = item.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
706 anchor = item.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
706 @parsed_headings << [level, anchor, item]
707 @parsed_headings << [level, anchor, item]
707 "<h#{level} #{attrs} id=\"#{anchor}\">#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
708 "<h#{level} #{attrs} id=\"#{anchor}\">#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
708 end
709 end
709 end
710 end
710
711
711 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
712 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
712
713
713 # Renders the TOC with given headings
714 # Renders the TOC with given headings
714 def replace_toc(text, headings)
715 def replace_toc(text, headings)
715 text.gsub!(TOC_RE) do
716 text.gsub!(TOC_RE) do
716 if headings.empty?
717 if headings.empty?
717 ''
718 ''
718 else
719 else
719 div_class = 'toc'
720 div_class = 'toc'
720 div_class << ' right' if $1 == '>'
721 div_class << ' right' if $1 == '>'
721 div_class << ' left' if $1 == '<'
722 div_class << ' left' if $1 == '<'
722 out = "<ul class=\"#{div_class}\"><li>"
723 out = "<ul class=\"#{div_class}\"><li>"
723 root = headings.map(&:first).min
724 root = headings.map(&:first).min
724 current = root
725 current = root
725 started = false
726 started = false
726 headings.each do |level, anchor, item|
727 headings.each do |level, anchor, item|
727 if level > current
728 if level > current
728 out << '<ul><li>' * (level - current)
729 out << '<ul><li>' * (level - current)
729 elsif level < current
730 elsif level < current
730 out << "</li></ul>\n" * (current - level) + "</li><li>"
731 out << "</li></ul>\n" * (current - level) + "</li><li>"
731 elsif started
732 elsif started
732 out << '</li><li>'
733 out << '</li><li>'
733 end
734 end
734 out << "<a href=\"##{anchor}\">#{item}</a>"
735 out << "<a href=\"##{anchor}\">#{item}</a>"
735 current = level
736 current = level
736 started = true
737 started = true
737 end
738 end
738 out << '</li></ul>' * (current - root)
739 out << '</li></ul>' * (current - root)
739 out << '</li></ul>'
740 out << '</li></ul>'
740 end
741 end
741 end
742 end
742 end
743 end
743
744
744 # Same as Rails' simple_format helper without using paragraphs
745 # Same as Rails' simple_format helper without using paragraphs
745 def simple_format_without_paragraph(text)
746 def simple_format_without_paragraph(text)
746 text.to_s.
747 text.to_s.
747 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
748 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
748 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
749 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
749 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
750 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
750 end
751 end
751
752
752 def lang_options_for_select(blank=true)
753 def lang_options_for_select(blank=true)
753 (blank ? [["(auto)", ""]] : []) +
754 (blank ? [["(auto)", ""]] : []) +
754 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
755 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
755 end
756 end
756
757
757 def label_tag_for(name, option_tags = nil, options = {})
758 def label_tag_for(name, option_tags = nil, options = {})
758 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
759 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
759 content_tag("label", label_text)
760 content_tag("label", label_text)
760 end
761 end
761
762
762 def labelled_tabular_form_for(name, object, options, &proc)
763 def labelled_tabular_form_for(name, object, options, &proc)
763 options[:html] ||= {}
764 options[:html] ||= {}
764 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
765 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
765 form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
766 form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
766 end
767 end
767
768
768 def back_url_hidden_field_tag
769 def back_url_hidden_field_tag
769 back_url = params[:back_url] || request.env['HTTP_REFERER']
770 back_url = params[:back_url] || request.env['HTTP_REFERER']
770 back_url = CGI.unescape(back_url.to_s)
771 back_url = CGI.unescape(back_url.to_s)
771 hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
772 hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
772 end
773 end
773
774
774 def check_all_links(form_name)
775 def check_all_links(form_name)
775 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
776 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
776 " | " +
777 " | " +
777 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
778 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
778 end
779 end
779
780
780 def progress_bar(pcts, options={})
781 def progress_bar(pcts, options={})
781 pcts = [pcts, pcts] unless pcts.is_a?(Array)
782 pcts = [pcts, pcts] unless pcts.is_a?(Array)
782 pcts = pcts.collect(&:round)
783 pcts = pcts.collect(&:round)
783 pcts[1] = pcts[1] - pcts[0]
784 pcts[1] = pcts[1] - pcts[0]
784 pcts << (100 - pcts[1] - pcts[0])
785 pcts << (100 - pcts[1] - pcts[0])
785 width = options[:width] || '100px;'
786 width = options[:width] || '100px;'
786 legend = options[:legend] || ''
787 legend = options[:legend] || ''
787 content_tag('table',
788 content_tag('table',
788 content_tag('tr',
789 content_tag('tr',
789 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : '') +
790 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : '') +
790 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : '') +
791 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : '') +
791 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : '')
792 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : '')
792 ), :class => 'progress', :style => "width: #{width};") +
793 ), :class => 'progress', :style => "width: #{width};") +
793 content_tag('p', legend, :class => 'pourcent')
794 content_tag('p', legend, :class => 'pourcent')
794 end
795 end
795
796
796 def checked_image(checked=true)
797 def checked_image(checked=true)
797 if checked
798 if checked
798 image_tag 'toggle_check.png'
799 image_tag 'toggle_check.png'
799 end
800 end
800 end
801 end
801
802
802 def context_menu(url)
803 def context_menu(url)
803 unless @context_menu_included
804 unless @context_menu_included
804 content_for :header_tags do
805 content_for :header_tags do
805 javascript_include_tag('context_menu') +
806 javascript_include_tag('context_menu') +
806 stylesheet_link_tag('context_menu')
807 stylesheet_link_tag('context_menu')
807 end
808 end
808 if l(:direction) == 'rtl'
809 if l(:direction) == 'rtl'
809 content_for :header_tags do
810 content_for :header_tags do
810 stylesheet_link_tag('context_menu_rtl')
811 stylesheet_link_tag('context_menu_rtl')
811 end
812 end
812 end
813 end
813 @context_menu_included = true
814 @context_menu_included = true
814 end
815 end
815 javascript_tag "new ContextMenu('#{ url_for(url) }')"
816 javascript_tag "new ContextMenu('#{ url_for(url) }')"
816 end
817 end
817
818
818 def context_menu_link(name, url, options={})
819 def context_menu_link(name, url, options={})
819 options[:class] ||= ''
820 options[:class] ||= ''
820 if options.delete(:selected)
821 if options.delete(:selected)
821 options[:class] << ' icon-checked disabled'
822 options[:class] << ' icon-checked disabled'
822 options[:disabled] = true
823 options[:disabled] = true
823 end
824 end
824 if options.delete(:disabled)
825 if options.delete(:disabled)
825 options.delete(:method)
826 options.delete(:method)
826 options.delete(:confirm)
827 options.delete(:confirm)
827 options.delete(:onclick)
828 options.delete(:onclick)
828 options[:class] << ' disabled'
829 options[:class] << ' disabled'
829 url = '#'
830 url = '#'
830 end
831 end
831 link_to name, url, options
832 link_to name, url, options
832 end
833 end
833
834
834 def calendar_for(field_id)
835 def calendar_for(field_id)
835 include_calendar_headers_tags
836 include_calendar_headers_tags
836 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
837 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
837 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
838 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
838 end
839 end
839
840
840 def include_calendar_headers_tags
841 def include_calendar_headers_tags
841 unless @calendar_headers_tags_included
842 unless @calendar_headers_tags_included
842 @calendar_headers_tags_included = true
843 @calendar_headers_tags_included = true
843 content_for :header_tags do
844 content_for :header_tags do
844 start_of_week = case Setting.start_of_week.to_i
845 start_of_week = case Setting.start_of_week.to_i
845 when 1
846 when 1
846 'Calendar._FD = 1;' # Monday
847 'Calendar._FD = 1;' # Monday
847 when 7
848 when 7
848 'Calendar._FD = 0;' # Sunday
849 'Calendar._FD = 0;' # Sunday
849 else
850 else
850 '' # use language
851 '' # use language
851 end
852 end
852
853
853 javascript_include_tag('calendar/calendar') +
854 javascript_include_tag('calendar/calendar') +
854 javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
855 javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
855 javascript_tag(start_of_week) +
856 javascript_tag(start_of_week) +
856 javascript_include_tag('calendar/calendar-setup') +
857 javascript_include_tag('calendar/calendar-setup') +
857 stylesheet_link_tag('calendar')
858 stylesheet_link_tag('calendar')
858 end
859 end
859 end
860 end
860 end
861 end
861
862
862 def content_for(name, content = nil, &block)
863 def content_for(name, content = nil, &block)
863 @has_content ||= {}
864 @has_content ||= {}
864 @has_content[name] = true
865 @has_content[name] = true
865 super(name, content, &block)
866 super(name, content, &block)
866 end
867 end
867
868
868 def has_content?(name)
869 def has_content?(name)
869 (@has_content && @has_content[name]) || false
870 (@has_content && @has_content[name]) || false
870 end
871 end
871
872
872 # Returns the avatar image tag for the given +user+ if avatars are enabled
873 # Returns the avatar image tag for the given +user+ if avatars are enabled
873 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
874 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
874 def avatar(user, options = { })
875 def avatar(user, options = { })
875 if Setting.gravatar_enabled?
876 if Setting.gravatar_enabled?
876 options.merge!({:ssl => (defined?(request) && request.ssl?), :default => Setting.gravatar_default})
877 options.merge!({:ssl => (defined?(request) && request.ssl?), :default => Setting.gravatar_default})
877 email = nil
878 email = nil
878 if user.respond_to?(:mail)
879 if user.respond_to?(:mail)
879 email = user.mail
880 email = user.mail
880 elsif user.to_s =~ %r{<(.+?)>}
881 elsif user.to_s =~ %r{<(.+?)>}
881 email = $1
882 email = $1
882 end
883 end
883 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
884 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
884 else
885 else
885 ''
886 ''
886 end
887 end
887 end
888 end
888
889
889 def favicon
890 def favicon
890 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />"
891 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />"
891 end
892 end
892
893
893 # Returns true if arg is expected in the API response
894 # Returns true if arg is expected in the API response
894 def include_in_api_response?(arg)
895 def include_in_api_response?(arg)
895 unless @included_in_api_response
896 unless @included_in_api_response
896 param = params[:include]
897 param = params[:include]
897 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
898 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
898 @included_in_api_response.collect!(&:strip)
899 @included_in_api_response.collect!(&:strip)
899 end
900 end
900 @included_in_api_response.include?(arg.to_s)
901 @included_in_api_response.include?(arg.to_s)
901 end
902 end
902
903
903 # Returns options or nil if nometa param or X-Redmine-Nometa header
904 # Returns options or nil if nometa param or X-Redmine-Nometa header
904 # was set in the request
905 # was set in the request
905 def api_meta(options)
906 def api_meta(options)
906 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
907 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
907 # compatibility mode for activeresource clients that raise
908 # compatibility mode for activeresource clients that raise
908 # an error when unserializing an array with attributes
909 # an error when unserializing an array with attributes
909 nil
910 nil
910 else
911 else
911 options
912 options
912 end
913 end
913 end
914 end
914
915
915 private
916 private
916
917
917 def wiki_helper
918 def wiki_helper
918 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
919 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
919 extend helper
920 extend helper
920 return self
921 return self
921 end
922 end
922
923
923 def link_to_remote_content_update(text, url_params)
924 def link_to_remote_content_update(text, url_params)
924 link_to_remote(text,
925 link_to_remote(text,
925 {:url => url_params, :method => :get, :update => 'content', :complete => 'window.scrollTo(0,0)'},
926 {:url => url_params, :method => :get, :update => 'content', :complete => 'window.scrollTo(0,0)'},
926 {:href => url_for(:params => url_params)}
927 {:href => url_for(:params => url_params)}
927 )
928 )
928 end
929 end
929
930
930 end
931 end
@@ -1,49 +1,52
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 Document < ActiveRecord::Base
18 class Document < ActiveRecord::Base
19 belongs_to :project
19 belongs_to :project
20 belongs_to :category, :class_name => "DocumentCategory", :foreign_key => "category_id"
20 belongs_to :category, :class_name => "DocumentCategory", :foreign_key => "category_id"
21 acts_as_attachable :delete_permission => :manage_documents
21 acts_as_attachable :delete_permission => :manage_documents
22
22
23 acts_as_searchable :columns => ['title', "#{table_name}.description"], :include => :project
23 acts_as_searchable :columns => ['title', "#{table_name}.description"], :include => :project
24 acts_as_event :title => Proc.new {|o| "#{l(:label_document)}: #{o.title}"},
24 acts_as_event :title => Proc.new {|o| "#{l(:label_document)}: #{o.title}"},
25 :author => Proc.new {|o| (a = o.attachments.find(:first, :order => "#{Attachment.table_name}.created_on ASC")) ? a.author : nil },
25 :author => Proc.new {|o| (a = o.attachments.find(:first, :order => "#{Attachment.table_name}.created_on ASC")) ? a.author : nil },
26 :url => Proc.new {|o| {:controller => 'documents', :action => 'show', :id => o.id}}
26 :url => Proc.new {|o| {:controller => 'documents', :action => 'show', :id => o.id}}
27 acts_as_activity_provider :find_options => {:include => :project}
27 acts_as_activity_provider :find_options => {:include => :project}
28
28
29 validates_presence_of :project, :title, :category
29 validates_presence_of :project, :title, :category
30 validates_length_of :title, :maximum => 60
30 validates_length_of :title, :maximum => 60
31
31
32 named_scope :visible, lambda {|*args| { :include => :project,
33 :conditions => Project.allowed_to_condition(args.first || User.current, :view_documents) } }
34
32 def visible?(user=User.current)
35 def visible?(user=User.current)
33 !user.nil? && user.allowed_to?(:view_documents, project)
36 !user.nil? && user.allowed_to?(:view_documents, project)
34 end
37 end
35
38
36 def after_initialize
39 def after_initialize
37 if new_record?
40 if new_record?
38 self.category ||= DocumentCategory.default
41 self.category ||= DocumentCategory.default
39 end
42 end
40 end
43 end
41
44
42 def updated_on
45 def updated_on
43 unless @updated_on
46 unless @updated_on
44 a = attachments.find(:first, :order => 'created_on DESC')
47 a = attachments.find(:first, :order => 'created_on DESC')
45 @updated_on = (a && a.created_on) || created_on
48 @updated_on = (a && a.created_on) || created_on
46 end
49 end
47 @updated_on
50 @updated_on
48 end
51 end
49 end
52 end
@@ -1,98 +1,101
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 class Message < ActiveRecord::Base
18 class Message < ActiveRecord::Base
19 belongs_to :board
19 belongs_to :board
20 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
20 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
21 acts_as_tree :counter_cache => :replies_count, :order => "#{Message.table_name}.created_on ASC"
21 acts_as_tree :counter_cache => :replies_count, :order => "#{Message.table_name}.created_on ASC"
22 acts_as_attachable
22 acts_as_attachable
23 belongs_to :last_reply, :class_name => 'Message', :foreign_key => 'last_reply_id'
23 belongs_to :last_reply, :class_name => 'Message', :foreign_key => 'last_reply_id'
24
24
25 acts_as_searchable :columns => ['subject', 'content'],
25 acts_as_searchable :columns => ['subject', 'content'],
26 :include => {:board => :project},
26 :include => {:board => :project},
27 :project_key => 'project_id',
27 :project_key => 'project_id',
28 :date_column => "#{table_name}.created_on"
28 :date_column => "#{table_name}.created_on"
29 acts_as_event :title => Proc.new {|o| "#{o.board.name}: #{o.subject}"},
29 acts_as_event :title => Proc.new {|o| "#{o.board.name}: #{o.subject}"},
30 :description => :content,
30 :description => :content,
31 :type => Proc.new {|o| o.parent_id.nil? ? 'message' : 'reply'},
31 :type => Proc.new {|o| o.parent_id.nil? ? 'message' : 'reply'},
32 :url => Proc.new {|o| {:controller => 'messages', :action => 'show', :board_id => o.board_id}.merge(o.parent_id.nil? ? {:id => o.id} :
32 :url => Proc.new {|o| {:controller => 'messages', :action => 'show', :board_id => o.board_id}.merge(o.parent_id.nil? ? {:id => o.id} :
33 {:id => o.parent_id, :r => o.id, :anchor => "message-#{o.id}"})}
33 {:id => o.parent_id, :r => o.id, :anchor => "message-#{o.id}"})}
34
34
35 acts_as_activity_provider :find_options => {:include => [{:board => :project}, :author]},
35 acts_as_activity_provider :find_options => {:include => [{:board => :project}, :author]},
36 :author_key => :author_id
36 :author_key => :author_id
37 acts_as_watchable
37 acts_as_watchable
38
38
39 attr_protected :locked, :sticky
39 attr_protected :locked, :sticky
40 validates_presence_of :board, :subject, :content
40 validates_presence_of :board, :subject, :content
41 validates_length_of :subject, :maximum => 255
41 validates_length_of :subject, :maximum => 255
42
42
43 after_create :add_author_as_watcher
43 after_create :add_author_as_watcher
44
44
45 named_scope :visible, lambda {|*args| { :include => {:board => :project},
46 :conditions => Project.allowed_to_condition(args.first || User.current, :view_messages) } }
47
45 def visible?(user=User.current)
48 def visible?(user=User.current)
46 !user.nil? && user.allowed_to?(:view_messages, project)
49 !user.nil? && user.allowed_to?(:view_messages, project)
47 end
50 end
48
51
49 def validate_on_create
52 def validate_on_create
50 # Can not reply to a locked topic
53 # Can not reply to a locked topic
51 errors.add_to_base 'Topic is locked' if root.locked? && self != root
54 errors.add_to_base 'Topic is locked' if root.locked? && self != root
52 end
55 end
53
56
54 def after_create
57 def after_create
55 if parent
58 if parent
56 parent.reload.update_attribute(:last_reply_id, self.id)
59 parent.reload.update_attribute(:last_reply_id, self.id)
57 end
60 end
58 board.reset_counters!
61 board.reset_counters!
59 end
62 end
60
63
61 def after_update
64 def after_update
62 if board_id_changed?
65 if board_id_changed?
63 Message.update_all("board_id = #{board_id}", ["id = ? OR parent_id = ?", root.id, root.id])
66 Message.update_all("board_id = #{board_id}", ["id = ? OR parent_id = ?", root.id, root.id])
64 Board.reset_counters!(board_id_was)
67 Board.reset_counters!(board_id_was)
65 Board.reset_counters!(board_id)
68 Board.reset_counters!(board_id)
66 end
69 end
67 end
70 end
68
71
69 def after_destroy
72 def after_destroy
70 board.reset_counters!
73 board.reset_counters!
71 end
74 end
72
75
73 def sticky=(arg)
76 def sticky=(arg)
74 write_attribute :sticky, (arg == true || arg.to_s == '1' ? 1 : 0)
77 write_attribute :sticky, (arg == true || arg.to_s == '1' ? 1 : 0)
75 end
78 end
76
79
77 def sticky?
80 def sticky?
78 sticky == 1
81 sticky == 1
79 end
82 end
80
83
81 def project
84 def project
82 board.project
85 board.project
83 end
86 end
84
87
85 def editable_by?(usr)
88 def editable_by?(usr)
86 usr && usr.logged? && (usr.allowed_to?(:edit_messages, project) || (self.author == usr && usr.allowed_to?(:edit_own_messages, project)))
89 usr && usr.logged? && (usr.allowed_to?(:edit_messages, project) || (self.author == usr && usr.allowed_to?(:edit_own_messages, project)))
87 end
90 end
88
91
89 def destroyable_by?(usr)
92 def destroyable_by?(usr)
90 usr && usr.logged? && (usr.allowed_to?(:delete_messages, project) || (self.author == usr && usr.allowed_to?(:delete_own_messages, project)))
93 usr && usr.logged? && (usr.allowed_to?(:delete_messages, project) || (self.author == usr && usr.allowed_to?(:delete_own_messages, project)))
91 end
94 end
92
95
93 private
96 private
94
97
95 def add_author_as_watcher
98 def add_author_as_watcher
96 Watcher.create(:watchable => self.root, :user => author)
99 Watcher.create(:watchable => self.root, :user => author)
97 end
100 end
98 end
101 end
General Comments 0
You need to be logged in to leave comments. Login now