##// END OF EJS Templates
Adds a macro for inserting thumbnails in formatted text (#3510)....
Jean-Philippe Lang -
r9830:537be80be26c
parent child
Show More
@@ -1,139 +1,139
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 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 before_filter :find_project, :except => :upload
19 before_filter :find_project, :except => :upload
20 before_filter :file_readable, :read_authorize, :only => [:show, :download, :thumbnail]
20 before_filter :file_readable, :read_authorize, :only => [:show, :download, :thumbnail]
21 before_filter :delete_authorize, :only => :destroy
21 before_filter :delete_authorize, :only => :destroy
22 before_filter :authorize_global, :only => :upload
22 before_filter :authorize_global, :only => :upload
23
23
24 accept_api_auth :show, :download, :upload
24 accept_api_auth :show, :download, :upload
25
25
26 def show
26 def show
27 respond_to do |format|
27 respond_to do |format|
28 format.html {
28 format.html {
29 if @attachment.is_diff?
29 if @attachment.is_diff?
30 @diff = File.new(@attachment.diskfile, "rb").read
30 @diff = File.new(@attachment.diskfile, "rb").read
31 @diff_type = params[:type] || User.current.pref[:diff_type] || 'inline'
31 @diff_type = params[:type] || User.current.pref[:diff_type] || 'inline'
32 @diff_type = 'inline' unless %w(inline sbs).include?(@diff_type)
32 @diff_type = 'inline' unless %w(inline sbs).include?(@diff_type)
33 # Save diff type as user preference
33 # Save diff type as user preference
34 if User.current.logged? && @diff_type != User.current.pref[:diff_type]
34 if User.current.logged? && @diff_type != User.current.pref[:diff_type]
35 User.current.pref[:diff_type] = @diff_type
35 User.current.pref[:diff_type] = @diff_type
36 User.current.preference.save
36 User.current.preference.save
37 end
37 end
38 render :action => 'diff'
38 render :action => 'diff'
39 elsif @attachment.is_text? && @attachment.filesize <= Setting.file_max_size_displayed.to_i.kilobyte
39 elsif @attachment.is_text? && @attachment.filesize <= Setting.file_max_size_displayed.to_i.kilobyte
40 @content = File.new(@attachment.diskfile, "rb").read
40 @content = File.new(@attachment.diskfile, "rb").read
41 render :action => 'file'
41 render :action => 'file'
42 else
42 else
43 download
43 download
44 end
44 end
45 }
45 }
46 format.api
46 format.api
47 end
47 end
48 end
48 end
49
49
50 def download
50 def download
51 if @attachment.container.is_a?(Version) || @attachment.container.is_a?(Project)
51 if @attachment.container.is_a?(Version) || @attachment.container.is_a?(Project)
52 @attachment.increment_download
52 @attachment.increment_download
53 end
53 end
54
54
55 if stale?(:etag => @attachment.digest)
55 if stale?(:etag => @attachment.digest)
56 # images are sent inline
56 # images are sent inline
57 send_file @attachment.diskfile, :filename => filename_for_content_disposition(@attachment.filename),
57 send_file @attachment.diskfile, :filename => filename_for_content_disposition(@attachment.filename),
58 :type => detect_content_type(@attachment),
58 :type => detect_content_type(@attachment),
59 :disposition => (@attachment.image? ? 'inline' : 'attachment')
59 :disposition => (@attachment.image? ? 'inline' : 'attachment')
60 end
60 end
61 end
61 end
62
62
63 def thumbnail
63 def thumbnail
64 if @attachment.thumbnailable? && Setting.thumbnails_enabled? && thumbnail = @attachment.thumbnail
64 if @attachment.thumbnailable? && thumbnail = @attachment.thumbnail(:size => params[:size])
65 if stale?(:etag => thumbnail)
65 if stale?(:etag => thumbnail)
66 send_file thumbnail,
66 send_file thumbnail,
67 :filename => filename_for_content_disposition(@attachment.filename),
67 :filename => filename_for_content_disposition(@attachment.filename),
68 :type => detect_content_type(@attachment),
68 :type => detect_content_type(@attachment),
69 :disposition => 'inline'
69 :disposition => 'inline'
70 end
70 end
71 else
71 else
72 # No thumbnail for the attachment or thumbnail could not be created
72 # No thumbnail for the attachment or thumbnail could not be created
73 render :nothing => true, :status => 404
73 render :nothing => true, :status => 404
74 end
74 end
75 end
75 end
76
76
77 def upload
77 def upload
78 # Make sure that API users get used to set this content type
78 # Make sure that API users get used to set this content type
79 # as it won't trigger Rails' automatic parsing of the request body for parameters
79 # as it won't trigger Rails' automatic parsing of the request body for parameters
80 unless request.content_type == 'application/octet-stream'
80 unless request.content_type == 'application/octet-stream'
81 render :nothing => true, :status => 406
81 render :nothing => true, :status => 406
82 return
82 return
83 end
83 end
84
84
85 @attachment = Attachment.new(:file => request.raw_post)
85 @attachment = Attachment.new(:file => request.raw_post)
86 @attachment.author = User.current
86 @attachment.author = User.current
87 @attachment.filename = Redmine::Utils.random_hex(16)
87 @attachment.filename = Redmine::Utils.random_hex(16)
88
88
89 if @attachment.save
89 if @attachment.save
90 respond_to do |format|
90 respond_to do |format|
91 format.api { render :action => 'upload', :status => :created }
91 format.api { render :action => 'upload', :status => :created }
92 end
92 end
93 else
93 else
94 respond_to do |format|
94 respond_to do |format|
95 format.api { render_validation_errors(@attachment) }
95 format.api { render_validation_errors(@attachment) }
96 end
96 end
97 end
97 end
98 end
98 end
99
99
100 def destroy
100 def destroy
101 if @attachment.container.respond_to?(:init_journal)
101 if @attachment.container.respond_to?(:init_journal)
102 @attachment.container.init_journal(User.current)
102 @attachment.container.init_journal(User.current)
103 end
103 end
104 # Make sure association callbacks are called
104 # Make sure association callbacks are called
105 @attachment.container.attachments.delete(@attachment)
105 @attachment.container.attachments.delete(@attachment)
106 redirect_to_referer_or project_path(@project)
106 redirect_to_referer_or project_path(@project)
107 end
107 end
108
108
109 private
109 private
110 def find_project
110 def find_project
111 @attachment = Attachment.find(params[:id])
111 @attachment = Attachment.find(params[:id])
112 # Show 404 if the filename in the url is wrong
112 # Show 404 if the filename in the url is wrong
113 raise ActiveRecord::RecordNotFound if params[:filename] && params[:filename] != @attachment.filename
113 raise ActiveRecord::RecordNotFound if params[:filename] && params[:filename] != @attachment.filename
114 @project = @attachment.project
114 @project = @attachment.project
115 rescue ActiveRecord::RecordNotFound
115 rescue ActiveRecord::RecordNotFound
116 render_404
116 render_404
117 end
117 end
118
118
119 # Checks that the file exists and is readable
119 # Checks that the file exists and is readable
120 def file_readable
120 def file_readable
121 @attachment.readable? ? true : render_404
121 @attachment.readable? ? true : render_404
122 end
122 end
123
123
124 def read_authorize
124 def read_authorize
125 @attachment.visible? ? true : deny_access
125 @attachment.visible? ? true : deny_access
126 end
126 end
127
127
128 def delete_authorize
128 def delete_authorize
129 @attachment.deletable? ? true : deny_access
129 @attachment.deletable? ? true : deny_access
130 end
130 end
131
131
132 def detect_content_type(attachment)
132 def detect_content_type(attachment)
133 content_type = attachment.content_type
133 content_type = attachment.content_type
134 if content_type.blank?
134 if content_type.blank?
135 content_type = Redmine::MimeType.of(attachment.filename)
135 content_type = Redmine::MimeType.of(attachment.filename)
136 end
136 end
137 content_type.to_s
137 content_type.to_s
138 end
138 end
139 end
139 end
@@ -1,279 +1,287
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 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 :filename, :author
24 validates_presence_of :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 validates_length_of :description, :maximum => 255
27 validates_length_of :description, :maximum => 255
28 validate :validate_max_file_size
28 validate :validate_max_file_size
29
29
30 acts_as_event :title => :filename,
30 acts_as_event :title => :filename,
31 :url => Proc.new {|o| {:controller => 'attachments', :action => 'download', :id => o.id, :filename => o.filename}}
31 :url => Proc.new {|o| {:controller => 'attachments', :action => 'download', :id => o.id, :filename => o.filename}}
32
32
33 acts_as_activity_provider :type => 'files',
33 acts_as_activity_provider :type => 'files',
34 :permission => :view_files,
34 :permission => :view_files,
35 :author_key => :author_id,
35 :author_key => :author_id,
36 :find_options => {:select => "#{Attachment.table_name}.*",
36 :find_options => {:select => "#{Attachment.table_name}.*",
37 :joins => "LEFT JOIN #{Version.table_name} ON #{Attachment.table_name}.container_type='Version' AND #{Version.table_name}.id = #{Attachment.table_name}.container_id " +
37 :joins => "LEFT JOIN #{Version.table_name} ON #{Attachment.table_name}.container_type='Version' AND #{Version.table_name}.id = #{Attachment.table_name}.container_id " +
38 "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 "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 )"}
39
39
40 acts_as_activity_provider :type => 'documents',
40 acts_as_activity_provider :type => 'documents',
41 :permission => :view_documents,
41 :permission => :view_documents,
42 :author_key => :author_id,
42 :author_key => :author_id,
43 :find_options => {:select => "#{Attachment.table_name}.*",
43 :find_options => {:select => "#{Attachment.table_name}.*",
44 :joins => "LEFT JOIN #{Document.table_name} ON #{Attachment.table_name}.container_type='Document' AND #{Document.table_name}.id = #{Attachment.table_name}.container_id " +
44 :joins => "LEFT JOIN #{Document.table_name} ON #{Attachment.table_name}.container_type='Document' AND #{Document.table_name}.id = #{Attachment.table_name}.container_id " +
45 "LEFT JOIN #{Project.table_name} ON #{Document.table_name}.project_id = #{Project.table_name}.id"}
45 "LEFT JOIN #{Project.table_name} ON #{Document.table_name}.project_id = #{Project.table_name}.id"}
46
46
47 cattr_accessor :storage_path
47 cattr_accessor :storage_path
48 @@storage_path = Redmine::Configuration['attachments_storage_path'] || File.join(Rails.root, "files")
48 @@storage_path = Redmine::Configuration['attachments_storage_path'] || File.join(Rails.root, "files")
49
49
50 cattr_accessor :thumbnails_storage_path
50 cattr_accessor :thumbnails_storage_path
51 @@thumbnails_storage_path = File.join(Rails.root, "tmp", "thumbnails")
51 @@thumbnails_storage_path = File.join(Rails.root, "tmp", "thumbnails")
52
52
53 before_save :files_to_final_location
53 before_save :files_to_final_location
54 after_destroy :delete_from_disk
54 after_destroy :delete_from_disk
55
55
56 # Returns an unsaved copy of the attachment
56 # Returns an unsaved copy of the attachment
57 def copy(attributes=nil)
57 def copy(attributes=nil)
58 copy = self.class.new
58 copy = self.class.new
59 copy.attributes = self.attributes.dup.except("id", "downloads")
59 copy.attributes = self.attributes.dup.except("id", "downloads")
60 copy.attributes = attributes if attributes
60 copy.attributes = attributes if attributes
61 copy
61 copy
62 end
62 end
63
63
64 def validate_max_file_size
64 def validate_max_file_size
65 if @temp_file && self.filesize > Setting.attachment_max_size.to_i.kilobytes
65 if @temp_file && self.filesize > Setting.attachment_max_size.to_i.kilobytes
66 errors.add(:base, l(:error_attachment_too_big, :max_size => Setting.attachment_max_size.to_i.kilobytes))
66 errors.add(:base, l(:error_attachment_too_big, :max_size => Setting.attachment_max_size.to_i.kilobytes))
67 end
67 end
68 end
68 end
69
69
70 def file=(incoming_file)
70 def file=(incoming_file)
71 unless incoming_file.nil?
71 unless incoming_file.nil?
72 @temp_file = incoming_file
72 @temp_file = incoming_file
73 if @temp_file.size > 0
73 if @temp_file.size > 0
74 if @temp_file.respond_to?(:original_filename)
74 if @temp_file.respond_to?(:original_filename)
75 self.filename = @temp_file.original_filename
75 self.filename = @temp_file.original_filename
76 self.filename.force_encoding("UTF-8") if filename.respond_to?(:force_encoding)
76 self.filename.force_encoding("UTF-8") if filename.respond_to?(:force_encoding)
77 end
77 end
78 if @temp_file.respond_to?(:content_type)
78 if @temp_file.respond_to?(:content_type)
79 self.content_type = @temp_file.content_type.to_s.chomp
79 self.content_type = @temp_file.content_type.to_s.chomp
80 end
80 end
81 if content_type.blank? && filename.present?
81 if content_type.blank? && filename.present?
82 self.content_type = Redmine::MimeType.of(filename)
82 self.content_type = Redmine::MimeType.of(filename)
83 end
83 end
84 self.filesize = @temp_file.size
84 self.filesize = @temp_file.size
85 end
85 end
86 end
86 end
87 end
87 end
88
88
89 def file
89 def file
90 nil
90 nil
91 end
91 end
92
92
93 def filename=(arg)
93 def filename=(arg)
94 write_attribute :filename, sanitize_filename(arg.to_s)
94 write_attribute :filename, sanitize_filename(arg.to_s)
95 if new_record? && disk_filename.blank?
95 if new_record? && disk_filename.blank?
96 self.disk_filename = Attachment.disk_filename(filename)
96 self.disk_filename = Attachment.disk_filename(filename)
97 end
97 end
98 filename
98 filename
99 end
99 end
100
100
101 # Copies the temporary file to its final location
101 # Copies the temporary file to its final location
102 # and computes its MD5 hash
102 # and computes its MD5 hash
103 def files_to_final_location
103 def files_to_final_location
104 if @temp_file && (@temp_file.size > 0)
104 if @temp_file && (@temp_file.size > 0)
105 logger.info("Saving attachment '#{self.diskfile}' (#{@temp_file.size} bytes)")
105 logger.info("Saving attachment '#{self.diskfile}' (#{@temp_file.size} bytes)")
106 md5 = Digest::MD5.new
106 md5 = Digest::MD5.new
107 File.open(diskfile, "wb") do |f|
107 File.open(diskfile, "wb") do |f|
108 if @temp_file.respond_to?(:read)
108 if @temp_file.respond_to?(:read)
109 buffer = ""
109 buffer = ""
110 while (buffer = @temp_file.read(8192))
110 while (buffer = @temp_file.read(8192))
111 f.write(buffer)
111 f.write(buffer)
112 md5.update(buffer)
112 md5.update(buffer)
113 end
113 end
114 else
114 else
115 f.write(@temp_file)
115 f.write(@temp_file)
116 md5.update(@temp_file)
116 md5.update(@temp_file)
117 end
117 end
118 end
118 end
119 self.digest = md5.hexdigest
119 self.digest = md5.hexdigest
120 end
120 end
121 @temp_file = nil
121 @temp_file = nil
122 # Don't save the content type if it's longer than the authorized length
122 # Don't save the content type if it's longer than the authorized length
123 if self.content_type && self.content_type.length > 255
123 if self.content_type && self.content_type.length > 255
124 self.content_type = nil
124 self.content_type = nil
125 end
125 end
126 end
126 end
127
127
128 # Deletes the file from the file system if it's not referenced by other attachments
128 # Deletes the file from the file system if it's not referenced by other attachments
129 def delete_from_disk
129 def delete_from_disk
130 if Attachment.where("disk_filename = ? AND id <> ?", disk_filename, id).empty?
130 if Attachment.where("disk_filename = ? AND id <> ?", disk_filename, id).empty?
131 delete_from_disk!
131 delete_from_disk!
132 end
132 end
133 end
133 end
134
134
135 # Returns file's location on disk
135 # Returns file's location on disk
136 def diskfile
136 def diskfile
137 File.join(self.class.storage_path, disk_filename.to_s)
137 File.join(self.class.storage_path, disk_filename.to_s)
138 end
138 end
139
139
140 def title
140 def title
141 title = filename.to_s
141 title = filename.to_s
142 if description.present?
142 if description.present?
143 title << " (#{description})"
143 title << " (#{description})"
144 end
144 end
145 title
145 title
146 end
146 end
147
147
148 def increment_download
148 def increment_download
149 increment!(:downloads)
149 increment!(:downloads)
150 end
150 end
151
151
152 def project
152 def project
153 container.try(:project)
153 container.try(:project)
154 end
154 end
155
155
156 def visible?(user=User.current)
156 def visible?(user=User.current)
157 container && container.attachments_visible?(user)
157 container && container.attachments_visible?(user)
158 end
158 end
159
159
160 def deletable?(user=User.current)
160 def deletable?(user=User.current)
161 container && container.attachments_deletable?(user)
161 container && container.attachments_deletable?(user)
162 end
162 end
163
163
164 def image?
164 def image?
165 !!(self.filename =~ /\.(bmp|gif|jpg|jpe|jpeg|png)$/i)
165 !!(self.filename =~ /\.(bmp|gif|jpg|jpe|jpeg|png)$/i)
166 end
166 end
167
167
168 def thumbnailable?
168 def thumbnailable?
169 image?
169 image?
170 end
170 end
171
171
172 # Returns the full path the attachment thumbnail, or nil
172 # Returns the full path the attachment thumbnail, or nil
173 # if the thumbnail cannot be generated.
173 # if the thumbnail cannot be generated.
174 def thumbnail
174 def thumbnail(options={})
175 if thumbnailable? && readable?
175 if thumbnailable? && readable?
176 size = Setting.thumbnails_size.to_i
176 size = options[:size].to_i
177 if size > 0
178 # Limit the number of thumbnails per image
179 size = (size / 50) * 50
180 # Maximum thumbnail size
181 size = 800 if size > 800
182 else
183 size = Setting.thumbnails_size.to_i
184 end
177 size = 100 unless size > 0
185 size = 100 unless size > 0
178 target = File.join(self.class.thumbnails_storage_path, "#{id}_#{digest}_#{size}.thumb")
186 target = File.join(self.class.thumbnails_storage_path, "#{id}_#{digest}_#{size}.thumb")
179
187
180 begin
188 begin
181 Redmine::Thumbnail.generate(self.diskfile, target, size)
189 Redmine::Thumbnail.generate(self.diskfile, target, size)
182 rescue => e
190 rescue => e
183 logger.error "An error occured while generating thumbnail for #{disk_filename} to #{target}\nException was: #{e.message}" if logger
191 logger.error "An error occured while generating thumbnail for #{disk_filename} to #{target}\nException was: #{e.message}" if logger
184 return nil
192 return nil
185 end
193 end
186 end
194 end
187 end
195 end
188
196
189 # Deletes all thumbnails
197 # Deletes all thumbnails
190 def self.clear_thumbnails
198 def self.clear_thumbnails
191 Dir.glob(File.join(thumbnails_storage_path, "*.thumb")).each do |file|
199 Dir.glob(File.join(thumbnails_storage_path, "*.thumb")).each do |file|
192 File.delete file
200 File.delete file
193 end
201 end
194 end
202 end
195
203
196 def is_text?
204 def is_text?
197 Redmine::MimeType.is_type?('text', filename)
205 Redmine::MimeType.is_type?('text', filename)
198 end
206 end
199
207
200 def is_diff?
208 def is_diff?
201 self.filename =~ /\.(patch|diff)$/i
209 self.filename =~ /\.(patch|diff)$/i
202 end
210 end
203
211
204 # Returns true if the file is readable
212 # Returns true if the file is readable
205 def readable?
213 def readable?
206 File.readable?(diskfile)
214 File.readable?(diskfile)
207 end
215 end
208
216
209 # Returns the attachment token
217 # Returns the attachment token
210 def token
218 def token
211 "#{id}.#{digest}"
219 "#{id}.#{digest}"
212 end
220 end
213
221
214 # Finds an attachment that matches the given token and that has no container
222 # Finds an attachment that matches the given token and that has no container
215 def self.find_by_token(token)
223 def self.find_by_token(token)
216 if token.to_s =~ /^(\d+)\.([0-9a-f]+)$/
224 if token.to_s =~ /^(\d+)\.([0-9a-f]+)$/
217 attachment_id, attachment_digest = $1, $2
225 attachment_id, attachment_digest = $1, $2
218 attachment = Attachment.where(:id => attachment_id, :digest => attachment_digest).first
226 attachment = Attachment.where(:id => attachment_id, :digest => attachment_digest).first
219 if attachment && attachment.container.nil?
227 if attachment && attachment.container.nil?
220 attachment
228 attachment
221 end
229 end
222 end
230 end
223 end
231 end
224
232
225 # Bulk attaches a set of files to an object
233 # Bulk attaches a set of files to an object
226 #
234 #
227 # Returns a Hash of the results:
235 # Returns a Hash of the results:
228 # :files => array of the attached files
236 # :files => array of the attached files
229 # :unsaved => array of the files that could not be attached
237 # :unsaved => array of the files that could not be attached
230 def self.attach_files(obj, attachments)
238 def self.attach_files(obj, attachments)
231 result = obj.save_attachments(attachments, User.current)
239 result = obj.save_attachments(attachments, User.current)
232 obj.attach_saved_attachments
240 obj.attach_saved_attachments
233 result
241 result
234 end
242 end
235
243
236 def self.latest_attach(attachments, filename)
244 def self.latest_attach(attachments, filename)
237 attachments.sort_by(&:created_on).reverse.detect {
245 attachments.sort_by(&:created_on).reverse.detect {
238 |att| att.filename.downcase == filename.downcase
246 |att| att.filename.downcase == filename.downcase
239 }
247 }
240 end
248 end
241
249
242 def self.prune(age=1.day)
250 def self.prune(age=1.day)
243 Attachment.where("created_on < ? AND (container_type IS NULL OR container_type = '')", Time.now - age).destroy_all
251 Attachment.where("created_on < ? AND (container_type IS NULL OR container_type = '')", Time.now - age).destroy_all
244 end
252 end
245
253
246 private
254 private
247
255
248 # Physically deletes the file from the file system
256 # Physically deletes the file from the file system
249 def delete_from_disk!
257 def delete_from_disk!
250 if disk_filename.present? && File.exist?(diskfile)
258 if disk_filename.present? && File.exist?(diskfile)
251 File.delete(diskfile)
259 File.delete(diskfile)
252 end
260 end
253 end
261 end
254
262
255 def sanitize_filename(value)
263 def sanitize_filename(value)
256 # get only the filename, not the whole path
264 # get only the filename, not the whole path
257 just_filename = value.gsub(/^.*(\\|\/)/, '')
265 just_filename = value.gsub(/^.*(\\|\/)/, '')
258
266
259 # Finally, replace invalid characters with underscore
267 # Finally, replace invalid characters with underscore
260 @filename = just_filename.gsub(/[\/\?\%\*\:\|\"\'<>]+/, '_')
268 @filename = just_filename.gsub(/[\/\?\%\*\:\|\"\'<>]+/, '_')
261 end
269 end
262
270
263 # Returns an ASCII or hashed filename
271 # Returns an ASCII or hashed filename
264 def self.disk_filename(filename)
272 def self.disk_filename(filename)
265 timestamp = DateTime.now.strftime("%y%m%d%H%M%S")
273 timestamp = DateTime.now.strftime("%y%m%d%H%M%S")
266 ascii = ''
274 ascii = ''
267 if filename =~ %r{^[a-zA-Z0-9_\.\-]*$}
275 if filename =~ %r{^[a-zA-Z0-9_\.\-]*$}
268 ascii = filename
276 ascii = filename
269 else
277 else
270 ascii = Digest::MD5.hexdigest(filename)
278 ascii = Digest::MD5.hexdigest(filename)
271 # keep the extension if any
279 # keep the extension if any
272 ascii << $1 if filename =~ %r{(\.[a-zA-Z0-9]+)$}
280 ascii << $1 if filename =~ %r{(\.[a-zA-Z0-9]+)$}
273 end
281 end
274 while File.exist?(File.join(@@storage_path, "#{timestamp}_#{ascii}"))
282 while File.exist?(File.join(@@storage_path, "#{timestamp}_#{ascii}"))
275 timestamp.succ!
283 timestamp.succ!
276 end
284 end
277 "#{timestamp}_#{ascii}"
285 "#{timestamp}_#{ascii}"
278 end
286 end
279 end
287 end
@@ -1,340 +1,340
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 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 RedmineApp::Application.routes.draw do
18 RedmineApp::Application.routes.draw do
19 root :to => 'welcome#index', :as => 'home'
19 root :to => 'welcome#index', :as => 'home'
20
20
21 match 'login', :to => 'account#login', :as => 'signin'
21 match 'login', :to => 'account#login', :as => 'signin'
22 match 'logout', :to => 'account#logout', :as => 'signout'
22 match 'logout', :to => 'account#logout', :as => 'signout'
23 match 'account/register', :to => 'account#register', :via => [:get, :post], :as => 'register'
23 match 'account/register', :to => 'account#register', :via => [:get, :post], :as => 'register'
24 match 'account/lost_password', :to => 'account#lost_password', :via => [:get, :post], :as => 'lost_password'
24 match 'account/lost_password', :to => 'account#lost_password', :via => [:get, :post], :as => 'lost_password'
25 match 'account/activate', :to => 'account#activate', :via => :get
25 match 'account/activate', :to => 'account#activate', :via => :get
26
26
27 match '/news/preview', :controller => 'previews', :action => 'news', :as => 'preview_news'
27 match '/news/preview', :controller => 'previews', :action => 'news', :as => 'preview_news'
28 match '/issues/preview/new/:project_id', :to => 'previews#issue', :as => 'preview_new_issue'
28 match '/issues/preview/new/:project_id', :to => 'previews#issue', :as => 'preview_new_issue'
29 match '/issues/preview/edit/:id', :to => 'previews#issue', :as => 'preview_edit_issue'
29 match '/issues/preview/edit/:id', :to => 'previews#issue', :as => 'preview_edit_issue'
30 match '/issues/preview', :to => 'previews#issue', :as => 'preview_issue'
30 match '/issues/preview', :to => 'previews#issue', :as => 'preview_issue'
31
31
32 match 'projects/:id/wiki', :to => 'wikis#edit', :via => :post
32 match 'projects/:id/wiki', :to => 'wikis#edit', :via => :post
33 match 'projects/:id/wiki/destroy', :to => 'wikis#destroy', :via => [:get, :post]
33 match 'projects/:id/wiki/destroy', :to => 'wikis#destroy', :via => [:get, :post]
34
34
35 match 'boards/:board_id/topics/new', :to => 'messages#new', :via => [:get, :post]
35 match 'boards/:board_id/topics/new', :to => 'messages#new', :via => [:get, :post]
36 get 'boards/:board_id/topics/:id', :to => 'messages#show', :as => 'board_message'
36 get 'boards/:board_id/topics/:id', :to => 'messages#show', :as => 'board_message'
37 match 'boards/:board_id/topics/quote/:id', :to => 'messages#quote', :via => [:get, :post]
37 match 'boards/:board_id/topics/quote/:id', :to => 'messages#quote', :via => [:get, :post]
38 get 'boards/:board_id/topics/:id/edit', :to => 'messages#edit'
38 get 'boards/:board_id/topics/:id/edit', :to => 'messages#edit'
39
39
40 post 'boards/:board_id/topics/preview', :to => 'messages#preview'
40 post 'boards/:board_id/topics/preview', :to => 'messages#preview'
41 post 'boards/:board_id/topics/:id/replies', :to => 'messages#reply'
41 post 'boards/:board_id/topics/:id/replies', :to => 'messages#reply'
42 post 'boards/:board_id/topics/:id/edit', :to => 'messages#edit'
42 post 'boards/:board_id/topics/:id/edit', :to => 'messages#edit'
43 post 'boards/:board_id/topics/:id/destroy', :to => 'messages#destroy'
43 post 'boards/:board_id/topics/:id/destroy', :to => 'messages#destroy'
44
44
45 # Misc issue routes. TODO: move into resources
45 # Misc issue routes. TODO: move into resources
46 match '/issues/auto_complete', :to => 'auto_completes#issues', :via => :get, :as => 'auto_complete_issues'
46 match '/issues/auto_complete', :to => 'auto_completes#issues', :via => :get, :as => 'auto_complete_issues'
47 match '/issues/context_menu', :to => 'context_menus#issues', :as => 'issues_context_menu'
47 match '/issues/context_menu', :to => 'context_menus#issues', :as => 'issues_context_menu'
48 match '/issues/changes', :to => 'journals#index', :as => 'issue_changes'
48 match '/issues/changes', :to => 'journals#index', :as => 'issue_changes'
49 match '/issues/:id/quoted', :to => 'journals#new', :id => /\d+/, :via => :post, :as => 'quoted_issue'
49 match '/issues/:id/quoted', :to => 'journals#new', :id => /\d+/, :via => :post, :as => 'quoted_issue'
50
50
51 match '/journals/diff/:id', :to => 'journals#diff', :id => /\d+/, :via => :get
51 match '/journals/diff/:id', :to => 'journals#diff', :id => /\d+/, :via => :get
52 match '/journals/edit/:id', :to => 'journals#edit', :id => /\d+/, :via => [:get, :post]
52 match '/journals/edit/:id', :to => 'journals#edit', :id => /\d+/, :via => [:get, :post]
53
53
54 match '/projects/:project_id/issues/gantt', :to => 'gantts#show'
54 match '/projects/:project_id/issues/gantt', :to => 'gantts#show'
55 match '/issues/gantt', :to => 'gantts#show'
55 match '/issues/gantt', :to => 'gantts#show'
56
56
57 match '/projects/:project_id/issues/calendar', :to => 'calendars#show'
57 match '/projects/:project_id/issues/calendar', :to => 'calendars#show'
58 match '/issues/calendar', :to => 'calendars#show'
58 match '/issues/calendar', :to => 'calendars#show'
59
59
60 match 'projects/:id/issues/report', :to => 'reports#issue_report', :via => :get
60 match 'projects/:id/issues/report', :to => 'reports#issue_report', :via => :get
61 match 'projects/:id/issues/report/:detail', :to => 'reports#issue_report_details', :via => :get
61 match 'projects/:id/issues/report/:detail', :to => 'reports#issue_report_details', :via => :get
62
62
63 match 'my/account', :controller => 'my', :action => 'account', :via => [:get, :post]
63 match 'my/account', :controller => 'my', :action => 'account', :via => [:get, :post]
64 match 'my/account/destroy', :controller => 'my', :action => 'destroy', :via => [:get, :post]
64 match 'my/account/destroy', :controller => 'my', :action => 'destroy', :via => [:get, :post]
65 match 'my/page', :controller => 'my', :action => 'page', :via => :get
65 match 'my/page', :controller => 'my', :action => 'page', :via => :get
66 match 'my', :controller => 'my', :action => 'index', :via => :get # Redirects to my/page
66 match 'my', :controller => 'my', :action => 'index', :via => :get # Redirects to my/page
67 match 'my/reset_rss_key', :controller => 'my', :action => 'reset_rss_key', :via => :post
67 match 'my/reset_rss_key', :controller => 'my', :action => 'reset_rss_key', :via => :post
68 match 'my/reset_api_key', :controller => 'my', :action => 'reset_api_key', :via => :post
68 match 'my/reset_api_key', :controller => 'my', :action => 'reset_api_key', :via => :post
69 match 'my/password', :controller => 'my', :action => 'password', :via => [:get, :post]
69 match 'my/password', :controller => 'my', :action => 'password', :via => [:get, :post]
70 match 'my/page_layout', :controller => 'my', :action => 'page_layout', :via => :get
70 match 'my/page_layout', :controller => 'my', :action => 'page_layout', :via => :get
71 match 'my/add_block', :controller => 'my', :action => 'add_block', :via => :post
71 match 'my/add_block', :controller => 'my', :action => 'add_block', :via => :post
72 match 'my/remove_block', :controller => 'my', :action => 'remove_block', :via => :post
72 match 'my/remove_block', :controller => 'my', :action => 'remove_block', :via => :post
73 match 'my/order_blocks', :controller => 'my', :action => 'order_blocks', :via => :post
73 match 'my/order_blocks', :controller => 'my', :action => 'order_blocks', :via => :post
74
74
75 resources :users
75 resources :users
76 match 'users/:id/memberships/:membership_id', :to => 'users#edit_membership', :via => :put, :as => 'user_membership'
76 match 'users/:id/memberships/:membership_id', :to => 'users#edit_membership', :via => :put, :as => 'user_membership'
77 match 'users/:id/memberships/:membership_id', :to => 'users#destroy_membership', :via => :delete
77 match 'users/:id/memberships/:membership_id', :to => 'users#destroy_membership', :via => :delete
78 match 'users/:id/memberships', :to => 'users#edit_membership', :via => :post, :as => 'user_memberships'
78 match 'users/:id/memberships', :to => 'users#edit_membership', :via => :post, :as => 'user_memberships'
79
79
80 match 'watchers/new', :controller=> 'watchers', :action => 'new', :via => :get
80 match 'watchers/new', :controller=> 'watchers', :action => 'new', :via => :get
81 match 'watchers', :controller=> 'watchers', :action => 'create', :via => :post
81 match 'watchers', :controller=> 'watchers', :action => 'create', :via => :post
82 match 'watchers/append', :controller=> 'watchers', :action => 'append', :via => :post
82 match 'watchers/append', :controller=> 'watchers', :action => 'append', :via => :post
83 match 'watchers/destroy', :controller=> 'watchers', :action => 'destroy', :via => :post
83 match 'watchers/destroy', :controller=> 'watchers', :action => 'destroy', :via => :post
84 match 'watchers/watch', :controller=> 'watchers', :action => 'watch', :via => :post
84 match 'watchers/watch', :controller=> 'watchers', :action => 'watch', :via => :post
85 match 'watchers/unwatch', :controller=> 'watchers', :action => 'unwatch', :via => :post
85 match 'watchers/unwatch', :controller=> 'watchers', :action => 'unwatch', :via => :post
86 match 'watchers/autocomplete_for_user', :controller=> 'watchers', :action => 'autocomplete_for_user', :via => :get
86 match 'watchers/autocomplete_for_user', :controller=> 'watchers', :action => 'autocomplete_for_user', :via => :get
87
87
88 match 'projects/:id/settings/:tab', :to => "projects#settings"
88 match 'projects/:id/settings/:tab', :to => "projects#settings"
89
89
90 resources :projects do
90 resources :projects do
91 member do
91 member do
92 get 'settings'
92 get 'settings'
93 post 'modules'
93 post 'modules'
94 post 'archive'
94 post 'archive'
95 post 'unarchive'
95 post 'unarchive'
96 post 'close'
96 post 'close'
97 post 'reopen'
97 post 'reopen'
98 match 'copy', :via => [:get, :post]
98 match 'copy', :via => [:get, :post]
99 end
99 end
100
100
101 resources :memberships, :shallow => true, :controller => 'members', :only => [:index, :show, :create, :update, :destroy] do
101 resources :memberships, :shallow => true, :controller => 'members', :only => [:index, :show, :create, :update, :destroy] do
102 collection do
102 collection do
103 get 'autocomplete'
103 get 'autocomplete'
104 end
104 end
105 end
105 end
106
106
107 resource :enumerations, :controller => 'project_enumerations', :only => [:update, :destroy]
107 resource :enumerations, :controller => 'project_enumerations', :only => [:update, :destroy]
108
108
109 match 'issues/:copy_from/copy', :to => 'issues#new'
109 match 'issues/:copy_from/copy', :to => 'issues#new'
110 resources :issues, :only => [:index, :new, :create] do
110 resources :issues, :only => [:index, :new, :create] do
111 resources :time_entries, :controller => 'timelog' do
111 resources :time_entries, :controller => 'timelog' do
112 collection do
112 collection do
113 get 'report'
113 get 'report'
114 end
114 end
115 end
115 end
116 end
116 end
117 # issue form update
117 # issue form update
118 match 'issues/new', :controller => 'issues', :action => 'new', :via => [:put, :post], :as => 'issue_form'
118 match 'issues/new', :controller => 'issues', :action => 'new', :via => [:put, :post], :as => 'issue_form'
119
119
120 resources :files, :only => [:index, :new, :create]
120 resources :files, :only => [:index, :new, :create]
121
121
122 resources :versions, :except => [:index, :show, :edit, :update, :destroy] do
122 resources :versions, :except => [:index, :show, :edit, :update, :destroy] do
123 collection do
123 collection do
124 put 'close_completed'
124 put 'close_completed'
125 end
125 end
126 end
126 end
127 match 'versions.:format', :to => 'versions#index'
127 match 'versions.:format', :to => 'versions#index'
128 match 'roadmap', :to => 'versions#index', :format => false
128 match 'roadmap', :to => 'versions#index', :format => false
129 match 'versions', :to => 'versions#index'
129 match 'versions', :to => 'versions#index'
130
130
131 resources :news, :except => [:show, :edit, :update, :destroy]
131 resources :news, :except => [:show, :edit, :update, :destroy]
132 resources :time_entries, :controller => 'timelog' do
132 resources :time_entries, :controller => 'timelog' do
133 get 'report', :on => :collection
133 get 'report', :on => :collection
134 end
134 end
135 resources :queries, :only => [:new, :create]
135 resources :queries, :only => [:new, :create]
136 resources :issue_categories, :shallow => true
136 resources :issue_categories, :shallow => true
137 resources :documents, :except => [:show, :edit, :update, :destroy]
137 resources :documents, :except => [:show, :edit, :update, :destroy]
138 resources :boards
138 resources :boards
139 resources :repositories, :shallow => true, :except => [:index, :show] do
139 resources :repositories, :shallow => true, :except => [:index, :show] do
140 member do
140 member do
141 match 'committers', :via => [:get, :post]
141 match 'committers', :via => [:get, :post]
142 end
142 end
143 end
143 end
144
144
145 match 'wiki/index', :controller => 'wiki', :action => 'index', :via => :get
145 match 'wiki/index', :controller => 'wiki', :action => 'index', :via => :get
146 match 'wiki/:id/diff/:version/vs/:version_from', :controller => 'wiki', :action => 'diff'
146 match 'wiki/:id/diff/:version/vs/:version_from', :controller => 'wiki', :action => 'diff'
147 match 'wiki/:id/diff/:version', :controller => 'wiki', :action => 'diff'
147 match 'wiki/:id/diff/:version', :controller => 'wiki', :action => 'diff'
148 resources :wiki, :except => [:index, :new, :create] do
148 resources :wiki, :except => [:index, :new, :create] do
149 member do
149 member do
150 get 'rename'
150 get 'rename'
151 post 'rename'
151 post 'rename'
152 get 'history'
152 get 'history'
153 get 'diff'
153 get 'diff'
154 match 'preview', :via => [:post, :put]
154 match 'preview', :via => [:post, :put]
155 post 'protect'
155 post 'protect'
156 post 'add_attachment'
156 post 'add_attachment'
157 end
157 end
158 collection do
158 collection do
159 get 'export'
159 get 'export'
160 get 'date_index'
160 get 'date_index'
161 end
161 end
162 end
162 end
163 match 'wiki', :controller => 'wiki', :action => 'show', :via => :get
163 match 'wiki', :controller => 'wiki', :action => 'show', :via => :get
164 match 'wiki/:id/annotate/:version', :controller => 'wiki', :action => 'annotate'
164 match 'wiki/:id/annotate/:version', :controller => 'wiki', :action => 'annotate'
165 end
165 end
166
166
167 resources :issues do
167 resources :issues do
168 collection do
168 collection do
169 match 'bulk_edit', :via => [:get, :post]
169 match 'bulk_edit', :via => [:get, :post]
170 post 'bulk_update'
170 post 'bulk_update'
171 end
171 end
172 resources :time_entries, :controller => 'timelog' do
172 resources :time_entries, :controller => 'timelog' do
173 collection do
173 collection do
174 get 'report'
174 get 'report'
175 end
175 end
176 end
176 end
177 resources :relations, :shallow => true, :controller => 'issue_relations', :only => [:index, :show, :create, :destroy]
177 resources :relations, :shallow => true, :controller => 'issue_relations', :only => [:index, :show, :create, :destroy]
178 end
178 end
179 match '/issues', :controller => 'issues', :action => 'destroy', :via => :delete
179 match '/issues', :controller => 'issues', :action => 'destroy', :via => :delete
180
180
181 resources :queries, :except => [:show]
181 resources :queries, :except => [:show]
182
182
183 resources :news, :only => [:index, :show, :edit, :update, :destroy]
183 resources :news, :only => [:index, :show, :edit, :update, :destroy]
184 match '/news/:id/comments', :to => 'comments#create', :via => :post
184 match '/news/:id/comments', :to => 'comments#create', :via => :post
185 match '/news/:id/comments/:comment_id', :to => 'comments#destroy', :via => :delete
185 match '/news/:id/comments/:comment_id', :to => 'comments#destroy', :via => :delete
186
186
187 resources :versions, :only => [:show, :edit, :update, :destroy] do
187 resources :versions, :only => [:show, :edit, :update, :destroy] do
188 post 'status_by', :on => :member
188 post 'status_by', :on => :member
189 end
189 end
190
190
191 resources :documents, :only => [:show, :edit, :update, :destroy] do
191 resources :documents, :only => [:show, :edit, :update, :destroy] do
192 post 'add_attachment', :on => :member
192 post 'add_attachment', :on => :member
193 end
193 end
194
194
195 match '/time_entries/context_menu', :to => 'context_menus#time_entries', :as => :time_entries_context_menu
195 match '/time_entries/context_menu', :to => 'context_menus#time_entries', :as => :time_entries_context_menu
196
196
197 resources :time_entries, :controller => 'timelog', :except => :destroy do
197 resources :time_entries, :controller => 'timelog', :except => :destroy do
198 collection do
198 collection do
199 get 'report'
199 get 'report'
200 get 'bulk_edit'
200 get 'bulk_edit'
201 post 'bulk_update'
201 post 'bulk_update'
202 end
202 end
203 end
203 end
204 match '/time_entries/:id', :to => 'timelog#destroy', :via => :delete, :id => /\d+/
204 match '/time_entries/:id', :to => 'timelog#destroy', :via => :delete, :id => /\d+/
205 # TODO: delete /time_entries for bulk deletion
205 # TODO: delete /time_entries for bulk deletion
206 match '/time_entries/destroy', :to => 'timelog#destroy', :via => :delete
206 match '/time_entries/destroy', :to => 'timelog#destroy', :via => :delete
207
207
208 # TODO: port to be part of the resources route(s)
208 # TODO: port to be part of the resources route(s)
209 match 'projects/:id/settings/:tab', :to => 'projects#settings', :via => :get
209 match 'projects/:id/settings/:tab', :to => 'projects#settings', :via => :get
210
210
211 get 'projects/:id/activity', :to => 'activities#index'
211 get 'projects/:id/activity', :to => 'activities#index'
212 get 'projects/:id/activity.:format', :to => 'activities#index'
212 get 'projects/:id/activity.:format', :to => 'activities#index'
213 get 'activity', :to => 'activities#index'
213 get 'activity', :to => 'activities#index'
214
214
215 # repositories routes
215 # repositories routes
216 get 'projects/:id/repository/:repository_id/statistics', :to => 'repositories#stats'
216 get 'projects/:id/repository/:repository_id/statistics', :to => 'repositories#stats'
217 get 'projects/:id/repository/:repository_id/graph', :to => 'repositories#graph'
217 get 'projects/:id/repository/:repository_id/graph', :to => 'repositories#graph'
218
218
219 get 'projects/:id/repository/:repository_id/changes(/*path(.:ext))',
219 get 'projects/:id/repository/:repository_id/changes(/*path(.:ext))',
220 :to => 'repositories#changes'
220 :to => 'repositories#changes'
221
221
222 get 'projects/:id/repository/:repository_id/revisions/:rev', :to => 'repositories#revision'
222 get 'projects/:id/repository/:repository_id/revisions/:rev', :to => 'repositories#revision'
223 get 'projects/:id/repository/:repository_id/revision', :to => 'repositories#revision'
223 get 'projects/:id/repository/:repository_id/revision', :to => 'repositories#revision'
224 post 'projects/:id/repository/:repository_id/revisions/:rev/issues', :to => 'repositories#add_related_issue'
224 post 'projects/:id/repository/:repository_id/revisions/:rev/issues', :to => 'repositories#add_related_issue'
225 delete 'projects/:id/repository/:repository_id/revisions/:rev/issues/:issue_id', :to => 'repositories#remove_related_issue'
225 delete 'projects/:id/repository/:repository_id/revisions/:rev/issues/:issue_id', :to => 'repositories#remove_related_issue'
226 get 'projects/:id/repository/:repository_id/revisions', :to => 'repositories#revisions'
226 get 'projects/:id/repository/:repository_id/revisions', :to => 'repositories#revisions'
227 get 'projects/:id/repository/:repository_id/revisions/:rev/:action(/*path(.:ext))',
227 get 'projects/:id/repository/:repository_id/revisions/:rev/:action(/*path(.:ext))',
228 :controller => 'repositories',
228 :controller => 'repositories',
229 :format => false,
229 :format => false,
230 :constraints => {
230 :constraints => {
231 :action => /(browse|show|entry|raw|annotate|diff)/,
231 :action => /(browse|show|entry|raw|annotate|diff)/,
232 :rev => /[a-z0-9\.\-_]+/
232 :rev => /[a-z0-9\.\-_]+/
233 }
233 }
234
234
235 get 'projects/:id/repository/statistics', :to => 'repositories#stats'
235 get 'projects/:id/repository/statistics', :to => 'repositories#stats'
236 get 'projects/:id/repository/graph', :to => 'repositories#graph'
236 get 'projects/:id/repository/graph', :to => 'repositories#graph'
237
237
238 get 'projects/:id/repository/changes(/*path(.:ext))',
238 get 'projects/:id/repository/changes(/*path(.:ext))',
239 :to => 'repositories#changes'
239 :to => 'repositories#changes'
240
240
241 get 'projects/:id/repository/revisions', :to => 'repositories#revisions'
241 get 'projects/:id/repository/revisions', :to => 'repositories#revisions'
242 get 'projects/:id/repository/revisions/:rev', :to => 'repositories#revision'
242 get 'projects/:id/repository/revisions/:rev', :to => 'repositories#revision'
243 get 'projects/:id/repository/revision', :to => 'repositories#revision'
243 get 'projects/:id/repository/revision', :to => 'repositories#revision'
244 post 'projects/:id/repository/revisions/:rev/issues', :to => 'repositories#add_related_issue'
244 post 'projects/:id/repository/revisions/:rev/issues', :to => 'repositories#add_related_issue'
245 delete 'projects/:id/repository/revisions/:rev/issues/:issue_id', :to => 'repositories#remove_related_issue'
245 delete 'projects/:id/repository/revisions/:rev/issues/:issue_id', :to => 'repositories#remove_related_issue'
246 get 'projects/:id/repository/revisions/:rev/:action(/*path(.:ext))',
246 get 'projects/:id/repository/revisions/:rev/:action(/*path(.:ext))',
247 :controller => 'repositories',
247 :controller => 'repositories',
248 :format => false,
248 :format => false,
249 :constraints => {
249 :constraints => {
250 :action => /(browse|show|entry|raw|annotate|diff)/,
250 :action => /(browse|show|entry|raw|annotate|diff)/,
251 :rev => /[a-z0-9\.\-_]+/
251 :rev => /[a-z0-9\.\-_]+/
252 }
252 }
253 get 'projects/:id/repository/:repository_id/:action(/*path(.:ext))',
253 get 'projects/:id/repository/:repository_id/:action(/*path(.:ext))',
254 :controller => 'repositories',
254 :controller => 'repositories',
255 :action => /(browse|show|entry|raw|changes|annotate|diff)/
255 :action => /(browse|show|entry|raw|changes|annotate|diff)/
256 get 'projects/:id/repository/:action(/*path(.:ext))',
256 get 'projects/:id/repository/:action(/*path(.:ext))',
257 :controller => 'repositories',
257 :controller => 'repositories',
258 :action => /(browse|show|entry|raw|changes|annotate|diff)/
258 :action => /(browse|show|entry|raw|changes|annotate|diff)/
259
259
260 get 'projects/:id/repository/:repository_id', :to => 'repositories#show', :path => nil
260 get 'projects/:id/repository/:repository_id', :to => 'repositories#show', :path => nil
261 get 'projects/:id/repository', :to => 'repositories#show', :path => nil
261 get 'projects/:id/repository', :to => 'repositories#show', :path => nil
262
262
263 # additional routes for having the file name at the end of url
263 # additional routes for having the file name at the end of url
264 match 'attachments/:id/:filename', :controller => 'attachments', :action => 'show', :id => /\d+/, :filename => /.*/, :via => :get
264 match 'attachments/:id/:filename', :controller => 'attachments', :action => 'show', :id => /\d+/, :filename => /.*/, :via => :get
265 match 'attachments/download/:id/:filename', :controller => 'attachments', :action => 'download', :id => /\d+/, :filename => /.*/, :via => :get
265 match 'attachments/download/:id/:filename', :controller => 'attachments', :action => 'download', :id => /\d+/, :filename => /.*/, :via => :get
266 match 'attachments/download/:id', :controller => 'attachments', :action => 'download', :id => /\d+/, :via => :get
266 match 'attachments/download/:id', :controller => 'attachments', :action => 'download', :id => /\d+/, :via => :get
267 match 'attachments/thumbnail/:id', :controller => 'attachments', :action => 'thumbnail', :id => /\d+/, :via => :get
267 match 'attachments/thumbnail/:id(/:size)', :controller => 'attachments', :action => 'thumbnail', :id => /\d+/, :via => :get, :size => /\d+/
268 resources :attachments, :only => [:show, :destroy]
268 resources :attachments, :only => [:show, :destroy]
269
269
270 resources :groups do
270 resources :groups do
271 member do
271 member do
272 get 'autocomplete_for_user'
272 get 'autocomplete_for_user'
273 end
273 end
274 end
274 end
275
275
276 match 'groups/:id/users', :controller => 'groups', :action => 'add_users', :id => /\d+/, :via => :post, :as => 'group_users'
276 match 'groups/:id/users', :controller => 'groups', :action => 'add_users', :id => /\d+/, :via => :post, :as => 'group_users'
277 match 'groups/:id/users/:user_id', :controller => 'groups', :action => 'remove_user', :id => /\d+/, :via => :delete, :as => 'group_user'
277 match 'groups/:id/users/:user_id', :controller => 'groups', :action => 'remove_user', :id => /\d+/, :via => :delete, :as => 'group_user'
278 match 'groups/destroy_membership/:id', :controller => 'groups', :action => 'destroy_membership', :id => /\d+/, :via => :post
278 match 'groups/destroy_membership/:id', :controller => 'groups', :action => 'destroy_membership', :id => /\d+/, :via => :post
279 match 'groups/edit_membership/:id', :controller => 'groups', :action => 'edit_membership', :id => /\d+/, :via => :post
279 match 'groups/edit_membership/:id', :controller => 'groups', :action => 'edit_membership', :id => /\d+/, :via => :post
280
280
281 resources :trackers, :except => :show
281 resources :trackers, :except => :show
282 resources :issue_statuses, :except => :show do
282 resources :issue_statuses, :except => :show do
283 collection do
283 collection do
284 post 'update_issue_done_ratio'
284 post 'update_issue_done_ratio'
285 end
285 end
286 end
286 end
287 resources :custom_fields, :except => :show
287 resources :custom_fields, :except => :show
288 resources :roles, :except => :show do
288 resources :roles, :except => :show do
289 collection do
289 collection do
290 match 'permissions', :via => [:get, :post]
290 match 'permissions', :via => [:get, :post]
291 end
291 end
292 end
292 end
293 resources :enumerations, :except => :show
293 resources :enumerations, :except => :show
294
294
295 get 'projects/:id/search', :controller => 'search', :action => 'index'
295 get 'projects/:id/search', :controller => 'search', :action => 'index'
296 get 'search', :controller => 'search', :action => 'index'
296 get 'search', :controller => 'search', :action => 'index'
297
297
298 match 'mail_handler', :controller => 'mail_handler', :action => 'index', :via => :post
298 match 'mail_handler', :controller => 'mail_handler', :action => 'index', :via => :post
299
299
300 match 'admin', :controller => 'admin', :action => 'index', :via => :get
300 match 'admin', :controller => 'admin', :action => 'index', :via => :get
301 match 'admin/projects', :controller => 'admin', :action => 'projects', :via => :get
301 match 'admin/projects', :controller => 'admin', :action => 'projects', :via => :get
302 match 'admin/plugins', :controller => 'admin', :action => 'plugins', :via => :get
302 match 'admin/plugins', :controller => 'admin', :action => 'plugins', :via => :get
303 match 'admin/info', :controller => 'admin', :action => 'info', :via => :get
303 match 'admin/info', :controller => 'admin', :action => 'info', :via => :get
304 match 'admin/test_email', :controller => 'admin', :action => 'test_email', :via => :get
304 match 'admin/test_email', :controller => 'admin', :action => 'test_email', :via => :get
305 match 'admin/default_configuration', :controller => 'admin', :action => 'default_configuration', :via => :post
305 match 'admin/default_configuration', :controller => 'admin', :action => 'default_configuration', :via => :post
306
306
307 resources :auth_sources do
307 resources :auth_sources do
308 member do
308 member do
309 get 'test_connection'
309 get 'test_connection'
310 end
310 end
311 end
311 end
312
312
313 match 'workflows', :controller => 'workflows', :action => 'index', :via => :get
313 match 'workflows', :controller => 'workflows', :action => 'index', :via => :get
314 match 'workflows/edit', :controller => 'workflows', :action => 'edit', :via => [:get, :post]
314 match 'workflows/edit', :controller => 'workflows', :action => 'edit', :via => [:get, :post]
315 match 'workflows/permissions', :controller => 'workflows', :action => 'permissions', :via => [:get, :post]
315 match 'workflows/permissions', :controller => 'workflows', :action => 'permissions', :via => [:get, :post]
316 match 'workflows/copy', :controller => 'workflows', :action => 'copy', :via => [:get, :post]
316 match 'workflows/copy', :controller => 'workflows', :action => 'copy', :via => [:get, :post]
317 match 'settings', :controller => 'settings', :action => 'index', :via => :get
317 match 'settings', :controller => 'settings', :action => 'index', :via => :get
318 match 'settings/edit', :controller => 'settings', :action => 'edit', :via => [:get, :post]
318 match 'settings/edit', :controller => 'settings', :action => 'edit', :via => [:get, :post]
319 match 'settings/plugin/:id', :controller => 'settings', :action => 'plugin', :via => [:get, :post]
319 match 'settings/plugin/:id', :controller => 'settings', :action => 'plugin', :via => [:get, :post]
320
320
321 match 'sys/projects', :to => 'sys#projects', :via => :get
321 match 'sys/projects', :to => 'sys#projects', :via => :get
322 match 'sys/projects/:id/repository', :to => 'sys#create_project_repository', :via => :post
322 match 'sys/projects/:id/repository', :to => 'sys#create_project_repository', :via => :post
323 match 'sys/fetch_changesets', :to => 'sys#fetch_changesets', :via => :get
323 match 'sys/fetch_changesets', :to => 'sys#fetch_changesets', :via => :get
324
324
325 match 'uploads', :to => 'attachments#upload', :via => :post
325 match 'uploads', :to => 'attachments#upload', :via => :post
326
326
327 get 'robots.txt', :to => 'welcome#robots'
327 get 'robots.txt', :to => 'welcome#robots'
328
328
329 Dir.glob File.expand_path("plugins/*", Rails.root) do |plugin_dir|
329 Dir.glob File.expand_path("plugins/*", Rails.root) do |plugin_dir|
330 file = File.join(plugin_dir, "config/routes.rb")
330 file = File.join(plugin_dir, "config/routes.rb")
331 if File.exists?(file)
331 if File.exists?(file)
332 begin
332 begin
333 instance_eval File.read(file)
333 instance_eval File.read(file)
334 rescue Exception => e
334 rescue Exception => e
335 puts "An error occurred while loading the routes definition of #{File.basename(plugin_dir)} plugin (#{file}): #{e.message}."
335 puts "An error occurred while loading the routes definition of #{File.basename(plugin_dir)} plugin (#{file}): #{e.message}."
336 exit 1
336 exit 1
337 end
337 end
338 end
338 end
339 end
339 end
340 end
340 end
@@ -1,121 +1,139
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 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 module Redmine
18 module Redmine
19 module WikiFormatting
19 module WikiFormatting
20 module Macros
20 module Macros
21 module Definitions
21 module Definitions
22 def exec_macro(name, obj, args)
22 def exec_macro(name, obj, args)
23 method_name = "macro_#{name}"
23 method_name = "macro_#{name}"
24 send(method_name, obj, args) if respond_to?(method_name)
24 send(method_name, obj, args) if respond_to?(method_name)
25 end
25 end
26
26
27 def extract_macro_options(args, *keys)
27 def extract_macro_options(args, *keys)
28 options = {}
28 options = {}
29 while args.last.to_s.strip =~ %r{^(.+)\=(.+)$} && keys.include?($1.downcase.to_sym)
29 while args.last.to_s.strip =~ %r{^(.+)\=(.+)$} && keys.include?($1.downcase.to_sym)
30 options[$1.downcase.to_sym] = $2
30 options[$1.downcase.to_sym] = $2
31 args.pop
31 args.pop
32 end
32 end
33 return [args, options]
33 return [args, options]
34 end
34 end
35 end
35 end
36
36
37 @@available_macros = {}
37 @@available_macros = {}
38
38
39 class << self
39 class << self
40 # Called with a block to define additional macros.
40 # Called with a block to define additional macros.
41 # Macro blocks accept 2 arguments:
41 # Macro blocks accept 2 arguments:
42 # * obj: the object that is rendered
42 # * obj: the object that is rendered
43 # * args: macro arguments
43 # * args: macro arguments
44 #
44 #
45 # Plugins can use this method to define new macros:
45 # Plugins can use this method to define new macros:
46 #
46 #
47 # Redmine::WikiFormatting::Macros.register do
47 # Redmine::WikiFormatting::Macros.register do
48 # desc "This is my macro"
48 # desc "This is my macro"
49 # macro :my_macro do |obj, args|
49 # macro :my_macro do |obj, args|
50 # "My macro output"
50 # "My macro output"
51 # end
51 # end
52 # end
52 # end
53 def register(&block)
53 def register(&block)
54 class_eval(&block) if block_given?
54 class_eval(&block) if block_given?
55 end
55 end
56
56
57 private
57 private
58 # Defines a new macro with the given name and block.
58 # Defines a new macro with the given name and block.
59 def macro(name, &block)
59 def macro(name, &block)
60 name = name.to_sym if name.is_a?(String)
60 name = name.to_sym if name.is_a?(String)
61 @@available_macros[name] = @@desc || ''
61 @@available_macros[name] = @@desc || ''
62 @@desc = nil
62 @@desc = nil
63 raise "Can not create a macro without a block!" unless block_given?
63 raise "Can not create a macro without a block!" unless block_given?
64 Definitions.send :define_method, "macro_#{name}".downcase, &block
64 Definitions.send :define_method, "macro_#{name}".downcase, &block
65 end
65 end
66
66
67 # Sets description for the next macro to be defined
67 # Sets description for the next macro to be defined
68 def desc(txt)
68 def desc(txt)
69 @@desc = txt
69 @@desc = txt
70 end
70 end
71 end
71 end
72
72
73 # Builtin macros
73 # Builtin macros
74 desc "Sample macro."
74 desc "Sample macro."
75 macro :hello_world do |obj, args|
75 macro :hello_world do |obj, args|
76 "Hello world! Object: #{obj.class.name}, " + (args.empty? ? "Called with no argument." : "Arguments: #{args.join(', ')}")
76 "Hello world! Object: #{obj.class.name}, " + (args.empty? ? "Called with no argument." : "Arguments: #{args.join(', ')}")
77 end
77 end
78
78
79 desc "Displays a list of all available macros, including description if available."
79 desc "Displays a list of all available macros, including description if available."
80 macro :macro_list do |obj, args|
80 macro :macro_list do |obj, args|
81 out = ''.html_safe
81 out = ''.html_safe
82 @@available_macros.keys.collect(&:to_s).sort.each do |macro|
82 @@available_macros.keys.collect(&:to_s).sort.each do |macro|
83 out << content_tag('dt', content_tag('code', macro))
83 out << content_tag('dt', content_tag('code', macro))
84 out << content_tag('dd', textilizable(@@available_macros[macro.to_sym]))
84 out << content_tag('dd', textilizable(@@available_macros[macro.to_sym]))
85 end
85 end
86 content_tag('dl', out)
86 content_tag('dl', out)
87 end
87 end
88
88
89 desc "Displays a list of child pages. With no argument, it displays the child pages of the current wiki page. Examples:\n\n" +
89 desc "Displays a list of child pages. With no argument, it displays the child pages of the current wiki page. Examples:\n\n" +
90 " !{{child_pages}} -- can be used from a wiki page only\n" +
90 " !{{child_pages}} -- can be used from a wiki page only\n" +
91 " !{{child_pages(Foo)}} -- lists all children of page Foo\n" +
91 " !{{child_pages(Foo)}} -- lists all children of page Foo\n" +
92 " !{{child_pages(Foo, parent=1)}} -- same as above with a link to page Foo"
92 " !{{child_pages(Foo, parent=1)}} -- same as above with a link to page Foo"
93 macro :child_pages do |obj, args|
93 macro :child_pages do |obj, args|
94 args, options = extract_macro_options(args, :parent)
94 args, options = extract_macro_options(args, :parent)
95 page = nil
95 page = nil
96 if args.size > 0
96 if args.size > 0
97 page = Wiki.find_page(args.first.to_s, :project => @project)
97 page = Wiki.find_page(args.first.to_s, :project => @project)
98 elsif obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)
98 elsif obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)
99 page = obj.page
99 page = obj.page
100 else
100 else
101 raise 'With no argument, this macro can be called from wiki pages only.'
101 raise 'With no argument, this macro can be called from wiki pages only.'
102 end
102 end
103 raise 'Page not found' if page.nil? || !User.current.allowed_to?(:view_wiki_pages, page.wiki.project)
103 raise 'Page not found' if page.nil? || !User.current.allowed_to?(:view_wiki_pages, page.wiki.project)
104 pages = ([page] + page.descendants).group_by(&:parent_id)
104 pages = ([page] + page.descendants).group_by(&:parent_id)
105 render_page_hierarchy(pages, options[:parent] ? page.parent_id : page.id)
105 render_page_hierarchy(pages, options[:parent] ? page.parent_id : page.id)
106 end
106 end
107
107
108 desc "Include a wiki page. Example:\n\n !{{include(Foo)}}\n\nor to include a page of a specific project wiki:\n\n !{{include(projectname:Foo)}}"
108 desc "Include a wiki page. Example:\n\n !{{include(Foo)}}\n\nor to include a page of a specific project wiki:\n\n !{{include(projectname:Foo)}}"
109 macro :include do |obj, args|
109 macro :include do |obj, args|
110 page = Wiki.find_page(args.first.to_s, :project => @project)
110 page = Wiki.find_page(args.first.to_s, :project => @project)
111 raise 'Page not found' if page.nil? || !User.current.allowed_to?(:view_wiki_pages, page.wiki.project)
111 raise 'Page not found' if page.nil? || !User.current.allowed_to?(:view_wiki_pages, page.wiki.project)
112 @included_wiki_pages ||= []
112 @included_wiki_pages ||= []
113 raise 'Circular inclusion detected' if @included_wiki_pages.include?(page.title)
113 raise 'Circular inclusion detected' if @included_wiki_pages.include?(page.title)
114 @included_wiki_pages << page.title
114 @included_wiki_pages << page.title
115 out = textilizable(page.content, :text, :attachments => page.attachments, :headings => false)
115 out = textilizable(page.content, :text, :attachments => page.attachments, :headings => false)
116 @included_wiki_pages.pop
116 @included_wiki_pages.pop
117 out
117 out
118 end
118 end
119
120 desc "Displays a clickable thumbnail of an attached image. Examples:\n\n<pre>{{thumbnail(image.png)}}\n{{thumbnail(image.png, size=300, title=Thumbnail)}}</pre>"
121 macro :thumbnail do |obj, args|
122 args, options = extract_macro_options(args, :size, :title)
123 filename = args.first
124 raise 'Filename required' unless filename.present?
125 size = options[:size]
126 raise 'Invalid size parameter' unless size.nil? || size.match(/^\d+$/)
127 size = size.to_i
128 size = nil unless size > 0
129 if obj && obj.respond_to?(:attachments) && attachment = Attachment.latest_attach(obj.attachments, filename)
130 title = options[:title] || attachment.title
131 img = image_tag(url_for(:controller => 'attachments', :action => 'thumbnail', :id => attachment, :size => size), :alt => attachment.filename)
132 link_to(img, url_for(:controller => 'attachments', :action => 'show', :id => attachment), :class => 'thumbnail', :title => title)
133 else
134 raise "Attachment #{filename} not found"
135 end
136 end
119 end
137 end
120 end
138 end
121 end
139 end
@@ -1,362 +1,363
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2012 Jean-Philippe Lang
4 # Copyright (C) 2006-2012 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 require 'attachments_controller'
21 require 'attachments_controller'
22
22
23 # Re-raise errors caught by the controller.
23 # Re-raise errors caught by the controller.
24 class AttachmentsController; def rescue_action(e) raise e end; end
24 class AttachmentsController; def rescue_action(e) raise e end; end
25
25
26 class AttachmentsControllerTest < ActionController::TestCase
26 class AttachmentsControllerTest < ActionController::TestCase
27 fixtures :users, :projects, :roles, :members, :member_roles,
27 fixtures :users, :projects, :roles, :members, :member_roles,
28 :enabled_modules, :issues, :trackers, :attachments,
28 :enabled_modules, :issues, :trackers, :attachments,
29 :versions, :wiki_pages, :wikis, :documents
29 :versions, :wiki_pages, :wikis, :documents
30
30
31 def setup
31 def setup
32 @controller = AttachmentsController.new
32 @controller = AttachmentsController.new
33 @request = ActionController::TestRequest.new
33 @request = ActionController::TestRequest.new
34 @response = ActionController::TestResponse.new
34 @response = ActionController::TestResponse.new
35 User.current = nil
35 User.current = nil
36 set_fixtures_attachments_directory
36 set_fixtures_attachments_directory
37 end
37 end
38
38
39 def teardown
39 def teardown
40 set_tmp_attachments_directory
40 set_tmp_attachments_directory
41 end
41 end
42
42
43 def test_show_diff
43 def test_show_diff
44 ['inline', 'sbs'].each do |dt|
44 ['inline', 'sbs'].each do |dt|
45 # 060719210727_changeset_utf8.diff
45 # 060719210727_changeset_utf8.diff
46 get :show, :id => 14, :type => dt
46 get :show, :id => 14, :type => dt
47 assert_response :success
47 assert_response :success
48 assert_template 'diff'
48 assert_template 'diff'
49 assert_equal 'text/html', @response.content_type
49 assert_equal 'text/html', @response.content_type
50 assert_tag 'th',
50 assert_tag 'th',
51 :attributes => {:class => /filename/},
51 :attributes => {:class => /filename/},
52 :content => /issues_controller.rb\t\(rΓ©vision 1484\)/
52 :content => /issues_controller.rb\t\(rΓ©vision 1484\)/
53 assert_tag 'td',
53 assert_tag 'td',
54 :attributes => {:class => /line-code/},
54 :attributes => {:class => /line-code/},
55 :content => /Demande créée avec succès/
55 :content => /Demande créée avec succès/
56 end
56 end
57 set_tmp_attachments_directory
57 set_tmp_attachments_directory
58 end
58 end
59
59
60 def test_show_diff_replcace_cannot_convert_content
60 def test_show_diff_replcace_cannot_convert_content
61 with_settings :repositories_encodings => 'UTF-8' do
61 with_settings :repositories_encodings => 'UTF-8' do
62 ['inline', 'sbs'].each do |dt|
62 ['inline', 'sbs'].each do |dt|
63 # 060719210727_changeset_iso8859-1.diff
63 # 060719210727_changeset_iso8859-1.diff
64 get :show, :id => 5, :type => dt
64 get :show, :id => 5, :type => dt
65 assert_response :success
65 assert_response :success
66 assert_template 'diff'
66 assert_template 'diff'
67 assert_equal 'text/html', @response.content_type
67 assert_equal 'text/html', @response.content_type
68 assert_tag 'th',
68 assert_tag 'th',
69 :attributes => {:class => "filename"},
69 :attributes => {:class => "filename"},
70 :content => /issues_controller.rb\t\(r\?vision 1484\)/
70 :content => /issues_controller.rb\t\(r\?vision 1484\)/
71 assert_tag 'td',
71 assert_tag 'td',
72 :attributes => {:class => /line-code/},
72 :attributes => {:class => /line-code/},
73 :content => /Demande cr\?\?e avec succ\?s/
73 :content => /Demande cr\?\?e avec succ\?s/
74 end
74 end
75 end
75 end
76 set_tmp_attachments_directory
76 set_tmp_attachments_directory
77 end
77 end
78
78
79 def test_show_diff_latin_1
79 def test_show_diff_latin_1
80 with_settings :repositories_encodings => 'UTF-8,ISO-8859-1' do
80 with_settings :repositories_encodings => 'UTF-8,ISO-8859-1' do
81 ['inline', 'sbs'].each do |dt|
81 ['inline', 'sbs'].each do |dt|
82 # 060719210727_changeset_iso8859-1.diff
82 # 060719210727_changeset_iso8859-1.diff
83 get :show, :id => 5, :type => dt
83 get :show, :id => 5, :type => dt
84 assert_response :success
84 assert_response :success
85 assert_template 'diff'
85 assert_template 'diff'
86 assert_equal 'text/html', @response.content_type
86 assert_equal 'text/html', @response.content_type
87 assert_tag 'th',
87 assert_tag 'th',
88 :attributes => {:class => "filename"},
88 :attributes => {:class => "filename"},
89 :content => /issues_controller.rb\t\(rΓ©vision 1484\)/
89 :content => /issues_controller.rb\t\(rΓ©vision 1484\)/
90 assert_tag 'td',
90 assert_tag 'td',
91 :attributes => {:class => /line-code/},
91 :attributes => {:class => /line-code/},
92 :content => /Demande créée avec succès/
92 :content => /Demande créée avec succès/
93 end
93 end
94 end
94 end
95 set_tmp_attachments_directory
95 set_tmp_attachments_directory
96 end
96 end
97
97
98 def test_save_diff_type
98 def test_save_diff_type
99 @request.session[:user_id] = 1 # admin
99 @request.session[:user_id] = 1 # admin
100 user = User.find(1)
100 user = User.find(1)
101 get :show, :id => 5
101 get :show, :id => 5
102 assert_response :success
102 assert_response :success
103 assert_template 'diff'
103 assert_template 'diff'
104 user.reload
104 user.reload
105 assert_equal "inline", user.pref[:diff_type]
105 assert_equal "inline", user.pref[:diff_type]
106 get :show, :id => 5, :type => 'sbs'
106 get :show, :id => 5, :type => 'sbs'
107 assert_response :success
107 assert_response :success
108 assert_template 'diff'
108 assert_template 'diff'
109 user.reload
109 user.reload
110 assert_equal "sbs", user.pref[:diff_type]
110 assert_equal "sbs", user.pref[:diff_type]
111 end
111 end
112
112
113 def test_show_text_file
113 def test_show_text_file
114 get :show, :id => 4
114 get :show, :id => 4
115 assert_response :success
115 assert_response :success
116 assert_template 'file'
116 assert_template 'file'
117 assert_equal 'text/html', @response.content_type
117 assert_equal 'text/html', @response.content_type
118 set_tmp_attachments_directory
118 set_tmp_attachments_directory
119 end
119 end
120
120
121 def test_show_text_file_utf_8
121 def test_show_text_file_utf_8
122 set_tmp_attachments_directory
122 set_tmp_attachments_directory
123 a = Attachment.new(:container => Issue.find(1),
123 a = Attachment.new(:container => Issue.find(1),
124 :file => uploaded_test_file("japanese-utf-8.txt", "text/plain"),
124 :file => uploaded_test_file("japanese-utf-8.txt", "text/plain"),
125 :author => User.find(1))
125 :author => User.find(1))
126 assert a.save
126 assert a.save
127 assert_equal 'japanese-utf-8.txt', a.filename
127 assert_equal 'japanese-utf-8.txt', a.filename
128
128
129 str_japanese = "\xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e"
129 str_japanese = "\xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e"
130 str_japanese.force_encoding('UTF-8') if str_japanese.respond_to?(:force_encoding)
130 str_japanese.force_encoding('UTF-8') if str_japanese.respond_to?(:force_encoding)
131
131
132 get :show, :id => a.id
132 get :show, :id => a.id
133 assert_response :success
133 assert_response :success
134 assert_template 'file'
134 assert_template 'file'
135 assert_equal 'text/html', @response.content_type
135 assert_equal 'text/html', @response.content_type
136 assert_tag :tag => 'th',
136 assert_tag :tag => 'th',
137 :content => '1',
137 :content => '1',
138 :attributes => { :class => 'line-num' },
138 :attributes => { :class => 'line-num' },
139 :sibling => { :tag => 'td', :content => /#{str_japanese}/ }
139 :sibling => { :tag => 'td', :content => /#{str_japanese}/ }
140 end
140 end
141
141
142 def test_show_text_file_replcace_cannot_convert_content
142 def test_show_text_file_replcace_cannot_convert_content
143 set_tmp_attachments_directory
143 set_tmp_attachments_directory
144 with_settings :repositories_encodings => 'UTF-8' do
144 with_settings :repositories_encodings => 'UTF-8' do
145 a = Attachment.new(:container => Issue.find(1),
145 a = Attachment.new(:container => Issue.find(1),
146 :file => uploaded_test_file("iso8859-1.txt", "text/plain"),
146 :file => uploaded_test_file("iso8859-1.txt", "text/plain"),
147 :author => User.find(1))
147 :author => User.find(1))
148 assert a.save
148 assert a.save
149 assert_equal 'iso8859-1.txt', a.filename
149 assert_equal 'iso8859-1.txt', a.filename
150
150
151 get :show, :id => a.id
151 get :show, :id => a.id
152 assert_response :success
152 assert_response :success
153 assert_template 'file'
153 assert_template 'file'
154 assert_equal 'text/html', @response.content_type
154 assert_equal 'text/html', @response.content_type
155 assert_tag :tag => 'th',
155 assert_tag :tag => 'th',
156 :content => '7',
156 :content => '7',
157 :attributes => { :class => 'line-num' },
157 :attributes => { :class => 'line-num' },
158 :sibling => { :tag => 'td', :content => /Demande cr\?\?e avec succ\?s/ }
158 :sibling => { :tag => 'td', :content => /Demande cr\?\?e avec succ\?s/ }
159 end
159 end
160 end
160 end
161
161
162 def test_show_text_file_latin_1
162 def test_show_text_file_latin_1
163 set_tmp_attachments_directory
163 set_tmp_attachments_directory
164 with_settings :repositories_encodings => 'UTF-8,ISO-8859-1' do
164 with_settings :repositories_encodings => 'UTF-8,ISO-8859-1' do
165 a = Attachment.new(:container => Issue.find(1),
165 a = Attachment.new(:container => Issue.find(1),
166 :file => uploaded_test_file("iso8859-1.txt", "text/plain"),
166 :file => uploaded_test_file("iso8859-1.txt", "text/plain"),
167 :author => User.find(1))
167 :author => User.find(1))
168 assert a.save
168 assert a.save
169 assert_equal 'iso8859-1.txt', a.filename
169 assert_equal 'iso8859-1.txt', a.filename
170
170
171 get :show, :id => a.id
171 get :show, :id => a.id
172 assert_response :success
172 assert_response :success
173 assert_template 'file'
173 assert_template 'file'
174 assert_equal 'text/html', @response.content_type
174 assert_equal 'text/html', @response.content_type
175 assert_tag :tag => 'th',
175 assert_tag :tag => 'th',
176 :content => '7',
176 :content => '7',
177 :attributes => { :class => 'line-num' },
177 :attributes => { :class => 'line-num' },
178 :sibling => { :tag => 'td', :content => /Demande créée avec succès/ }
178 :sibling => { :tag => 'td', :content => /Demande créée avec succès/ }
179 end
179 end
180 end
180 end
181
181
182 def test_show_text_file_should_send_if_too_big
182 def test_show_text_file_should_send_if_too_big
183 Setting.file_max_size_displayed = 512
183 Setting.file_max_size_displayed = 512
184 Attachment.find(4).update_attribute :filesize, 754.kilobyte
184 Attachment.find(4).update_attribute :filesize, 754.kilobyte
185
185
186 get :show, :id => 4
186 get :show, :id => 4
187 assert_response :success
187 assert_response :success
188 assert_equal 'application/x-ruby', @response.content_type
188 assert_equal 'application/x-ruby', @response.content_type
189 set_tmp_attachments_directory
189 set_tmp_attachments_directory
190 end
190 end
191
191
192 def test_show_other
192 def test_show_other
193 get :show, :id => 6
193 get :show, :id => 6
194 assert_response :success
194 assert_response :success
195 assert_equal 'application/octet-stream', @response.content_type
195 assert_equal 'application/octet-stream', @response.content_type
196 set_tmp_attachments_directory
196 set_tmp_attachments_directory
197 end
197 end
198
198
199 def test_show_file_from_private_issue_without_permission
199 def test_show_file_from_private_issue_without_permission
200 get :show, :id => 15
200 get :show, :id => 15
201 assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Fattachments%2F15'
201 assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Fattachments%2F15'
202 set_tmp_attachments_directory
202 set_tmp_attachments_directory
203 end
203 end
204
204
205 def test_show_file_from_private_issue_with_permission
205 def test_show_file_from_private_issue_with_permission
206 @request.session[:user_id] = 2
206 @request.session[:user_id] = 2
207 get :show, :id => 15
207 get :show, :id => 15
208 assert_response :success
208 assert_response :success
209 assert_tag 'h2', :content => /private.diff/
209 assert_tag 'h2', :content => /private.diff/
210 set_tmp_attachments_directory
210 set_tmp_attachments_directory
211 end
211 end
212
212
213 def test_show_file_without_container_should_be_denied
213 def test_show_file_without_container_should_be_denied
214 set_tmp_attachments_directory
214 set_tmp_attachments_directory
215 attachment = Attachment.create!(:file => uploaded_test_file("testfile.txt", "text/plain"), :author_id => 2)
215 attachment = Attachment.create!(:file => uploaded_test_file("testfile.txt", "text/plain"), :author_id => 2)
216
216
217 @request.session[:user_id] = 2
217 @request.session[:user_id] = 2
218 get :show, :id => attachment.id
218 get :show, :id => attachment.id
219 assert_response 403
219 assert_response 403
220 end
220 end
221
221
222 def test_show_invalid_should_respond_with_404
222 def test_show_invalid_should_respond_with_404
223 get :show, :id => 999
223 get :show, :id => 999
224 assert_response 404
224 assert_response 404
225 end
225 end
226
226
227 def test_download_text_file
227 def test_download_text_file
228 get :download, :id => 4
228 get :download, :id => 4
229 assert_response :success
229 assert_response :success
230 assert_equal 'application/x-ruby', @response.content_type
230 assert_equal 'application/x-ruby', @response.content_type
231 set_tmp_attachments_directory
231 set_tmp_attachments_directory
232 end
232 end
233
233
234 def test_download_version_file_with_issue_tracking_disabled
234 def test_download_version_file_with_issue_tracking_disabled
235 Project.find(1).disable_module! :issue_tracking
235 Project.find(1).disable_module! :issue_tracking
236 get :download, :id => 9
236 get :download, :id => 9
237 assert_response :success
237 assert_response :success
238 end
238 end
239
239
240 def test_download_should_assign_content_type_if_blank
240 def test_download_should_assign_content_type_if_blank
241 Attachment.find(4).update_attribute(:content_type, '')
241 Attachment.find(4).update_attribute(:content_type, '')
242
242
243 get :download, :id => 4
243 get :download, :id => 4
244 assert_response :success
244 assert_response :success
245 assert_equal 'text/x-ruby', @response.content_type
245 assert_equal 'text/x-ruby', @response.content_type
246 set_tmp_attachments_directory
246 set_tmp_attachments_directory
247 end
247 end
248
248
249 def test_download_missing_file
249 def test_download_missing_file
250 get :download, :id => 2
250 get :download, :id => 2
251 assert_response 404
251 assert_response 404
252 set_tmp_attachments_directory
252 set_tmp_attachments_directory
253 end
253 end
254
254
255 def test_download_should_be_denied_without_permission
255 def test_download_should_be_denied_without_permission
256 get :download, :id => 7
256 get :download, :id => 7
257 assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Fattachments%2Fdownload%2F7'
257 assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Fattachments%2Fdownload%2F7'
258 set_tmp_attachments_directory
258 set_tmp_attachments_directory
259 end
259 end
260
260
261 if convert_installed?
261 if convert_installed?
262 def test_thumbnail
262 def test_thumbnail
263 Attachment.clear_thumbnails
263 Attachment.clear_thumbnails
264 @request.session[:user_id] = 2
264 @request.session[:user_id] = 2
265 with_settings :thumbnails_enabled => '1' do
265
266 get :thumbnail, :id => 16
266 get :thumbnail, :id => 16
267 assert_response :success
267 assert_response :success
268 assert_equal 'image/png', response.content_type
268 assert_equal 'image/png', response.content_type
269 end
270 end
269 end
271
270
272 def test_thumbnail_should_return_404_for_non_image_attachment
271 def test_thumbnail_should_not_exceed_maximum_size
272 Redmine::Thumbnail.expects(:generate).with {|source, target, size| size == 800}
273
273 @request.session[:user_id] = 2
274 @request.session[:user_id] = 2
274 with_settings :thumbnails_enabled => '1' do
275 get :thumbnail, :id => 16, :size => 2000
275 get :thumbnail, :id => 15
276 assert_response 404
277 end
278 end
276 end
279
277
280 def test_thumbnail_should_return_404_if_thumbnails_not_enabled
278 def test_thumbnail_should_round_size
279 Redmine::Thumbnail.expects(:generate).with {|source, target, size| size == 250}
280
281 @request.session[:user_id] = 2
281 @request.session[:user_id] = 2
282 with_settings :thumbnails_enabled => '0' do
282 get :thumbnail, :id => 16, :size => 260
283 get :thumbnail, :id => 16
283 end
284 assert_response 404
284
285 end
285 def test_thumbnail_should_return_404_for_non_image_attachment
286 @request.session[:user_id] = 2
287
288 get :thumbnail, :id => 15
289 assert_response 404
286 end
290 end
287
291
288 def test_thumbnail_should_return_404_if_thumbnail_generation_failed
292 def test_thumbnail_should_return_404_if_thumbnail_generation_failed
289 Attachment.any_instance.stubs(:thumbnail).returns(nil)
293 Attachment.any_instance.stubs(:thumbnail).returns(nil)
290 @request.session[:user_id] = 2
294 @request.session[:user_id] = 2
291 with_settings :thumbnails_enabled => '1' do
295
292 get :thumbnail, :id => 16
296 get :thumbnail, :id => 16
293 assert_response 404
297 assert_response 404
294 end
295 end
298 end
296
299
297 def test_thumbnail_should_be_denied_without_permission
300 def test_thumbnail_should_be_denied_without_permission
298 with_settings :thumbnails_enabled => '1' do
301 get :thumbnail, :id => 16
299 get :thumbnail, :id => 16
302 assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Fattachments%2Fthumbnail%2F16'
300 assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Fattachments%2Fthumbnail%2F16'
301 end
302 end
303 end
303 else
304 else
304 puts '(ImageMagick convert not available)'
305 puts '(ImageMagick convert not available)'
305 end
306 end
306
307
307 def test_destroy_issue_attachment
308 def test_destroy_issue_attachment
308 set_tmp_attachments_directory
309 set_tmp_attachments_directory
309 issue = Issue.find(3)
310 issue = Issue.find(3)
310 @request.session[:user_id] = 2
311 @request.session[:user_id] = 2
311
312
312 assert_difference 'issue.attachments.count', -1 do
313 assert_difference 'issue.attachments.count', -1 do
313 assert_difference 'Journal.count' do
314 assert_difference 'Journal.count' do
314 delete :destroy, :id => 1
315 delete :destroy, :id => 1
315 assert_redirected_to '/projects/ecookbook'
316 assert_redirected_to '/projects/ecookbook'
316 end
317 end
317 end
318 end
318 assert_nil Attachment.find_by_id(1)
319 assert_nil Attachment.find_by_id(1)
319 j = Journal.first(:order => 'id DESC')
320 j = Journal.first(:order => 'id DESC')
320 assert_equal issue, j.journalized
321 assert_equal issue, j.journalized
321 assert_equal 'attachment', j.details.first.property
322 assert_equal 'attachment', j.details.first.property
322 assert_equal '1', j.details.first.prop_key
323 assert_equal '1', j.details.first.prop_key
323 assert_equal 'error281.txt', j.details.first.old_value
324 assert_equal 'error281.txt', j.details.first.old_value
324 assert_equal User.find(2), j.user
325 assert_equal User.find(2), j.user
325 end
326 end
326
327
327 def test_destroy_wiki_page_attachment
328 def test_destroy_wiki_page_attachment
328 set_tmp_attachments_directory
329 set_tmp_attachments_directory
329 @request.session[:user_id] = 2
330 @request.session[:user_id] = 2
330 assert_difference 'Attachment.count', -1 do
331 assert_difference 'Attachment.count', -1 do
331 delete :destroy, :id => 3
332 delete :destroy, :id => 3
332 assert_response 302
333 assert_response 302
333 end
334 end
334 end
335 end
335
336
336 def test_destroy_project_attachment
337 def test_destroy_project_attachment
337 set_tmp_attachments_directory
338 set_tmp_attachments_directory
338 @request.session[:user_id] = 2
339 @request.session[:user_id] = 2
339 assert_difference 'Attachment.count', -1 do
340 assert_difference 'Attachment.count', -1 do
340 delete :destroy, :id => 8
341 delete :destroy, :id => 8
341 assert_response 302
342 assert_response 302
342 end
343 end
343 end
344 end
344
345
345 def test_destroy_version_attachment
346 def test_destroy_version_attachment
346 set_tmp_attachments_directory
347 set_tmp_attachments_directory
347 @request.session[:user_id] = 2
348 @request.session[:user_id] = 2
348 assert_difference 'Attachment.count', -1 do
349 assert_difference 'Attachment.count', -1 do
349 delete :destroy, :id => 9
350 delete :destroy, :id => 9
350 assert_response 302
351 assert_response 302
351 end
352 end
352 end
353 end
353
354
354 def test_destroy_without_permission
355 def test_destroy_without_permission
355 set_tmp_attachments_directory
356 set_tmp_attachments_directory
356 assert_no_difference 'Attachment.count' do
357 assert_no_difference 'Attachment.count' do
357 delete :destroy, :id => 3
358 delete :destroy, :id => 3
358 end
359 end
359 assert_response 302
360 assert_response 302
360 assert Attachment.find_by_id(3)
361 assert Attachment.find_by_id(3)
361 end
362 end
362 end
363 end
@@ -1,65 +1,69
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 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.expand_path('../../../test_helper', __FILE__)
18 require File.expand_path('../../../test_helper', __FILE__)
19
19
20 class RoutingAttachmentsTest < ActionController::IntegrationTest
20 class RoutingAttachmentsTest < ActionController::IntegrationTest
21 def test_attachments
21 def test_attachments
22 assert_routing(
22 assert_routing(
23 { :method => 'get', :path => "/attachments/1" },
23 { :method => 'get', :path => "/attachments/1" },
24 { :controller => 'attachments', :action => 'show', :id => '1' }
24 { :controller => 'attachments', :action => 'show', :id => '1' }
25 )
25 )
26 assert_routing(
26 assert_routing(
27 { :method => 'get', :path => "/attachments/1.xml" },
27 { :method => 'get', :path => "/attachments/1.xml" },
28 { :controller => 'attachments', :action => 'show', :id => '1', :format => 'xml' }
28 { :controller => 'attachments', :action => 'show', :id => '1', :format => 'xml' }
29 )
29 )
30 assert_routing(
30 assert_routing(
31 { :method => 'get', :path => "/attachments/1.json" },
31 { :method => 'get', :path => "/attachments/1.json" },
32 { :controller => 'attachments', :action => 'show', :id => '1', :format => 'json' }
32 { :controller => 'attachments', :action => 'show', :id => '1', :format => 'json' }
33 )
33 )
34 assert_routing(
34 assert_routing(
35 { :method => 'get', :path => "/attachments/1/filename.ext" },
35 { :method => 'get', :path => "/attachments/1/filename.ext" },
36 { :controller => 'attachments', :action => 'show', :id => '1',
36 { :controller => 'attachments', :action => 'show', :id => '1',
37 :filename => 'filename.ext' }
37 :filename => 'filename.ext' }
38 )
38 )
39 assert_routing(
39 assert_routing(
40 { :method => 'get', :path => "/attachments/download/1" },
40 { :method => 'get', :path => "/attachments/download/1" },
41 { :controller => 'attachments', :action => 'download', :id => '1' }
41 { :controller => 'attachments', :action => 'download', :id => '1' }
42 )
42 )
43 assert_routing(
43 assert_routing(
44 { :method => 'get', :path => "/attachments/download/1/filename.ext" },
44 { :method => 'get', :path => "/attachments/download/1/filename.ext" },
45 { :controller => 'attachments', :action => 'download', :id => '1',
45 { :controller => 'attachments', :action => 'download', :id => '1',
46 :filename => 'filename.ext' }
46 :filename => 'filename.ext' }
47 )
47 )
48 assert_routing(
48 assert_routing(
49 { :method => 'get', :path => "/attachments/thumbnail/1" },
49 { :method => 'get', :path => "/attachments/thumbnail/1" },
50 { :controller => 'attachments', :action => 'thumbnail', :id => '1' }
50 { :controller => 'attachments', :action => 'thumbnail', :id => '1' }
51 )
51 )
52 assert_routing(
52 assert_routing(
53 { :method => 'get', :path => "/attachments/thumbnail/1/200" },
54 { :controller => 'attachments', :action => 'thumbnail', :id => '1', :size => '200' }
55 )
56 assert_routing(
53 { :method => 'delete', :path => "/attachments/1" },
57 { :method => 'delete', :path => "/attachments/1" },
54 { :controller => 'attachments', :action => 'destroy', :id => '1' }
58 { :controller => 'attachments', :action => 'destroy', :id => '1' }
55 )
59 )
56 assert_routing(
60 assert_routing(
57 { :method => 'post', :path => '/uploads.xml' },
61 { :method => 'post', :path => '/uploads.xml' },
58 { :controller => 'attachments', :action => 'upload', :format => 'xml' }
62 { :controller => 'attachments', :action => 'upload', :format => 'xml' }
59 )
63 )
60 assert_routing(
64 assert_routing(
61 { :method => 'post', :path => '/uploads.json' },
65 { :method => 'post', :path => '/uploads.json' },
62 { :controller => 'attachments', :action => 'upload', :format => 'json' }
66 { :controller => 'attachments', :action => 'upload', :format => 'json' }
63 )
67 )
64 end
68 end
65 end
69 end
@@ -1,122 +1,142
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 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.expand_path('../../../../../test_helper', __FILE__)
18 require File.expand_path('../../../../../test_helper', __FILE__)
19
19
20 class Redmine::WikiFormatting::MacrosTest < ActionView::TestCase
20 class Redmine::WikiFormatting::MacrosTest < ActionView::TestCase
21 include ApplicationHelper
21 include ApplicationHelper
22 include ActionView::Helpers::TextHelper
22 include ActionView::Helpers::TextHelper
23 include ActionView::Helpers::SanitizeHelper
23 include ActionView::Helpers::SanitizeHelper
24 include ERB::Util
24 include ERB::Util
25 extend ActionView::Helpers::SanitizeHelper::ClassMethods
25 extend ActionView::Helpers::SanitizeHelper::ClassMethods
26
26
27 fixtures :projects, :roles, :enabled_modules, :users,
27 fixtures :projects, :roles, :enabled_modules, :users,
28 :repositories, :changesets,
28 :repositories, :changesets,
29 :trackers, :issue_statuses, :issues,
29 :trackers, :issue_statuses, :issues,
30 :versions, :documents,
30 :versions, :documents,
31 :wikis, :wiki_pages, :wiki_contents,
31 :wikis, :wiki_pages, :wiki_contents,
32 :boards, :messages,
32 :boards, :messages,
33 :attachments
33 :attachments
34
34
35 def setup
35 def setup
36 super
36 super
37 @project = nil
37 @project = nil
38 end
38 end
39
39
40 def teardown
40 def teardown
41 end
41 end
42
42
43 def test_macro_registration
43 def test_macro_registration
44 Redmine::WikiFormatting::Macros.register do
44 Redmine::WikiFormatting::Macros.register do
45 macro :foo do |obj, args|
45 macro :foo do |obj, args|
46 "Foo macro output"
46 "Foo macro output"
47 end
47 end
48 end
48 end
49
49
50 text = "{{foo}}"
50 text = "{{foo}}"
51 assert_equal '<p>Foo macro output</p>', textilizable(text)
51 assert_equal '<p>Foo macro output</p>', textilizable(text)
52 end
52 end
53
53
54 def test_macro_hello_world
54 def test_macro_hello_world
55 text = "{{hello_world}}"
55 text = "{{hello_world}}"
56 assert textilizable(text).match(/Hello world!/)
56 assert textilizable(text).match(/Hello world!/)
57 # escaping
57 # escaping
58 text = "!{{hello_world}}"
58 text = "!{{hello_world}}"
59 assert_equal '<p>{{hello_world}}</p>', textilizable(text)
59 assert_equal '<p>{{hello_world}}</p>', textilizable(text)
60 end
60 end
61
61
62 def test_macro_macro_list
62 def test_macro_macro_list
63 text = "{{macro_list}}"
63 text = "{{macro_list}}"
64 assert_match %r{<code>hello_world</code>}, textilizable(text)
64 assert_match %r{<code>hello_world</code>}, textilizable(text)
65 end
65 end
66
66
67 def test_macro_include
67 def test_macro_include
68 @project = Project.find(1)
68 @project = Project.find(1)
69 # include a page of the current project wiki
69 # include a page of the current project wiki
70 text = "{{include(Another page)}}"
70 text = "{{include(Another page)}}"
71 assert textilizable(text).match(/This is a link to a ticket/)
71 assert textilizable(text).match(/This is a link to a ticket/)
72
72
73 @project = nil
73 @project = nil
74 # include a page of a specific project wiki
74 # include a page of a specific project wiki
75 text = "{{include(ecookbook:Another page)}}"
75 text = "{{include(ecookbook:Another page)}}"
76 assert textilizable(text).match(/This is a link to a ticket/)
76 assert textilizable(text).match(/This is a link to a ticket/)
77
77
78 text = "{{include(ecookbook:)}}"
78 text = "{{include(ecookbook:)}}"
79 assert textilizable(text).match(/CookBook documentation/)
79 assert textilizable(text).match(/CookBook documentation/)
80
80
81 text = "{{include(unknowidentifier:somepage)}}"
81 text = "{{include(unknowidentifier:somepage)}}"
82 assert textilizable(text).match(/Page not found/)
82 assert textilizable(text).match(/Page not found/)
83 end
83 end
84
84
85 def test_macro_child_pages
85 def test_macro_child_pages
86 expected = "<p><ul class=\"pages-hierarchy\">\n" +
86 expected = "<p><ul class=\"pages-hierarchy\">\n" +
87 "<li><a href=\"/projects/ecookbook/wiki/Child_1\">Child 1</a></li>\n" +
87 "<li><a href=\"/projects/ecookbook/wiki/Child_1\">Child 1</a></li>\n" +
88 "<li><a href=\"/projects/ecookbook/wiki/Child_2\">Child 2</a></li>\n" +
88 "<li><a href=\"/projects/ecookbook/wiki/Child_2\">Child 2</a></li>\n" +
89 "</ul>\n</p>"
89 "</ul>\n</p>"
90
90
91 @project = Project.find(1)
91 @project = Project.find(1)
92 # child pages of the current wiki page
92 # child pages of the current wiki page
93 assert_equal expected, textilizable("{{child_pages}}", :object => WikiPage.find(2).content)
93 assert_equal expected, textilizable("{{child_pages}}", :object => WikiPage.find(2).content)
94 # child pages of another page
94 # child pages of another page
95 assert_equal expected, textilizable("{{child_pages(Another_page)}}", :object => WikiPage.find(1).content)
95 assert_equal expected, textilizable("{{child_pages(Another_page)}}", :object => WikiPage.find(1).content)
96
96
97 @project = Project.find(2)
97 @project = Project.find(2)
98 assert_equal expected, textilizable("{{child_pages(ecookbook:Another_page)}}", :object => WikiPage.find(1).content)
98 assert_equal expected, textilizable("{{child_pages(ecookbook:Another_page)}}", :object => WikiPage.find(1).content)
99 end
99 end
100
100
101 def test_macro_child_pages_with_option
101 def test_macro_child_pages_with_option
102 expected = "<p><ul class=\"pages-hierarchy\">\n" +
102 expected = "<p><ul class=\"pages-hierarchy\">\n" +
103 "<li><a href=\"/projects/ecookbook/wiki/Another_page\">Another page</a>\n" +
103 "<li><a href=\"/projects/ecookbook/wiki/Another_page\">Another page</a>\n" +
104 "<ul class=\"pages-hierarchy\">\n" +
104 "<ul class=\"pages-hierarchy\">\n" +
105 "<li><a href=\"/projects/ecookbook/wiki/Child_1\">Child 1</a></li>\n" +
105 "<li><a href=\"/projects/ecookbook/wiki/Child_1\">Child 1</a></li>\n" +
106 "<li><a href=\"/projects/ecookbook/wiki/Child_2\">Child 2</a></li>\n" +
106 "<li><a href=\"/projects/ecookbook/wiki/Child_2\">Child 2</a></li>\n" +
107 "</ul>\n</li>\n</ul>\n</p>"
107 "</ul>\n</li>\n</ul>\n</p>"
108
108
109 @project = Project.find(1)
109 @project = Project.find(1)
110 # child pages of the current wiki page
110 # child pages of the current wiki page
111 assert_equal expected, textilizable("{{child_pages(parent=1)}}", :object => WikiPage.find(2).content)
111 assert_equal expected, textilizable("{{child_pages(parent=1)}}", :object => WikiPage.find(2).content)
112 # child pages of another page
112 # child pages of another page
113 assert_equal expected, textilizable("{{child_pages(Another_page, parent=1)}}", :object => WikiPage.find(1).content)
113 assert_equal expected, textilizable("{{child_pages(Another_page, parent=1)}}", :object => WikiPage.find(1).content)
114
114
115 @project = Project.find(2)
115 @project = Project.find(2)
116 assert_equal expected, textilizable("{{child_pages(ecookbook:Another_page, parent=1)}}", :object => WikiPage.find(1).content)
116 assert_equal expected, textilizable("{{child_pages(ecookbook:Another_page, parent=1)}}", :object => WikiPage.find(1).content)
117 end
117 end
118
118
119 def test_macro_child_pages_without_wiki_page_should_fail
119 def test_macro_child_pages_without_wiki_page_should_fail
120 assert_match /can be called from wiki pages only/, textilizable("{{child_pages}}")
120 assert_match /can be called from wiki pages only/, textilizable("{{child_pages}}")
121 end
121 end
122
123 def test_macro_thumbnail
124 assert_equal '<p><a href="/attachments/17" class="thumbnail" title="testfile.PNG"><img alt="testfile.PNG" src="/attachments/thumbnail/17" /></a></p>',
125 textilizable("{{thumbnail(testfile.png)}}", :object => Issue.find(14))
126 end
127
128 def test_macro_thumbnail_with_size
129 assert_equal '<p><a href="/attachments/17" class="thumbnail" title="testfile.PNG"><img alt="testfile.PNG" src="/attachments/thumbnail/17/200" /></a></p>',
130 textilizable("{{thumbnail(testfile.png, size=200)}}", :object => Issue.find(14))
131 end
132
133 def test_macro_thumbnail_with_title
134 assert_equal '<p><a href="/attachments/17" class="thumbnail" title="Cool image"><img alt="testfile.PNG" src="/attachments/thumbnail/17" /></a></p>',
135 textilizable("{{thumbnail(testfile.png, title=Cool image)}}", :object => Issue.find(14))
136 end
137
138 def test_macro_thumbnail_with_invalid_filename_should_fail
139 assert_include 'test.png not found',
140 textilizable("{{thumbnail(test.png)}}", :object => Issue.find(14))
141 end
122 end
142 end
General Comments 0
You need to be logged in to leave comments. Login now