| 1 | Index: trac/attachment.py |
|---|
| 2 | =================================================================== |
|---|
| 3 | --- trac/attachment.py (revision 8466) |
|---|
| 4 | +++ trac/attachment.py (working copy) |
|---|
| 5 | @@ -20,6 +20,7 @@ |
|---|
| 6 | import os |
|---|
| 7 | import re |
|---|
| 8 | import shutil |
|---|
| 9 | +import Image |
|---|
| 10 | import time |
|---|
| 11 | import unicodedata |
|---|
| 12 | |
|---|
| 13 | @@ -116,6 +117,7 @@ |
|---|
| 14 | self.env = env |
|---|
| 15 | self.parent_realm = self.resource.parent.realm |
|---|
| 16 | self.parent_id = unicode(self.resource.parent.id) |
|---|
| 17 | + self.first1000bytes = None |
|---|
| 18 | if self.resource.id: |
|---|
| 19 | self._fetch(self.resource.id, db) |
|---|
| 20 | else: |
|---|
| 21 | @@ -125,6 +127,8 @@ |
|---|
| 22 | self.date = None |
|---|
| 23 | self.author = None |
|---|
| 24 | self.ipnr = None |
|---|
| 25 | + self.mimetype = None |
|---|
| 26 | + self.charset = None |
|---|
| 27 | |
|---|
| 28 | def _set_filename(self, val): |
|---|
| 29 | self.resource.id = val |
|---|
| 30 | @@ -152,7 +156,48 @@ |
|---|
| 31 | self.date = datetime.fromtimestamp(time, utc) |
|---|
| 32 | self.author = row[4] |
|---|
| 33 | self.ipnr = row[5] |
|---|
| 34 | + self._get_mimetype() |
|---|
| 35 | + self._get_charset() |
|---|
| 36 | |
|---|
| 37 | + def _get_first_1000_bytes(self): |
|---|
| 38 | + if not self.first1000bytes: |
|---|
| 39 | + fd = self.open() |
|---|
| 40 | + try: |
|---|
| 41 | + str_data = fd.read(1000) |
|---|
| 42 | + fd.seek(0) |
|---|
| 43 | + finally: |
|---|
| 44 | + fd.close() |
|---|
| 45 | + |
|---|
| 46 | + def _get_charset(self): |
|---|
| 47 | + self._get_first_1000_bytes() |
|---|
| 48 | + mimeview = Mimeview(self.env) |
|---|
| 49 | + self.charset = mimeview.get_charset(self.first1000bytes, self.mimetype) |
|---|
| 50 | + |
|---|
| 51 | + def _get_mimetype(self): |
|---|
| 52 | + self._get_first_1000_bytes() |
|---|
| 53 | + mimeview = Mimeview(self.env) |
|---|
| 54 | + self.mimetype = mimeview.get_mimetype(self.filename, self.first1000bytes) |
|---|
| 55 | + if not self.mimetype: |
|---|
| 56 | + self.mimetype = 'application/octet-stream' |
|---|
| 57 | + |
|---|
| 58 | + def getThumbail(self, size = (160,160)): |
|---|
| 59 | + image = Image.open(self.path) |
|---|
| 60 | + thumbDir=os.path.join(self._get_dirPath(),'.thumb') |
|---|
| 61 | + thumbFile=os.path.join(thumbDir,unicode_quote(self.filename)) |
|---|
| 62 | + if not os.path.isdir(thumbDir): |
|---|
| 63 | + os.mkdir(thumbDir) |
|---|
| 64 | + # 200 x 150 |
|---|
| 65 | + if os.path.exists(thumbFile): |
|---|
| 66 | + return thumbFile |
|---|
| 67 | + if image.size[0] < size[0] and image.size[0] < size[0]: |
|---|
| 68 | + shutil.copyfile(self.path,thumbFile) |
|---|
| 69 | + return thumbFile |
|---|
| 70 | + image.thumbnail(size) |
|---|
| 71 | + image.save(thumbFile) |
|---|
| 72 | + image.close() |
|---|
| 73 | + def _get_dirPath(self): |
|---|
| 74 | + return os.path.normpath(os.path.join(self.env.path, 'attachments', self.parent_realm, |
|---|
| 75 | + unicode_quote(self.parent_id))) |
|---|
| 76 | def _get_path(self): |
|---|
| 77 | path = os.path.join(self.env.path, 'attachments', self.parent_realm, |
|---|
| 78 | unicode_quote(self.parent_id)) |
|---|
| 79 | @@ -266,6 +311,8 @@ |
|---|
| 80 | attachment.date = datetime.fromtimestamp(time, utc) |
|---|
| 81 | attachment.author = author |
|---|
| 82 | attachment.ipnr = ipnr |
|---|
| 83 | + attachment._get_mimetype() |
|---|
| 84 | + attachment._get_charset() |
|---|
| 85 | yield attachment |
|---|
| 86 | |
|---|
| 87 | def delete_all(cls, env, parent_realm, parent_id, db): |
|---|
| 88 | @@ -349,12 +396,14 @@ |
|---|
| 89 | # IRequestHandler methods |
|---|
| 90 | |
|---|
| 91 | def match_request(self, req): |
|---|
| 92 | - match = re.match(r'/(raw-)?attachment/([^/]+)(?:/(.*))?$', |
|---|
| 93 | + match = re.match(r'/(raw-|thumb-)?attachment/([^/]+)(?:/(.*))?$', |
|---|
| 94 | req.path_info) |
|---|
| 95 | if match: |
|---|
| 96 | raw, realm, path = match.groups() |
|---|
| 97 | - if raw: |
|---|
| 98 | + if raw == 'raw-': |
|---|
| 99 | req.args['format'] = 'raw' |
|---|
| 100 | + elif raw == 'thumb-': |
|---|
| 101 | + req.args['format'] = 'thumb' |
|---|
| 102 | req.args['realm'] = realm |
|---|
| 103 | if path: |
|---|
| 104 | req.args['path'] = path |
|---|
| 105 | @@ -415,6 +464,7 @@ |
|---|
| 106 | |
|---|
| 107 | def get_link_resolvers(self): |
|---|
| 108 | yield ('raw-attachment', self._format_link) |
|---|
| 109 | + yield ('thumb-attachment', self._format_link) |
|---|
| 110 | yield ('attachment', self._format_link) |
|---|
| 111 | |
|---|
| 112 | # Public methods |
|---|
| 113 | @@ -520,6 +570,9 @@ |
|---|
| 114 | if format == 'raw': |
|---|
| 115 | kwargs.pop('format') |
|---|
| 116 | prefix = 'raw-attachment' |
|---|
| 117 | + elif format == 'thumb': |
|---|
| 118 | + kwargs.pop('format') |
|---|
| 119 | + prefix = 'thumb-attachment' |
|---|
| 120 | parent_href = unicode_unquote(get_resource_url(self.env, |
|---|
| 121 | resource.parent(version=None), Href(''))) |
|---|
| 122 | if not resource.id: |
|---|
| 123 | @@ -544,14 +597,10 @@ |
|---|
| 124 | |
|---|
| 125 | # Internal methods |
|---|
| 126 | |
|---|
| 127 | - def _do_save(self, req, attachment): |
|---|
| 128 | + def _do_save_one(self, req, attachment, upload, description): |
|---|
| 129 | req.perm(attachment.resource).require('ATTACHMENT_CREATE') |
|---|
| 130 | |
|---|
| 131 | - if 'cancel' in req.args: |
|---|
| 132 | - req.redirect(get_resource_url(self.env, attachment.resource.parent, |
|---|
| 133 | - req.href)) |
|---|
| 134 | |
|---|
| 135 | - upload = req.args['attachment'] |
|---|
| 136 | if not hasattr(upload, 'filename') or not upload.filename: |
|---|
| 137 | raise TracError(_('No file uploaded')) |
|---|
| 138 | if hasattr(upload.file, 'fileno'): |
|---|
| 139 | @@ -579,7 +628,7 @@ |
|---|
| 140 | raise TracError(_('No file uploaded')) |
|---|
| 141 | # Now the filename is known, update the attachment resource |
|---|
| 142 | # attachment.filename = filename |
|---|
| 143 | - attachment.description = req.args.get('description', '') |
|---|
| 144 | + attachment.description = description |
|---|
| 145 | attachment.author = get_reporter_id(req, 'author') |
|---|
| 146 | attachment.ipnr = req.remote_addr |
|---|
| 147 | |
|---|
| 148 | @@ -608,8 +657,31 @@ |
|---|
| 149 | attachment.filename = None |
|---|
| 150 | attachment.insert(filename, upload.file, size) |
|---|
| 151 | |
|---|
| 152 | - req.redirect(get_resource_url(self.env, attachment.resource(id=None), |
|---|
| 153 | + def _do_save_many(self,req, attachment): |
|---|
| 154 | + curr_attach = 0 |
|---|
| 155 | + while curr_attach < int(req.args['multiple_attachments']): |
|---|
| 156 | + upload=req.args['attachment-%d' % curr_attach] |
|---|
| 157 | + if hasattr(upload, 'filename') and upload.filename: |
|---|
| 158 | + attachment.filename = None |
|---|
| 159 | + self._do_save_one(req, attachment, upload, req.args.get('description-%d' % curr_attach, '')) |
|---|
| 160 | + curr_attach=curr_attach+1 |
|---|
| 161 | + |
|---|
| 162 | + def _do_save(self, req, attachment): |
|---|
| 163 | + req.perm(attachment.resource).require('ATTACHMENT_CREATE') |
|---|
| 164 | + |
|---|
| 165 | + if 'cancel' in req.args: |
|---|
| 166 | + req.redirect(get_resource_url(self.env, attachment.resource.parent, |
|---|
| 167 | + req.href)) |
|---|
| 168 | + |
|---|
| 169 | + if 'multiple_attachments' in req.args: |
|---|
| 170 | + self._do_save_many(req, attachment) |
|---|
| 171 | + req.redirect(get_resource_url(self.env, attachment.resource(id=None), |
|---|
| 172 | req.href)) |
|---|
| 173 | + else: |
|---|
| 174 | + self._do_save_one(req, attachment, req.args['attachment'], req.args.get('description', '')) |
|---|
| 175 | + # Redirect the user to list of attachments (must add a trailing '/') |
|---|
| 176 | + req.redirect(get_resource_url(self.env, attachment.resource(id=None), |
|---|
| 177 | + req.href)) |
|---|
| 178 | |
|---|
| 179 | def _do_delete(self, req, attachment): |
|---|
| 180 | req.perm(attachment.resource).require('ATTACHMENT_DELETE') |
|---|
| 181 | @@ -655,20 +727,20 @@ |
|---|
| 182 | 'title': get_resource_name(self.env, attachment.resource), |
|---|
| 183 | 'attachment': attachment} |
|---|
| 184 | |
|---|
| 185 | + mime_type = attachment.mimetype |
|---|
| 186 | fd = attachment.open() |
|---|
| 187 | try: |
|---|
| 188 | mimeview = Mimeview(self.env) |
|---|
| 189 | |
|---|
| 190 | - # MIME type detection |
|---|
| 191 | - str_data = fd.read(1000) |
|---|
| 192 | - fd.seek(0) |
|---|
| 193 | - |
|---|
| 194 | - mime_type = mimeview.get_mimetype(attachment.filename, str_data) |
|---|
| 195 | - |
|---|
| 196 | # Eventually send the file directly |
|---|
| 197 | format = req.args.get('format') |
|---|
| 198 | - if format in ('raw', 'txt'): |
|---|
| 199 | + if format == 'thumb': |
|---|
| 200 | if not self.render_unsafe_content: |
|---|
| 201 | + req.send_header('Content-Disposition', 'attachment') |
|---|
| 202 | + if attachment.mimetype[0:5] in ('image'): |
|---|
| 203 | + req.send_file(attachment.getThumbail(), mime_type) |
|---|
| 204 | + elif format in ('raw', 'txt'): |
|---|
| 205 | + if not self.render_unsafe_content: |
|---|
| 206 | # Force browser to download files instead of rendering |
|---|
| 207 | # them, since they might contain malicious code enabling |
|---|
| 208 | # XSS attacks |
|---|
| 209 | @@ -730,6 +802,8 @@ |
|---|
| 210 | format = None |
|---|
| 211 | if ns.startswith('raw'): |
|---|
| 212 | format = 'raw' |
|---|
| 213 | + elif ns.startswith('thumb'): |
|---|
| 214 | + format = 'thumb' |
|---|
| 215 | href = get_resource_url(self.env, attachment, formatter.href, |
|---|
| 216 | format=format) |
|---|
| 217 | return tag.a(label, class_='attachment', href=href + params, |
|---|
| 218 | Index: trac/htdocs/js/trac.js |
|---|
| 219 | =================================================================== |
|---|
| 220 | --- trac/htdocs/js/trac.js (revision 8466) |
|---|
| 221 | +++ trac/htdocs/js/trac.js (working copy) |
|---|
| 222 | @@ -71,5 +71,79 @@ |
|---|
| 223 | window.getAncestorByTagName = function(elem, tagName) { |
|---|
| 224 | return $(elem).parents(tagName).get(0); |
|---|
| 225 | } |
|---|
| 226 | +})(jQuery); |
|---|
| 227 | |
|---|
| 228 | -})(jQuery); |
|---|
| 229 | \ No newline at end of file |
|---|
| 230 | +var ATTACHFILE_COUNTER=0; |
|---|
| 231 | +function manageMultipleAttachFields(_obj){ |
|---|
| 232 | + if (_obj.value == '') { |
|---|
| 233 | + if($("#multiAttach_tbody").get(0).rows.length == 1) return; |
|---|
| 234 | + $("#multiAttach_tbody").get(0).deleteRow($(_obj).attr("attachnum")*1); |
|---|
| 235 | + ATTACHFILE_COUNTER=0; |
|---|
| 236 | + $("#multiAttach_tbody tr").each(function(index, element){ |
|---|
| 237 | + $("input", $("td", element).get(0)) |
|---|
| 238 | + .attr("attachnum",ATTACHFILE_COUNTER) |
|---|
| 239 | + .get(0).name = "attachment-"+ATTACHFILE_COUNTER; |
|---|
| 240 | + $("input", $("td", element).get(1)) |
|---|
| 241 | + .get(0).name = "description-"+ATTACHFILE_COUNTER; |
|---|
| 242 | + ATTACHFILE_COUNTER++; |
|---|
| 243 | + }); |
|---|
| 244 | + $("#multiAttach_count").get(0).value = $("#multiAttach_count").val()*1-1; |
|---|
| 245 | + } else { |
|---|
| 246 | + if ($(_obj).attr("attachnum") != $("#multiAttach_count").val()-1) return; |
|---|
| 247 | + var tr = $("#multiAttach_tbody").get(0).insertRow(-1); |
|---|
| 248 | + var td = tr.insertCell(-1); |
|---|
| 249 | + $('<input type="file" onchange="manageMultipleAttachFields(this)">') |
|---|
| 250 | + .attr("attachnum",$("#multiAttach_count").val()) |
|---|
| 251 | + .appendTo(td) |
|---|
| 252 | + .get(0).name = "attachment-"+$("#multiAttach_count").val(); |
|---|
| 253 | + |
|---|
| 254 | + td = tr.insertCell(-1); |
|---|
| 255 | + $('<input type="text">') |
|---|
| 256 | + .attr("size",60) |
|---|
| 257 | + .appendTo(td) |
|---|
| 258 | + .get(0).name = "description-"+$("#multiAttach_count").val(); |
|---|
| 259 | + $("#multiAttach_count").get(0).value = $("#multiAttach_count").val()*1+1; |
|---|
| 260 | + } |
|---|
| 261 | +} |
|---|
| 262 | + |
|---|
| 263 | Index: trac/templates/macros.html |
|---|
| 264 | =================================================================== |
|---|
| 265 | --- trac/templates/macros.html (revision 8466) |
|---|
| 266 | +++ trac/templates/macros.html (working copy) |
|---|
| 267 | @@ -189,16 +189,45 @@ |
|---|
| 268 | </ul> |
|---|
| 269 | </py:when> |
|---|
| 270 | <py:when test="not compact"> |
|---|
| 271 | - <h2>Attachments</h2> |
|---|
| 272 | <div py:if="alist.attachments or alist.can_create" id="attachments"> |
|---|
| 273 | + <h2>Attachments (Files)</h2> |
|---|
| 274 | <dl py:if="alist.attachments" class="attachments"> |
|---|
| 275 | - <py:for each="attachment in alist.attachments"> |
|---|
| 276 | + <py:for each="attachment in [f for f in alist.attachments if f.mimetype[0:5] not in ('image') ]"> |
|---|
| 277 | <dt>${show_one_attachment(attachment)}</dt> |
|---|
| 278 | <dd py:if="attachment.description"> |
|---|
| 279 | ${wiki_to_oneliner(context, attachment.description)} |
|---|
| 280 | </dd> |
|---|
| 281 | </py:for> |
|---|
| 282 | </dl> |
|---|
| 283 | + <h2>Attachments (Images)</h2> |
|---|
| 284 | + <py:with vars="attachs = [f for f in alist.attachments if f.mimetype[0:5] in ('image') ]; fullrow = len(attachs)"> |
|---|
| 285 | + <table align="center"> |
|---|
| 286 | + <tr py:for="row in group(attachs, 3)"> |
|---|
| 287 | + <py:for each="oneattach in row"> |
|---|
| 288 | + <td py:if="not oneattach" width="210"> |
|---|
| 289 | + |
|---|
| 290 | + </td> |
|---|
| 291 | + <td py:if="oneattach" align="center" style="border: 1px solid #f0f0f0; text-align: center;"> |
|---|
| 292 | + <div style="width: 200px; height: 200px;"> |
|---|
| 293 | + <a href="${url_of(oneattach.resource)}"> |
|---|
| 294 | + <img src="${url_of(oneattach.resource,format='thumb')}" |
|---|
| 295 | + style="cursor: pointer;" |
|---|
| 296 | + border="0" |
|---|
| 297 | + id="img_${oneattach.filename}" |
|---|
| 298 | + /> |
|---|
| 299 | + </a> |
|---|
| 300 | + </div> |
|---|
| 301 | + <div style="background-color: #f0f0f0;"> |
|---|
| 302 | + <py:with vars="attach_size = round(float(oneattach.size)/float(1024),3)"> |
|---|
| 303 | + <small>${oneattach.filename} (${attach_size} kb)<br /> |
|---|
| 304 | + <i>${oneattach.description}</i> |
|---|
| 305 | + </small> |
|---|
| 306 | + </py:with> |
|---|
| 307 | + </div> |
|---|
| 308 | + </td> |
|---|
| 309 | + </py:for> |
|---|
| 310 | + </tr></table> |
|---|
| 311 | + </py:with> |
|---|
| 312 | ${attach_file_form(alist, add_button_title)} |
|---|
| 313 | </div> |
|---|
| 314 | </py:when> |
|---|
| 315 | Index: trac/templates/attachment.html |
|---|
| 316 | =================================================================== |
|---|
| 317 | --- trac/templates/attachment.html (revision 8466) |
|---|
| 318 | +++ trac/templates/attachment.html (working copy) |
|---|
| 319 | @@ -12,10 +12,7 @@ |
|---|
| 320 | |
|---|
| 321 | <body py:with="parent = attachments and attachments.parent or |
|---|
| 322 | attachment.resource.parent"> |
|---|
| 323 | - <div py:choose="mode" id="content" class="attachment"> |
|---|
| 324 | - <py:when test="'new'"> |
|---|
| 325 | - <h1>Add Attachment to <a href="${url_of(parent)}">${name_of(parent)}</a></h1> |
|---|
| 326 | - <form id="attachment" method="post" enctype="multipart/form-data" action=""> |
|---|
| 327 | + <py:def function="attach_oneFile()"> |
|---|
| 328 | <div class="field"> |
|---|
| 329 | <label>File<py:if test="max_size >= 0"> (size limit |
|---|
| 330 | ${pretty_size(max_size, format='%d')})</py:if>:<br /> |
|---|
| 331 | @@ -41,6 +38,44 @@ |
|---|
| 332 | </div> |
|---|
| 333 | <br /> |
|---|
| 334 | </fieldset> |
|---|
| 335 | + |
|---|
| 336 | + </py:def> |
|---|
| 337 | + <py:def function="attach_multiFile()"> |
|---|
| 338 | + <table><thead> |
|---|
| 339 | + <tr><th align="left">File</th><th align="left">Description</th></tr> |
|---|
| 340 | + </thead> |
|---|
| 341 | + <tbody id="multiAttach_tbody"> |
|---|
| 342 | + <tr> |
|---|
| 343 | + <td><input type="file" attachnum="0" name="attachment-0" onchange="manageMultipleAttachFields(this);"/></td> |
|---|
| 344 | + <td><input type="text" name="description-0" size="60" /></td> |
|---|
| 345 | + </tr> |
|---|
| 346 | + </tbody></table> |
|---|
| 347 | + <input type="hidden" id="multiAttach_count" name="multiple_attachments" value="1" /> |
|---|
| 348 | + |
|---|
| 349 | + <fieldset> |
|---|
| 350 | + <legend>Attachment Info</legend> |
|---|
| 351 | + <py:if test="authname == 'anonymous'"> |
|---|
| 352 | + <div class="field"> |
|---|
| 353 | + <label>Your email or username:<br /> |
|---|
| 354 | + <input type="text" name="author" size="30" value="$author" /> |
|---|
| 355 | + </label> |
|---|
| 356 | + </div> |
|---|
| 357 | + </py:if> |
|---|
| 358 | + <div class="options"> |
|---|
| 359 | + <label><input type="checkbox" name="replace" /> |
|---|
| 360 | + Replace existing attachments of the same name</label> |
|---|
| 361 | + </div> |
|---|
| 362 | + <br /> |
|---|
| 363 | + </fieldset> |
|---|
| 364 | + |
|---|
| 365 | + </py:def> |
|---|
| 366 | + |
|---|
| 367 | + <div py:choose="mode" id="content" class="attachment"> |
|---|
| 368 | + <py:when test="'new'"> |
|---|
| 369 | + <h1>Add Attachment to <a href="${url_of(parent)}">${name_of(parent)}</a></h1> |
|---|
| 370 | + <form id="attachment" method="post" enctype="multipart/form-data" action=""> |
|---|
| 371 | + |
|---|
| 372 | + ${attach_multiFile()} |
|---|
| 373 | <div class="buttons"> |
|---|
| 374 | <input type="hidden" name="action" value="new" /> |
|---|
| 375 | <input type="hidden" name="realm" value="$parent.realm" /> |
|---|