##// END OF EJS Templates
move logic to use latest image file attachment to class method for common use (#3261)...
Toshi MARUYAMA -
r7788:564321b2d5b8
parent child
Show More
@@ -1,1055 +1,1055
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2011 Jean-Philippe Lang
4 # Copyright (C) 2006-2011 Jean-Philippe Lang
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
9 # of the License, or (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
20 require 'forwardable'
20 require 'forwardable'
21 require 'cgi'
21 require 'cgi'
22
22
23 module ApplicationHelper
23 module ApplicationHelper
24 include Redmine::WikiFormatting::Macros::Definitions
24 include Redmine::WikiFormatting::Macros::Definitions
25 include Redmine::I18n
25 include Redmine::I18n
26 include GravatarHelper::PublicMethods
26 include GravatarHelper::PublicMethods
27
27
28 extend Forwardable
28 extend Forwardable
29 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
29 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
30
30
31 # Return true if user is authorized for controller/action, otherwise false
31 # Return true if user is authorized for controller/action, otherwise false
32 def authorize_for(controller, action)
32 def authorize_for(controller, action)
33 User.current.allowed_to?({:controller => controller, :action => action}, @project)
33 User.current.allowed_to?({:controller => controller, :action => action}, @project)
34 end
34 end
35
35
36 # Display a link if user is authorized
36 # Display a link if user is authorized
37 #
37 #
38 # @param [String] name Anchor text (passed to link_to)
38 # @param [String] name Anchor text (passed to link_to)
39 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
39 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
40 # @param [optional, Hash] html_options Options passed to link_to
40 # @param [optional, Hash] html_options Options passed to link_to
41 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
41 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
42 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
42 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
43 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
43 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
44 end
44 end
45
45
46 # Display a link to remote if user is authorized
46 # Display a link to remote if user is authorized
47 def link_to_remote_if_authorized(name, options = {}, html_options = nil)
47 def link_to_remote_if_authorized(name, options = {}, html_options = nil)
48 url = options[:url] || {}
48 url = options[:url] || {}
49 link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action])
49 link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action])
50 end
50 end
51
51
52 # Displays a link to user's account page if active
52 # Displays a link to user's account page if active
53 def link_to_user(user, options={})
53 def link_to_user(user, options={})
54 if user.is_a?(User)
54 if user.is_a?(User)
55 name = h(user.name(options[:format]))
55 name = h(user.name(options[:format]))
56 if user.active?
56 if user.active?
57 link_to name, :controller => 'users', :action => 'show', :id => user
57 link_to name, :controller => 'users', :action => 'show', :id => user
58 else
58 else
59 name
59 name
60 end
60 end
61 else
61 else
62 h(user.to_s)
62 h(user.to_s)
63 end
63 end
64 end
64 end
65
65
66 # Displays a link to +issue+ with its subject.
66 # Displays a link to +issue+ with its subject.
67 # Examples:
67 # Examples:
68 #
68 #
69 # link_to_issue(issue) # => Defect #6: This is the subject
69 # link_to_issue(issue) # => Defect #6: This is the subject
70 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
70 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
71 # link_to_issue(issue, :subject => false) # => Defect #6
71 # link_to_issue(issue, :subject => false) # => Defect #6
72 # link_to_issue(issue, :project => true) # => Foo - Defect #6
72 # link_to_issue(issue, :project => true) # => Foo - Defect #6
73 #
73 #
74 def link_to_issue(issue, options={})
74 def link_to_issue(issue, options={})
75 title = nil
75 title = nil
76 subject = nil
76 subject = nil
77 if options[:subject] == false
77 if options[:subject] == false
78 title = truncate(issue.subject, :length => 60)
78 title = truncate(issue.subject, :length => 60)
79 else
79 else
80 subject = issue.subject
80 subject = issue.subject
81 if options[:truncate]
81 if options[:truncate]
82 subject = truncate(subject, :length => options[:truncate])
82 subject = truncate(subject, :length => options[:truncate])
83 end
83 end
84 end
84 end
85 s = link_to "#{h(issue.tracker)} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue},
85 s = link_to "#{h(issue.tracker)} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue},
86 :class => issue.css_classes,
86 :class => issue.css_classes,
87 :title => title
87 :title => title
88 s << ": #{h subject}" if subject
88 s << ": #{h subject}" if subject
89 s = "#{h issue.project} - " + s if options[:project]
89 s = "#{h issue.project} - " + s if options[:project]
90 s
90 s
91 end
91 end
92
92
93 # Generates a link to an attachment.
93 # Generates a link to an attachment.
94 # Options:
94 # Options:
95 # * :text - Link text (default to attachment filename)
95 # * :text - Link text (default to attachment filename)
96 # * :download - Force download (default: false)
96 # * :download - Force download (default: false)
97 def link_to_attachment(attachment, options={})
97 def link_to_attachment(attachment, options={})
98 text = options.delete(:text) || attachment.filename
98 text = options.delete(:text) || attachment.filename
99 action = options.delete(:download) ? 'download' : 'show'
99 action = options.delete(:download) ? 'download' : 'show'
100 link_to(h(text),
100 link_to(h(text),
101 {:controller => 'attachments', :action => action,
101 {:controller => 'attachments', :action => action,
102 :id => attachment, :filename => attachment.filename },
102 :id => attachment, :filename => attachment.filename },
103 options)
103 options)
104 end
104 end
105
105
106 # Generates a link to a SCM revision
106 # Generates a link to a SCM revision
107 # Options:
107 # Options:
108 # * :text - Link text (default to the formatted revision)
108 # * :text - Link text (default to the formatted revision)
109 def link_to_revision(revision, project, options={})
109 def link_to_revision(revision, project, options={})
110 text = options.delete(:text) || format_revision(revision)
110 text = options.delete(:text) || format_revision(revision)
111 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
111 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
112
112
113 link_to(h(text), {:controller => 'repositories', :action => 'revision', :id => project, :rev => rev},
113 link_to(h(text), {:controller => 'repositories', :action => 'revision', :id => project, :rev => rev},
114 :title => l(:label_revision_id, format_revision(revision)))
114 :title => l(:label_revision_id, format_revision(revision)))
115 end
115 end
116
116
117 # Generates a link to a message
117 # Generates a link to a message
118 def link_to_message(message, options={}, html_options = nil)
118 def link_to_message(message, options={}, html_options = nil)
119 link_to(
119 link_to(
120 h(truncate(message.subject, :length => 60)),
120 h(truncate(message.subject, :length => 60)),
121 { :controller => 'messages', :action => 'show',
121 { :controller => 'messages', :action => 'show',
122 :board_id => message.board_id,
122 :board_id => message.board_id,
123 :id => message.root,
123 :id => message.root,
124 :r => (message.parent_id && message.id),
124 :r => (message.parent_id && message.id),
125 :anchor => (message.parent_id ? "message-#{message.id}" : nil)
125 :anchor => (message.parent_id ? "message-#{message.id}" : nil)
126 }.merge(options),
126 }.merge(options),
127 html_options
127 html_options
128 )
128 )
129 end
129 end
130
130
131 # Generates a link to a project if active
131 # Generates a link to a project if active
132 # Examples:
132 # Examples:
133 #
133 #
134 # link_to_project(project) # => link to the specified project overview
134 # link_to_project(project) # => link to the specified project overview
135 # link_to_project(project, :action=>'settings') # => link to project settings
135 # link_to_project(project, :action=>'settings') # => link to project settings
136 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
136 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
137 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
137 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
138 #
138 #
139 def link_to_project(project, options={}, html_options = nil)
139 def link_to_project(project, options={}, html_options = nil)
140 if project.active?
140 if project.active?
141 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
141 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
142 link_to(h(project), url, html_options)
142 link_to(h(project), url, html_options)
143 else
143 else
144 h(project)
144 h(project)
145 end
145 end
146 end
146 end
147
147
148 def toggle_link(name, id, options={})
148 def toggle_link(name, id, options={})
149 onclick = "Element.toggle('#{id}'); "
149 onclick = "Element.toggle('#{id}'); "
150 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
150 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
151 onclick << "return false;"
151 onclick << "return false;"
152 link_to(name, "#", :onclick => onclick)
152 link_to(name, "#", :onclick => onclick)
153 end
153 end
154
154
155 def image_to_function(name, function, html_options = {})
155 def image_to_function(name, function, html_options = {})
156 html_options.symbolize_keys!
156 html_options.symbolize_keys!
157 tag(:input, html_options.merge({
157 tag(:input, html_options.merge({
158 :type => "image", :src => image_path(name),
158 :type => "image", :src => image_path(name),
159 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
159 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
160 }))
160 }))
161 end
161 end
162
162
163 def prompt_to_remote(name, text, param, url, html_options = {})
163 def prompt_to_remote(name, text, param, url, html_options = {})
164 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
164 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
165 link_to name, {}, html_options
165 link_to name, {}, html_options
166 end
166 end
167
167
168 def format_activity_title(text)
168 def format_activity_title(text)
169 h(truncate_single_line(text, :length => 100))
169 h(truncate_single_line(text, :length => 100))
170 end
170 end
171
171
172 def format_activity_day(date)
172 def format_activity_day(date)
173 date == Date.today ? l(:label_today).titleize : format_date(date)
173 date == Date.today ? l(:label_today).titleize : format_date(date)
174 end
174 end
175
175
176 def format_activity_description(text)
176 def format_activity_description(text)
177 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')).gsub(/[\r\n]+/, "<br />")
177 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')).gsub(/[\r\n]+/, "<br />")
178 end
178 end
179
179
180 def format_version_name(version)
180 def format_version_name(version)
181 if version.project == @project
181 if version.project == @project
182 h(version)
182 h(version)
183 else
183 else
184 h("#{version.project} - #{version}")
184 h("#{version.project} - #{version}")
185 end
185 end
186 end
186 end
187
187
188 def due_date_distance_in_words(date)
188 def due_date_distance_in_words(date)
189 if date
189 if date
190 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
190 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
191 end
191 end
192 end
192 end
193
193
194 def render_page_hierarchy(pages, node=nil, options={})
194 def render_page_hierarchy(pages, node=nil, options={})
195 content = ''
195 content = ''
196 if pages[node]
196 if pages[node]
197 content << "<ul class=\"pages-hierarchy\">\n"
197 content << "<ul class=\"pages-hierarchy\">\n"
198 pages[node].each do |page|
198 pages[node].each do |page|
199 content << "<li>"
199 content << "<li>"
200 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title},
200 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title},
201 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
201 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
202 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
202 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
203 content << "</li>\n"
203 content << "</li>\n"
204 end
204 end
205 content << "</ul>\n"
205 content << "</ul>\n"
206 end
206 end
207 content.html_safe
207 content.html_safe
208 end
208 end
209
209
210 # Renders flash messages
210 # Renders flash messages
211 def render_flash_messages
211 def render_flash_messages
212 s = ''
212 s = ''
213 flash.each do |k,v|
213 flash.each do |k,v|
214 s << content_tag('div', v, :class => "flash #{k}")
214 s << content_tag('div', v, :class => "flash #{k}")
215 end
215 end
216 s.html_safe
216 s.html_safe
217 end
217 end
218
218
219 # Renders tabs and their content
219 # Renders tabs and their content
220 def render_tabs(tabs)
220 def render_tabs(tabs)
221 if tabs.any?
221 if tabs.any?
222 render :partial => 'common/tabs', :locals => {:tabs => tabs}
222 render :partial => 'common/tabs', :locals => {:tabs => tabs}
223 else
223 else
224 content_tag 'p', l(:label_no_data), :class => "nodata"
224 content_tag 'p', l(:label_no_data), :class => "nodata"
225 end
225 end
226 end
226 end
227
227
228 # Renders the project quick-jump box
228 # Renders the project quick-jump box
229 def render_project_jump_box
229 def render_project_jump_box
230 return unless User.current.logged?
230 return unless User.current.logged?
231 projects = User.current.memberships.collect(&:project).compact.uniq
231 projects = User.current.memberships.collect(&:project).compact.uniq
232 if projects.any?
232 if projects.any?
233 s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
233 s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
234 "<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
234 "<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
235 '<option value="" disabled="disabled">---</option>'
235 '<option value="" disabled="disabled">---</option>'
236 s << project_tree_options_for_select(projects, :selected => @project) do |p|
236 s << project_tree_options_for_select(projects, :selected => @project) do |p|
237 { :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
237 { :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
238 end
238 end
239 s << '</select>'
239 s << '</select>'
240 s.html_safe
240 s.html_safe
241 end
241 end
242 end
242 end
243
243
244 def project_tree_options_for_select(projects, options = {})
244 def project_tree_options_for_select(projects, options = {})
245 s = ''
245 s = ''
246 project_tree(projects) do |project, level|
246 project_tree(projects) do |project, level|
247 name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ') : '')
247 name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ') : '')
248 tag_options = {:value => project.id}
248 tag_options = {:value => project.id}
249 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
249 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
250 tag_options[:selected] = 'selected'
250 tag_options[:selected] = 'selected'
251 else
251 else
252 tag_options[:selected] = nil
252 tag_options[:selected] = nil
253 end
253 end
254 tag_options.merge!(yield(project)) if block_given?
254 tag_options.merge!(yield(project)) if block_given?
255 s << content_tag('option', name_prefix + h(project), tag_options)
255 s << content_tag('option', name_prefix + h(project), tag_options)
256 end
256 end
257 s.html_safe
257 s.html_safe
258 end
258 end
259
259
260 # Yields the given block for each project with its level in the tree
260 # Yields the given block for each project with its level in the tree
261 #
261 #
262 # Wrapper for Project#project_tree
262 # Wrapper for Project#project_tree
263 def project_tree(projects, &block)
263 def project_tree(projects, &block)
264 Project.project_tree(projects, &block)
264 Project.project_tree(projects, &block)
265 end
265 end
266
266
267 def project_nested_ul(projects, &block)
267 def project_nested_ul(projects, &block)
268 s = ''
268 s = ''
269 if projects.any?
269 if projects.any?
270 ancestors = []
270 ancestors = []
271 projects.sort_by(&:lft).each do |project|
271 projects.sort_by(&:lft).each do |project|
272 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
272 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
273 s << "<ul>\n"
273 s << "<ul>\n"
274 else
274 else
275 ancestors.pop
275 ancestors.pop
276 s << "</li>"
276 s << "</li>"
277 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
277 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
278 ancestors.pop
278 ancestors.pop
279 s << "</ul></li>\n"
279 s << "</ul></li>\n"
280 end
280 end
281 end
281 end
282 s << "<li>"
282 s << "<li>"
283 s << yield(project).to_s
283 s << yield(project).to_s
284 ancestors << project
284 ancestors << project
285 end
285 end
286 s << ("</li></ul>\n" * ancestors.size)
286 s << ("</li></ul>\n" * ancestors.size)
287 end
287 end
288 s.html_safe
288 s.html_safe
289 end
289 end
290
290
291 def principals_check_box_tags(name, principals)
291 def principals_check_box_tags(name, principals)
292 s = ''
292 s = ''
293 principals.sort.each do |principal|
293 principals.sort.each do |principal|
294 s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
294 s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
295 end
295 end
296 s.html_safe
296 s.html_safe
297 end
297 end
298
298
299 # Returns a string for users/groups option tags
299 # Returns a string for users/groups option tags
300 def principals_options_for_select(collection, selected=nil)
300 def principals_options_for_select(collection, selected=nil)
301 s = ''
301 s = ''
302 groups = ''
302 groups = ''
303 collection.sort.each do |element|
303 collection.sort.each do |element|
304 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected)
304 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected)
305 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
305 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
306 end
306 end
307 unless groups.empty?
307 unless groups.empty?
308 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
308 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
309 end
309 end
310 s
310 s
311 end
311 end
312
312
313 # Truncates and returns the string as a single line
313 # Truncates and returns the string as a single line
314 def truncate_single_line(string, *args)
314 def truncate_single_line(string, *args)
315 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
315 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
316 end
316 end
317
317
318 # Truncates at line break after 250 characters or options[:length]
318 # Truncates at line break after 250 characters or options[:length]
319 def truncate_lines(string, options={})
319 def truncate_lines(string, options={})
320 length = options[:length] || 250
320 length = options[:length] || 250
321 if string.to_s =~ /\A(.{#{length}}.*?)$/m
321 if string.to_s =~ /\A(.{#{length}}.*?)$/m
322 "#{$1}..."
322 "#{$1}..."
323 else
323 else
324 string
324 string
325 end
325 end
326 end
326 end
327
327
328 def html_hours(text)
328 def html_hours(text)
329 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
329 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
330 end
330 end
331
331
332 def authoring(created, author, options={})
332 def authoring(created, author, options={})
333 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
333 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
334 end
334 end
335
335
336 def time_tag(time)
336 def time_tag(time)
337 text = distance_of_time_in_words(Time.now, time)
337 text = distance_of_time_in_words(Time.now, time)
338 if @project
338 if @project
339 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => time.to_date}, :title => format_time(time))
339 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => time.to_date}, :title => format_time(time))
340 else
340 else
341 content_tag('acronym', text, :title => format_time(time))
341 content_tag('acronym', text, :title => format_time(time))
342 end
342 end
343 end
343 end
344
344
345 def syntax_highlight(name, content)
345 def syntax_highlight(name, content)
346 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
346 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
347 end
347 end
348
348
349 def to_path_param(path)
349 def to_path_param(path)
350 path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
350 path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
351 end
351 end
352
352
353 def pagination_links_full(paginator, count=nil, options={})
353 def pagination_links_full(paginator, count=nil, options={})
354 page_param = options.delete(:page_param) || :page
354 page_param = options.delete(:page_param) || :page
355 per_page_links = options.delete(:per_page_links)
355 per_page_links = options.delete(:per_page_links)
356 url_param = params.dup
356 url_param = params.dup
357
357
358 html = ''
358 html = ''
359 if paginator.current.previous
359 if paginator.current.previous
360 # \xc2\xab(utf-8) = &#171;
360 # \xc2\xab(utf-8) = &#171;
361 html << link_to_content_update(
361 html << link_to_content_update(
362 "\xc2\xab " + l(:label_previous),
362 "\xc2\xab " + l(:label_previous),
363 url_param.merge(page_param => paginator.current.previous)) + ' '
363 url_param.merge(page_param => paginator.current.previous)) + ' '
364 end
364 end
365
365
366 html << (pagination_links_each(paginator, options) do |n|
366 html << (pagination_links_each(paginator, options) do |n|
367 link_to_content_update(n.to_s, url_param.merge(page_param => n))
367 link_to_content_update(n.to_s, url_param.merge(page_param => n))
368 end || '')
368 end || '')
369
369
370 if paginator.current.next
370 if paginator.current.next
371 # \xc2\xbb(utf-8) = &#187;
371 # \xc2\xbb(utf-8) = &#187;
372 html << ' ' + link_to_content_update(
372 html << ' ' + link_to_content_update(
373 (l(:label_next) + " \xc2\xbb"),
373 (l(:label_next) + " \xc2\xbb"),
374 url_param.merge(page_param => paginator.current.next))
374 url_param.merge(page_param => paginator.current.next))
375 end
375 end
376
376
377 unless count.nil?
377 unless count.nil?
378 html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
378 html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
379 if per_page_links != false && links = per_page_links(paginator.items_per_page)
379 if per_page_links != false && links = per_page_links(paginator.items_per_page)
380 html << " | #{links}"
380 html << " | #{links}"
381 end
381 end
382 end
382 end
383
383
384 html.html_safe
384 html.html_safe
385 end
385 end
386
386
387 def per_page_links(selected=nil)
387 def per_page_links(selected=nil)
388 links = Setting.per_page_options_array.collect do |n|
388 links = Setting.per_page_options_array.collect do |n|
389 n == selected ? n : link_to_content_update(n, params.merge(:per_page => n))
389 n == selected ? n : link_to_content_update(n, params.merge(:per_page => n))
390 end
390 end
391 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
391 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
392 end
392 end
393
393
394 def reorder_links(name, url, method = :post)
394 def reorder_links(name, url, method = :post)
395 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
395 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
396 url.merge({"#{name}[move_to]" => 'highest'}),
396 url.merge({"#{name}[move_to]" => 'highest'}),
397 :method => method, :title => l(:label_sort_highest)) +
397 :method => method, :title => l(:label_sort_highest)) +
398 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
398 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
399 url.merge({"#{name}[move_to]" => 'higher'}),
399 url.merge({"#{name}[move_to]" => 'higher'}),
400 :method => method, :title => l(:label_sort_higher)) +
400 :method => method, :title => l(:label_sort_higher)) +
401 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
401 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
402 url.merge({"#{name}[move_to]" => 'lower'}),
402 url.merge({"#{name}[move_to]" => 'lower'}),
403 :method => method, :title => l(:label_sort_lower)) +
403 :method => method, :title => l(:label_sort_lower)) +
404 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
404 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
405 url.merge({"#{name}[move_to]" => 'lowest'}),
405 url.merge({"#{name}[move_to]" => 'lowest'}),
406 :method => method, :title => l(:label_sort_lowest))
406 :method => method, :title => l(:label_sort_lowest))
407 end
407 end
408
408
409 def breadcrumb(*args)
409 def breadcrumb(*args)
410 elements = args.flatten
410 elements = args.flatten
411 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
411 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
412 end
412 end
413
413
414 def other_formats_links(&block)
414 def other_formats_links(&block)
415 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
415 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
416 yield Redmine::Views::OtherFormatsBuilder.new(self)
416 yield Redmine::Views::OtherFormatsBuilder.new(self)
417 concat('</p>'.html_safe)
417 concat('</p>'.html_safe)
418 end
418 end
419
419
420 def page_header_title
420 def page_header_title
421 if @project.nil? || @project.new_record?
421 if @project.nil? || @project.new_record?
422 h(Setting.app_title)
422 h(Setting.app_title)
423 else
423 else
424 b = []
424 b = []
425 ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
425 ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
426 if ancestors.any?
426 if ancestors.any?
427 root = ancestors.shift
427 root = ancestors.shift
428 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
428 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
429 if ancestors.size > 2
429 if ancestors.size > 2
430 b << "\xe2\x80\xa6"
430 b << "\xe2\x80\xa6"
431 ancestors = ancestors[-2, 2]
431 ancestors = ancestors[-2, 2]
432 end
432 end
433 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
433 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
434 end
434 end
435 b << h(@project)
435 b << h(@project)
436 b.join(" \xc2\xbb ").html_safe
436 b.join(" \xc2\xbb ").html_safe
437 end
437 end
438 end
438 end
439
439
440 def html_title(*args)
440 def html_title(*args)
441 if args.empty?
441 if args.empty?
442 title = @html_title || []
442 title = @html_title || []
443 title << @project.name if @project
443 title << @project.name if @project
444 title << Setting.app_title unless Setting.app_title == title.last
444 title << Setting.app_title unless Setting.app_title == title.last
445 title.select {|t| !t.blank? }.join(' - ')
445 title.select {|t| !t.blank? }.join(' - ')
446 else
446 else
447 @html_title ||= []
447 @html_title ||= []
448 @html_title += args
448 @html_title += args
449 end
449 end
450 end
450 end
451
451
452 # Returns the theme, controller name, and action as css classes for the
452 # Returns the theme, controller name, and action as css classes for the
453 # HTML body.
453 # HTML body.
454 def body_css_classes
454 def body_css_classes
455 css = []
455 css = []
456 if theme = Redmine::Themes.theme(Setting.ui_theme)
456 if theme = Redmine::Themes.theme(Setting.ui_theme)
457 css << 'theme-' + theme.name
457 css << 'theme-' + theme.name
458 end
458 end
459
459
460 css << 'controller-' + params[:controller]
460 css << 'controller-' + params[:controller]
461 css << 'action-' + params[:action]
461 css << 'action-' + params[:action]
462 css.join(' ')
462 css.join(' ')
463 end
463 end
464
464
465 def accesskey(s)
465 def accesskey(s)
466 Redmine::AccessKeys.key_for s
466 Redmine::AccessKeys.key_for s
467 end
467 end
468
468
469 # Formats text according to system settings.
469 # Formats text according to system settings.
470 # 2 ways to call this method:
470 # 2 ways to call this method:
471 # * with a String: textilizable(text, options)
471 # * with a String: textilizable(text, options)
472 # * with an object and one of its attribute: textilizable(issue, :description, options)
472 # * with an object and one of its attribute: textilizable(issue, :description, options)
473 def textilizable(*args)
473 def textilizable(*args)
474 options = args.last.is_a?(Hash) ? args.pop : {}
474 options = args.last.is_a?(Hash) ? args.pop : {}
475 case args.size
475 case args.size
476 when 1
476 when 1
477 obj = options[:object]
477 obj = options[:object]
478 text = args.shift
478 text = args.shift
479 when 2
479 when 2
480 obj = args.shift
480 obj = args.shift
481 attr = args.shift
481 attr = args.shift
482 text = obj.send(attr).to_s
482 text = obj.send(attr).to_s
483 else
483 else
484 raise ArgumentError, 'invalid arguments to textilizable'
484 raise ArgumentError, 'invalid arguments to textilizable'
485 end
485 end
486 return '' if text.blank?
486 return '' if text.blank?
487 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
487 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
488 only_path = options.delete(:only_path) == false ? false : true
488 only_path = options.delete(:only_path) == false ? false : true
489
489
490 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
490 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
491
491
492 @parsed_headings = []
492 @parsed_headings = []
493 @current_section = 0 if options[:edit_section_links]
493 @current_section = 0 if options[:edit_section_links]
494 text = parse_non_pre_blocks(text) do |text|
494 text = parse_non_pre_blocks(text) do |text|
495 [:parse_sections, :parse_inline_attachments, :parse_wiki_links, :parse_redmine_links, :parse_macros, :parse_headings].each do |method_name|
495 [:parse_sections, :parse_inline_attachments, :parse_wiki_links, :parse_redmine_links, :parse_macros, :parse_headings].each do |method_name|
496 send method_name, text, project, obj, attr, only_path, options
496 send method_name, text, project, obj, attr, only_path, options
497 end
497 end
498 end
498 end
499
499
500 if @parsed_headings.any?
500 if @parsed_headings.any?
501 replace_toc(text, @parsed_headings)
501 replace_toc(text, @parsed_headings)
502 end
502 end
503
503
504 text
504 text
505 end
505 end
506
506
507 def parse_non_pre_blocks(text)
507 def parse_non_pre_blocks(text)
508 s = StringScanner.new(text)
508 s = StringScanner.new(text)
509 tags = []
509 tags = []
510 parsed = ''
510 parsed = ''
511 while !s.eos?
511 while !s.eos?
512 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
512 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
513 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
513 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
514 if tags.empty?
514 if tags.empty?
515 yield text
515 yield text
516 end
516 end
517 parsed << text
517 parsed << text
518 if tag
518 if tag
519 if closing
519 if closing
520 if tags.last == tag.downcase
520 if tags.last == tag.downcase
521 tags.pop
521 tags.pop
522 end
522 end
523 else
523 else
524 tags << tag.downcase
524 tags << tag.downcase
525 end
525 end
526 parsed << full_tag
526 parsed << full_tag
527 end
527 end
528 end
528 end
529 # Close any non closing tags
529 # Close any non closing tags
530 while tag = tags.pop
530 while tag = tags.pop
531 parsed << "</#{tag}>"
531 parsed << "</#{tag}>"
532 end
532 end
533 parsed.html_safe
533 parsed.html_safe
534 end
534 end
535
535
536 def parse_inline_attachments(text, project, obj, attr, only_path, options)
536 def parse_inline_attachments(text, project, obj, attr, only_path, options)
537 # when using an image link, try to use an attachment, if possible
537 # when using an image link, try to use an attachment, if possible
538 if options[:attachments] || (obj && obj.respond_to?(:attachments))
538 if options[:attachments] || (obj && obj.respond_to?(:attachments))
539 attachments = nil
539 attachments = options[:attachments] || obj.attachments
540 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
540 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
541 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
541 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
542 attachments ||= (options[:attachments] || obj.attachments).sort_by(&:created_on).reverse
543 # search for the picture in attachments
542 # search for the picture in attachments
544 if found = attachments.detect { |att| att.filename.downcase == filename }
543 if found = Attachment.latest_attach(attachments, filename)
545 image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found
544 image_url = url_for :only_path => only_path, :controller => 'attachments',
545 :action => 'download', :id => found
546 desc = found.description.to_s.gsub('"', '')
546 desc = found.description.to_s.gsub('"', '')
547 if !desc.blank? && alttext.blank?
547 if !desc.blank? && alttext.blank?
548 alt = " title=\"#{desc}\" alt=\"#{desc}\""
548 alt = " title=\"#{desc}\" alt=\"#{desc}\""
549 end
549 end
550 "src=\"#{image_url}\"#{alt}".html_safe
550 "src=\"#{image_url}\"#{alt}".html_safe
551 else
551 else
552 m.html_safe
552 m.html_safe
553 end
553 end
554 end
554 end
555 end
555 end
556 end
556 end
557
557
558 # Wiki links
558 # Wiki links
559 #
559 #
560 # Examples:
560 # Examples:
561 # [[mypage]]
561 # [[mypage]]
562 # [[mypage|mytext]]
562 # [[mypage|mytext]]
563 # wiki links can refer other project wikis, using project name or identifier:
563 # wiki links can refer other project wikis, using project name or identifier:
564 # [[project:]] -> wiki starting page
564 # [[project:]] -> wiki starting page
565 # [[project:|mytext]]
565 # [[project:|mytext]]
566 # [[project:mypage]]
566 # [[project:mypage]]
567 # [[project:mypage|mytext]]
567 # [[project:mypage|mytext]]
568 def parse_wiki_links(text, project, obj, attr, only_path, options)
568 def parse_wiki_links(text, project, obj, attr, only_path, options)
569 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
569 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
570 link_project = project
570 link_project = project
571 esc, all, page, title = $1, $2, $3, $5
571 esc, all, page, title = $1, $2, $3, $5
572 if esc.nil?
572 if esc.nil?
573 if page =~ /^([^\:]+)\:(.*)$/
573 if page =~ /^([^\:]+)\:(.*)$/
574 link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
574 link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
575 page = $2
575 page = $2
576 title ||= $1 if page.blank?
576 title ||= $1 if page.blank?
577 end
577 end
578
578
579 if link_project && link_project.wiki
579 if link_project && link_project.wiki
580 # extract anchor
580 # extract anchor
581 anchor = nil
581 anchor = nil
582 if page =~ /^(.+?)\#(.+)$/
582 if page =~ /^(.+?)\#(.+)$/
583 page, anchor = $1, $2
583 page, anchor = $1, $2
584 end
584 end
585 anchor = sanitize_anchor_name(anchor) if anchor.present?
585 anchor = sanitize_anchor_name(anchor) if anchor.present?
586 # check if page exists
586 # check if page exists
587 wiki_page = link_project.wiki.find_page(page)
587 wiki_page = link_project.wiki.find_page(page)
588 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
588 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
589 "##{anchor}"
589 "##{anchor}"
590 else
590 else
591 case options[:wiki_links]
591 case options[:wiki_links]
592 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
592 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
593 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
593 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
594 else
594 else
595 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
595 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
596 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project, :id => wiki_page_id, :anchor => anchor)
596 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project, :id => wiki_page_id, :anchor => anchor)
597 end
597 end
598 end
598 end
599 link_to(title || h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
599 link_to(title || h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
600 else
600 else
601 # project or wiki doesn't exist
601 # project or wiki doesn't exist
602 all.html_safe
602 all.html_safe
603 end
603 end
604 else
604 else
605 all.html_safe
605 all.html_safe
606 end
606 end
607 end
607 end
608 end
608 end
609
609
610 # Redmine links
610 # Redmine links
611 #
611 #
612 # Examples:
612 # Examples:
613 # Issues:
613 # Issues:
614 # #52 -> Link to issue #52
614 # #52 -> Link to issue #52
615 # Changesets:
615 # Changesets:
616 # r52 -> Link to revision 52
616 # r52 -> Link to revision 52
617 # commit:a85130f -> Link to scmid starting with a85130f
617 # commit:a85130f -> Link to scmid starting with a85130f
618 # Documents:
618 # Documents:
619 # document#17 -> Link to document with id 17
619 # document#17 -> Link to document with id 17
620 # document:Greetings -> Link to the document with title "Greetings"
620 # document:Greetings -> Link to the document with title "Greetings"
621 # document:"Some document" -> Link to the document with title "Some document"
621 # document:"Some document" -> Link to the document with title "Some document"
622 # Versions:
622 # Versions:
623 # version#3 -> Link to version with id 3
623 # version#3 -> Link to version with id 3
624 # version:1.0.0 -> Link to version named "1.0.0"
624 # version:1.0.0 -> Link to version named "1.0.0"
625 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
625 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
626 # Attachments:
626 # Attachments:
627 # attachment:file.zip -> Link to the attachment of the current object named file.zip
627 # attachment:file.zip -> Link to the attachment of the current object named file.zip
628 # Source files:
628 # Source files:
629 # source:some/file -> Link to the file located at /some/file in the project's repository
629 # source:some/file -> Link to the file located at /some/file in the project's repository
630 # source:some/file@52 -> Link to the file's revision 52
630 # source:some/file@52 -> Link to the file's revision 52
631 # source:some/file#L120 -> Link to line 120 of the file
631 # source:some/file#L120 -> Link to line 120 of the file
632 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
632 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
633 # export:some/file -> Force the download of the file
633 # export:some/file -> Force the download of the file
634 # Forum messages:
634 # Forum messages:
635 # message#1218 -> Link to message with id 1218
635 # message#1218 -> Link to message with id 1218
636 #
636 #
637 # Links can refer other objects from other projects, using project identifier:
637 # Links can refer other objects from other projects, using project identifier:
638 # identifier:r52
638 # identifier:r52
639 # identifier:document:"Some document"
639 # identifier:document:"Some document"
640 # identifier:version:1.0.0
640 # identifier:version:1.0.0
641 # identifier:source:some/file
641 # identifier:source:some/file
642 def parse_redmine_links(text, project, obj, attr, only_path, options)
642 def parse_redmine_links(text, project, obj, attr, only_path, options)
643 text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-]+):)?(attachment|document|version|forum|news|commit|source|export|message|project)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|,|\s|\]|<|$)}) do |m|
643 text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-]+):)?(attachment|document|version|forum|news|commit|source|export|message|project)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|,|\s|\]|<|$)}) do |m|
644 leading, esc, project_prefix, project_identifier, prefix, sep, identifier = $1, $2, $3, $4, $5, $7 || $9, $8 || $10
644 leading, esc, project_prefix, project_identifier, prefix, sep, identifier = $1, $2, $3, $4, $5, $7 || $9, $8 || $10
645 link = nil
645 link = nil
646 if project_identifier
646 if project_identifier
647 project = Project.visible.find_by_identifier(project_identifier)
647 project = Project.visible.find_by_identifier(project_identifier)
648 end
648 end
649 if esc.nil?
649 if esc.nil?
650 if prefix.nil? && sep == 'r'
650 if prefix.nil? && sep == 'r'
651 # project.changesets.visible raises an SQL error because of a double join on repositories
651 # project.changesets.visible raises an SQL error because of a double join on repositories
652 if project && project.repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(project.repository.id, identifier))
652 if project && project.repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(project.repository.id, identifier))
653 link = link_to(h("#{project_prefix}r#{identifier}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
653 link = link_to(h("#{project_prefix}r#{identifier}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
654 :class => 'changeset',
654 :class => 'changeset',
655 :title => truncate_single_line(changeset.comments, :length => 100))
655 :title => truncate_single_line(changeset.comments, :length => 100))
656 end
656 end
657 elsif sep == '#'
657 elsif sep == '#'
658 oid = identifier.to_i
658 oid = identifier.to_i
659 case prefix
659 case prefix
660 when nil
660 when nil
661 if issue = Issue.visible.find_by_id(oid, :include => :status)
661 if issue = Issue.visible.find_by_id(oid, :include => :status)
662 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
662 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
663 :class => issue.css_classes,
663 :class => issue.css_classes,
664 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
664 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
665 end
665 end
666 when 'document'
666 when 'document'
667 if document = Document.visible.find_by_id(oid)
667 if document = Document.visible.find_by_id(oid)
668 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
668 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
669 :class => 'document'
669 :class => 'document'
670 end
670 end
671 when 'version'
671 when 'version'
672 if version = Version.visible.find_by_id(oid)
672 if version = Version.visible.find_by_id(oid)
673 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
673 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
674 :class => 'version'
674 :class => 'version'
675 end
675 end
676 when 'message'
676 when 'message'
677 if message = Message.visible.find_by_id(oid, :include => :parent)
677 if message = Message.visible.find_by_id(oid, :include => :parent)
678 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
678 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
679 end
679 end
680 when 'forum'
680 when 'forum'
681 if board = Board.visible.find_by_id(oid)
681 if board = Board.visible.find_by_id(oid)
682 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
682 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
683 :class => 'board'
683 :class => 'board'
684 end
684 end
685 when 'news'
685 when 'news'
686 if news = News.visible.find_by_id(oid)
686 if news = News.visible.find_by_id(oid)
687 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
687 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
688 :class => 'news'
688 :class => 'news'
689 end
689 end
690 when 'project'
690 when 'project'
691 if p = Project.visible.find_by_id(oid)
691 if p = Project.visible.find_by_id(oid)
692 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
692 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
693 end
693 end
694 end
694 end
695 elsif sep == ':'
695 elsif sep == ':'
696 # removes the double quotes if any
696 # removes the double quotes if any
697 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
697 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
698 case prefix
698 case prefix
699 when 'document'
699 when 'document'
700 if project && document = project.documents.visible.find_by_title(name)
700 if project && document = project.documents.visible.find_by_title(name)
701 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
701 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
702 :class => 'document'
702 :class => 'document'
703 end
703 end
704 when 'version'
704 when 'version'
705 if project && version = project.versions.visible.find_by_name(name)
705 if project && version = project.versions.visible.find_by_name(name)
706 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
706 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
707 :class => 'version'
707 :class => 'version'
708 end
708 end
709 when 'forum'
709 when 'forum'
710 if project && board = project.boards.visible.find_by_name(name)
710 if project && board = project.boards.visible.find_by_name(name)
711 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
711 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
712 :class => 'board'
712 :class => 'board'
713 end
713 end
714 when 'news'
714 when 'news'
715 if project && news = project.news.visible.find_by_title(name)
715 if project && news = project.news.visible.find_by_title(name)
716 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
716 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
717 :class => 'news'
717 :class => 'news'
718 end
718 end
719 when 'commit'
719 when 'commit'
720 if project && project.repository && (changeset = Changeset.visible.find(:first, :conditions => ["repository_id = ? AND scmid LIKE ?", project.repository.id, "#{name}%"]))
720 if project && project.repository && (changeset = Changeset.visible.find(:first, :conditions => ["repository_id = ? AND scmid LIKE ?", project.repository.id, "#{name}%"]))
721 link = link_to h("#{project_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.identifier},
721 link = link_to h("#{project_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.identifier},
722 :class => 'changeset',
722 :class => 'changeset',
723 :title => truncate_single_line(h(changeset.comments), :length => 100)
723 :title => truncate_single_line(h(changeset.comments), :length => 100)
724 end
724 end
725 when 'source', 'export'
725 when 'source', 'export'
726 if project && project.repository && User.current.allowed_to?(:browse_repository, project)
726 if project && project.repository && User.current.allowed_to?(:browse_repository, project)
727 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
727 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
728 path, rev, anchor = $1, $3, $5
728 path, rev, anchor = $1, $3, $5
729 link = link_to h("#{project_prefix}#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project,
729 link = link_to h("#{project_prefix}#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project,
730 :path => to_path_param(path),
730 :path => to_path_param(path),
731 :rev => rev,
731 :rev => rev,
732 :anchor => anchor,
732 :anchor => anchor,
733 :format => (prefix == 'export' ? 'raw' : nil)},
733 :format => (prefix == 'export' ? 'raw' : nil)},
734 :class => (prefix == 'export' ? 'source download' : 'source')
734 :class => (prefix == 'export' ? 'source download' : 'source')
735 end
735 end
736 when 'attachment'
736 when 'attachment'
737 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
737 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
738 if attachments && attachment = attachments.detect {|a| a.filename == name }
738 if attachments && attachment = attachments.detect {|a| a.filename == name }
739 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
739 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
740 :class => 'attachment'
740 :class => 'attachment'
741 end
741 end
742 when 'project'
742 when 'project'
743 if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
743 if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
744 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
744 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
745 end
745 end
746 end
746 end
747 end
747 end
748 end
748 end
749 (leading + (link || "#{project_prefix}#{prefix}#{sep}#{identifier}")).html_safe
749 (leading + (link || "#{project_prefix}#{prefix}#{sep}#{identifier}")).html_safe
750 end
750 end
751 end
751 end
752
752
753 HEADING_RE = /(<h(1|2|3|4)( [^>]+)?>(.+?)<\/h(1|2|3|4)>)/i unless const_defined?(:HEADING_RE)
753 HEADING_RE = /(<h(1|2|3|4)( [^>]+)?>(.+?)<\/h(1|2|3|4)>)/i unless const_defined?(:HEADING_RE)
754
754
755 def parse_sections(text, project, obj, attr, only_path, options)
755 def parse_sections(text, project, obj, attr, only_path, options)
756 return unless options[:edit_section_links]
756 return unless options[:edit_section_links]
757 text.gsub!(HEADING_RE) do
757 text.gsub!(HEADING_RE) do
758 @current_section += 1
758 @current_section += 1
759 if @current_section > 1
759 if @current_section > 1
760 content_tag('div',
760 content_tag('div',
761 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
761 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
762 :class => 'contextual',
762 :class => 'contextual',
763 :title => l(:button_edit_section)) + $1
763 :title => l(:button_edit_section)) + $1
764 else
764 else
765 $1
765 $1
766 end
766 end
767 end
767 end
768 end
768 end
769
769
770 # Headings and TOC
770 # Headings and TOC
771 # Adds ids and links to headings unless options[:headings] is set to false
771 # Adds ids and links to headings unless options[:headings] is set to false
772 def parse_headings(text, project, obj, attr, only_path, options)
772 def parse_headings(text, project, obj, attr, only_path, options)
773 return if options[:headings] == false
773 return if options[:headings] == false
774
774
775 text.gsub!(HEADING_RE) do
775 text.gsub!(HEADING_RE) do
776 level, attrs, content = $2.to_i, $3, $4
776 level, attrs, content = $2.to_i, $3, $4
777 item = strip_tags(content).strip
777 item = strip_tags(content).strip
778 anchor = sanitize_anchor_name(item)
778 anchor = sanitize_anchor_name(item)
779 # used for single-file wiki export
779 # used for single-file wiki export
780 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
780 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
781 @parsed_headings << [level, anchor, item]
781 @parsed_headings << [level, anchor, item]
782 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
782 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
783 end
783 end
784 end
784 end
785
785
786 MACROS_RE = /
786 MACROS_RE = /
787 (!)? # escaping
787 (!)? # escaping
788 (
788 (
789 \{\{ # opening tag
789 \{\{ # opening tag
790 ([\w]+) # macro name
790 ([\w]+) # macro name
791 (\(([^\}]*)\))? # optional arguments
791 (\(([^\}]*)\))? # optional arguments
792 \}\} # closing tag
792 \}\} # closing tag
793 )
793 )
794 /x unless const_defined?(:MACROS_RE)
794 /x unless const_defined?(:MACROS_RE)
795
795
796 # Macros substitution
796 # Macros substitution
797 def parse_macros(text, project, obj, attr, only_path, options)
797 def parse_macros(text, project, obj, attr, only_path, options)
798 text.gsub!(MACROS_RE) do
798 text.gsub!(MACROS_RE) do
799 esc, all, macro = $1, $2, $3.downcase
799 esc, all, macro = $1, $2, $3.downcase
800 args = ($5 || '').split(',').each(&:strip)
800 args = ($5 || '').split(',').each(&:strip)
801 if esc.nil?
801 if esc.nil?
802 begin
802 begin
803 exec_macro(macro, obj, args)
803 exec_macro(macro, obj, args)
804 rescue => e
804 rescue => e
805 "<div class=\"flash error\">Error executing the <strong>#{macro}</strong> macro (#{e})</div>"
805 "<div class=\"flash error\">Error executing the <strong>#{macro}</strong> macro (#{e})</div>"
806 end || all
806 end || all
807 else
807 else
808 all
808 all
809 end
809 end
810 end
810 end
811 end
811 end
812
812
813 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
813 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
814
814
815 # Renders the TOC with given headings
815 # Renders the TOC with given headings
816 def replace_toc(text, headings)
816 def replace_toc(text, headings)
817 text.gsub!(TOC_RE) do
817 text.gsub!(TOC_RE) do
818 if headings.empty?
818 if headings.empty?
819 ''
819 ''
820 else
820 else
821 div_class = 'toc'
821 div_class = 'toc'
822 div_class << ' right' if $1 == '>'
822 div_class << ' right' if $1 == '>'
823 div_class << ' left' if $1 == '<'
823 div_class << ' left' if $1 == '<'
824 out = "<ul class=\"#{div_class}\"><li>"
824 out = "<ul class=\"#{div_class}\"><li>"
825 root = headings.map(&:first).min
825 root = headings.map(&:first).min
826 current = root
826 current = root
827 started = false
827 started = false
828 headings.each do |level, anchor, item|
828 headings.each do |level, anchor, item|
829 if level > current
829 if level > current
830 out << '<ul><li>' * (level - current)
830 out << '<ul><li>' * (level - current)
831 elsif level < current
831 elsif level < current
832 out << "</li></ul>\n" * (current - level) + "</li><li>"
832 out << "</li></ul>\n" * (current - level) + "</li><li>"
833 elsif started
833 elsif started
834 out << '</li><li>'
834 out << '</li><li>'
835 end
835 end
836 out << "<a href=\"##{anchor}\">#{item}</a>"
836 out << "<a href=\"##{anchor}\">#{item}</a>"
837 current = level
837 current = level
838 started = true
838 started = true
839 end
839 end
840 out << '</li></ul>' * (current - root)
840 out << '</li></ul>' * (current - root)
841 out << '</li></ul>'
841 out << '</li></ul>'
842 end
842 end
843 end
843 end
844 end
844 end
845
845
846 # Same as Rails' simple_format helper without using paragraphs
846 # Same as Rails' simple_format helper without using paragraphs
847 def simple_format_without_paragraph(text)
847 def simple_format_without_paragraph(text)
848 text.to_s.
848 text.to_s.
849 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
849 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
850 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
850 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
851 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
851 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
852 html_safe
852 html_safe
853 end
853 end
854
854
855 def lang_options_for_select(blank=true)
855 def lang_options_for_select(blank=true)
856 (blank ? [["(auto)", ""]] : []) +
856 (blank ? [["(auto)", ""]] : []) +
857 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
857 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
858 end
858 end
859
859
860 def label_tag_for(name, option_tags = nil, options = {})
860 def label_tag_for(name, option_tags = nil, options = {})
861 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
861 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
862 content_tag("label", label_text)
862 content_tag("label", label_text)
863 end
863 end
864
864
865 def labelled_tabular_form_for(*args, &proc)
865 def labelled_tabular_form_for(*args, &proc)
866 args << {} unless args.last.is_a?(Hash)
866 args << {} unless args.last.is_a?(Hash)
867 options = args.last
867 options = args.last
868 options[:html] ||= {}
868 options[:html] ||= {}
869 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
869 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
870 options.merge!({:builder => TabularFormBuilder})
870 options.merge!({:builder => TabularFormBuilder})
871 form_for(*args, &proc)
871 form_for(*args, &proc)
872 end
872 end
873
873
874 def back_url_hidden_field_tag
874 def back_url_hidden_field_tag
875 back_url = params[:back_url] || request.env['HTTP_REFERER']
875 back_url = params[:back_url] || request.env['HTTP_REFERER']
876 back_url = CGI.unescape(back_url.to_s)
876 back_url = CGI.unescape(back_url.to_s)
877 hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
877 hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
878 end
878 end
879
879
880 def check_all_links(form_name)
880 def check_all_links(form_name)
881 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
881 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
882 " | ".html_safe +
882 " | ".html_safe +
883 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
883 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
884 end
884 end
885
885
886 def progress_bar(pcts, options={})
886 def progress_bar(pcts, options={})
887 pcts = [pcts, pcts] unless pcts.is_a?(Array)
887 pcts = [pcts, pcts] unless pcts.is_a?(Array)
888 pcts = pcts.collect(&:round)
888 pcts = pcts.collect(&:round)
889 pcts[1] = pcts[1] - pcts[0]
889 pcts[1] = pcts[1] - pcts[0]
890 pcts << (100 - pcts[1] - pcts[0])
890 pcts << (100 - pcts[1] - pcts[0])
891 width = options[:width] || '100px;'
891 width = options[:width] || '100px;'
892 legend = options[:legend] || ''
892 legend = options[:legend] || ''
893 content_tag('table',
893 content_tag('table',
894 content_tag('tr',
894 content_tag('tr',
895 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
895 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
896 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
896 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
897 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
897 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
898 ), :class => 'progress', :style => "width: #{width};").html_safe +
898 ), :class => 'progress', :style => "width: #{width};").html_safe +
899 content_tag('p', legend, :class => 'pourcent').html_safe
899 content_tag('p', legend, :class => 'pourcent').html_safe
900 end
900 end
901
901
902 def checked_image(checked=true)
902 def checked_image(checked=true)
903 if checked
903 if checked
904 image_tag 'toggle_check.png'
904 image_tag 'toggle_check.png'
905 end
905 end
906 end
906 end
907
907
908 def context_menu(url)
908 def context_menu(url)
909 unless @context_menu_included
909 unless @context_menu_included
910 content_for :header_tags do
910 content_for :header_tags do
911 javascript_include_tag('context_menu') +
911 javascript_include_tag('context_menu') +
912 stylesheet_link_tag('context_menu')
912 stylesheet_link_tag('context_menu')
913 end
913 end
914 if l(:direction) == 'rtl'
914 if l(:direction) == 'rtl'
915 content_for :header_tags do
915 content_for :header_tags do
916 stylesheet_link_tag('context_menu_rtl')
916 stylesheet_link_tag('context_menu_rtl')
917 end
917 end
918 end
918 end
919 @context_menu_included = true
919 @context_menu_included = true
920 end
920 end
921 javascript_tag "new ContextMenu('#{ url_for(url) }')"
921 javascript_tag "new ContextMenu('#{ url_for(url) }')"
922 end
922 end
923
923
924 def context_menu_link(name, url, options={})
924 def context_menu_link(name, url, options={})
925 options[:class] ||= ''
925 options[:class] ||= ''
926 if options.delete(:selected)
926 if options.delete(:selected)
927 options[:class] << ' icon-checked disabled'
927 options[:class] << ' icon-checked disabled'
928 options[:disabled] = true
928 options[:disabled] = true
929 end
929 end
930 if options.delete(:disabled)
930 if options.delete(:disabled)
931 options.delete(:method)
931 options.delete(:method)
932 options.delete(:confirm)
932 options.delete(:confirm)
933 options.delete(:onclick)
933 options.delete(:onclick)
934 options[:class] << ' disabled'
934 options[:class] << ' disabled'
935 url = '#'
935 url = '#'
936 end
936 end
937 link_to h(name), url, options
937 link_to h(name), url, options
938 end
938 end
939
939
940 def calendar_for(field_id)
940 def calendar_for(field_id)
941 include_calendar_headers_tags
941 include_calendar_headers_tags
942 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
942 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
943 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
943 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
944 end
944 end
945
945
946 def include_calendar_headers_tags
946 def include_calendar_headers_tags
947 unless @calendar_headers_tags_included
947 unless @calendar_headers_tags_included
948 @calendar_headers_tags_included = true
948 @calendar_headers_tags_included = true
949 content_for :header_tags do
949 content_for :header_tags do
950 start_of_week = case Setting.start_of_week.to_i
950 start_of_week = case Setting.start_of_week.to_i
951 when 1
951 when 1
952 'Calendar._FD = 1;' # Monday
952 'Calendar._FD = 1;' # Monday
953 when 7
953 when 7
954 'Calendar._FD = 0;' # Sunday
954 'Calendar._FD = 0;' # Sunday
955 when 6
955 when 6
956 'Calendar._FD = 6;' # Saturday
956 'Calendar._FD = 6;' # Saturday
957 else
957 else
958 '' # use language
958 '' # use language
959 end
959 end
960
960
961 javascript_include_tag('calendar/calendar') +
961 javascript_include_tag('calendar/calendar') +
962 javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
962 javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
963 javascript_tag(start_of_week) +
963 javascript_tag(start_of_week) +
964 javascript_include_tag('calendar/calendar-setup') +
964 javascript_include_tag('calendar/calendar-setup') +
965 stylesheet_link_tag('calendar')
965 stylesheet_link_tag('calendar')
966 end
966 end
967 end
967 end
968 end
968 end
969
969
970 def content_for(name, content = nil, &block)
970 def content_for(name, content = nil, &block)
971 @has_content ||= {}
971 @has_content ||= {}
972 @has_content[name] = true
972 @has_content[name] = true
973 super(name, content, &block)
973 super(name, content, &block)
974 end
974 end
975
975
976 def has_content?(name)
976 def has_content?(name)
977 (@has_content && @has_content[name]) || false
977 (@has_content && @has_content[name]) || false
978 end
978 end
979
979
980 def email_delivery_enabled?
980 def email_delivery_enabled?
981 !!ActionMailer::Base.perform_deliveries
981 !!ActionMailer::Base.perform_deliveries
982 end
982 end
983
983
984 # Returns the avatar image tag for the given +user+ if avatars are enabled
984 # Returns the avatar image tag for the given +user+ if avatars are enabled
985 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
985 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
986 def avatar(user, options = { })
986 def avatar(user, options = { })
987 if Setting.gravatar_enabled?
987 if Setting.gravatar_enabled?
988 options.merge!({:ssl => (defined?(request) && request.ssl?), :default => Setting.gravatar_default})
988 options.merge!({:ssl => (defined?(request) && request.ssl?), :default => Setting.gravatar_default})
989 email = nil
989 email = nil
990 if user.respond_to?(:mail)
990 if user.respond_to?(:mail)
991 email = user.mail
991 email = user.mail
992 elsif user.to_s =~ %r{<(.+?)>}
992 elsif user.to_s =~ %r{<(.+?)>}
993 email = $1
993 email = $1
994 end
994 end
995 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
995 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
996 else
996 else
997 ''
997 ''
998 end
998 end
999 end
999 end
1000
1000
1001 def sanitize_anchor_name(anchor)
1001 def sanitize_anchor_name(anchor)
1002 anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1002 anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1003 end
1003 end
1004
1004
1005 # Returns the javascript tags that are included in the html layout head
1005 # Returns the javascript tags that are included in the html layout head
1006 def javascript_heads
1006 def javascript_heads
1007 tags = javascript_include_tag(:defaults)
1007 tags = javascript_include_tag(:defaults)
1008 unless User.current.pref.warn_on_leaving_unsaved == '0'
1008 unless User.current.pref.warn_on_leaving_unsaved == '0'
1009 tags << "\n".html_safe + javascript_tag("Event.observe(window, 'load', function(){ new WarnLeavingUnsaved('#{escape_javascript( l(:text_warn_on_leaving_unsaved) )}'); });")
1009 tags << "\n".html_safe + javascript_tag("Event.observe(window, 'load', function(){ new WarnLeavingUnsaved('#{escape_javascript( l(:text_warn_on_leaving_unsaved) )}'); });")
1010 end
1010 end
1011 tags
1011 tags
1012 end
1012 end
1013
1013
1014 def favicon
1014 def favicon
1015 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />".html_safe
1015 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />".html_safe
1016 end
1016 end
1017
1017
1018 def robot_exclusion_tag
1018 def robot_exclusion_tag
1019 '<meta name="robots" content="noindex,follow,noarchive" />'
1019 '<meta name="robots" content="noindex,follow,noarchive" />'
1020 end
1020 end
1021
1021
1022 # Returns true if arg is expected in the API response
1022 # Returns true if arg is expected in the API response
1023 def include_in_api_response?(arg)
1023 def include_in_api_response?(arg)
1024 unless @included_in_api_response
1024 unless @included_in_api_response
1025 param = params[:include]
1025 param = params[:include]
1026 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1026 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1027 @included_in_api_response.collect!(&:strip)
1027 @included_in_api_response.collect!(&:strip)
1028 end
1028 end
1029 @included_in_api_response.include?(arg.to_s)
1029 @included_in_api_response.include?(arg.to_s)
1030 end
1030 end
1031
1031
1032 # Returns options or nil if nometa param or X-Redmine-Nometa header
1032 # Returns options or nil if nometa param or X-Redmine-Nometa header
1033 # was set in the request
1033 # was set in the request
1034 def api_meta(options)
1034 def api_meta(options)
1035 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1035 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1036 # compatibility mode for activeresource clients that raise
1036 # compatibility mode for activeresource clients that raise
1037 # an error when unserializing an array with attributes
1037 # an error when unserializing an array with attributes
1038 nil
1038 nil
1039 else
1039 else
1040 options
1040 options
1041 end
1041 end
1042 end
1042 end
1043
1043
1044 private
1044 private
1045
1045
1046 def wiki_helper
1046 def wiki_helper
1047 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1047 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1048 extend helper
1048 extend helper
1049 return self
1049 return self
1050 end
1050 end
1051
1051
1052 def link_to_content_update(text, url_params = {}, html_options = {})
1052 def link_to_content_update(text, url_params = {}, html_options = {})
1053 link_to(text, url_params, html_options)
1053 link_to(text, url_params, html_options)
1054 end
1054 end
1055 end
1055 end
@@ -1,197 +1,203
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require "digest/md5"
18 require "digest/md5"
19
19
20 class Attachment < ActiveRecord::Base
20 class Attachment < ActiveRecord::Base
21 belongs_to :container, :polymorphic => true
21 belongs_to :container, :polymorphic => true
22 belongs_to :author, :class_name => "User", :foreign_key => "author_id"
22 belongs_to :author, :class_name => "User", :foreign_key => "author_id"
23
23
24 validates_presence_of :container, :filename, :author
24 validates_presence_of :container, :filename, :author
25 validates_length_of :filename, :maximum => 255
25 validates_length_of :filename, :maximum => 255
26 validates_length_of :disk_filename, :maximum => 255
26 validates_length_of :disk_filename, :maximum => 255
27 validate :validate_max_file_size
27 validate :validate_max_file_size
28
28
29 acts_as_event :title => :filename,
29 acts_as_event :title => :filename,
30 :url => Proc.new {|o| {:controller => 'attachments', :action => 'download', :id => o.id, :filename => o.filename}}
30 :url => Proc.new {|o| {:controller => 'attachments', :action => 'download', :id => o.id, :filename => o.filename}}
31
31
32 acts_as_activity_provider :type => 'files',
32 acts_as_activity_provider :type => 'files',
33 :permission => :view_files,
33 :permission => :view_files,
34 :author_key => :author_id,
34 :author_key => :author_id,
35 :find_options => {:select => "#{Attachment.table_name}.*",
35 :find_options => {:select => "#{Attachment.table_name}.*",
36 :joins => "LEFT JOIN #{Version.table_name} ON #{Attachment.table_name}.container_type='Version' AND #{Version.table_name}.id = #{Attachment.table_name}.container_id " +
36 :joins => "LEFT JOIN #{Version.table_name} ON #{Attachment.table_name}.container_type='Version' AND #{Version.table_name}.id = #{Attachment.table_name}.container_id " +
37 "LEFT JOIN #{Project.table_name} ON #{Version.table_name}.project_id = #{Project.table_name}.id OR ( #{Attachment.table_name}.container_type='Project' AND #{Attachment.table_name}.container_id = #{Project.table_name}.id )"}
37 "LEFT JOIN #{Project.table_name} ON #{Version.table_name}.project_id = #{Project.table_name}.id OR ( #{Attachment.table_name}.container_type='Project' AND #{Attachment.table_name}.container_id = #{Project.table_name}.id )"}
38
38
39 acts_as_activity_provider :type => 'documents',
39 acts_as_activity_provider :type => 'documents',
40 :permission => :view_documents,
40 :permission => :view_documents,
41 :author_key => :author_id,
41 :author_key => :author_id,
42 :find_options => {:select => "#{Attachment.table_name}.*",
42 :find_options => {:select => "#{Attachment.table_name}.*",
43 :joins => "LEFT JOIN #{Document.table_name} ON #{Attachment.table_name}.container_type='Document' AND #{Document.table_name}.id = #{Attachment.table_name}.container_id " +
43 :joins => "LEFT JOIN #{Document.table_name} ON #{Attachment.table_name}.container_type='Document' AND #{Document.table_name}.id = #{Attachment.table_name}.container_id " +
44 "LEFT JOIN #{Project.table_name} ON #{Document.table_name}.project_id = #{Project.table_name}.id"}
44 "LEFT JOIN #{Project.table_name} ON #{Document.table_name}.project_id = #{Project.table_name}.id"}
45
45
46 cattr_accessor :storage_path
46 cattr_accessor :storage_path
47 @@storage_path = Redmine::Configuration['attachments_storage_path'] || "#{Rails.root}/files"
47 @@storage_path = Redmine::Configuration['attachments_storage_path'] || "#{Rails.root}/files"
48
48
49 before_save :files_to_final_location
49 before_save :files_to_final_location
50 after_destroy :delete_from_disk
50 after_destroy :delete_from_disk
51
51
52 def validate_max_file_size
52 def validate_max_file_size
53 if self.filesize > Setting.attachment_max_size.to_i.kilobytes
53 if self.filesize > Setting.attachment_max_size.to_i.kilobytes
54 errors.add(:base, :too_long, :count => Setting.attachment_max_size.to_i.kilobytes)
54 errors.add(:base, :too_long, :count => Setting.attachment_max_size.to_i.kilobytes)
55 end
55 end
56 end
56 end
57
57
58 def file=(incoming_file)
58 def file=(incoming_file)
59 unless incoming_file.nil?
59 unless incoming_file.nil?
60 @temp_file = incoming_file
60 @temp_file = incoming_file
61 if @temp_file.size > 0
61 if @temp_file.size > 0
62 self.filename = sanitize_filename(@temp_file.original_filename)
62 self.filename = sanitize_filename(@temp_file.original_filename)
63 self.disk_filename = Attachment.disk_filename(filename)
63 self.disk_filename = Attachment.disk_filename(filename)
64 self.content_type = @temp_file.content_type.to_s.chomp
64 self.content_type = @temp_file.content_type.to_s.chomp
65 if content_type.blank?
65 if content_type.blank?
66 self.content_type = Redmine::MimeType.of(filename)
66 self.content_type = Redmine::MimeType.of(filename)
67 end
67 end
68 self.filesize = @temp_file.size
68 self.filesize = @temp_file.size
69 end
69 end
70 end
70 end
71 end
71 end
72
72
73 def file
73 def file
74 nil
74 nil
75 end
75 end
76
76
77 # Copies the temporary file to its final location
77 # Copies the temporary file to its final location
78 # and computes its MD5 hash
78 # and computes its MD5 hash
79 def files_to_final_location
79 def files_to_final_location
80 if @temp_file && (@temp_file.size > 0)
80 if @temp_file && (@temp_file.size > 0)
81 logger.info("Saving attachment '#{self.diskfile}' (#{@temp_file.size} bytes)")
81 logger.info("Saving attachment '#{self.diskfile}' (#{@temp_file.size} bytes)")
82 md5 = Digest::MD5.new
82 md5 = Digest::MD5.new
83 File.open(diskfile, "wb") do |f|
83 File.open(diskfile, "wb") do |f|
84 buffer = ""
84 buffer = ""
85 while (buffer = @temp_file.read(8192))
85 while (buffer = @temp_file.read(8192))
86 f.write(buffer)
86 f.write(buffer)
87 md5.update(buffer)
87 md5.update(buffer)
88 end
88 end
89 end
89 end
90 self.digest = md5.hexdigest
90 self.digest = md5.hexdigest
91 end
91 end
92 @temp_file = nil
92 @temp_file = nil
93 # Don't save the content type if it's longer than the authorized length
93 # Don't save the content type if it's longer than the authorized length
94 if self.content_type && self.content_type.length > 255
94 if self.content_type && self.content_type.length > 255
95 self.content_type = nil
95 self.content_type = nil
96 end
96 end
97 end
97 end
98
98
99 # Deletes file on the disk
99 # Deletes file on the disk
100 def delete_from_disk
100 def delete_from_disk
101 File.delete(diskfile) if !filename.blank? && File.exist?(diskfile)
101 File.delete(diskfile) if !filename.blank? && File.exist?(diskfile)
102 end
102 end
103
103
104 # Returns file's location on disk
104 # Returns file's location on disk
105 def diskfile
105 def diskfile
106 "#{@@storage_path}/#{self.disk_filename}"
106 "#{@@storage_path}/#{self.disk_filename}"
107 end
107 end
108
108
109 def increment_download
109 def increment_download
110 increment!(:downloads)
110 increment!(:downloads)
111 end
111 end
112
112
113 def project
113 def project
114 container.project
114 container.project
115 end
115 end
116
116
117 def visible?(user=User.current)
117 def visible?(user=User.current)
118 container.attachments_visible?(user)
118 container.attachments_visible?(user)
119 end
119 end
120
120
121 def deletable?(user=User.current)
121 def deletable?(user=User.current)
122 container.attachments_deletable?(user)
122 container.attachments_deletable?(user)
123 end
123 end
124
124
125 def image?
125 def image?
126 self.filename =~ /\.(bmp|gif|jpg|jpe|jpeg|png)$/i
126 self.filename =~ /\.(bmp|gif|jpg|jpe|jpeg|png)$/i
127 end
127 end
128
128
129 def is_text?
129 def is_text?
130 Redmine::MimeType.is_type?('text', filename)
130 Redmine::MimeType.is_type?('text', filename)
131 end
131 end
132
132
133 def is_diff?
133 def is_diff?
134 self.filename =~ /\.(patch|diff)$/i
134 self.filename =~ /\.(patch|diff)$/i
135 end
135 end
136
136
137 # Returns true if the file is readable
137 # Returns true if the file is readable
138 def readable?
138 def readable?
139 File.readable?(diskfile)
139 File.readable?(diskfile)
140 end
140 end
141
141
142 # Bulk attaches a set of files to an object
142 # Bulk attaches a set of files to an object
143 #
143 #
144 # Returns a Hash of the results:
144 # Returns a Hash of the results:
145 # :files => array of the attached files
145 # :files => array of the attached files
146 # :unsaved => array of the files that could not be attached
146 # :unsaved => array of the files that could not be attached
147 def self.attach_files(obj, attachments)
147 def self.attach_files(obj, attachments)
148 attached = []
148 attached = []
149 if attachments && attachments.is_a?(Hash)
149 if attachments && attachments.is_a?(Hash)
150 attachments.each_value do |attachment|
150 attachments.each_value do |attachment|
151 file = attachment['file']
151 file = attachment['file']
152 next unless file && file.size > 0
152 next unless file && file.size > 0
153 a = Attachment.create(:container => obj,
153 a = Attachment.create(:container => obj,
154 :file => file,
154 :file => file,
155 :description => attachment['description'].to_s.strip,
155 :description => attachment['description'].to_s.strip,
156 :author => User.current)
156 :author => User.current)
157 obj.attachments << a
157 obj.attachments << a
158
158
159 if a.new_record?
159 if a.new_record?
160 obj.unsaved_attachments ||= []
160 obj.unsaved_attachments ||= []
161 obj.unsaved_attachments << a
161 obj.unsaved_attachments << a
162 else
162 else
163 attached << a
163 attached << a
164 end
164 end
165 end
165 end
166 end
166 end
167 {:files => attached, :unsaved => obj.unsaved_attachments}
167 {:files => attached, :unsaved => obj.unsaved_attachments}
168 end
168 end
169
169
170 def self.latest_attach(attachments, filename)
171 attachments.sort_by(&:created_on).reverse.detect {
172 |att| att.filename.downcase == filename.downcase
173 }
174 end
175
170 private
176 private
171 def sanitize_filename(value)
177 def sanitize_filename(value)
172 # get only the filename, not the whole path
178 # get only the filename, not the whole path
173 just_filename = value.gsub(/^.*(\\|\/)/, '')
179 just_filename = value.gsub(/^.*(\\|\/)/, '')
174 # NOTE: File.basename doesn't work right with Windows paths on Unix
180 # NOTE: File.basename doesn't work right with Windows paths on Unix
175 # INCORRECT: just_filename = File.basename(value.gsub('\\\\', '/'))
181 # INCORRECT: just_filename = File.basename(value.gsub('\\\\', '/'))
176
182
177 # Finally, replace all non alphanumeric, hyphens or periods with underscore
183 # Finally, replace all non alphanumeric, hyphens or periods with underscore
178 @filename = just_filename.gsub(/[^\w\.\-]/,'_')
184 @filename = just_filename.gsub(/[^\w\.\-]/,'_')
179 end
185 end
180
186
181 # Returns an ASCII or hashed filename
187 # Returns an ASCII or hashed filename
182 def self.disk_filename(filename)
188 def self.disk_filename(filename)
183 timestamp = DateTime.now.strftime("%y%m%d%H%M%S")
189 timestamp = DateTime.now.strftime("%y%m%d%H%M%S")
184 ascii = ''
190 ascii = ''
185 if filename =~ %r{^[a-zA-Z0-9_\.\-]*$}
191 if filename =~ %r{^[a-zA-Z0-9_\.\-]*$}
186 ascii = filename
192 ascii = filename
187 else
193 else
188 ascii = Digest::MD5.hexdigest(filename)
194 ascii = Digest::MD5.hexdigest(filename)
189 # keep the extension if any
195 # keep the extension if any
190 ascii << $1 if filename =~ %r{(\.[a-zA-Z0-9]+)$}
196 ascii << $1 if filename =~ %r{(\.[a-zA-Z0-9]+)$}
191 end
197 end
192 while File.exist?(File.join(@@storage_path, "#{timestamp}_#{ascii}"))
198 while File.exist?(File.join(@@storage_path, "#{timestamp}_#{ascii}"))
193 timestamp.succ!
199 timestamp.succ!
194 end
200 end
195 "#{timestamp}_#{ascii}"
201 "#{timestamp}_#{ascii}"
196 end
202 end
197 end
203 end
@@ -1,124 +1,144
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2011 Jean-Philippe Lang
4 # Copyright (C) 2006-2011 Jean-Philippe Lang
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
9 # of the License, or (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
20 require File.expand_path('../../test_helper', __FILE__)
20 require File.expand_path('../../test_helper', __FILE__)
21
21
22 class AttachmentTest < ActiveSupport::TestCase
22 class AttachmentTest < ActiveSupport::TestCase
23 fixtures :issues, :users
23 fixtures :issues, :users
24
24
25 def setup
25 def setup
26 set_tmp_attachments_directory
26 set_tmp_attachments_directory
27 end
27 end
28
28
29 def test_create
29 def test_create
30 a = Attachment.new(:container => Issue.find(1),
30 a = Attachment.new(:container => Issue.find(1),
31 :file => uploaded_test_file("testfile.txt", "text/plain"),
31 :file => uploaded_test_file("testfile.txt", "text/plain"),
32 :author => User.find(1))
32 :author => User.find(1))
33 assert a.save
33 assert a.save
34 assert_equal 'testfile.txt', a.filename
34 assert_equal 'testfile.txt', a.filename
35 assert_equal 59, a.filesize
35 assert_equal 59, a.filesize
36 assert_equal 'text/plain', a.content_type
36 assert_equal 'text/plain', a.content_type
37 assert_equal 0, a.downloads
37 assert_equal 0, a.downloads
38 assert_equal '1478adae0d4eb06d35897518540e25d6', a.digest
38 assert_equal '1478adae0d4eb06d35897518540e25d6', a.digest
39 assert File.exist?(a.diskfile)
39 assert File.exist?(a.diskfile)
40 assert_equal 59, File.size(a.diskfile)
40 assert_equal 59, File.size(a.diskfile)
41 end
41 end
42
42
43 def test_destroy
43 def test_destroy
44 a = Attachment.new(:container => Issue.find(1),
44 a = Attachment.new(:container => Issue.find(1),
45 :file => uploaded_test_file("testfile.txt", "text/plain"),
45 :file => uploaded_test_file("testfile.txt", "text/plain"),
46 :author => User.find(1))
46 :author => User.find(1))
47 assert a.save
47 assert a.save
48 assert_equal 'testfile.txt', a.filename
48 assert_equal 'testfile.txt', a.filename
49 assert_equal 59, a.filesize
49 assert_equal 59, a.filesize
50 assert_equal 'text/plain', a.content_type
50 assert_equal 'text/plain', a.content_type
51 assert_equal 0, a.downloads
51 assert_equal 0, a.downloads
52 assert_equal '1478adae0d4eb06d35897518540e25d6', a.digest
52 assert_equal '1478adae0d4eb06d35897518540e25d6', a.digest
53 diskfile = a.diskfile
53 diskfile = a.diskfile
54 assert File.exist?(diskfile)
54 assert File.exist?(diskfile)
55 assert_equal 59, File.size(a.diskfile)
55 assert_equal 59, File.size(a.diskfile)
56 assert a.destroy
56 assert a.destroy
57 assert !File.exist?(diskfile)
57 assert !File.exist?(diskfile)
58 end
58 end
59
59
60 def test_create_should_auto_assign_content_type
60 def test_create_should_auto_assign_content_type
61 a = Attachment.new(:container => Issue.find(1),
61 a = Attachment.new(:container => Issue.find(1),
62 :file => uploaded_test_file("testfile.txt", ""),
62 :file => uploaded_test_file("testfile.txt", ""),
63 :author => User.find(1))
63 :author => User.find(1))
64 assert a.save
64 assert a.save
65 assert_equal 'text/plain', a.content_type
65 assert_equal 'text/plain', a.content_type
66 end
66 end
67
67
68 def test_identical_attachments_at_the_same_time_should_not_overwrite
68 def test_identical_attachments_at_the_same_time_should_not_overwrite
69 a1 = Attachment.create!(:container => Issue.find(1),
69 a1 = Attachment.create!(:container => Issue.find(1),
70 :file => uploaded_test_file("testfile.txt", ""),
70 :file => uploaded_test_file("testfile.txt", ""),
71 :author => User.find(1))
71 :author => User.find(1))
72 a2 = Attachment.create!(:container => Issue.find(1),
72 a2 = Attachment.create!(:container => Issue.find(1),
73 :file => uploaded_test_file("testfile.txt", ""),
73 :file => uploaded_test_file("testfile.txt", ""),
74 :author => User.find(1))
74 :author => User.find(1))
75 assert a1.disk_filename != a2.disk_filename
75 assert a1.disk_filename != a2.disk_filename
76 end
76 end
77
77
78 def test_diskfilename
78 def test_diskfilename
79 assert Attachment.disk_filename("test_file.txt") =~ /^\d{12}_test_file.txt$/
79 assert Attachment.disk_filename("test_file.txt") =~ /^\d{12}_test_file.txt$/
80 assert_equal 'test_file.txt', Attachment.disk_filename("test_file.txt")[13..-1]
80 assert_equal 'test_file.txt', Attachment.disk_filename("test_file.txt")[13..-1]
81 assert_equal '770c509475505f37c2b8fb6030434d6b.txt', Attachment.disk_filename("test_accentuΓ©.txt")[13..-1]
81 assert_equal '770c509475505f37c2b8fb6030434d6b.txt', Attachment.disk_filename("test_accentuΓ©.txt")[13..-1]
82 assert_equal 'f8139524ebb8f32e51976982cd20a85d', Attachment.disk_filename("test_accentuΓ©")[13..-1]
82 assert_equal 'f8139524ebb8f32e51976982cd20a85d', Attachment.disk_filename("test_accentuΓ©")[13..-1]
83 assert_equal 'cbb5b0f30978ba03731d61f9f6d10011', Attachment.disk_filename("test_accentuΓ©.Γ§a")[13..-1]
83 assert_equal 'cbb5b0f30978ba03731d61f9f6d10011', Attachment.disk_filename("test_accentuΓ©.Γ§a")[13..-1]
84 end
84 end
85
85
86 context "Attachmnet.attach_files" do
86 context "Attachmnet.attach_files" do
87 should "attach the file" do
87 should "attach the file" do
88 issue = Issue.first
88 issue = Issue.first
89 assert_difference 'Attachment.count' do
89 assert_difference 'Attachment.count' do
90 Attachment.attach_files(issue,
90 Attachment.attach_files(issue,
91 '1' => {
91 '1' => {
92 'file' => uploaded_test_file('testfile.txt', 'text/plain'),
92 'file' => uploaded_test_file('testfile.txt', 'text/plain'),
93 'description' => 'test'
93 'description' => 'test'
94 })
94 })
95 end
95 end
96
96
97 attachment = Attachment.first(:order => 'id DESC')
97 attachment = Attachment.first(:order => 'id DESC')
98 assert_equal issue, attachment.container
98 assert_equal issue, attachment.container
99 assert_equal 'testfile.txt', attachment.filename
99 assert_equal 'testfile.txt', attachment.filename
100 assert_equal 59, attachment.filesize
100 assert_equal 59, attachment.filesize
101 assert_equal 'test', attachment.description
101 assert_equal 'test', attachment.description
102 assert_equal 'text/plain', attachment.content_type
102 assert_equal 'text/plain', attachment.content_type
103 assert File.exists?(attachment.diskfile)
103 assert File.exists?(attachment.diskfile)
104 assert_equal 59, File.size(attachment.diskfile)
104 assert_equal 59, File.size(attachment.diskfile)
105 end
105 end
106
106
107 should "add unsaved files to the object as unsaved attachments" do
107 should "add unsaved files to the object as unsaved attachments" do
108 # Max size of 0 to force Attachment creation failures
108 # Max size of 0 to force Attachment creation failures
109 with_settings(:attachment_max_size => 0) do
109 with_settings(:attachment_max_size => 0) do
110 @project = Project.generate!
110 @project = Project.generate!
111 response = Attachment.attach_files(@project, {
111 response = Attachment.attach_files(@project, {
112 '1' => {'file' => mock_file, 'description' => 'test'},
112 '1' => {'file' => mock_file, 'description' => 'test'},
113 '2' => {'file' => mock_file, 'description' => 'test'}
113 '2' => {'file' => mock_file, 'description' => 'test'}
114 })
114 })
115
115
116 assert response[:unsaved].present?
116 assert response[:unsaved].present?
117 assert_equal 2, response[:unsaved].length
117 assert_equal 2, response[:unsaved].length
118 assert response[:unsaved].first.new_record?
118 assert response[:unsaved].first.new_record?
119 assert response[:unsaved].second.new_record?
119 assert response[:unsaved].second.new_record?
120 assert_equal response[:unsaved], @project.unsaved_attachments
120 assert_equal response[:unsaved], @project.unsaved_attachments
121 end
121 end
122 end
122 end
123 end
123 end
124
125 def test_latest_attach
126 Attachment.storage_path = "#{Rails.root}/test/fixtures/files"
127 a1 = Attachment.find(16)
128 assert_equal "testfile.png", a1.filename
129 assert a1.readable?
130 assert (! a1.visible?(User.anonymous))
131 assert a1.visible?(User.find(2))
132 a2 = Attachment.find(17)
133 assert_equal "testfile.PNG", a2.filename
134 assert a2.readable?
135 assert (! a2.visible?(User.anonymous))
136 assert a2.visible?(User.find(2))
137 assert a1.created_on < a2.created_on
138
139 la1 = Attachment.latest_attach([a1, a2], "testfile.png")
140 assert_equal 17, la1.id
141 la2 = Attachment.latest_attach([a1, a2], "Testfile.PNG")
142 assert_equal 17, la2.id
143 end
124 end
144 end
General Comments 0
You need to be logged in to leave comments. Login now