##// END OF EJS Templates
Appends the filename to the attachment url so that clients that ignore content-disposition http header get the real filename (#1649)....
Jean-Philippe Lang -
r1669:9b579de9e234
parent child
Show More
@@ -1,53 +1,56
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class AttachmentsController < ApplicationController
18 class AttachmentsController < ApplicationController
19 layout 'base'
19 layout 'base'
20 before_filter :find_project
20 before_filter :find_project
21
21
22 def show
22 def show
23 if @attachment.is_diff?
23 if @attachment.is_diff?
24 @diff = File.new(@attachment.diskfile, "rb").read
24 @diff = File.new(@attachment.diskfile, "rb").read
25 render :action => 'diff'
25 render :action => 'diff'
26 elsif @attachment.is_text?
26 elsif @attachment.is_text?
27 @content = File.new(@attachment.diskfile, "rb").read
27 @content = File.new(@attachment.diskfile, "rb").read
28 render :action => 'file'
28 render :action => 'file'
29 elsif
29 elsif
30 download
30 download
31 end
31 end
32 end
32 end
33
33
34 def download
34 def download
35 @attachment.increment_download if @attachment.container.is_a?(Version)
35 @attachment.increment_download if @attachment.container.is_a?(Version)
36
36
37 # images are sent inline
37 # images are sent inline
38 send_file @attachment.diskfile, :filename => filename_for_content_disposition(@attachment.filename),
38 send_file @attachment.diskfile, :filename => filename_for_content_disposition(@attachment.filename),
39 :type => @attachment.content_type,
39 :type => @attachment.content_type,
40 :disposition => (@attachment.image? ? 'inline' : 'attachment')
40 :disposition => (@attachment.image? ? 'inline' : 'attachment')
41 end
41 end
42
42
43 private
43 private
44 def find_project
44 def find_project
45 @attachment = Attachment.find(params[:id])
45 @attachment = Attachment.find(params[:id])
46 # Show 404 if the filename in the url is wrong
47 raise ActiveRecord::RecordNotFound if params[:filename] && params[:filename] != @attachment.filename
48
46 @project = @attachment.project
49 @project = @attachment.project
47 permission = @attachment.container.is_a?(Version) ? :view_files : "view_#{@attachment.container.class.name.underscore.pluralize}".to_sym
50 permission = @attachment.container.is_a?(Version) ? :view_files : "view_#{@attachment.container.class.name.underscore.pluralize}".to_sym
48 allowed = User.current.allowed_to?(permission, @project)
51 allowed = User.current.allowed_to?(permission, @project)
49 allowed ? true : (User.current.logged? ? render_403 : require_login)
52 allowed ? true : (User.current.logged? ? render_403 : require_login)
50 rescue ActiveRecord::RecordNotFound
53 rescue ActiveRecord::RecordNotFound
51 render_404
54 render_404
52 end
55 end
53 end
56 end
@@ -1,522 +1,533
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require 'coderay'
18 require 'coderay'
19 require 'coderay/helpers/file_type'
19 require 'coderay/helpers/file_type'
20
20
21 module ApplicationHelper
21 module ApplicationHelper
22 include Redmine::WikiFormatting::Macros::Definitions
22 include Redmine::WikiFormatting::Macros::Definitions
23
23
24 def current_role
24 def current_role
25 @current_role ||= User.current.role_for_project(@project)
25 @current_role ||= User.current.role_for_project(@project)
26 end
26 end
27
27
28 # Return true if user is authorized for controller/action, otherwise false
28 # Return true if user is authorized for controller/action, otherwise false
29 def authorize_for(controller, action)
29 def authorize_for(controller, action)
30 User.current.allowed_to?({:controller => controller, :action => action}, @project)
30 User.current.allowed_to?({:controller => controller, :action => action}, @project)
31 end
31 end
32
32
33 # Display a link if user is authorized
33 # Display a link if user is authorized
34 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
34 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
35 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
35 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
36 end
36 end
37
37
38 # Display a link to user's account page
38 # Display a link to user's account page
39 def link_to_user(user)
39 def link_to_user(user)
40 user ? link_to(user, :controller => 'account', :action => 'show', :id => user) : 'Anonymous'
40 user ? link_to(user, :controller => 'account', :action => 'show', :id => user) : 'Anonymous'
41 end
41 end
42
42
43 def link_to_issue(issue, options={})
43 def link_to_issue(issue, options={})
44 options[:class] ||= ''
44 options[:class] ||= ''
45 options[:class] << ' issue'
45 options[:class] << ' issue'
46 options[:class] << ' closed' if issue.closed?
46 options[:class] << ' closed' if issue.closed?
47 link_to "#{issue.tracker.name} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue}, options
47 link_to "#{issue.tracker.name} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue}, options
48 end
48 end
49
49
50 # Generates a link to an attachment.
51 # Options:
52 # * :text - Link text (default to attachment filename)
53 # * :download - Force download (default: false)
54 def link_to_attachment(attachment, options={})
55 text = options.delete(:text) || attachment.filename
56 action = options.delete(:download) ? 'download' : 'show'
57
58 link_to(h(text), {:controller => 'attachments', :action => action, :id => attachment, :filename => attachment.filename }, options)
59 end
60
50 def toggle_link(name, id, options={})
61 def toggle_link(name, id, options={})
51 onclick = "Element.toggle('#{id}'); "
62 onclick = "Element.toggle('#{id}'); "
52 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
63 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
53 onclick << "return false;"
64 onclick << "return false;"
54 link_to(name, "#", :onclick => onclick)
65 link_to(name, "#", :onclick => onclick)
55 end
66 end
56
67
57 def image_to_function(name, function, html_options = {})
68 def image_to_function(name, function, html_options = {})
58 html_options.symbolize_keys!
69 html_options.symbolize_keys!
59 tag(:input, html_options.merge({
70 tag(:input, html_options.merge({
60 :type => "image", :src => image_path(name),
71 :type => "image", :src => image_path(name),
61 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
72 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
62 }))
73 }))
63 end
74 end
64
75
65 def prompt_to_remote(name, text, param, url, html_options = {})
76 def prompt_to_remote(name, text, param, url, html_options = {})
66 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
77 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
67 link_to name, {}, html_options
78 link_to name, {}, html_options
68 end
79 end
69
80
70 def format_date(date)
81 def format_date(date)
71 return nil unless date
82 return nil unless date
72 # "Setting.date_format.size < 2" is a temporary fix (content of date_format setting changed)
83 # "Setting.date_format.size < 2" is a temporary fix (content of date_format setting changed)
73 @date_format ||= (Setting.date_format.blank? || Setting.date_format.size < 2 ? l(:general_fmt_date) : Setting.date_format)
84 @date_format ||= (Setting.date_format.blank? || Setting.date_format.size < 2 ? l(:general_fmt_date) : Setting.date_format)
74 date.strftime(@date_format)
85 date.strftime(@date_format)
75 end
86 end
76
87
77 def format_time(time, include_date = true)
88 def format_time(time, include_date = true)
78 return nil unless time
89 return nil unless time
79 time = time.to_time if time.is_a?(String)
90 time = time.to_time if time.is_a?(String)
80 zone = User.current.time_zone
91 zone = User.current.time_zone
81 local = zone ? time.in_time_zone(zone) : (time.utc? ? time.utc_to_local : time)
92 local = zone ? time.in_time_zone(zone) : (time.utc? ? time.utc_to_local : time)
82 @date_format ||= (Setting.date_format.blank? || Setting.date_format.size < 2 ? l(:general_fmt_date) : Setting.date_format)
93 @date_format ||= (Setting.date_format.blank? || Setting.date_format.size < 2 ? l(:general_fmt_date) : Setting.date_format)
83 @time_format ||= (Setting.time_format.blank? ? l(:general_fmt_time) : Setting.time_format)
94 @time_format ||= (Setting.time_format.blank? ? l(:general_fmt_time) : Setting.time_format)
84 include_date ? local.strftime("#{@date_format} #{@time_format}") : local.strftime(@time_format)
95 include_date ? local.strftime("#{@date_format} #{@time_format}") : local.strftime(@time_format)
85 end
96 end
86
97
87 # Truncates and returns the string as a single line
98 # Truncates and returns the string as a single line
88 def truncate_single_line(string, *args)
99 def truncate_single_line(string, *args)
89 truncate(string, *args).gsub(%r{[\r\n]+}m, ' ')
100 truncate(string, *args).gsub(%r{[\r\n]+}m, ' ')
90 end
101 end
91
102
92 def html_hours(text)
103 def html_hours(text)
93 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>')
104 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>')
94 end
105 end
95
106
96 def authoring(created, author)
107 def authoring(created, author)
97 time_tag = content_tag('acronym', distance_of_time_in_words(Time.now, created), :title => format_time(created))
108 time_tag = content_tag('acronym', distance_of_time_in_words(Time.now, created), :title => format_time(created))
98 author_tag = (author.is_a?(User) && !author.anonymous?) ? link_to(h(author), :controller => 'account', :action => 'show', :id => author) : h(author || 'Anonymous')
109 author_tag = (author.is_a?(User) && !author.anonymous?) ? link_to(h(author), :controller => 'account', :action => 'show', :id => author) : h(author || 'Anonymous')
99 l(:label_added_time_by, author_tag, time_tag)
110 l(:label_added_time_by, author_tag, time_tag)
100 end
111 end
101
112
102 def l_or_humanize(s)
113 def l_or_humanize(s)
103 l_has_string?("label_#{s}".to_sym) ? l("label_#{s}".to_sym) : s.to_s.humanize
114 l_has_string?("label_#{s}".to_sym) ? l("label_#{s}".to_sym) : s.to_s.humanize
104 end
115 end
105
116
106 def day_name(day)
117 def day_name(day)
107 l(:general_day_names).split(',')[day-1]
118 l(:general_day_names).split(',')[day-1]
108 end
119 end
109
120
110 def month_name(month)
121 def month_name(month)
111 l(:actionview_datehelper_select_month_names).split(',')[month-1]
122 l(:actionview_datehelper_select_month_names).split(',')[month-1]
112 end
123 end
113
124
114 def syntax_highlight(name, content)
125 def syntax_highlight(name, content)
115 type = CodeRay::FileType[name]
126 type = CodeRay::FileType[name]
116 type ? CodeRay.scan(content, type).html : h(content)
127 type ? CodeRay.scan(content, type).html : h(content)
117 end
128 end
118
129
119 def to_path_param(path)
130 def to_path_param(path)
120 path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
131 path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
121 end
132 end
122
133
123 def pagination_links_full(paginator, count=nil, options={})
134 def pagination_links_full(paginator, count=nil, options={})
124 page_param = options.delete(:page_param) || :page
135 page_param = options.delete(:page_param) || :page
125 url_param = params.dup
136 url_param = params.dup
126 # don't reuse params if filters are present
137 # don't reuse params if filters are present
127 url_param.clear if url_param.has_key?(:set_filter)
138 url_param.clear if url_param.has_key?(:set_filter)
128
139
129 html = ''
140 html = ''
130 html << link_to_remote(('&#171; ' + l(:label_previous)),
141 html << link_to_remote(('&#171; ' + l(:label_previous)),
131 {:update => 'content',
142 {:update => 'content',
132 :url => url_param.merge(page_param => paginator.current.previous),
143 :url => url_param.merge(page_param => paginator.current.previous),
133 :complete => 'window.scrollTo(0,0)'},
144 :complete => 'window.scrollTo(0,0)'},
134 {:href => url_for(:params => url_param.merge(page_param => paginator.current.previous))}) + ' ' if paginator.current.previous
145 {:href => url_for(:params => url_param.merge(page_param => paginator.current.previous))}) + ' ' if paginator.current.previous
135
146
136 html << (pagination_links_each(paginator, options) do |n|
147 html << (pagination_links_each(paginator, options) do |n|
137 link_to_remote(n.to_s,
148 link_to_remote(n.to_s,
138 {:url => {:params => url_param.merge(page_param => n)},
149 {:url => {:params => url_param.merge(page_param => n)},
139 :update => 'content',
150 :update => 'content',
140 :complete => 'window.scrollTo(0,0)'},
151 :complete => 'window.scrollTo(0,0)'},
141 {:href => url_for(:params => url_param.merge(page_param => n))})
152 {:href => url_for(:params => url_param.merge(page_param => n))})
142 end || '')
153 end || '')
143
154
144 html << ' ' + link_to_remote((l(:label_next) + ' &#187;'),
155 html << ' ' + link_to_remote((l(:label_next) + ' &#187;'),
145 {:update => 'content',
156 {:update => 'content',
146 :url => url_param.merge(page_param => paginator.current.next),
157 :url => url_param.merge(page_param => paginator.current.next),
147 :complete => 'window.scrollTo(0,0)'},
158 :complete => 'window.scrollTo(0,0)'},
148 {:href => url_for(:params => url_param.merge(page_param => paginator.current.next))}) if paginator.current.next
159 {:href => url_for(:params => url_param.merge(page_param => paginator.current.next))}) if paginator.current.next
149
160
150 unless count.nil?
161 unless count.nil?
151 html << [" (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})", per_page_links(paginator.items_per_page)].compact.join(' | ')
162 html << [" (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})", per_page_links(paginator.items_per_page)].compact.join(' | ')
152 end
163 end
153
164
154 html
165 html
155 end
166 end
156
167
157 def per_page_links(selected=nil)
168 def per_page_links(selected=nil)
158 url_param = params.dup
169 url_param = params.dup
159 url_param.clear if url_param.has_key?(:set_filter)
170 url_param.clear if url_param.has_key?(:set_filter)
160
171
161 links = Setting.per_page_options_array.collect do |n|
172 links = Setting.per_page_options_array.collect do |n|
162 n == selected ? n : link_to_remote(n, {:update => "content", :url => params.dup.merge(:per_page => n)},
173 n == selected ? n : link_to_remote(n, {:update => "content", :url => params.dup.merge(:per_page => n)},
163 {:href => url_for(url_param.merge(:per_page => n))})
174 {:href => url_for(url_param.merge(:per_page => n))})
164 end
175 end
165 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
176 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
166 end
177 end
167
178
168 def breadcrumb(*args)
179 def breadcrumb(*args)
169 content_tag('p', args.join(' &#187; ') + ' &#187; ', :class => 'breadcrumb')
180 content_tag('p', args.join(' &#187; ') + ' &#187; ', :class => 'breadcrumb')
170 end
181 end
171
182
172 def html_title(*args)
183 def html_title(*args)
173 if args.empty?
184 if args.empty?
174 title = []
185 title = []
175 title << @project.name if @project
186 title << @project.name if @project
176 title += @html_title if @html_title
187 title += @html_title if @html_title
177 title << Setting.app_title
188 title << Setting.app_title
178 title.compact.join(' - ')
189 title.compact.join(' - ')
179 else
190 else
180 @html_title ||= []
191 @html_title ||= []
181 @html_title += args
192 @html_title += args
182 end
193 end
183 end
194 end
184
195
185 def accesskey(s)
196 def accesskey(s)
186 Redmine::AccessKeys.key_for s
197 Redmine::AccessKeys.key_for s
187 end
198 end
188
199
189 # Formats text according to system settings.
200 # Formats text according to system settings.
190 # 2 ways to call this method:
201 # 2 ways to call this method:
191 # * with a String: textilizable(text, options)
202 # * with a String: textilizable(text, options)
192 # * with an object and one of its attribute: textilizable(issue, :description, options)
203 # * with an object and one of its attribute: textilizable(issue, :description, options)
193 def textilizable(*args)
204 def textilizable(*args)
194 options = args.last.is_a?(Hash) ? args.pop : {}
205 options = args.last.is_a?(Hash) ? args.pop : {}
195 case args.size
206 case args.size
196 when 1
207 when 1
197 obj = nil
208 obj = nil
198 text = args.shift
209 text = args.shift
199 when 2
210 when 2
200 obj = args.shift
211 obj = args.shift
201 text = obj.send(args.shift).to_s
212 text = obj.send(args.shift).to_s
202 else
213 else
203 raise ArgumentError, 'invalid arguments to textilizable'
214 raise ArgumentError, 'invalid arguments to textilizable'
204 end
215 end
205 return '' if text.blank?
216 return '' if text.blank?
206
217
207 only_path = options.delete(:only_path) == false ? false : true
218 only_path = options.delete(:only_path) == false ? false : true
208
219
209 # when using an image link, try to use an attachment, if possible
220 # when using an image link, try to use an attachment, if possible
210 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
221 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
211
222
212 if attachments
223 if attachments
213 text = text.gsub(/!((\<|\=|\>)?(\([^\)]+\))?(\[[^\]]+\])?(\{[^\}]+\})?)(\S+\.(gif|jpg|jpeg|png))!/) do |m|
224 text = text.gsub(/!((\<|\=|\>)?(\([^\)]+\))?(\[[^\]]+\])?(\{[^\}]+\})?)(\S+\.(gif|jpg|jpeg|png))!/) do |m|
214 style = $1
225 style = $1
215 filename = $6
226 filename = $6
216 rf = Regexp.new(filename, Regexp::IGNORECASE)
227 rf = Regexp.new(filename, Regexp::IGNORECASE)
217 # search for the picture in attachments
228 # search for the picture in attachments
218 if found = attachments.detect { |att| att.filename =~ rf }
229 if found = attachments.detect { |att| att.filename =~ rf }
219 image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found
230 image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found
220 desc = found.description.to_s.gsub(/^([^\(\)]*).*$/, "\\1")
231 desc = found.description.to_s.gsub(/^([^\(\)]*).*$/, "\\1")
221 alt = desc.blank? ? nil : "(#{desc})"
232 alt = desc.blank? ? nil : "(#{desc})"
222 "!#{style}#{image_url}#{alt}!"
233 "!#{style}#{image_url}#{alt}!"
223 else
234 else
224 "!#{style}#{filename}!"
235 "!#{style}#{filename}!"
225 end
236 end
226 end
237 end
227 end
238 end
228
239
229 text = (Setting.text_formatting == 'textile') ?
240 text = (Setting.text_formatting == 'textile') ?
230 Redmine::WikiFormatting.to_html(text) { |macro, args| exec_macro(macro, obj, args) } :
241 Redmine::WikiFormatting.to_html(text) { |macro, args| exec_macro(macro, obj, args) } :
231 simple_format(auto_link(h(text)))
242 simple_format(auto_link(h(text)))
232
243
233 # different methods for formatting wiki links
244 # different methods for formatting wiki links
234 case options[:wiki_links]
245 case options[:wiki_links]
235 when :local
246 when :local
236 # used for local links to html files
247 # used for local links to html files
237 format_wiki_link = Proc.new {|project, title| "#{title}.html" }
248 format_wiki_link = Proc.new {|project, title| "#{title}.html" }
238 when :anchor
249 when :anchor
239 # used for single-file wiki export
250 # used for single-file wiki export
240 format_wiki_link = Proc.new {|project, title| "##{title}" }
251 format_wiki_link = Proc.new {|project, title| "##{title}" }
241 else
252 else
242 format_wiki_link = Proc.new {|project, title| url_for(:only_path => only_path, :controller => 'wiki', :action => 'index', :id => project, :page => title) }
253 format_wiki_link = Proc.new {|project, title| url_for(:only_path => only_path, :controller => 'wiki', :action => 'index', :id => project, :page => title) }
243 end
254 end
244
255
245 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
256 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
246
257
247 # Wiki links
258 # Wiki links
248 #
259 #
249 # Examples:
260 # Examples:
250 # [[mypage]]
261 # [[mypage]]
251 # [[mypage|mytext]]
262 # [[mypage|mytext]]
252 # wiki links can refer other project wikis, using project name or identifier:
263 # wiki links can refer other project wikis, using project name or identifier:
253 # [[project:]] -> wiki starting page
264 # [[project:]] -> wiki starting page
254 # [[project:|mytext]]
265 # [[project:|mytext]]
255 # [[project:mypage]]
266 # [[project:mypage]]
256 # [[project:mypage|mytext]]
267 # [[project:mypage|mytext]]
257 text = text.gsub(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
268 text = text.gsub(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
258 link_project = project
269 link_project = project
259 esc, all, page, title = $1, $2, $3, $5
270 esc, all, page, title = $1, $2, $3, $5
260 if esc.nil?
271 if esc.nil?
261 if page =~ /^([^\:]+)\:(.*)$/
272 if page =~ /^([^\:]+)\:(.*)$/
262 link_project = Project.find_by_name($1) || Project.find_by_identifier($1)
273 link_project = Project.find_by_name($1) || Project.find_by_identifier($1)
263 page = $2
274 page = $2
264 title ||= $1 if page.blank?
275 title ||= $1 if page.blank?
265 end
276 end
266
277
267 if link_project && link_project.wiki
278 if link_project && link_project.wiki
268 # check if page exists
279 # check if page exists
269 wiki_page = link_project.wiki.find_page(page)
280 wiki_page = link_project.wiki.find_page(page)
270 link_to((title || page), format_wiki_link.call(link_project, Wiki.titleize(page)),
281 link_to((title || page), format_wiki_link.call(link_project, Wiki.titleize(page)),
271 :class => ('wiki-page' + (wiki_page ? '' : ' new')))
282 :class => ('wiki-page' + (wiki_page ? '' : ' new')))
272 else
283 else
273 # project or wiki doesn't exist
284 # project or wiki doesn't exist
274 title || page
285 title || page
275 end
286 end
276 else
287 else
277 all
288 all
278 end
289 end
279 end
290 end
280
291
281 # Redmine links
292 # Redmine links
282 #
293 #
283 # Examples:
294 # Examples:
284 # Issues:
295 # Issues:
285 # #52 -> Link to issue #52
296 # #52 -> Link to issue #52
286 # Changesets:
297 # Changesets:
287 # r52 -> Link to revision 52
298 # r52 -> Link to revision 52
288 # commit:a85130f -> Link to scmid starting with a85130f
299 # commit:a85130f -> Link to scmid starting with a85130f
289 # Documents:
300 # Documents:
290 # document#17 -> Link to document with id 17
301 # document#17 -> Link to document with id 17
291 # document:Greetings -> Link to the document with title "Greetings"
302 # document:Greetings -> Link to the document with title "Greetings"
292 # document:"Some document" -> Link to the document with title "Some document"
303 # document:"Some document" -> Link to the document with title "Some document"
293 # Versions:
304 # Versions:
294 # version#3 -> Link to version with id 3
305 # version#3 -> Link to version with id 3
295 # version:1.0.0 -> Link to version named "1.0.0"
306 # version:1.0.0 -> Link to version named "1.0.0"
296 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
307 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
297 # Attachments:
308 # Attachments:
298 # attachment:file.zip -> Link to the attachment of the current object named file.zip
309 # attachment:file.zip -> Link to the attachment of the current object named file.zip
299 # Source files:
310 # Source files:
300 # source:some/file -> Link to the file located at /some/file in the project's repository
311 # source:some/file -> Link to the file located at /some/file in the project's repository
301 # source:some/file@52 -> Link to the file's revision 52
312 # source:some/file@52 -> Link to the file's revision 52
302 # source:some/file#L120 -> Link to line 120 of the file
313 # source:some/file#L120 -> Link to line 120 of the file
303 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
314 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
304 # export:some/file -> Force the download of the file
315 # export:some/file -> Force the download of the file
305 text = text.gsub(%r{([\s\(,\-\>]|^)(!)?(attachment|document|version|commit|source|export)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|\s|<|$)}) do |m|
316 text = text.gsub(%r{([\s\(,\-\>]|^)(!)?(attachment|document|version|commit|source|export)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|\s|<|$)}) do |m|
306 leading, esc, prefix, sep, oid = $1, $2, $3, $5 || $7, $6 || $8
317 leading, esc, prefix, sep, oid = $1, $2, $3, $5 || $7, $6 || $8
307 link = nil
318 link = nil
308 if esc.nil?
319 if esc.nil?
309 if prefix.nil? && sep == 'r'
320 if prefix.nil? && sep == 'r'
310 if project && (changeset = project.changesets.find_by_revision(oid))
321 if project && (changeset = project.changesets.find_by_revision(oid))
311 link = link_to("r#{oid}", {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => oid},
322 link = link_to("r#{oid}", {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => oid},
312 :class => 'changeset',
323 :class => 'changeset',
313 :title => truncate_single_line(changeset.comments, 100))
324 :title => truncate_single_line(changeset.comments, 100))
314 end
325 end
315 elsif sep == '#'
326 elsif sep == '#'
316 oid = oid.to_i
327 oid = oid.to_i
317 case prefix
328 case prefix
318 when nil
329 when nil
319 if issue = Issue.find_by_id(oid, :include => [:project, :status], :conditions => Project.visible_by(User.current))
330 if issue = Issue.find_by_id(oid, :include => [:project, :status], :conditions => Project.visible_by(User.current))
320 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
331 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
321 :class => (issue.closed? ? 'issue closed' : 'issue'),
332 :class => (issue.closed? ? 'issue closed' : 'issue'),
322 :title => "#{truncate(issue.subject, 100)} (#{issue.status.name})")
333 :title => "#{truncate(issue.subject, 100)} (#{issue.status.name})")
323 link = content_tag('del', link) if issue.closed?
334 link = content_tag('del', link) if issue.closed?
324 end
335 end
325 when 'document'
336 when 'document'
326 if document = Document.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
337 if document = Document.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
327 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
338 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
328 :class => 'document'
339 :class => 'document'
329 end
340 end
330 when 'version'
341 when 'version'
331 if version = Version.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
342 if version = Version.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
332 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
343 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
333 :class => 'version'
344 :class => 'version'
334 end
345 end
335 end
346 end
336 elsif sep == ':'
347 elsif sep == ':'
337 # removes the double quotes if any
348 # removes the double quotes if any
338 name = oid.gsub(%r{^"(.*)"$}, "\\1")
349 name = oid.gsub(%r{^"(.*)"$}, "\\1")
339 case prefix
350 case prefix
340 when 'document'
351 when 'document'
341 if project && document = project.documents.find_by_title(name)
352 if project && document = project.documents.find_by_title(name)
342 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
353 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
343 :class => 'document'
354 :class => 'document'
344 end
355 end
345 when 'version'
356 when 'version'
346 if project && version = project.versions.find_by_name(name)
357 if project && version = project.versions.find_by_name(name)
347 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
358 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
348 :class => 'version'
359 :class => 'version'
349 end
360 end
350 when 'commit'
361 when 'commit'
351 if project && (changeset = project.changesets.find(:first, :conditions => ["scmid LIKE ?", "#{name}%"]))
362 if project && (changeset = project.changesets.find(:first, :conditions => ["scmid LIKE ?", "#{name}%"]))
352 link = link_to h("#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
363 link = link_to h("#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
353 :class => 'changeset',
364 :class => 'changeset',
354 :title => truncate_single_line(changeset.comments, 100)
365 :title => truncate_single_line(changeset.comments, 100)
355 end
366 end
356 when 'source', 'export'
367 when 'source', 'export'
357 if project && project.repository
368 if project && project.repository
358 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
369 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
359 path, rev, anchor = $1, $3, $5
370 path, rev, anchor = $1, $3, $5
360 link = link_to h("#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project,
371 link = link_to h("#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project,
361 :path => to_path_param(path),
372 :path => to_path_param(path),
362 :rev => rev,
373 :rev => rev,
363 :anchor => anchor,
374 :anchor => anchor,
364 :format => (prefix == 'export' ? 'raw' : nil)},
375 :format => (prefix == 'export' ? 'raw' : nil)},
365 :class => (prefix == 'export' ? 'source download' : 'source')
376 :class => (prefix == 'export' ? 'source download' : 'source')
366 end
377 end
367 when 'attachment'
378 when 'attachment'
368 if attachments && attachment = attachments.detect {|a| a.filename == name }
379 if attachments && attachment = attachments.detect {|a| a.filename == name }
369 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
380 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
370 :class => 'attachment'
381 :class => 'attachment'
371 end
382 end
372 end
383 end
373 end
384 end
374 end
385 end
375 leading + (link || "#{prefix}#{sep}#{oid}")
386 leading + (link || "#{prefix}#{sep}#{oid}")
376 end
387 end
377
388
378 text
389 text
379 end
390 end
380
391
381 # Same as Rails' simple_format helper without using paragraphs
392 # Same as Rails' simple_format helper without using paragraphs
382 def simple_format_without_paragraph(text)
393 def simple_format_without_paragraph(text)
383 text.to_s.
394 text.to_s.
384 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
395 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
385 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
396 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
386 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
397 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
387 end
398 end
388
399
389 def error_messages_for(object_name, options = {})
400 def error_messages_for(object_name, options = {})
390 options = options.symbolize_keys
401 options = options.symbolize_keys
391 object = instance_variable_get("@#{object_name}")
402 object = instance_variable_get("@#{object_name}")
392 if object && !object.errors.empty?
403 if object && !object.errors.empty?
393 # build full_messages here with controller current language
404 # build full_messages here with controller current language
394 full_messages = []
405 full_messages = []
395 object.errors.each do |attr, msg|
406 object.errors.each do |attr, msg|
396 next if msg.nil?
407 next if msg.nil?
397 msg = msg.first if msg.is_a? Array
408 msg = msg.first if msg.is_a? Array
398 if attr == "base"
409 if attr == "base"
399 full_messages << l(msg)
410 full_messages << l(msg)
400 else
411 else
401 full_messages << "&#171; " + (l_has_string?("field_" + attr) ? l("field_" + attr) : object.class.human_attribute_name(attr)) + " &#187; " + l(msg) unless attr == "custom_values"
412 full_messages << "&#171; " + (l_has_string?("field_" + attr) ? l("field_" + attr) : object.class.human_attribute_name(attr)) + " &#187; " + l(msg) unless attr == "custom_values"
402 end
413 end
403 end
414 end
404 # retrieve custom values error messages
415 # retrieve custom values error messages
405 if object.errors[:custom_values]
416 if object.errors[:custom_values]
406 object.custom_values.each do |v|
417 object.custom_values.each do |v|
407 v.errors.each do |attr, msg|
418 v.errors.each do |attr, msg|
408 next if msg.nil?
419 next if msg.nil?
409 msg = msg.first if msg.is_a? Array
420 msg = msg.first if msg.is_a? Array
410 full_messages << "&#171; " + v.custom_field.name + " &#187; " + l(msg)
421 full_messages << "&#171; " + v.custom_field.name + " &#187; " + l(msg)
411 end
422 end
412 end
423 end
413 end
424 end
414 content_tag("div",
425 content_tag("div",
415 content_tag(
426 content_tag(
416 options[:header_tag] || "span", lwr(:gui_validation_error, full_messages.length) + ":"
427 options[:header_tag] || "span", lwr(:gui_validation_error, full_messages.length) + ":"
417 ) +
428 ) +
418 content_tag("ul", full_messages.collect { |msg| content_tag("li", msg) }),
429 content_tag("ul", full_messages.collect { |msg| content_tag("li", msg) }),
419 "id" => options[:id] || "errorExplanation", "class" => options[:class] || "errorExplanation"
430 "id" => options[:id] || "errorExplanation", "class" => options[:class] || "errorExplanation"
420 )
431 )
421 else
432 else
422 ""
433 ""
423 end
434 end
424 end
435 end
425
436
426 def lang_options_for_select(blank=true)
437 def lang_options_for_select(blank=true)
427 (blank ? [["(auto)", ""]] : []) +
438 (blank ? [["(auto)", ""]] : []) +
428 GLoc.valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
439 GLoc.valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
429 end
440 end
430
441
431 def label_tag_for(name, option_tags = nil, options = {})
442 def label_tag_for(name, option_tags = nil, options = {})
432 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
443 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
433 content_tag("label", label_text)
444 content_tag("label", label_text)
434 end
445 end
435
446
436 def labelled_tabular_form_for(name, object, options, &proc)
447 def labelled_tabular_form_for(name, object, options, &proc)
437 options[:html] ||= {}
448 options[:html] ||= {}
438 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
449 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
439 form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
450 form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
440 end
451 end
441
452
442 def back_url_hidden_field_tag
453 def back_url_hidden_field_tag
443 hidden_field_tag 'back_url', (params[:back_url] || request.env['HTTP_REFERER'])
454 hidden_field_tag 'back_url', (params[:back_url] || request.env['HTTP_REFERER'])
444 end
455 end
445
456
446 def check_all_links(form_name)
457 def check_all_links(form_name)
447 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
458 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
448 " | " +
459 " | " +
449 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
460 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
450 end
461 end
451
462
452 def progress_bar(pcts, options={})
463 def progress_bar(pcts, options={})
453 pcts = [pcts, pcts] unless pcts.is_a?(Array)
464 pcts = [pcts, pcts] unless pcts.is_a?(Array)
454 pcts[1] = pcts[1] - pcts[0]
465 pcts[1] = pcts[1] - pcts[0]
455 pcts << (100 - pcts[1] - pcts[0])
466 pcts << (100 - pcts[1] - pcts[0])
456 width = options[:width] || '100px;'
467 width = options[:width] || '100px;'
457 legend = options[:legend] || ''
468 legend = options[:legend] || ''
458 content_tag('table',
469 content_tag('table',
459 content_tag('tr',
470 content_tag('tr',
460 (pcts[0] > 0 ? content_tag('td', '', :width => "#{pcts[0].floor}%;", :class => 'closed') : '') +
471 (pcts[0] > 0 ? content_tag('td', '', :width => "#{pcts[0].floor}%;", :class => 'closed') : '') +
461 (pcts[1] > 0 ? content_tag('td', '', :width => "#{pcts[1].floor}%;", :class => 'done') : '') +
472 (pcts[1] > 0 ? content_tag('td', '', :width => "#{pcts[1].floor}%;", :class => 'done') : '') +
462 (pcts[2] > 0 ? content_tag('td', '', :width => "#{pcts[2].floor}%;", :class => 'todo') : '')
473 (pcts[2] > 0 ? content_tag('td', '', :width => "#{pcts[2].floor}%;", :class => 'todo') : '')
463 ), :class => 'progress', :style => "width: #{width};") +
474 ), :class => 'progress', :style => "width: #{width};") +
464 content_tag('p', legend, :class => 'pourcent')
475 content_tag('p', legend, :class => 'pourcent')
465 end
476 end
466
477
467 def context_menu_link(name, url, options={})
478 def context_menu_link(name, url, options={})
468 options[:class] ||= ''
479 options[:class] ||= ''
469 if options.delete(:selected)
480 if options.delete(:selected)
470 options[:class] << ' icon-checked disabled'
481 options[:class] << ' icon-checked disabled'
471 options[:disabled] = true
482 options[:disabled] = true
472 end
483 end
473 if options.delete(:disabled)
484 if options.delete(:disabled)
474 options.delete(:method)
485 options.delete(:method)
475 options.delete(:confirm)
486 options.delete(:confirm)
476 options.delete(:onclick)
487 options.delete(:onclick)
477 options[:class] << ' disabled'
488 options[:class] << ' disabled'
478 url = '#'
489 url = '#'
479 end
490 end
480 link_to name, url, options
491 link_to name, url, options
481 end
492 end
482
493
483 def calendar_for(field_id)
494 def calendar_for(field_id)
484 include_calendar_headers_tags
495 include_calendar_headers_tags
485 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
496 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
486 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
497 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
487 end
498 end
488
499
489 def include_calendar_headers_tags
500 def include_calendar_headers_tags
490 unless @calendar_headers_tags_included
501 unless @calendar_headers_tags_included
491 @calendar_headers_tags_included = true
502 @calendar_headers_tags_included = true
492 content_for :header_tags do
503 content_for :header_tags do
493 javascript_include_tag('calendar/calendar') +
504 javascript_include_tag('calendar/calendar') +
494 javascript_include_tag("calendar/lang/calendar-#{current_language}.js") +
505 javascript_include_tag("calendar/lang/calendar-#{current_language}.js") +
495 javascript_include_tag('calendar/calendar-setup') +
506 javascript_include_tag('calendar/calendar-setup') +
496 stylesheet_link_tag('calendar')
507 stylesheet_link_tag('calendar')
497 end
508 end
498 end
509 end
499 end
510 end
500
511
501 def wikitoolbar_for(field_id)
512 def wikitoolbar_for(field_id)
502 return '' unless Setting.text_formatting == 'textile'
513 return '' unless Setting.text_formatting == 'textile'
503
514
504 help_link = l(:setting_text_formatting) + ': ' +
515 help_link = l(:setting_text_formatting) + ': ' +
505 link_to(l(:label_help), compute_public_path('wiki_syntax', 'help', 'html'),
516 link_to(l(:label_help), compute_public_path('wiki_syntax', 'help', 'html'),
506 :onclick => "window.open(\"#{ compute_public_path('wiki_syntax', 'help', 'html') }\", \"\", \"resizable=yes, location=no, width=300, height=640, menubar=no, status=no, scrollbars=yes\"); return false;")
517 :onclick => "window.open(\"#{ compute_public_path('wiki_syntax', 'help', 'html') }\", \"\", \"resizable=yes, location=no, width=300, height=640, menubar=no, status=no, scrollbars=yes\"); return false;")
507
518
508 javascript_include_tag('jstoolbar/jstoolbar') +
519 javascript_include_tag('jstoolbar/jstoolbar') +
509 javascript_include_tag("jstoolbar/lang/jstoolbar-#{current_language}") +
520 javascript_include_tag("jstoolbar/lang/jstoolbar-#{current_language}") +
510 javascript_tag("var toolbar = new jsToolBar($('#{field_id}')); toolbar.setHelpLink('#{help_link}'); toolbar.draw();")
521 javascript_tag("var toolbar = new jsToolBar($('#{field_id}')); toolbar.setHelpLink('#{help_link}'); toolbar.draw();")
511 end
522 end
512
523
513 def content_for(name, content = nil, &block)
524 def content_for(name, content = nil, &block)
514 @has_content ||= {}
525 @has_content ||= {}
515 @has_content[name] = true
526 @has_content[name] = true
516 super(name, content, &block)
527 super(name, content, &block)
517 end
528 end
518
529
519 def has_content?(name)
530 def has_content?(name)
520 (@has_content && @has_content[name]) || false
531 (@has_content && @has_content[name]) || false
521 end
532 end
522 end
533 end
@@ -1,184 +1,184
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
2 # Copyright (C) 2006 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require 'csv'
18 require 'csv'
19
19
20 module IssuesHelper
20 module IssuesHelper
21 include ApplicationHelper
21 include ApplicationHelper
22
22
23 def render_issue_tooltip(issue)
23 def render_issue_tooltip(issue)
24 @cached_label_start_date ||= l(:field_start_date)
24 @cached_label_start_date ||= l(:field_start_date)
25 @cached_label_due_date ||= l(:field_due_date)
25 @cached_label_due_date ||= l(:field_due_date)
26 @cached_label_assigned_to ||= l(:field_assigned_to)
26 @cached_label_assigned_to ||= l(:field_assigned_to)
27 @cached_label_priority ||= l(:field_priority)
27 @cached_label_priority ||= l(:field_priority)
28
28
29 link_to_issue(issue) + ": #{h(issue.subject)}<br /><br />" +
29 link_to_issue(issue) + ": #{h(issue.subject)}<br /><br />" +
30 "<strong>#{@cached_label_start_date}</strong>: #{format_date(issue.start_date)}<br />" +
30 "<strong>#{@cached_label_start_date}</strong>: #{format_date(issue.start_date)}<br />" +
31 "<strong>#{@cached_label_due_date}</strong>: #{format_date(issue.due_date)}<br />" +
31 "<strong>#{@cached_label_due_date}</strong>: #{format_date(issue.due_date)}<br />" +
32 "<strong>#{@cached_label_assigned_to}</strong>: #{issue.assigned_to}<br />" +
32 "<strong>#{@cached_label_assigned_to}</strong>: #{issue.assigned_to}<br />" +
33 "<strong>#{@cached_label_priority}</strong>: #{issue.priority.name}"
33 "<strong>#{@cached_label_priority}</strong>: #{issue.priority.name}"
34 end
34 end
35
35
36 def sidebar_queries
36 def sidebar_queries
37 unless @sidebar_queries
37 unless @sidebar_queries
38 # User can see public queries and his own queries
38 # User can see public queries and his own queries
39 visible = ARCondition.new(["is_public = ? OR user_id = ?", true, (User.current.logged? ? User.current.id : 0)])
39 visible = ARCondition.new(["is_public = ? OR user_id = ?", true, (User.current.logged? ? User.current.id : 0)])
40 # Project specific queries and global queries
40 # Project specific queries and global queries
41 visible << (@project.nil? ? ["project_id IS NULL"] : ["project_id IS NULL OR project_id = ?", @project.id])
41 visible << (@project.nil? ? ["project_id IS NULL"] : ["project_id IS NULL OR project_id = ?", @project.id])
42 @sidebar_queries = Query.find(:all,
42 @sidebar_queries = Query.find(:all,
43 :order => "name ASC",
43 :order => "name ASC",
44 :conditions => visible.conditions)
44 :conditions => visible.conditions)
45 end
45 end
46 @sidebar_queries
46 @sidebar_queries
47 end
47 end
48
48
49 def show_detail(detail, no_html=false)
49 def show_detail(detail, no_html=false)
50 case detail.property
50 case detail.property
51 when 'attr'
51 when 'attr'
52 label = l(("field_" + detail.prop_key.to_s.gsub(/\_id$/, "")).to_sym)
52 label = l(("field_" + detail.prop_key.to_s.gsub(/\_id$/, "")).to_sym)
53 case detail.prop_key
53 case detail.prop_key
54 when 'due_date', 'start_date'
54 when 'due_date', 'start_date'
55 value = format_date(detail.value.to_date) if detail.value
55 value = format_date(detail.value.to_date) if detail.value
56 old_value = format_date(detail.old_value.to_date) if detail.old_value
56 old_value = format_date(detail.old_value.to_date) if detail.old_value
57 when 'project_id'
57 when 'project_id'
58 p = Project.find_by_id(detail.value) and value = p.name if detail.value
58 p = Project.find_by_id(detail.value) and value = p.name if detail.value
59 p = Project.find_by_id(detail.old_value) and old_value = p.name if detail.old_value
59 p = Project.find_by_id(detail.old_value) and old_value = p.name if detail.old_value
60 when 'status_id'
60 when 'status_id'
61 s = IssueStatus.find_by_id(detail.value) and value = s.name if detail.value
61 s = IssueStatus.find_by_id(detail.value) and value = s.name if detail.value
62 s = IssueStatus.find_by_id(detail.old_value) and old_value = s.name if detail.old_value
62 s = IssueStatus.find_by_id(detail.old_value) and old_value = s.name if detail.old_value
63 when 'tracker_id'
63 when 'tracker_id'
64 t = Tracker.find_by_id(detail.value) and value = t.name if detail.value
64 t = Tracker.find_by_id(detail.value) and value = t.name if detail.value
65 t = Tracker.find_by_id(detail.old_value) and old_value = t.name if detail.old_value
65 t = Tracker.find_by_id(detail.old_value) and old_value = t.name if detail.old_value
66 when 'assigned_to_id'
66 when 'assigned_to_id'
67 u = User.find_by_id(detail.value) and value = u.name if detail.value
67 u = User.find_by_id(detail.value) and value = u.name if detail.value
68 u = User.find_by_id(detail.old_value) and old_value = u.name if detail.old_value
68 u = User.find_by_id(detail.old_value) and old_value = u.name if detail.old_value
69 when 'priority_id'
69 when 'priority_id'
70 e = Enumeration.find_by_id(detail.value) and value = e.name if detail.value
70 e = Enumeration.find_by_id(detail.value) and value = e.name if detail.value
71 e = Enumeration.find_by_id(detail.old_value) and old_value = e.name if detail.old_value
71 e = Enumeration.find_by_id(detail.old_value) and old_value = e.name if detail.old_value
72 when 'category_id'
72 when 'category_id'
73 c = IssueCategory.find_by_id(detail.value) and value = c.name if detail.value
73 c = IssueCategory.find_by_id(detail.value) and value = c.name if detail.value
74 c = IssueCategory.find_by_id(detail.old_value) and old_value = c.name if detail.old_value
74 c = IssueCategory.find_by_id(detail.old_value) and old_value = c.name if detail.old_value
75 when 'fixed_version_id'
75 when 'fixed_version_id'
76 v = Version.find_by_id(detail.value) and value = v.name if detail.value
76 v = Version.find_by_id(detail.value) and value = v.name if detail.value
77 v = Version.find_by_id(detail.old_value) and old_value = v.name if detail.old_value
77 v = Version.find_by_id(detail.old_value) and old_value = v.name if detail.old_value
78 end
78 end
79 when 'cf'
79 when 'cf'
80 custom_field = CustomField.find_by_id(detail.prop_key)
80 custom_field = CustomField.find_by_id(detail.prop_key)
81 if custom_field
81 if custom_field
82 label = custom_field.name
82 label = custom_field.name
83 value = format_value(detail.value, custom_field.field_format) if detail.value
83 value = format_value(detail.value, custom_field.field_format) if detail.value
84 old_value = format_value(detail.old_value, custom_field.field_format) if detail.old_value
84 old_value = format_value(detail.old_value, custom_field.field_format) if detail.old_value
85 end
85 end
86 when 'attachment'
86 when 'attachment'
87 label = l(:label_attachment)
87 label = l(:label_attachment)
88 end
88 end
89
89
90 label ||= detail.prop_key
90 label ||= detail.prop_key
91 value ||= detail.value
91 value ||= detail.value
92 old_value ||= detail.old_value
92 old_value ||= detail.old_value
93
93
94 unless no_html
94 unless no_html
95 label = content_tag('strong', label)
95 label = content_tag('strong', label)
96 old_value = content_tag("i", h(old_value)) if detail.old_value
96 old_value = content_tag("i", h(old_value)) if detail.old_value
97 old_value = content_tag("strike", old_value) if detail.old_value and (!detail.value or detail.value.empty?)
97 old_value = content_tag("strike", old_value) if detail.old_value and (!detail.value or detail.value.empty?)
98 if detail.property == 'attachment' && !value.blank? && Attachment.find_by_id(detail.prop_key)
98 if detail.property == 'attachment' && !value.blank? && a = Attachment.find_by_id(detail.prop_key)
99 # Link to the attachment if it has not been removed
99 # Link to the attachment if it has not been removed
100 value = link_to(value, :controller => 'attachments', :action => 'show', :id => detail.prop_key)
100 value = link_to_attachment(a)
101 else
101 else
102 value = content_tag("i", h(value)) if value
102 value = content_tag("i", h(value)) if value
103 end
103 end
104 end
104 end
105
105
106 if !detail.value.blank?
106 if !detail.value.blank?
107 case detail.property
107 case detail.property
108 when 'attr', 'cf'
108 when 'attr', 'cf'
109 if !detail.old_value.blank?
109 if !detail.old_value.blank?
110 label + " " + l(:text_journal_changed, old_value, value)
110 label + " " + l(:text_journal_changed, old_value, value)
111 else
111 else
112 label + " " + l(:text_journal_set_to, value)
112 label + " " + l(:text_journal_set_to, value)
113 end
113 end
114 when 'attachment'
114 when 'attachment'
115 "#{label} #{value} #{l(:label_added)}"
115 "#{label} #{value} #{l(:label_added)}"
116 end
116 end
117 else
117 else
118 case detail.property
118 case detail.property
119 when 'attr', 'cf'
119 when 'attr', 'cf'
120 label + " " + l(:text_journal_deleted) + " (#{old_value})"
120 label + " " + l(:text_journal_deleted) + " (#{old_value})"
121 when 'attachment'
121 when 'attachment'
122 "#{label} #{old_value} #{l(:label_deleted)}"
122 "#{label} #{old_value} #{l(:label_deleted)}"
123 end
123 end
124 end
124 end
125 end
125 end
126
126
127 def issues_to_csv(issues, project = nil)
127 def issues_to_csv(issues, project = nil)
128 ic = Iconv.new(l(:general_csv_encoding), 'UTF-8')
128 ic = Iconv.new(l(:general_csv_encoding), 'UTF-8')
129 decimal_separator = l(:general_csv_decimal_separator)
129 decimal_separator = l(:general_csv_decimal_separator)
130 export = StringIO.new
130 export = StringIO.new
131 CSV::Writer.generate(export, l(:general_csv_separator)) do |csv|
131 CSV::Writer.generate(export, l(:general_csv_separator)) do |csv|
132 # csv header fields
132 # csv header fields
133 headers = [ "#",
133 headers = [ "#",
134 l(:field_status),
134 l(:field_status),
135 l(:field_project),
135 l(:field_project),
136 l(:field_tracker),
136 l(:field_tracker),
137 l(:field_priority),
137 l(:field_priority),
138 l(:field_subject),
138 l(:field_subject),
139 l(:field_assigned_to),
139 l(:field_assigned_to),
140 l(:field_category),
140 l(:field_category),
141 l(:field_fixed_version),
141 l(:field_fixed_version),
142 l(:field_author),
142 l(:field_author),
143 l(:field_start_date),
143 l(:field_start_date),
144 l(:field_due_date),
144 l(:field_due_date),
145 l(:field_done_ratio),
145 l(:field_done_ratio),
146 l(:field_estimated_hours),
146 l(:field_estimated_hours),
147 l(:field_created_on),
147 l(:field_created_on),
148 l(:field_updated_on)
148 l(:field_updated_on)
149 ]
149 ]
150 # Export project custom fields if project is given
150 # Export project custom fields if project is given
151 # otherwise export custom fields marked as "For all projects"
151 # otherwise export custom fields marked as "For all projects"
152 custom_fields = project.nil? ? IssueCustomField.for_all : project.all_issue_custom_fields
152 custom_fields = project.nil? ? IssueCustomField.for_all : project.all_issue_custom_fields
153 custom_fields.each {|f| headers << f.name}
153 custom_fields.each {|f| headers << f.name}
154 # Description in the last column
154 # Description in the last column
155 headers << l(:field_description)
155 headers << l(:field_description)
156 csv << headers.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
156 csv << headers.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
157 # csv lines
157 # csv lines
158 issues.each do |issue|
158 issues.each do |issue|
159 fields = [issue.id,
159 fields = [issue.id,
160 issue.status.name,
160 issue.status.name,
161 issue.project.name,
161 issue.project.name,
162 issue.tracker.name,
162 issue.tracker.name,
163 issue.priority.name,
163 issue.priority.name,
164 issue.subject,
164 issue.subject,
165 issue.assigned_to,
165 issue.assigned_to,
166 issue.category,
166 issue.category,
167 issue.fixed_version,
167 issue.fixed_version,
168 issue.author.name,
168 issue.author.name,
169 format_date(issue.start_date),
169 format_date(issue.start_date),
170 format_date(issue.due_date),
170 format_date(issue.due_date),
171 issue.done_ratio,
171 issue.done_ratio,
172 issue.estimated_hours.to_s.gsub('.', decimal_separator),
172 issue.estimated_hours.to_s.gsub('.', decimal_separator),
173 format_time(issue.created_on),
173 format_time(issue.created_on),
174 format_time(issue.updated_on)
174 format_time(issue.updated_on)
175 ]
175 ]
176 custom_fields.each {|f| fields << show_value(issue.custom_value_for(f)) }
176 custom_fields.each {|f| fields << show_value(issue.custom_value_for(f)) }
177 fields << issue.description
177 fields << issue.description
178 csv << fields.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
178 csv << fields.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
179 end
179 end
180 end
180 end
181 export.rewind
181 export.rewind
182 export
182 export
183 end
183 end
184 end
184 end
@@ -1,122 +1,122
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require "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
27
28 acts_as_event :title => :filename,
28 acts_as_event :title => :filename,
29 :url => Proc.new {|o| {:controller => 'attachments', :action => 'download', :id => o.id}}
29 :url => Proc.new {|o| {:controller => 'attachments', :action => 'download', :id => o.id, :filename => o.filename}}
30
30
31 cattr_accessor :storage_path
31 cattr_accessor :storage_path
32 @@storage_path = "#{RAILS_ROOT}/files"
32 @@storage_path = "#{RAILS_ROOT}/files"
33
33
34 def validate
34 def validate
35 errors.add_to_base :too_long if self.filesize > Setting.attachment_max_size.to_i.kilobytes
35 errors.add_to_base :too_long if self.filesize > Setting.attachment_max_size.to_i.kilobytes
36 end
36 end
37
37
38 def file=(incoming_file)
38 def file=(incoming_file)
39 unless incoming_file.nil?
39 unless incoming_file.nil?
40 @temp_file = incoming_file
40 @temp_file = incoming_file
41 if @temp_file.size > 0
41 if @temp_file.size > 0
42 self.filename = sanitize_filename(@temp_file.original_filename)
42 self.filename = sanitize_filename(@temp_file.original_filename)
43 self.disk_filename = Attachment.disk_filename(filename)
43 self.disk_filename = Attachment.disk_filename(filename)
44 self.content_type = @temp_file.content_type.to_s.chomp
44 self.content_type = @temp_file.content_type.to_s.chomp
45 self.filesize = @temp_file.size
45 self.filesize = @temp_file.size
46 end
46 end
47 end
47 end
48 end
48 end
49
49
50 def file
50 def file
51 nil
51 nil
52 end
52 end
53
53
54 # Copy temp file to its final location
54 # Copy temp file to its final location
55 def before_save
55 def before_save
56 if @temp_file && (@temp_file.size > 0)
56 if @temp_file && (@temp_file.size > 0)
57 logger.debug("saving '#{self.diskfile}'")
57 logger.debug("saving '#{self.diskfile}'")
58 File.open(diskfile, "wb") do |f|
58 File.open(diskfile, "wb") do |f|
59 f.write(@temp_file.read)
59 f.write(@temp_file.read)
60 end
60 end
61 self.digest = Digest::MD5.hexdigest(File.read(diskfile))
61 self.digest = Digest::MD5.hexdigest(File.read(diskfile))
62 end
62 end
63 # Don't save the content type if it's longer than the authorized length
63 # Don't save the content type if it's longer than the authorized length
64 if self.content_type && self.content_type.length > 255
64 if self.content_type && self.content_type.length > 255
65 self.content_type = nil
65 self.content_type = nil
66 end
66 end
67 end
67 end
68
68
69 # Deletes file on the disk
69 # Deletes file on the disk
70 def after_destroy
70 def after_destroy
71 File.delete(diskfile) if !filename.blank? && File.exist?(diskfile)
71 File.delete(diskfile) if !filename.blank? && File.exist?(diskfile)
72 end
72 end
73
73
74 # Returns file's location on disk
74 # Returns file's location on disk
75 def diskfile
75 def diskfile
76 "#{@@storage_path}/#{self.disk_filename}"
76 "#{@@storage_path}/#{self.disk_filename}"
77 end
77 end
78
78
79 def increment_download
79 def increment_download
80 increment!(:downloads)
80 increment!(:downloads)
81 end
81 end
82
82
83 def project
83 def project
84 container.project
84 container.project
85 end
85 end
86
86
87 def image?
87 def image?
88 self.filename =~ /\.(jpe?g|gif|png)$/i
88 self.filename =~ /\.(jpe?g|gif|png)$/i
89 end
89 end
90
90
91 def is_text?
91 def is_text?
92 Redmine::MimeType.is_type?('text', filename)
92 Redmine::MimeType.is_type?('text', filename)
93 end
93 end
94
94
95 def is_diff?
95 def is_diff?
96 self.filename =~ /\.(patch|diff)$/i
96 self.filename =~ /\.(patch|diff)$/i
97 end
97 end
98
98
99 private
99 private
100 def sanitize_filename(value)
100 def sanitize_filename(value)
101 # get only the filename, not the whole path
101 # get only the filename, not the whole path
102 just_filename = value.gsub(/^.*(\\|\/)/, '')
102 just_filename = value.gsub(/^.*(\\|\/)/, '')
103 # NOTE: File.basename doesn't work right with Windows paths on Unix
103 # NOTE: File.basename doesn't work right with Windows paths on Unix
104 # INCORRECT: just_filename = File.basename(value.gsub('\\\\', '/'))
104 # INCORRECT: just_filename = File.basename(value.gsub('\\\\', '/'))
105
105
106 # Finally, replace all non alphanumeric, hyphens or periods with underscore
106 # Finally, replace all non alphanumeric, hyphens or periods with underscore
107 @filename = just_filename.gsub(/[^\w\.\-]/,'_')
107 @filename = just_filename.gsub(/[^\w\.\-]/,'_')
108 end
108 end
109
109
110 # Returns an ASCII or hashed filename
110 # Returns an ASCII or hashed filename
111 def self.disk_filename(filename)
111 def self.disk_filename(filename)
112 df = DateTime.now.strftime("%y%m%d%H%M%S") + "_"
112 df = DateTime.now.strftime("%y%m%d%H%M%S") + "_"
113 if filename =~ %r{^[a-zA-Z0-9_\.\-]*$}
113 if filename =~ %r{^[a-zA-Z0-9_\.\-]*$}
114 df << filename
114 df << filename
115 else
115 else
116 df << Digest::MD5.hexdigest(filename)
116 df << Digest::MD5.hexdigest(filename)
117 # keep the extension if any
117 # keep the extension if any
118 df << $1 if filename =~ %r{(\.[a-zA-Z0-9]+)$}
118 df << $1 if filename =~ %r{(\.[a-zA-Z0-9]+)$}
119 end
119 end
120 df
120 df
121 end
121 end
122 end
122 end
@@ -1,18 +1,18
1 <div class="attachments">
1 <div class="attachments">
2 <% for attachment in attachments %>
2 <% for attachment in attachments %>
3 <p><%= link_to attachment.filename, {:controller => 'attachments', :action => 'show', :id => attachment }, :class => 'icon icon-attachment' -%>
3 <p><%= link_to_attachment attachment, :class => 'icon icon-attachment' -%>
4 <%= h(" - #{attachment.description}") unless attachment.description.blank? %>
4 <%= h(" - #{attachment.description}") unless attachment.description.blank? %>
5 <span class="size">(<%= number_to_human_size attachment.filesize %>)</span>
5 <span class="size">(<%= number_to_human_size attachment.filesize %>)</span>
6 <% if options[:delete_url] %>
6 <% if options[:delete_url] %>
7 <%= link_to image_tag('delete.png'), options[:delete_url].update({:attachment_id => attachment}),
7 <%= link_to image_tag('delete.png'), options[:delete_url].update({:attachment_id => attachment}),
8 :confirm => l(:text_are_you_sure),
8 :confirm => l(:text_are_you_sure),
9 :method => :post,
9 :method => :post,
10 :class => 'delete',
10 :class => 'delete',
11 :title => l(:button_delete) %>
11 :title => l(:button_delete) %>
12 <% end %>
12 <% end %>
13 <% unless options[:no_author] %>
13 <% unless options[:no_author] %>
14 <span class="author"><%= attachment.author %>, <%= format_time(attachment.created_on) %></span>
14 <span class="author"><%= attachment.author %>, <%= format_time(attachment.created_on) %></span>
15 <% end %>
15 <% end %>
16 </p>
16 </p>
17 <% end %>
17 <% end %>
18 </div>
18 </div>
@@ -1,15 +1,15
1 <h2><%=h @attachment.filename %></h2>
1 <h2><%=h @attachment.filename %></h2>
2
2
3 <div class="attachments">
3 <div class="attachments">
4 <p><%= h("#{@attachment.description} - ") unless @attachment.description.blank? %>
4 <p><%= h("#{@attachment.description} - ") unless @attachment.description.blank? %>
5 <span class="author"><%= @attachment.author %>, <%= format_time(@attachment.created_on) %></span></p>
5 <span class="author"><%= @attachment.author %>, <%= format_time(@attachment.created_on) %></span></p>
6 <p><%= link_to l(:button_download), {:controller => 'attachments', :action => 'download', :id => @attachment } -%>
6 <p><%= link_to_attachment @attachment, :text => l(:button_download), :download => true -%>
7 <span class="size">(<%= number_to_human_size @attachment.filesize %>)</span></p>
7 <span class="size">(<%= number_to_human_size @attachment.filesize %>)</span></p>
8
8
9 </div>
9 </div>
10 &nbsp;
10 &nbsp;
11 <%= render :partial => 'common/diff', :locals => {:diff => @diff, :diff_type => @diff_type} %>
11 <%= render :partial => 'common/diff', :locals => {:diff => @diff, :diff_type => @diff_type} %>
12
12
13 <% content_for :header_tags do -%>
13 <% content_for :header_tags do -%>
14 <%= stylesheet_link_tag "scm" -%>
14 <%= stylesheet_link_tag "scm" -%>
15 <% end -%>
15 <% end -%>
@@ -1,15 +1,15
1 <h2><%=h @attachment.filename %></h2>
1 <h2><%=h @attachment.filename %></h2>
2
2
3 <div class="attachments">
3 <div class="attachments">
4 <p><%= h("#{@attachment.description} - ") unless @attachment.description.blank? %>
4 <p><%= h("#{@attachment.description} - ") unless @attachment.description.blank? %>
5 <span class="author"><%= @attachment.author %>, <%= format_time(@attachment.created_on) %></span></p>
5 <span class="author"><%= @attachment.author %>, <%= format_time(@attachment.created_on) %></span></p>
6 <p><%= link_to l(:button_download), {:controller => 'attachments', :action => 'download', :id => @attachment } -%>
6 <p><%= link_to_attachment @attachment, :text => l(:button_download), :download => true -%>
7 <span class="size">(<%= number_to_human_size @attachment.filesize %>)</span></p>
7 <span class="size">(<%= number_to_human_size @attachment.filesize %>)</span></p>
8
8
9 </div>
9 </div>
10 &nbsp;
10 &nbsp;
11 <%= render :partial => 'common/file', :locals => {:content => @content, :filename => @attachment.filename} %>
11 <%= render :partial => 'common/file', :locals => {:content => @content, :filename => @attachment.filename} %>
12
12
13 <% content_for :header_tags do -%>
13 <% content_for :header_tags do -%>
14 <%= stylesheet_link_tag "scm" -%>
14 <%= stylesheet_link_tag "scm" -%>
15 <% end -%>
15 <% end -%>
@@ -1,45 +1,44
1 <div class="contextual">
1 <div class="contextual">
2 <%= link_to_if_authorized l(:label_attachment_new), {:controller => 'projects', :action => 'add_file', :id => @project}, :class => 'icon icon-add' %>
2 <%= link_to_if_authorized l(:label_attachment_new), {:controller => 'projects', :action => 'add_file', :id => @project}, :class => 'icon icon-add' %>
3 </div>
3 </div>
4
4
5 <h2><%=l(:label_attachment_plural)%></h2>
5 <h2><%=l(:label_attachment_plural)%></h2>
6
6
7 <% delete_allowed = authorize_for('versions', 'destroy_file') %>
7 <% delete_allowed = authorize_for('versions', 'destroy_file') %>
8
8
9 <table class="list">
9 <table class="list">
10 <thead><tr>
10 <thead><tr>
11 <th><%=l(:field_version)%></th>
11 <th><%=l(:field_version)%></th>
12 <%= sort_header_tag("#{Attachment.table_name}.filename", :caption => l(:field_filename)) %>
12 <%= sort_header_tag("#{Attachment.table_name}.filename", :caption => l(:field_filename)) %>
13 <%= sort_header_tag("#{Attachment.table_name}.created_on", :caption => l(:label_date), :default_order => 'desc') %>
13 <%= sort_header_tag("#{Attachment.table_name}.created_on", :caption => l(:label_date), :default_order => 'desc') %>
14 <%= sort_header_tag("#{Attachment.table_name}.filesize", :caption => l(:field_filesize), :default_order => 'desc') %>
14 <%= sort_header_tag("#{Attachment.table_name}.filesize", :caption => l(:field_filesize), :default_order => 'desc') %>
15 <%= sort_header_tag("#{Attachment.table_name}.downloads", :caption => l(:label_downloads_abbr), :default_order => 'desc') %>
15 <%= sort_header_tag("#{Attachment.table_name}.downloads", :caption => l(:label_downloads_abbr), :default_order => 'desc') %>
16 <th>MD5</th>
16 <th>MD5</th>
17 <% if delete_allowed %><th></th><% end %>
17 <% if delete_allowed %><th></th><% end %>
18 </tr></thead>
18 </tr></thead>
19 <tbody>
19 <tbody>
20 <% for version in @versions %>
20 <% for version in @versions %>
21 <% unless version.attachments.empty? %>
21 <% unless version.attachments.empty? %>
22 <tr><th colspan="7" align="left"><span class="icon icon-package"><b><%= version.name %></b></span></th></tr>
22 <tr><th colspan="7" align="left"><span class="icon icon-package"><b><%= version.name %></b></span></th></tr>
23 <% for file in version.attachments %>
23 <% for file in version.attachments %>
24 <tr class="<%= cycle("odd", "even") %>">
24 <tr class="<%= cycle("odd", "even") %>">
25 <td></td>
25 <td></td>
26 <td><%= link_to(h(file.filename), {:controller => 'attachments', :action => 'download', :id => file},
26 <td><%= link_to_attachment file, :download => true, :title => file.description %></td>
27 :title => file.description) %></td>
28 <td align="center"><%= format_time(file.created_on) %></td>
27 <td align="center"><%= format_time(file.created_on) %></td>
29 <td align="center"><%= number_to_human_size(file.filesize) %></td>
28 <td align="center"><%= number_to_human_size(file.filesize) %></td>
30 <td align="center"><%= file.downloads %></td>
29 <td align="center"><%= file.downloads %></td>
31 <td align="center"><small><%= file.digest %></small></td>
30 <td align="center"><small><%= file.digest %></small></td>
32 <% if delete_allowed %>
31 <% if delete_allowed %>
33 <td align="center">
32 <td align="center">
34 <%= link_to_if_authorized image_tag('delete.png'), {:controller => 'versions', :action => 'destroy_file', :id => version, :attachment_id => file}, :confirm => l(:text_are_you_sure), :method => :post %>
33 <%= link_to_if_authorized image_tag('delete.png'), {:controller => 'versions', :action => 'destroy_file', :id => version, :attachment_id => file}, :confirm => l(:text_are_you_sure), :method => :post %>
35 </td>
34 </td>
36 <% end %>
35 <% end %>
37 </tr>
36 </tr>
38 <% end
37 <% end
39 reset_cycle %>
38 reset_cycle %>
40 <% end %>
39 <% end %>
41 <% end %>
40 <% end %>
42 </tbody>
41 </tbody>
43 </table>
42 </table>
44
43
45 <% html_title(l(:label_attachment_plural)) -%>
44 <% html_title(l(:label_attachment_plural)) -%>
@@ -1,44 +1,46
1 ActionController::Routing::Routes.draw do |map|
1 ActionController::Routing::Routes.draw do |map|
2 # Add your own custom routes here.
2 # Add your own custom routes here.
3 # The priority is based upon order of creation: first created -> highest priority.
3 # The priority is based upon order of creation: first created -> highest priority.
4
4
5 # Here's a sample route:
5 # Here's a sample route:
6 # map.connect 'products/:id', :controller => 'catalog', :action => 'view'
6 # map.connect 'products/:id', :controller => 'catalog', :action => 'view'
7 # Keep in mind you can assign values other than :controller and :action
7 # Keep in mind you can assign values other than :controller and :action
8
8
9 map.home '', :controller => 'welcome'
9 map.home '', :controller => 'welcome'
10 map.signin 'login', :controller => 'account', :action => 'login'
10 map.signin 'login', :controller => 'account', :action => 'login'
11 map.signout 'logout', :controller => 'account', :action => 'logout'
11 map.signout 'logout', :controller => 'account', :action => 'logout'
12
12
13 map.connect 'wiki/:id/:page/:action', :controller => 'wiki', :page => nil
13 map.connect 'wiki/:id/:page/:action', :controller => 'wiki', :page => nil
14 map.connect 'roles/workflow/:id/:role_id/:tracker_id', :controller => 'roles', :action => 'workflow'
14 map.connect 'roles/workflow/:id/:role_id/:tracker_id', :controller => 'roles', :action => 'workflow'
15 map.connect 'help/:ctrl/:page', :controller => 'help'
15 map.connect 'help/:ctrl/:page', :controller => 'help'
16 #map.connect ':controller/:action/:id/:sort_key/:sort_order'
16 #map.connect ':controller/:action/:id/:sort_key/:sort_order'
17
17
18 map.connect 'issues/:issue_id/relations/:action/:id', :controller => 'issue_relations'
18 map.connect 'issues/:issue_id/relations/:action/:id', :controller => 'issue_relations'
19 map.connect 'projects/:project_id/issues/:action', :controller => 'issues'
19 map.connect 'projects/:project_id/issues/:action', :controller => 'issues'
20 map.connect 'projects/:project_id/news/:action', :controller => 'news'
20 map.connect 'projects/:project_id/news/:action', :controller => 'news'
21 map.connect 'projects/:project_id/documents/:action', :controller => 'documents'
21 map.connect 'projects/:project_id/documents/:action', :controller => 'documents'
22 map.connect 'projects/:project_id/boards/:action/:id', :controller => 'boards'
22 map.connect 'projects/:project_id/boards/:action/:id', :controller => 'boards'
23 map.connect 'projects/:project_id/timelog/:action/:id', :controller => 'timelog'
23 map.connect 'projects/:project_id/timelog/:action/:id', :controller => 'timelog'
24 map.connect 'boards/:board_id/topics/:action/:id', :controller => 'messages'
24 map.connect 'boards/:board_id/topics/:action/:id', :controller => 'messages'
25
25
26 map.with_options :controller => 'repositories' do |omap|
26 map.with_options :controller => 'repositories' do |omap|
27 omap.repositories_show 'repositories/browse/:id/*path', :action => 'browse'
27 omap.repositories_show 'repositories/browse/:id/*path', :action => 'browse'
28 omap.repositories_changes 'repositories/changes/:id/*path', :action => 'changes'
28 omap.repositories_changes 'repositories/changes/:id/*path', :action => 'changes'
29 omap.repositories_diff 'repositories/diff/:id/*path', :action => 'diff'
29 omap.repositories_diff 'repositories/diff/:id/*path', :action => 'diff'
30 omap.repositories_entry 'repositories/entry/:id/*path', :action => 'entry'
30 omap.repositories_entry 'repositories/entry/:id/*path', :action => 'entry'
31 omap.repositories_entry 'repositories/annotate/:id/*path', :action => 'annotate'
31 omap.repositories_entry 'repositories/annotate/:id/*path', :action => 'annotate'
32 omap.repositories_revision 'repositories/revision/:id/:rev', :action => 'revision'
32 omap.repositories_revision 'repositories/revision/:id/:rev', :action => 'revision'
33 end
33 end
34
34
35 map.connect 'attachments/:id', :controller => 'attachments', :action => 'show'
35 map.connect 'attachments/:id', :controller => 'attachments', :action => 'show', :id => /\d+/
36 map.connect 'attachments/:id/:filename', :controller => 'attachments', :action => 'show', :id => /\d+/, :filename => /.*/
37 map.connect 'attachments/download/:id/:filename', :controller => 'attachments', :action => 'download', :id => /\d+/, :filename => /.*/
36
38
37 # Allow downloading Web Service WSDL as a file with an extension
39 # Allow downloading Web Service WSDL as a file with an extension
38 # instead of a file named 'wsdl'
40 # instead of a file named 'wsdl'
39 map.connect ':controller/service.wsdl', :action => 'wsdl'
41 map.connect ':controller/service.wsdl', :action => 'wsdl'
40
42
41
43
42 # Install the default route as the lowest priority.
44 # Install the default route as the lowest priority.
43 map.connect ':controller/:action/:id'
45 map.connect ':controller/:action/:id'
44 end
46 end
@@ -1,64 +1,79
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 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 File.dirname(__FILE__) + '/../test_helper'
18 require File.dirname(__FILE__) + '/../test_helper'
19 require 'attachments_controller'
19 require 'attachments_controller'
20
20
21 # Re-raise errors caught by the controller.
21 # Re-raise errors caught by the controller.
22 class AttachmentsController; def rescue_action(e) raise e end; end
22 class AttachmentsController; def rescue_action(e) raise e end; end
23
23
24
24
25 class AttachmentsControllerTest < Test::Unit::TestCase
25 class AttachmentsControllerTest < Test::Unit::TestCase
26 fixtures :users, :projects, :issues, :attachments
26 fixtures :users, :projects, :issues, :attachments
27
27
28 def setup
28 def setup
29 @controller = AttachmentsController.new
29 @controller = AttachmentsController.new
30 @request = ActionController::TestRequest.new
30 @request = ActionController::TestRequest.new
31 @response = ActionController::TestResponse.new
31 @response = ActionController::TestResponse.new
32 Attachment.storage_path = "#{RAILS_ROOT}/test/fixtures/files"
32 Attachment.storage_path = "#{RAILS_ROOT}/test/fixtures/files"
33 User.current = nil
33 User.current = nil
34 end
34 end
35
35
36 def test_routing
37 assert_routing('/attachments/1', :controller => 'attachments', :action => 'show', :id => '1')
38 assert_routing('/attachments/1/filename.ext', :controller => 'attachments', :action => 'show', :id => '1', :filename => 'filename.ext')
39 assert_routing('/attachments/download/1', :controller => 'attachments', :action => 'download', :id => '1')
40 assert_routing('/attachments/download/1/filename.ext', :controller => 'attachments', :action => 'download', :id => '1', :filename => 'filename.ext')
41 end
42
43 def test_recognizes
44 assert_recognizes({:controller => 'attachments', :action => 'show', :id => '1'}, '/attachments/1')
45 assert_recognizes({:controller => 'attachments', :action => 'show', :id => '1'}, '/attachments/show/1')
46 assert_recognizes({:controller => 'attachments', :action => 'show', :id => '1', :filename => 'filename.ext'}, '/attachments/1/filename.ext')
47 assert_recognizes({:controller => 'attachments', :action => 'download', :id => '1'}, '/attachments/download/1')
48 assert_recognizes({:controller => 'attachments', :action => 'download', :id => '1', :filename => 'filename.ext'},'/attachments/download/1/filename.ext')
49 end
50
36 def test_show_diff
51 def test_show_diff
37 get :show, :id => 5
52 get :show, :id => 5
38 assert_response :success
53 assert_response :success
39 assert_template 'diff'
54 assert_template 'diff'
40 end
55 end
41
56
42 def test_show_text_file
57 def test_show_text_file
43 get :show, :id => 4
58 get :show, :id => 4
44 assert_response :success
59 assert_response :success
45 assert_template 'file'
60 assert_template 'file'
46 end
61 end
47
62
48 def test_show_other
63 def test_show_other
49 get :show, :id => 6
64 get :show, :id => 6
50 assert_response :success
65 assert_response :success
51 assert_equal 'application/octet-stream', @response.content_type
66 assert_equal 'application/octet-stream', @response.content_type
52 end
67 end
53
68
54 def test_download_text_file
69 def test_download_text_file
55 get :download, :id => 4
70 get :download, :id => 4
56 assert_response :success
71 assert_response :success
57 assert_equal 'application/x-ruby', @response.content_type
72 assert_equal 'application/x-ruby', @response.content_type
58 end
73 end
59
74
60 def test_anonymous_on_private_private
75 def test_anonymous_on_private_private
61 get :download, :id => 7
76 get :download, :id => 7
62 assert_redirected_to 'account/login'
77 assert_redirected_to 'account/login'
63 end
78 end
64 end
79 end
General Comments 0
You need to be logged in to leave comments. Login now