##// END OF EJS Templates
Auto-detect attachment content type when blank (#3782)....
Jean-Philippe Lang -
r3144:ff7eb7b23a6a
parent child
Show More
@@ -1,79 +1,87
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class AttachmentsController < ApplicationController
19 19 before_filter :find_project
20 20 before_filter :file_readable, :read_authorize, :except => :destroy
21 21 before_filter :delete_authorize, :only => :destroy
22 22
23 23 verify :method => :post, :only => :destroy
24 24
25 25 def show
26 26 if @attachment.is_diff?
27 27 @diff = File.new(@attachment.diskfile, "rb").read
28 28 render :action => 'diff'
29 29 elsif @attachment.is_text? && @attachment.filesize <= Setting.file_max_size_displayed.to_i.kilobyte
30 30 @content = File.new(@attachment.diskfile, "rb").read
31 31 render :action => 'file'
32 32 else
33 33 download
34 34 end
35 35 end
36 36
37 37 def download
38 38 if @attachment.container.is_a?(Version) || @attachment.container.is_a?(Project)
39 39 @attachment.increment_download
40 40 end
41 41
42 42 # images are sent inline
43 43 send_file @attachment.diskfile, :filename => filename_for_content_disposition(@attachment.filename),
44 :type => @attachment.content_type,
44 :type => detect_content_type(@attachment),
45 45 :disposition => (@attachment.image? ? 'inline' : 'attachment')
46 46
47 47 end
48 48
49 49 def destroy
50 50 # Make sure association callbacks are called
51 51 @attachment.container.attachments.delete(@attachment)
52 52 redirect_to :back
53 53 rescue ::ActionController::RedirectBackError
54 54 redirect_to :controller => 'projects', :action => 'show', :id => @project
55 55 end
56 56
57 57 private
58 58 def find_project
59 59 @attachment = Attachment.find(params[:id])
60 60 # Show 404 if the filename in the url is wrong
61 61 raise ActiveRecord::RecordNotFound if params[:filename] && params[:filename] != @attachment.filename
62 62 @project = @attachment.project
63 63 rescue ActiveRecord::RecordNotFound
64 64 render_404
65 65 end
66 66
67 67 # Checks that the file exists and is readable
68 68 def file_readable
69 69 @attachment.readable? ? true : render_404
70 70 end
71 71
72 72 def read_authorize
73 73 @attachment.visible? ? true : deny_access
74 74 end
75 75
76 76 def delete_authorize
77 77 @attachment.deletable? ? true : deny_access
78 78 end
79
80 def detect_content_type(attachment)
81 content_type = attachment.content_type
82 if content_type.blank?
83 content_type = Redmine::MimeType.of(attachment.filename)
84 end
85 content_type
86 end
79 87 end
@@ -1,157 +1,160
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require "digest/md5"
19 19
20 20 class Attachment < ActiveRecord::Base
21 21 belongs_to :container, :polymorphic => true
22 22 belongs_to :author, :class_name => "User", :foreign_key => "author_id"
23 23
24 24 validates_presence_of :container, :filename, :author
25 25 validates_length_of :filename, :maximum => 255
26 26 validates_length_of :disk_filename, :maximum => 255
27 27
28 28 acts_as_event :title => :filename,
29 29 :url => Proc.new {|o| {:controller => 'attachments', :action => 'download', :id => o.id, :filename => o.filename}}
30 30
31 31 acts_as_activity_provider :type => 'files',
32 32 :permission => :view_files,
33 33 :author_key => :author_id,
34 34 :find_options => {:select => "#{Attachment.table_name}.*",
35 35 :joins => "LEFT JOIN #{Version.table_name} ON #{Attachment.table_name}.container_type='Version' AND #{Version.table_name}.id = #{Attachment.table_name}.container_id " +
36 36 "LEFT JOIN #{Project.table_name} ON #{Version.table_name}.project_id = #{Project.table_name}.id OR ( #{Attachment.table_name}.container_type='Project' AND #{Attachment.table_name}.container_id = #{Project.table_name}.id )"}
37 37
38 38 acts_as_activity_provider :type => 'documents',
39 39 :permission => :view_documents,
40 40 :author_key => :author_id,
41 41 :find_options => {:select => "#{Attachment.table_name}.*",
42 42 :joins => "LEFT JOIN #{Document.table_name} ON #{Attachment.table_name}.container_type='Document' AND #{Document.table_name}.id = #{Attachment.table_name}.container_id " +
43 43 "LEFT JOIN #{Project.table_name} ON #{Document.table_name}.project_id = #{Project.table_name}.id"}
44 44
45 45 cattr_accessor :storage_path
46 46 @@storage_path = "#{RAILS_ROOT}/files"
47 47
48 48 def validate
49 49 if self.filesize > Setting.attachment_max_size.to_i.kilobytes
50 50 errors.add(:base, :too_long, :count => Setting.attachment_max_size.to_i.kilobytes)
51 51 end
52 52 end
53 53
54 54 def file=(incoming_file)
55 55 unless incoming_file.nil?
56 56 @temp_file = incoming_file
57 57 if @temp_file.size > 0
58 58 self.filename = sanitize_filename(@temp_file.original_filename)
59 59 self.disk_filename = Attachment.disk_filename(filename)
60 60 self.content_type = @temp_file.content_type.to_s.chomp
61 if content_type.blank?
62 self.content_type = Redmine::MimeType.of(filename)
63 end
61 64 self.filesize = @temp_file.size
62 65 end
63 66 end
64 67 end
65 68
66 69 def file
67 70 nil
68 71 end
69 72
70 73 # Copies the temporary file to its final location
71 74 # and computes its MD5 hash
72 75 def before_save
73 76 if @temp_file && (@temp_file.size > 0)
74 77 logger.debug("saving '#{self.diskfile}'")
75 78 md5 = Digest::MD5.new
76 79 File.open(diskfile, "wb") do |f|
77 80 buffer = ""
78 81 while (buffer = @temp_file.read(8192))
79 82 f.write(buffer)
80 83 md5.update(buffer)
81 84 end
82 85 end
83 86 self.digest = md5.hexdigest
84 87 end
85 88 # Don't save the content type if it's longer than the authorized length
86 89 if self.content_type && self.content_type.length > 255
87 90 self.content_type = nil
88 91 end
89 92 end
90 93
91 94 # Deletes file on the disk
92 95 def after_destroy
93 96 File.delete(diskfile) if !filename.blank? && File.exist?(diskfile)
94 97 end
95 98
96 99 # Returns file's location on disk
97 100 def diskfile
98 101 "#{@@storage_path}/#{self.disk_filename}"
99 102 end
100 103
101 104 def increment_download
102 105 increment!(:downloads)
103 106 end
104 107
105 108 def project
106 109 container.project
107 110 end
108 111
109 112 def visible?(user=User.current)
110 113 container.attachments_visible?(user)
111 114 end
112 115
113 116 def deletable?(user=User.current)
114 117 container.attachments_deletable?(user)
115 118 end
116 119
117 120 def image?
118 121 self.filename =~ /\.(jpe?g|gif|png)$/i
119 122 end
120 123
121 124 def is_text?
122 125 Redmine::MimeType.is_type?('text', filename)
123 126 end
124 127
125 128 def is_diff?
126 129 self.filename =~ /\.(patch|diff)$/i
127 130 end
128 131
129 132 # Returns true if the file is readable
130 133 def readable?
131 134 File.readable?(diskfile)
132 135 end
133 136
134 137 private
135 138 def sanitize_filename(value)
136 139 # get only the filename, not the whole path
137 140 just_filename = value.gsub(/^.*(\\|\/)/, '')
138 141 # NOTE: File.basename doesn't work right with Windows paths on Unix
139 142 # INCORRECT: just_filename = File.basename(value.gsub('\\\\', '/'))
140 143
141 144 # Finally, replace all non alphanumeric, hyphens or periods with underscore
142 145 @filename = just_filename.gsub(/[^\w\.\-]/,'_')
143 146 end
144 147
145 148 # Returns an ASCII or hashed filename
146 149 def self.disk_filename(filename)
147 150 df = DateTime.now.strftime("%y%m%d%H%M%S") + "_"
148 151 if filename =~ %r{^[a-zA-Z0-9_\.\-]*$}
149 152 df << filename
150 153 else
151 154 df << Digest::MD5.hexdigest(filename)
152 155 # keep the extension if any
153 156 df << $1 if filename =~ %r{(\.[a-zA-Z0-9]+)$}
154 157 end
155 158 df
156 159 end
157 160 end
@@ -1,81 +1,96
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
1 # Redmine - project management software
2 # Copyright (C) 2006-2009 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 module Redmine
19 19 module MimeType
20 20
21 21 MIME_TYPES = {
22 22 'text/plain' => 'txt,tpl,properties,patch,diff,ini,readme,install,upgrade',
23 23 'text/css' => 'css',
24 24 'text/html' => 'html,htm,xhtml',
25 25 'text/jsp' => 'jsp',
26 26 'text/x-c' => 'c,cpp,cc,h,hh',
27 27 'text/x-csharp' => 'cs',
28 28 'text/x-java' => 'java',
29 29 'text/x-javascript' => 'js',
30 30 'text/x-html-template' => 'rhtml',
31 31 'text/x-perl' => 'pl,pm',
32 32 'text/x-php' => 'php,php3,php4,php5',
33 33 'text/x-python' => 'py',
34 34 'text/x-ruby' => 'rb,rbw,ruby,rake,erb',
35 35 'text/x-csh' => 'csh',
36 36 'text/x-sh' => 'sh',
37 37 'text/xml' => 'xml,xsd,mxml',
38 38 'text/yaml' => 'yml,yaml',
39 'text/csv' => 'csv',
39 40 'image/gif' => 'gif',
40 41 'image/jpeg' => 'jpg,jpeg,jpe',
41 42 'image/png' => 'png',
42 43 'image/tiff' => 'tiff,tif',
43 44 'image/x-ms-bmp' => 'bmp',
44 45 'image/x-xpixmap' => 'xpm',
45 46 'application/pdf' => 'pdf',
47 'application/rtf' => 'rtf',
48 'application/msword' => 'doc',
49 'application/vnd.ms-excel' => 'xls',
50 'application/vnd.ms-powerpoint' => 'ppt,pps',
51 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'docx',
52 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => 'xlsx',
53 'application/vnd.openxmlformats-officedocument.presentationml.presentation' => 'pptx',
54 'application/vnd.openxmlformats-officedocument.presentationml.slideshow' => 'ppsx',
55 'application/vnd.oasis.opendocument.spreadsheet' => 'ods',
56 'application/vnd.oasis.opendocument.text' => 'odt',
57 'application/vnd.oasis.opendocument.presentation' => 'odp',
58 'application/x-7z-compressed' => '7z',
59 'application/x-rar-compressed' => 'rar',
60 'application/x-tar' => 'tar',
46 61 'application/zip' => 'zip',
47 62 'application/x-gzip' => 'gz',
48 63 }.freeze
49 64
50 65 EXTENSIONS = MIME_TYPES.inject({}) do |map, (type, exts)|
51 66 exts.split(',').each {|ext| map[ext.strip] = type}
52 67 map
53 68 end
54 69
55 70 # returns mime type for name or nil if unknown
56 71 def self.of(name)
57 72 return nil unless name
58 73 m = name.to_s.match(/(^|\.)([^\.]+)$/)
59 74 EXTENSIONS[m[2].downcase] if m
60 75 end
61 76
62 77 # Returns the css class associated to
63 78 # the mime type of name
64 79 def self.css_class_of(name)
65 80 mime = of(name)
66 81 mime && mime.gsub('/', '-')
67 82 end
68 83
69 84 def self.main_mimetype_of(name)
70 85 mimetype = of(name)
71 86 mimetype.split('/').first if mimetype
72 87 end
73 88
74 89 # return true if mime-type for name is type/*
75 90 # otherwise false
76 91 def self.is_type?(type, name)
77 92 main_mimetype = main_mimetype_of(name)
78 93 type.to_s == main_mimetype
79 94 end
80 95 end
81 96 end
@@ -1,764 +1,764
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require 'active_record'
19 19 require 'iconv'
20 20 require 'pp'
21 21
22 22 namespace :redmine do
23 23 desc 'Trac migration script'
24 24 task :migrate_from_trac => :environment do
25 25
26 26 module TracMigrate
27 27 TICKET_MAP = []
28 28
29 29 DEFAULT_STATUS = IssueStatus.default
30 30 assigned_status = IssueStatus.find_by_position(2)
31 31 resolved_status = IssueStatus.find_by_position(3)
32 32 feedback_status = IssueStatus.find_by_position(4)
33 33 closed_status = IssueStatus.find :first, :conditions => { :is_closed => true }
34 34 STATUS_MAPPING = {'new' => DEFAULT_STATUS,
35 35 'reopened' => feedback_status,
36 36 'assigned' => assigned_status,
37 37 'closed' => closed_status
38 38 }
39 39
40 40 priorities = IssuePriority.all
41 41 DEFAULT_PRIORITY = priorities[0]
42 42 PRIORITY_MAPPING = {'lowest' => priorities[0],
43 43 'low' => priorities[0],
44 44 'normal' => priorities[1],
45 45 'high' => priorities[2],
46 46 'highest' => priorities[3],
47 47 # ---
48 48 'trivial' => priorities[0],
49 49 'minor' => priorities[1],
50 50 'major' => priorities[2],
51 51 'critical' => priorities[3],
52 52 'blocker' => priorities[4]
53 53 }
54 54
55 55 TRACKER_BUG = Tracker.find_by_position(1)
56 56 TRACKER_FEATURE = Tracker.find_by_position(2)
57 57 DEFAULT_TRACKER = TRACKER_BUG
58 58 TRACKER_MAPPING = {'defect' => TRACKER_BUG,
59 59 'enhancement' => TRACKER_FEATURE,
60 60 'task' => TRACKER_FEATURE,
61 61 'patch' =>TRACKER_FEATURE
62 62 }
63 63
64 64 roles = Role.find(:all, :conditions => {:builtin => 0}, :order => 'position ASC')
65 65 manager_role = roles[0]
66 66 developer_role = roles[1]
67 67 DEFAULT_ROLE = roles.last
68 68 ROLE_MAPPING = {'admin' => manager_role,
69 69 'developer' => developer_role
70 70 }
71 71
72 72 class ::Time
73 73 class << self
74 74 alias :real_now :now
75 75 def now
76 76 real_now - @fake_diff.to_i
77 77 end
78 78 def fake(time)
79 79 @fake_diff = real_now - time
80 80 res = yield
81 81 @fake_diff = 0
82 82 res
83 83 end
84 84 end
85 85 end
86 86
87 87 class TracComponent < ActiveRecord::Base
88 88 set_table_name :component
89 89 end
90 90
91 91 class TracMilestone < ActiveRecord::Base
92 92 set_table_name :milestone
93 93 # If this attribute is set a milestone has a defined target timepoint
94 94 def due
95 95 if read_attribute(:due) && read_attribute(:due) > 0
96 96 Time.at(read_attribute(:due)).to_date
97 97 else
98 98 nil
99 99 end
100 100 end
101 101 # This is the real timepoint at which the milestone has finished.
102 102 def completed
103 103 if read_attribute(:completed) && read_attribute(:completed) > 0
104 104 Time.at(read_attribute(:completed)).to_date
105 105 else
106 106 nil
107 107 end
108 108 end
109 109
110 110 def description
111 111 # Attribute is named descr in Trac v0.8.x
112 112 has_attribute?(:descr) ? read_attribute(:descr) : read_attribute(:description)
113 113 end
114 114 end
115 115
116 116 class TracTicketCustom < ActiveRecord::Base
117 117 set_table_name :ticket_custom
118 118 end
119 119
120 120 class TracAttachment < ActiveRecord::Base
121 121 set_table_name :attachment
122 122 set_inheritance_column :none
123 123
124 124 def time; Time.at(read_attribute(:time)) end
125 125
126 126 def original_filename
127 127 filename
128 128 end
129 129
130 130 def content_type
131 Redmine::MimeType.of(filename) || ''
131 ''
132 132 end
133 133
134 134 def exist?
135 135 File.file? trac_fullpath
136 136 end
137 137
138 138 def open
139 139 File.open("#{trac_fullpath}", 'rb') {|f|
140 140 @file = f
141 141 yield self
142 142 }
143 143 end
144 144
145 145 def read(*args)
146 146 @file.read(*args)
147 147 end
148 148
149 149 def description
150 150 read_attribute(:description).to_s.slice(0,255)
151 151 end
152 152
153 153 private
154 154 def trac_fullpath
155 155 attachment_type = read_attribute(:type)
156 156 trac_file = filename.gsub( /[^a-zA-Z0-9\-_\.!~*']/n ) {|x| sprintf('%%%02x', x[0]) }
157 157 "#{TracMigrate.trac_attachments_directory}/#{attachment_type}/#{id}/#{trac_file}"
158 158 end
159 159 end
160 160
161 161 class TracTicket < ActiveRecord::Base
162 162 set_table_name :ticket
163 163 set_inheritance_column :none
164 164
165 165 # ticket changes: only migrate status changes and comments
166 166 has_many :changes, :class_name => "TracTicketChange", :foreign_key => :ticket
167 167 has_many :attachments, :class_name => "TracAttachment",
168 168 :finder_sql => "SELECT DISTINCT attachment.* FROM #{TracMigrate::TracAttachment.table_name}" +
169 169 " WHERE #{TracMigrate::TracAttachment.table_name}.type = 'ticket'" +
170 170 ' AND #{TracMigrate::TracAttachment.table_name}.id = \'#{id}\''
171 171 has_many :customs, :class_name => "TracTicketCustom", :foreign_key => :ticket
172 172
173 173 def ticket_type
174 174 read_attribute(:type)
175 175 end
176 176
177 177 def summary
178 178 read_attribute(:summary).blank? ? "(no subject)" : read_attribute(:summary)
179 179 end
180 180
181 181 def description
182 182 read_attribute(:description).blank? ? summary : read_attribute(:description)
183 183 end
184 184
185 185 def time; Time.at(read_attribute(:time)) end
186 186 def changetime; Time.at(read_attribute(:changetime)) end
187 187 end
188 188
189 189 class TracTicketChange < ActiveRecord::Base
190 190 set_table_name :ticket_change
191 191
192 192 def time; Time.at(read_attribute(:time)) end
193 193 end
194 194
195 195 TRAC_WIKI_PAGES = %w(InterMapTxt InterTrac InterWiki RecentChanges SandBox TracAccessibility TracAdmin TracBackup TracBrowser TracCgi TracChangeset \
196 196 TracEnvironment TracFastCgi TracGuide TracImport TracIni TracInstall TracInterfaceCustomization \
197 197 TracLinks TracLogging TracModPython TracNotification TracPermissions TracPlugins TracQuery \
198 198 TracReports TracRevisionLog TracRoadmap TracRss TracSearch TracStandalone TracSupport TracSyntaxColoring TracTickets \
199 199 TracTicketsCustomFields TracTimeline TracUnicode TracUpgrade TracWiki WikiDeletePage WikiFormatting \
200 200 WikiHtml WikiMacros WikiNewPage WikiPageNames WikiProcessors WikiRestructuredText WikiRestructuredTextLinks \
201 201 CamelCase TitleIndex)
202 202
203 203 class TracWikiPage < ActiveRecord::Base
204 204 set_table_name :wiki
205 205 set_primary_key :name
206 206
207 207 has_many :attachments, :class_name => "TracAttachment",
208 208 :finder_sql => "SELECT DISTINCT attachment.* FROM #{TracMigrate::TracAttachment.table_name}" +
209 209 " WHERE #{TracMigrate::TracAttachment.table_name}.type = 'wiki'" +
210 210 ' AND #{TracMigrate::TracAttachment.table_name}.id = \'#{id}\''
211 211
212 212 def self.columns
213 213 # Hides readonly Trac field to prevent clash with AR readonly? method (Rails 2.0)
214 214 super.select {|column| column.name.to_s != 'readonly'}
215 215 end
216 216
217 217 def time; Time.at(read_attribute(:time)) end
218 218 end
219 219
220 220 class TracPermission < ActiveRecord::Base
221 221 set_table_name :permission
222 222 end
223 223
224 224 class TracSessionAttribute < ActiveRecord::Base
225 225 set_table_name :session_attribute
226 226 end
227 227
228 228 def self.find_or_create_user(username, project_member = false)
229 229 return User.anonymous if username.blank?
230 230
231 231 u = User.find_by_login(username)
232 232 if !u
233 233 # Create a new user if not found
234 234 mail = username[0,limit_for(User, 'mail')]
235 235 if mail_attr = TracSessionAttribute.find_by_sid_and_name(username, 'email')
236 236 mail = mail_attr.value
237 237 end
238 238 mail = "#{mail}@foo.bar" unless mail.include?("@")
239 239
240 240 name = username
241 241 if name_attr = TracSessionAttribute.find_by_sid_and_name(username, 'name')
242 242 name = name_attr.value
243 243 end
244 244 name =~ (/(.*)(\s+\w+)?/)
245 245 fn = $1.strip
246 246 ln = ($2 || '-').strip
247 247
248 248 u = User.new :mail => mail.gsub(/[^-@a-z0-9\.]/i, '-'),
249 249 :firstname => fn[0, limit_for(User, 'firstname')].gsub(/[^\w\s\'\-]/i, '-'),
250 250 :lastname => ln[0, limit_for(User, 'lastname')].gsub(/[^\w\s\'\-]/i, '-')
251 251
252 252 u.login = username[0,limit_for(User, 'login')].gsub(/[^a-z0-9_\-@\.]/i, '-')
253 253 u.password = 'trac'
254 254 u.admin = true if TracPermission.find_by_username_and_action(username, 'admin')
255 255 # finally, a default user is used if the new user is not valid
256 256 u = User.find(:first) unless u.save
257 257 end
258 258 # Make sure he is a member of the project
259 259 if project_member && !u.member_of?(@target_project)
260 260 role = DEFAULT_ROLE
261 261 if u.admin
262 262 role = ROLE_MAPPING['admin']
263 263 elsif TracPermission.find_by_username_and_action(username, 'developer')
264 264 role = ROLE_MAPPING['developer']
265 265 end
266 266 Member.create(:user => u, :project => @target_project, :roles => [role])
267 267 u.reload
268 268 end
269 269 u
270 270 end
271 271
272 272 # Basic wiki syntax conversion
273 273 def self.convert_wiki_text(text)
274 274 # Titles
275 275 text = text.gsub(/^(\=+)\s(.+)\s(\=+)/) {|s| "\nh#{$1.length}. #{$2}\n"}
276 276 # External Links
277 277 text = text.gsub(/\[(http[^\s]+)\s+([^\]]+)\]/) {|s| "\"#{$2}\":#{$1}"}
278 278 # Ticket links:
279 279 # [ticket:234 Text],[ticket:234 This is a test]
280 280 text = text.gsub(/\[ticket\:([^\ ]+)\ (.+?)\]/, '"\2":/issues/show/\1')
281 281 # ticket:1234
282 282 # #1 is working cause Redmine uses the same syntax.
283 283 text = text.gsub(/ticket\:([^\ ]+)/, '#\1')
284 284 # Milestone links:
285 285 # [milestone:"0.1.0 Mercury" Milestone 0.1.0 (Mercury)]
286 286 # The text "Milestone 0.1.0 (Mercury)" is not converted,
287 287 # cause Redmine's wiki does not support this.
288 288 text = text.gsub(/\[milestone\:\"([^\"]+)\"\ (.+?)\]/, 'version:"\1"')
289 289 # [milestone:"0.1.0 Mercury"]
290 290 text = text.gsub(/\[milestone\:\"([^\"]+)\"\]/, 'version:"\1"')
291 291 text = text.gsub(/milestone\:\"([^\"]+)\"/, 'version:"\1"')
292 292 # milestone:0.1.0
293 293 text = text.gsub(/\[milestone\:([^\ ]+)\]/, 'version:\1')
294 294 text = text.gsub(/milestone\:([^\ ]+)/, 'version:\1')
295 295 # Internal Links
296 296 text = text.gsub(/\[\[BR\]\]/, "\n") # This has to go before the rules below
297 297 text = text.gsub(/\[\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
298 298 text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
299 299 text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
300 300 text = text.gsub(/\[wiki:([^\s\]]+)\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
301 301 text = text.gsub(/\[wiki:([^\s\]]+)\s(.*)\]/) {|s| "[[#{$1.delete(',./?;|:')}|#{$2.delete(',./?;|:')}]]"}
302 302
303 303 # Links to pages UsingJustWikiCaps
304 304 text = text.gsub(/([^!]|^)(^| )([A-Z][a-z]+[A-Z][a-zA-Z]+)/, '\\1\\2[[\3]]')
305 305 # Normalize things that were supposed to not be links
306 306 # like !NotALink
307 307 text = text.gsub(/(^| )!([A-Z][A-Za-z]+)/, '\1\2')
308 308 # Revisions links
309 309 text = text.gsub(/\[(\d+)\]/, 'r\1')
310 310 # Ticket number re-writing
311 311 text = text.gsub(/#(\d+)/) do |s|
312 312 if $1.length < 10
313 313 TICKET_MAP[$1.to_i] ||= $1
314 314 "\##{TICKET_MAP[$1.to_i] || $1}"
315 315 else
316 316 s
317 317 end
318 318 end
319 319 # We would like to convert the Code highlighting too
320 320 # This will go into the next line.
321 321 shebang_line = false
322 322 # Reguar expression for start of code
323 323 pre_re = /\{\{\{/
324 324 # Code hightlighing...
325 325 shebang_re = /^\#\!([a-z]+)/
326 326 # Regular expression for end of code
327 327 pre_end_re = /\}\}\}/
328 328
329 329 # Go through the whole text..extract it line by line
330 330 text = text.gsub(/^(.*)$/) do |line|
331 331 m_pre = pre_re.match(line)
332 332 if m_pre
333 333 line = '<pre>'
334 334 else
335 335 m_sl = shebang_re.match(line)
336 336 if m_sl
337 337 shebang_line = true
338 338 line = '<code class="' + m_sl[1] + '">'
339 339 end
340 340 m_pre_end = pre_end_re.match(line)
341 341 if m_pre_end
342 342 line = '</pre>'
343 343 if shebang_line
344 344 line = '</code>' + line
345 345 end
346 346 end
347 347 end
348 348 line
349 349 end
350 350
351 351 # Highlighting
352 352 text = text.gsub(/'''''([^\s])/, '_*\1')
353 353 text = text.gsub(/([^\s])'''''/, '\1*_')
354 354 text = text.gsub(/'''/, '*')
355 355 text = text.gsub(/''/, '_')
356 356 text = text.gsub(/__/, '+')
357 357 text = text.gsub(/~~/, '-')
358 358 text = text.gsub(/`/, '@')
359 359 text = text.gsub(/,,/, '~')
360 360 # Lists
361 361 text = text.gsub(/^([ ]+)\* /) {|s| '*' * $1.length + " "}
362 362
363 363 text
364 364 end
365 365
366 366 def self.migrate
367 367 establish_connection
368 368
369 369 # Quick database test
370 370 TracComponent.count
371 371
372 372 migrated_components = 0
373 373 migrated_milestones = 0
374 374 migrated_tickets = 0
375 375 migrated_custom_values = 0
376 376 migrated_ticket_attachments = 0
377 377 migrated_wiki_edits = 0
378 378 migrated_wiki_attachments = 0
379 379
380 380 #Wiki system initializing...
381 381 @target_project.wiki.destroy if @target_project.wiki
382 382 @target_project.reload
383 383 wiki = Wiki.new(:project => @target_project, :start_page => 'WikiStart')
384 384 wiki_edit_count = 0
385 385
386 386 # Components
387 387 print "Migrating components"
388 388 issues_category_map = {}
389 389 TracComponent.find(:all).each do |component|
390 390 print '.'
391 391 STDOUT.flush
392 392 c = IssueCategory.new :project => @target_project,
393 393 :name => encode(component.name[0, limit_for(IssueCategory, 'name')])
394 394 next unless c.save
395 395 issues_category_map[component.name] = c
396 396 migrated_components += 1
397 397 end
398 398 puts
399 399
400 400 # Milestones
401 401 print "Migrating milestones"
402 402 version_map = {}
403 403 TracMilestone.find(:all).each do |milestone|
404 404 print '.'
405 405 STDOUT.flush
406 406 # First we try to find the wiki page...
407 407 p = wiki.find_or_new_page(milestone.name.to_s)
408 408 p.content = WikiContent.new(:page => p) if p.new_record?
409 409 p.content.text = milestone.description.to_s
410 410 p.content.author = find_or_create_user('trac')
411 411 p.content.comments = 'Milestone'
412 412 p.save
413 413
414 414 v = Version.new :project => @target_project,
415 415 :name => encode(milestone.name[0, limit_for(Version, 'name')]),
416 416 :description => nil,
417 417 :wiki_page_title => milestone.name.to_s,
418 418 :effective_date => milestone.completed
419 419
420 420 next unless v.save
421 421 version_map[milestone.name] = v
422 422 migrated_milestones += 1
423 423 end
424 424 puts
425 425
426 426 # Custom fields
427 427 # TODO: read trac.ini instead
428 428 print "Migrating custom fields"
429 429 custom_field_map = {}
430 430 TracTicketCustom.find_by_sql("SELECT DISTINCT name FROM #{TracTicketCustom.table_name}").each do |field|
431 431 print '.'
432 432 STDOUT.flush
433 433 # Redmine custom field name
434 434 field_name = encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize
435 435 # Find if the custom already exists in Redmine
436 436 f = IssueCustomField.find_by_name(field_name)
437 437 # Or create a new one
438 438 f ||= IssueCustomField.create(:name => encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize,
439 439 :field_format => 'string')
440 440
441 441 next if f.new_record?
442 442 f.trackers = Tracker.find(:all)
443 443 f.projects << @target_project
444 444 custom_field_map[field.name] = f
445 445 end
446 446 puts
447 447
448 448 # Trac 'resolution' field as a Redmine custom field
449 449 r = IssueCustomField.find(:first, :conditions => { :name => "Resolution" })
450 450 r = IssueCustomField.new(:name => 'Resolution',
451 451 :field_format => 'list',
452 452 :is_filter => true) if r.nil?
453 453 r.trackers = Tracker.find(:all)
454 454 r.projects << @target_project
455 455 r.possible_values = (r.possible_values + %w(fixed invalid wontfix duplicate worksforme)).flatten.compact.uniq
456 456 r.save!
457 457 custom_field_map['resolution'] = r
458 458
459 459 # Tickets
460 460 print "Migrating tickets"
461 461 TracTicket.find_each(:batch_size => 200) do |ticket|
462 462 print '.'
463 463 STDOUT.flush
464 464 i = Issue.new :project => @target_project,
465 465 :subject => encode(ticket.summary[0, limit_for(Issue, 'subject')]),
466 466 :description => convert_wiki_text(encode(ticket.description)),
467 467 :priority => PRIORITY_MAPPING[ticket.priority] || DEFAULT_PRIORITY,
468 468 :created_on => ticket.time
469 469 i.author = find_or_create_user(ticket.reporter)
470 470 i.category = issues_category_map[ticket.component] unless ticket.component.blank?
471 471 i.fixed_version = version_map[ticket.milestone] unless ticket.milestone.blank?
472 472 i.status = STATUS_MAPPING[ticket.status] || DEFAULT_STATUS
473 473 i.tracker = TRACKER_MAPPING[ticket.ticket_type] || DEFAULT_TRACKER
474 474 i.id = ticket.id unless Issue.exists?(ticket.id)
475 475 next unless Time.fake(ticket.changetime) { i.save }
476 476 TICKET_MAP[ticket.id] = i.id
477 477 migrated_tickets += 1
478 478
479 479 # Owner
480 480 unless ticket.owner.blank?
481 481 i.assigned_to = find_or_create_user(ticket.owner, true)
482 482 Time.fake(ticket.changetime) { i.save }
483 483 end
484 484
485 485 # Comments and status/resolution changes
486 486 ticket.changes.group_by(&:time).each do |time, changeset|
487 487 status_change = changeset.select {|change| change.field == 'status'}.first
488 488 resolution_change = changeset.select {|change| change.field == 'resolution'}.first
489 489 comment_change = changeset.select {|change| change.field == 'comment'}.first
490 490
491 491 n = Journal.new :notes => (comment_change ? convert_wiki_text(encode(comment_change.newvalue)) : ''),
492 492 :created_on => time
493 493 n.user = find_or_create_user(changeset.first.author)
494 494 n.journalized = i
495 495 if status_change &&
496 496 STATUS_MAPPING[status_change.oldvalue] &&
497 497 STATUS_MAPPING[status_change.newvalue] &&
498 498 (STATUS_MAPPING[status_change.oldvalue] != STATUS_MAPPING[status_change.newvalue])
499 499 n.details << JournalDetail.new(:property => 'attr',
500 500 :prop_key => 'status_id',
501 501 :old_value => STATUS_MAPPING[status_change.oldvalue].id,
502 502 :value => STATUS_MAPPING[status_change.newvalue].id)
503 503 end
504 504 if resolution_change
505 505 n.details << JournalDetail.new(:property => 'cf',
506 506 :prop_key => custom_field_map['resolution'].id,
507 507 :old_value => resolution_change.oldvalue,
508 508 :value => resolution_change.newvalue)
509 509 end
510 510 n.save unless n.details.empty? && n.notes.blank?
511 511 end
512 512
513 513 # Attachments
514 514 ticket.attachments.each do |attachment|
515 515 next unless attachment.exist?
516 516 attachment.open {
517 517 a = Attachment.new :created_on => attachment.time
518 518 a.file = attachment
519 519 a.author = find_or_create_user(attachment.author)
520 520 a.container = i
521 521 a.description = attachment.description
522 522 migrated_ticket_attachments += 1 if a.save
523 523 }
524 524 end
525 525
526 526 # Custom fields
527 527 custom_values = ticket.customs.inject({}) do |h, custom|
528 528 if custom_field = custom_field_map[custom.name]
529 529 h[custom_field.id] = custom.value
530 530 migrated_custom_values += 1
531 531 end
532 532 h
533 533 end
534 534 if custom_field_map['resolution'] && !ticket.resolution.blank?
535 535 custom_values[custom_field_map['resolution'].id] = ticket.resolution
536 536 end
537 537 i.custom_field_values = custom_values
538 538 i.save_custom_field_values
539 539 end
540 540
541 541 # update issue id sequence if needed (postgresql)
542 542 Issue.connection.reset_pk_sequence!(Issue.table_name) if Issue.connection.respond_to?('reset_pk_sequence!')
543 543 puts
544 544
545 545 # Wiki
546 546 print "Migrating wiki"
547 547 if wiki.save
548 548 TracWikiPage.find(:all, :order => 'name, version').each do |page|
549 549 # Do not migrate Trac manual wiki pages
550 550 next if TRAC_WIKI_PAGES.include?(page.name)
551 551 wiki_edit_count += 1
552 552 print '.'
553 553 STDOUT.flush
554 554 p = wiki.find_or_new_page(page.name)
555 555 p.content = WikiContent.new(:page => p) if p.new_record?
556 556 p.content.text = page.text
557 557 p.content.author = find_or_create_user(page.author) unless page.author.blank? || page.author == 'trac'
558 558 p.content.comments = page.comment
559 559 Time.fake(page.time) { p.new_record? ? p.save : p.content.save }
560 560
561 561 next if p.content.new_record?
562 562 migrated_wiki_edits += 1
563 563
564 564 # Attachments
565 565 page.attachments.each do |attachment|
566 566 next unless attachment.exist?
567 567 next if p.attachments.find_by_filename(attachment.filename.gsub(/^.*(\\|\/)/, '').gsub(/[^\w\.\-]/,'_')) #add only once per page
568 568 attachment.open {
569 569 a = Attachment.new :created_on => attachment.time
570 570 a.file = attachment
571 571 a.author = find_or_create_user(attachment.author)
572 572 a.description = attachment.description
573 573 a.container = p
574 574 migrated_wiki_attachments += 1 if a.save
575 575 }
576 576 end
577 577 end
578 578
579 579 wiki.reload
580 580 wiki.pages.each do |page|
581 581 page.content.text = convert_wiki_text(page.content.text)
582 582 Time.fake(page.content.updated_on) { page.content.save }
583 583 end
584 584 end
585 585 puts
586 586
587 587 puts
588 588 puts "Components: #{migrated_components}/#{TracComponent.count}"
589 589 puts "Milestones: #{migrated_milestones}/#{TracMilestone.count}"
590 590 puts "Tickets: #{migrated_tickets}/#{TracTicket.count}"
591 591 puts "Ticket files: #{migrated_ticket_attachments}/" + TracAttachment.count(:conditions => {:type => 'ticket'}).to_s
592 592 puts "Custom values: #{migrated_custom_values}/#{TracTicketCustom.count}"
593 593 puts "Wiki edits: #{migrated_wiki_edits}/#{wiki_edit_count}"
594 594 puts "Wiki files: #{migrated_wiki_attachments}/" + TracAttachment.count(:conditions => {:type => 'wiki'}).to_s
595 595 end
596 596
597 597 def self.limit_for(klass, attribute)
598 598 klass.columns_hash[attribute.to_s].limit
599 599 end
600 600
601 601 def self.encoding(charset)
602 602 @ic = Iconv.new('UTF-8', charset)
603 603 rescue Iconv::InvalidEncoding
604 604 puts "Invalid encoding!"
605 605 return false
606 606 end
607 607
608 608 def self.set_trac_directory(path)
609 609 @@trac_directory = path
610 610 raise "This directory doesn't exist!" unless File.directory?(path)
611 611 raise "#{trac_attachments_directory} doesn't exist!" unless File.directory?(trac_attachments_directory)
612 612 @@trac_directory
613 613 rescue Exception => e
614 614 puts e
615 615 return false
616 616 end
617 617
618 618 def self.trac_directory
619 619 @@trac_directory
620 620 end
621 621
622 622 def self.set_trac_adapter(adapter)
623 623 return false if adapter.blank?
624 624 raise "Unknown adapter: #{adapter}!" unless %w(sqlite sqlite3 mysql postgresql).include?(adapter)
625 625 # If adapter is sqlite or sqlite3, make sure that trac.db exists
626 626 raise "#{trac_db_path} doesn't exist!" if %w(sqlite sqlite3).include?(adapter) && !File.exist?(trac_db_path)
627 627 @@trac_adapter = adapter
628 628 rescue Exception => e
629 629 puts e
630 630 return false
631 631 end
632 632
633 633 def self.set_trac_db_host(host)
634 634 return nil if host.blank?
635 635 @@trac_db_host = host
636 636 end
637 637
638 638 def self.set_trac_db_port(port)
639 639 return nil if port.to_i == 0
640 640 @@trac_db_port = port.to_i
641 641 end
642 642
643 643 def self.set_trac_db_name(name)
644 644 return nil if name.blank?
645 645 @@trac_db_name = name
646 646 end
647 647
648 648 def self.set_trac_db_username(username)
649 649 @@trac_db_username = username
650 650 end
651 651
652 652 def self.set_trac_db_password(password)
653 653 @@trac_db_password = password
654 654 end
655 655
656 656 def self.set_trac_db_schema(schema)
657 657 @@trac_db_schema = schema
658 658 end
659 659
660 660 mattr_reader :trac_directory, :trac_adapter, :trac_db_host, :trac_db_port, :trac_db_name, :trac_db_schema, :trac_db_username, :trac_db_password
661 661
662 662 def self.trac_db_path; "#{trac_directory}/db/trac.db" end
663 663 def self.trac_attachments_directory; "#{trac_directory}/attachments" end
664 664
665 665 def self.target_project_identifier(identifier)
666 666 project = Project.find_by_identifier(identifier)
667 667 if !project
668 668 # create the target project
669 669 project = Project.new :name => identifier.humanize,
670 670 :description => ''
671 671 project.identifier = identifier
672 672 puts "Unable to create a project with identifier '#{identifier}'!" unless project.save
673 673 # enable issues and wiki for the created project
674 674 project.enabled_module_names = ['issue_tracking', 'wiki']
675 675 else
676 676 puts
677 677 puts "This project already exists in your Redmine database."
678 678 print "Are you sure you want to append data to this project ? [Y/n] "
679 679 exit if STDIN.gets.match(/^n$/i)
680 680 end
681 681 project.trackers << TRACKER_BUG unless project.trackers.include?(TRACKER_BUG)
682 682 project.trackers << TRACKER_FEATURE unless project.trackers.include?(TRACKER_FEATURE)
683 683 @target_project = project.new_record? ? nil : project
684 684 end
685 685
686 686 def self.connection_params
687 687 if %w(sqlite sqlite3).include?(trac_adapter)
688 688 {:adapter => trac_adapter,
689 689 :database => trac_db_path}
690 690 else
691 691 {:adapter => trac_adapter,
692 692 :database => trac_db_name,
693 693 :host => trac_db_host,
694 694 :port => trac_db_port,
695 695 :username => trac_db_username,
696 696 :password => trac_db_password,
697 697 :schema_search_path => trac_db_schema
698 698 }
699 699 end
700 700 end
701 701
702 702 def self.establish_connection
703 703 constants.each do |const|
704 704 klass = const_get(const)
705 705 next unless klass.respond_to? 'establish_connection'
706 706 klass.establish_connection connection_params
707 707 end
708 708 end
709 709
710 710 private
711 711 def self.encode(text)
712 712 @ic.iconv text
713 713 rescue
714 714 text
715 715 end
716 716 end
717 717
718 718 puts
719 719 if Redmine::DefaultData::Loader.no_data?
720 720 puts "Redmine configuration need to be loaded before importing data."
721 721 puts "Please, run this first:"
722 722 puts
723 723 puts " rake redmine:load_default_data RAILS_ENV=\"#{ENV['RAILS_ENV']}\""
724 724 exit
725 725 end
726 726
727 727 puts "WARNING: a new project will be added to Redmine during this process."
728 728 print "Are you sure you want to continue ? [y/N] "
729 729 break unless STDIN.gets.match(/^y$/i)
730 730 puts
731 731
732 732 def prompt(text, options = {}, &block)
733 733 default = options[:default] || ''
734 734 while true
735 735 print "#{text} [#{default}]: "
736 736 value = STDIN.gets.chomp!
737 737 value = default if value.blank?
738 738 break if yield value
739 739 end
740 740 end
741 741
742 742 DEFAULT_PORTS = {'mysql' => 3306, 'postgresql' => 5432}
743 743
744 744 prompt('Trac directory') {|directory| TracMigrate.set_trac_directory directory.strip}
745 745 prompt('Trac database adapter (sqlite, sqlite3, mysql, postgresql)', :default => 'sqlite') {|adapter| TracMigrate.set_trac_adapter adapter}
746 746 unless %w(sqlite sqlite3).include?(TracMigrate.trac_adapter)
747 747 prompt('Trac database host', :default => 'localhost') {|host| TracMigrate.set_trac_db_host host}
748 748 prompt('Trac database port', :default => DEFAULT_PORTS[TracMigrate.trac_adapter]) {|port| TracMigrate.set_trac_db_port port}
749 749 prompt('Trac database name') {|name| TracMigrate.set_trac_db_name name}
750 750 prompt('Trac database schema', :default => 'public') {|schema| TracMigrate.set_trac_db_schema schema}
751 751 prompt('Trac database username') {|username| TracMigrate.set_trac_db_username username}
752 752 prompt('Trac database password') {|password| TracMigrate.set_trac_db_password password}
753 753 end
754 754 prompt('Trac database encoding', :default => 'UTF-8') {|encoding| TracMigrate.encoding encoding}
755 755 prompt('Target project identifier') {|identifier| TracMigrate.target_project_identifier identifier}
756 756 puts
757 757
758 758 # Turn off email notifications
759 759 Setting.notified_events = []
760 760
761 761 TracMigrate.migrate
762 762 end
763 763 end
764 764
@@ -1,142 +1,150
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.dirname(__FILE__) + '/../test_helper'
19 19 require 'attachments_controller'
20 20
21 21 # Re-raise errors caught by the controller.
22 22 class AttachmentsController; def rescue_action(e) raise e end; end
23 23
24 24
25 25 class AttachmentsControllerTest < ActionController::TestCase
26 26 fixtures :users, :projects, :roles, :members, :member_roles, :enabled_modules, :issues, :trackers, :attachments,
27 27 :versions, :wiki_pages, :wikis, :documents
28 28
29 29 def setup
30 30 @controller = AttachmentsController.new
31 31 @request = ActionController::TestRequest.new
32 32 @response = ActionController::TestResponse.new
33 33 Attachment.storage_path = "#{RAILS_ROOT}/test/fixtures/files"
34 34 User.current = nil
35 35 end
36 36
37 37 def test_routing
38 38 assert_routing('/attachments/1', :controller => 'attachments', :action => 'show', :id => '1')
39 39 assert_routing('/attachments/1/filename.ext', :controller => 'attachments', :action => 'show', :id => '1', :filename => 'filename.ext')
40 40 assert_routing('/attachments/download/1', :controller => 'attachments', :action => 'download', :id => '1')
41 41 assert_routing('/attachments/download/1/filename.ext', :controller => 'attachments', :action => 'download', :id => '1', :filename => 'filename.ext')
42 42 end
43 43
44 44 def test_recognizes
45 45 assert_recognizes({:controller => 'attachments', :action => 'show', :id => '1'}, '/attachments/1')
46 46 assert_recognizes({:controller => 'attachments', :action => 'show', :id => '1'}, '/attachments/show/1')
47 47 assert_recognizes({:controller => 'attachments', :action => 'show', :id => '1', :filename => 'filename.ext'}, '/attachments/1/filename.ext')
48 48 assert_recognizes({:controller => 'attachments', :action => 'download', :id => '1'}, '/attachments/download/1')
49 49 assert_recognizes({:controller => 'attachments', :action => 'download', :id => '1', :filename => 'filename.ext'},'/attachments/download/1/filename.ext')
50 50 end
51 51
52 52 def test_show_diff
53 53 get :show, :id => 5
54 54 assert_response :success
55 55 assert_template 'diff'
56 56 assert_equal 'text/html', @response.content_type
57 57 end
58 58
59 59 def test_show_text_file
60 60 get :show, :id => 4
61 61 assert_response :success
62 62 assert_template 'file'
63 63 assert_equal 'text/html', @response.content_type
64 64 end
65 65
66 66 def test_show_text_file_should_send_if_too_big
67 67 Setting.file_max_size_displayed = 512
68 68 Attachment.find(4).update_attribute :filesize, 754.kilobyte
69 69
70 70 get :show, :id => 4
71 71 assert_response :success
72 72 assert_equal 'application/x-ruby', @response.content_type
73 73 end
74 74
75 75 def test_show_other
76 76 get :show, :id => 6
77 77 assert_response :success
78 78 assert_equal 'application/octet-stream', @response.content_type
79 79 end
80 80
81 81 def test_download_text_file
82 82 get :download, :id => 4
83 83 assert_response :success
84 84 assert_equal 'application/x-ruby', @response.content_type
85 85 end
86 86
87 def test_download_should_assign_content_type_if_blank
88 Attachment.find(4).update_attribute(:content_type, '')
89
90 get :download, :id => 4
91 assert_response :success
92 assert_equal 'text/x-ruby', @response.content_type
93 end
94
87 95 def test_download_missing_file
88 96 get :download, :id => 2
89 97 assert_response 404
90 98 end
91 99
92 100 def test_anonymous_on_private_private
93 101 get :download, :id => 7
94 102 assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Fattachments%2Fdownload%2F7'
95 103 end
96 104
97 105 def test_destroy_issue_attachment
98 106 issue = Issue.find(3)
99 107 @request.session[:user_id] = 2
100 108
101 109 assert_difference 'issue.attachments.count', -1 do
102 110 post :destroy, :id => 1
103 111 end
104 112 # no referrer
105 113 assert_redirected_to 'projects/ecookbook'
106 114 assert_nil Attachment.find_by_id(1)
107 115 j = issue.journals.find(:first, :order => 'created_on DESC')
108 116 assert_equal 'attachment', j.details.first.property
109 117 assert_equal '1', j.details.first.prop_key
110 118 assert_equal 'error281.txt', j.details.first.old_value
111 119 end
112 120
113 121 def test_destroy_wiki_page_attachment
114 122 @request.session[:user_id] = 2
115 123 assert_difference 'Attachment.count', -1 do
116 124 post :destroy, :id => 3
117 125 assert_response 302
118 126 end
119 127 end
120 128
121 129 def test_destroy_project_attachment
122 130 @request.session[:user_id] = 2
123 131 assert_difference 'Attachment.count', -1 do
124 132 post :destroy, :id => 8
125 133 assert_response 302
126 134 end
127 135 end
128 136
129 137 def test_destroy_version_attachment
130 138 @request.session[:user_id] = 2
131 139 assert_difference 'Attachment.count', -1 do
132 140 post :destroy, :id => 9
133 141 assert_response 302
134 142 end
135 143 end
136 144
137 145 def test_destroy_without_permission
138 146 post :destroy, :id => 3
139 147 assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Fattachments%2Fdestroy%2F3'
140 148 assert Attachment.find_by_id(3)
141 149 end
142 150 end
@@ -1,48 +1,56
1 1 # encoding: utf-8
2 2 #
3 3 # redMine - project management software
4 4 # Copyright (C) 2006-2007 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 require File.dirname(__FILE__) + '/../test_helper'
21 21
22 22 class AttachmentTest < ActiveSupport::TestCase
23 23 fixtures :issues, :users
24 24
25 25 def setup
26 26 end
27 27
28 28 def test_create
29 29 a = Attachment.new(:container => Issue.find(1),
30 30 :file => uploaded_test_file("testfile.txt", "text/plain"),
31 31 :author => User.find(1))
32 32 assert a.save
33 33 assert_equal 'testfile.txt', a.filename
34 34 assert_equal 59, a.filesize
35 35 assert_equal 'text/plain', a.content_type
36 36 assert_equal 0, a.downloads
37 37 assert_equal Digest::MD5.hexdigest(uploaded_test_file("testfile.txt", "text/plain").read), a.digest
38 38 assert File.exist?(a.diskfile)
39 39 end
40 40
41 def test_create_should_auto_assign_content_type
42 a = Attachment.new(:container => Issue.find(1),
43 :file => uploaded_test_file("testfile.txt", ""),
44 :author => User.find(1))
45 assert a.save
46 assert_equal 'text/plain', a.content_type
47 end
48
41 49 def test_diskfilename
42 50 assert Attachment.disk_filename("test_file.txt") =~ /^\d{12}_test_file.txt$/
43 51 assert_equal 'test_file.txt', Attachment.disk_filename("test_file.txt")[13..-1]
44 52 assert_equal '770c509475505f37c2b8fb6030434d6b.txt', Attachment.disk_filename("test_accentué.txt")[13..-1]
45 53 assert_equal 'f8139524ebb8f32e51976982cd20a85d', Attachment.disk_filename("test_accentué")[13..-1]
46 54 assert_equal 'cbb5b0f30978ba03731d61f9f6d10011', Attachment.disk_filename("test_accentué.ça")[13..-1]
47 55 end
48 56 end
General Comments 0
You need to be logged in to leave comments. Login now