django-extended-attachments
changeset 0:b244a4b6f89c
initial import from robobb
line diff
1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/.hgignore Tue Aug 03 14:39:36 2010 +0400 1.3 @@ -0,0 +1,6 @@ 1.4 +syntax: glob 1.5 + 1.6 +*.pyc 1.7 +*.swp 1.8 +*.mo 1.9 +*.log
2.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 2.2 +++ b/AUTHORS Tue Aug 03 14:39:36 2010 +0400 2.3 @@ -0,0 +1,8 @@ 2.4 +Vladimir Ermakov <vooon341@gmail.com> 2.5 + 2.6 +django-attachments: 2.7 +Martin Mahner <http://www.mahner.org/> 2.8 +Jannis Leidel <http://jannisleidel.com/> 2.9 + 2.10 +django-filetransfers: 2.11 +Waldemar Kornewald <http://www.allbuttonspressed.com>
3.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 3.2 +++ b/LICENSE Tue Aug 03 14:39:36 2010 +0400 3.3 @@ -0,0 +1,34 @@ 3.4 +django extended attachments 3.5 +--------------------------- 3.6 + 3.7 +Copyright (c) 2010, Vladimir Ermakov <vooon341@gmail.com> 3.8 +Copyright (c) 2009, Martin Mahner <http://www.mahner.org/> 3.9 +All rights reserved. 3.10 + 3.11 +Based on django-attachments written by Martin Mahner, 3.12 +and django-filetransfers written by Waldemar Kornewald. 3.13 + 3.14 +Redistribution and use in source and binary forms, with or without modification, 3.15 +are permitted provided that the following conditions are met: 3.16 + 3.17 + * Redistributions of source code must retain the above copyright notice, 3.18 + this list of conditions and the following disclaimer. 3.19 + * Redistributions in binary form must reproduce the above copyright notice, 3.20 + this list of conditions and the following disclaimer in the documentation 3.21 + and/or other materials provided with the distribution. 3.22 + * Neither the name django-attachments nor the names of its contributors 3.23 + may be used to endorse or promote products derived from this software without 3.24 + specific prior written permission. 3.25 + * If you meet the author(s) some day, and you think this stuff is worth it, 3.26 + you can buy the author(s) a beer in return. 3.27 + 3.28 +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 3.29 +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 3.30 +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 3.31 +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 3.32 +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 3.33 +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 3.34 +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 3.35 +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 3.36 +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 3.37 +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
4.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 4.2 +++ b/MANIFEST.in Tue Aug 03 14:39:36 2010 +0400 4.3 @@ -0,0 +1,5 @@ 4.4 +include LICENSE 4.5 +include README.rst 4.6 +recursive-include attachments/fixtures * 4.7 +recursive-include attachments/templates/attachments * 4.8 +recursive-include attachments/locale *
5.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 5.2 +++ b/README.rst Tue Aug 03 14:39:36 2010 +0400 5.3 @@ -0,0 +1,21 @@ 5.4 +=========================== 5.5 +django-extended-attachments 5.6 +=========================== 5.7 + 5.8 +django-extended-attachments is a file storage application 5.9 +with following features: 5.10 + 5.11 +- Automatic thumbnail generation 5.12 +- Antivirus check 5.13 +- SHA1 checksums 5.14 +- MediaWiki file path style 5.15 +- File description 5.16 +- Access permission check callbacks 5.17 + 5.18 + 5.19 +Requires 5.20 +~~~~~~~~ 5.21 + 5.22 +- django-extensions 5.23 +- django >= 1.2 5.24 +- python >= 2.6
6.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 6.2 +++ b/attachments/__init__.py Tue Aug 03 14:39:36 2010 +0400 6.3 @@ -0,0 +1,1 @@ 6.4 +# -*- coding: utf-8 -*-
7.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 7.2 +++ b/attachments/admin.py Tue Aug 03 14:39:36 2010 +0400 7.3 @@ -0,0 +1,46 @@ 7.4 +# -*- coding: utf-8 -*- 7.5 + 7.6 +from django.contrib import admin 7.7 +from django.contrib.contenttypes import generic 7.8 +from django.utils.translation import ugettext_lazy as _ 7.9 +from models import Attachment, Thumbnail 7.10 +from django import forms 7.11 + 7.12 + 7.13 +class AttachmentInlines(generic.GenericStackedInline): 7.14 + model = Attachment 7.15 + extra = 1 7.16 + 7.17 + 7.18 +class AttachmentAdmin(admin.ModelAdmin): 7.19 + list_display = ('name', 'user', 'created', 'mime_type', 'size', 'av_state') 7.20 + list_filter = ('av_state', 'mime_type') 7.21 + search_fields = ('name', 'user', 'mime_type') 7.22 + readonly_fields = ('hash', 'size', 'checksum') 7.23 + fieldsets = ( 7.24 + (None, { 7.25 + 'fields': ('user', 'created'), 7.26 + }), 7.27 + (_('Relations'), { 7.28 + 'fields': ('content_type', 'object_id'), 7.29 + }), 7.30 + (_('Attachment'), { 7.31 + 'fields': ('hash', 'name', 'description', 'file', 'size', 'mime_type'), 7.32 + }), 7.33 + (_('Antivirus'), { 7.34 + 'fields': ('av_state', 'av_result'), 7.35 + }), 7.36 + (_('Statistics'), { 7.37 + 'fields': ('download_count', ), 7.38 + }), 7.39 + (_('Extra'), { 7.40 + 'fields': ('checksum', 'thumbnail') 7.41 + }), 7.42 + ) 7.43 + 7.44 + 7.45 +class ThumbnailAdmin(admin.ModelAdmin): 7.46 + list_display = ('attachment', 'width', 'height') 7.47 + 7.48 +admin.site.register(Attachment, AttachmentAdmin) 7.49 +admin.site.register(Thumbnail, ThumbnailAdmin)
8.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 8.2 +++ b/attachments/backends/__init__.py Tue Aug 03 14:39:36 2010 +0400 8.3 @@ -0,0 +1,39 @@ 8.4 +# -*- coding: utf-8 -*- 8.5 +""" 8.6 +Attachments backends base classes 8.7 +""" 8.8 +from abc import abstractmethod 8.9 + 8.10 + 8.11 +class AntivirusBase(object): 8.12 + @abstractmethod 8.13 + def scan_file(self, filename): 8.14 + """ 8.15 + Scans file and return result tuple: 8.16 + - scan result: True -- infected, False -- not 8.17 + - antivirus report string 8.18 + """ 8.19 + return (Fasle, '') 8.20 + 8.21 + 8.22 +class ThumbnailerBase(object): 8.23 + """ 8.24 + Thumbnailer base class. 8.25 + ``mime_types`` property describes what files can be processed. 8.26 + * -- means everything. 8.27 + """ 8.28 + mime_types = ( 8.29 + 'image/jpeg', 8.30 + 'image/png', 8.31 + 'image/*', 8.32 + ) 8.33 + 8.34 + @abstractmethod 8.35 + def thumbnail(self, src_path, dst_path, width, heigth, mime_type): 8.36 + """ 8.37 + Generates thumbnail if it can and return result tuple: 8.38 + - status (True if success) 8.39 + - thumbnail mime type 8.40 + """ 8.41 + return (False, 'image/jpeg') 8.42 +
9.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 9.2 +++ b/attachments/backends/av_clamav.py Tue Aug 03 14:39:36 2010 +0400 9.3 @@ -0,0 +1,11 @@ 9.4 +# -*- coding: utf-8 -*- 9.5 + 9.6 +from attachments.backends import AntivirusBase 9.7 +import pyclamav 9.8 + 9.9 + 9.10 +class Antivirus(AntivirusBase): 9.11 + def scan_file(self, filepath): 9.12 + found, virus = pyclamav.scanfile(filepath) 9.13 + return found, virus 9.14 +
10.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 10.2 +++ b/attachments/backends/av_clamd.py Tue Aug 03 14:39:36 2010 +0400 10.3 @@ -0,0 +1,21 @@ 10.4 +# -*- coding: utf-8 -*- 10.5 + 10.6 +from attachments.backends import AntivirusBase 10.7 +from attachments.settings import ANTIVIRUS_CLAMD_HOST, \ 10.8 + ANTIVIRUS_CLAMD_PORT, ANTIVIRUS_CLAMD_UNIX_SOCK 10.9 +import pyclamd 10.10 + 10.11 + 10.12 +class Antivirus(AntivirusBase): 10.13 + def __init__(self): 10.14 + if ANTIVIRUS_CLAMD_UNIX_SOCK: 10.15 + pyclamd.init_unix_socket(ANTIVIRUS_CLAMD_UNIX_SOCK) 10.16 + else: 10.17 + pyclamd.init_network_socket(ANTIVIRUS_CLAMD_HOST, 10.18 + ANTIVIRUS_CLAMD_PORT) 10.19 + 10.20 + def scan_file(self, filepath): 10.21 + found = pyclamd.scan_file(filepath) 10.22 + virus = found and '\n'.join(found.values()) or '' 10.23 + return found, virus 10.24 +
11.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 11.2 +++ b/attachments/backends/dl_default.py Tue Aug 03 14:39:36 2010 +0400 11.3 @@ -0,0 +1,25 @@ 11.4 +from django.http import HttpResponse 11.5 +from django.utils.encoding import smart_str 11.6 + 11.7 + 11.8 +def serve_file(request, file, save_as, content_type, **kwargs): 11.9 + """ 11.10 + Serves the file in chunks for efficiency reasons, but the transfer still 11.11 + goes through Django itself, so it's much worse than using the web server, 11.12 + but at least it works with all configurations. 11.13 + """ 11.14 + response = HttpResponse(ChunkedFile(file), content_type=content_type) 11.15 + if save_as: 11.16 + response['Content-Disposition'] = smart_str(u'attachment; filename=%s' % save_as) 11.17 + if file.size is not None: 11.18 + response['Content-Length'] = file.size 11.19 + return response 11.20 + 11.21 + 11.22 +class ChunkedFile(object): 11.23 + def __init__(self, file): 11.24 + self.file = file 11.25 + 11.26 + def __iter__(self): 11.27 + return self.file.chunks() 11.28 +
12.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 12.2 +++ b/attachments/backends/dl_xsendfile.py Tue Aug 03 14:39:36 2010 +0400 12.3 @@ -0,0 +1,14 @@ 12.4 +from django.http import HttpResponse 12.5 +from django.utils.encoding import smart_str 12.6 + 12.7 + 12.8 +def serve_file(request, file, save_as, content_type, **kwargs): 12.9 + """Lets the web server serve the file using the X-Sendfile extension""" 12.10 + response = HttpResponse(content_type=content_type) 12.11 + response['X-Sendfile'] = file.path 12.12 + if save_as: 12.13 + response['Content-Disposition'] = smart_str(u'attachment; filename=%s' % save_as) 12.14 + if file.size is not None: 12.15 + response['Content-Length'] = file.size 12.16 + return response 12.17 +
13.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 13.2 +++ b/attachments/backends/th_graphicsmagick.py Tue Aug 03 14:39:36 2010 +0400 13.3 @@ -0,0 +1,51 @@ 13.4 +# -*- coding: utf-8 -*- 13.5 + 13.6 +import os 13.7 +import subprocess 13.8 +from attachments.backends import ThumbnailerBase 13.9 + 13.10 + 13.11 +class Thumbnailer(ThumbnailerBase): 13.12 + mime_types = ( 13.13 + 'image/*', 13.14 + ) 13.15 + 13.16 + def thumbnail(self, src_path, dst_path, width, heigth, mime_type): 13.17 + _, src_format = mime_type.split('/', 1) 13.18 + if src_format == 'png': 13.19 + save_format = 'PNG' 13.20 + save_mime = 'image/png' 13.21 + elif src_format == 'gif': 13.22 + save_format = 'GIF' 13.23 + save_mime = 'image/gif' 13.24 + else: 13.25 + save_format = 'JPEG' 13.26 + save_mime = 'image/jpeg' 13.27 + 13.28 + args = [ 13.29 + 'gm', 13.30 + 'convert', 13.31 + '-quality', '85', 13.32 + '-colorspace', 'rgb', 13.33 + '-colors', '256', 13.34 + '-antialias', 13.35 + '-resize', '{w}x{h}'.format(w=width, h=heigth), 13.36 + '{src}'.format(src=src_path), 13.37 + '{format}:{dst}'.format(format=save_format, dst=dst_path), 13.38 + ] 13.39 + 13.40 + proc = subprocess.Popen(args, 13.41 + stdout=subprocess.PIPE, 13.42 + stderr=subprocess.PIPE, 13.43 + env={'LANG': 'C'}, 13.44 + ) 13.45 + out = proc.communicate() 13.46 + 13.47 + if out[0]: 13.48 + return (False, 'gm convert error:'+out[1]) 13.49 + 13.50 + if not os.path.isfile(dst_path): 13.51 + return (False, 'failed') 13.52 + 13.53 + return (True, save_mime) 13.54 +
14.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 14.2 +++ b/attachments/backends/th_pdf.py Tue Aug 03 14:39:36 2010 +0400 14.3 @@ -0,0 +1,41 @@ 14.4 +# -*- coding: utf-8 -*- 14.5 + 14.6 +import os 14.7 +import subprocess 14.8 +from attachments.backends import ThumbnailerBase 14.9 + 14.10 + 14.11 +class Thumbnailer(ThumbnailerBase): 14.12 + mime_types = ( 14.13 + 'application/pdf', 14.14 + 'application/x-pdf', 14.15 + ) 14.16 + 14.17 + def thumbnail(self, src_path, dst_path, width, heigth, mime_type): 14.18 + args = [ 14.19 + 'gm', 14.20 + 'convert', 14.21 + '-quality', '85', 14.22 + '-colorspace', 'rgb', 14.23 + '-colors', '256', 14.24 + '-antialias', 14.25 + '-resize', '{w}x{h}'.format(w=width, h=heigth), 14.26 + 'PDF:{src}[0]'.format(src=src_path), 14.27 + 'PNG:{dst}'.format(dst=dst_path), 14.28 + ] 14.29 + 14.30 + proc = subprocess.Popen(args, 14.31 + stdout=subprocess.PIPE, 14.32 + stderr=subprocess.PIPE, 14.33 + env={'LANG': 'C'}, 14.34 + ) 14.35 + out = proc.communicate() 14.36 + 14.37 + if out[0]: 14.38 + return (False, 'Convert error:'+out[1]) 14.39 + 14.40 + if not os.path.isfile(dst_path): 14.41 + return (False, 'failed') 14.42 + 14.43 + return (True, 'image/png') 14.44 +
15.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 15.2 +++ b/attachments/backends/th_pil.py Tue Aug 03 14:39:36 2010 +0400 15.3 @@ -0,0 +1,40 @@ 15.4 +# -*- coding: utf-8 -*- 15.5 + 15.6 +from attachments.backends import ThumbnailerBase 15.7 + 15.8 +try: 15.9 + import Image 15.10 +except ImportError: 15.11 + from PIL import Image 15.12 + 15.13 + 15.14 +class Thumbnailer(ThumbnailerBase): 15.15 + mime_types = ( 15.16 + 'image/*', 15.17 + ) 15.18 + 15.19 + def thumbnail(self, src_path, dst_path, width, heigth, mime_type): 15.20 + _, src_format = mime_type.split('/', 1) 15.21 + if src_format == 'png': 15.22 + save_format = 'PNG' 15.23 + save_mime = 'image/png' 15.24 + elif src_format == 'gif': 15.25 + save_format = 'GIF' 15.26 + save_mime = 'image/gif' 15.27 + else: 15.28 + save_format = 'JPEG' 15.29 + save_mime = 'image/jpeg' 15.30 + 15.31 + try: 15.32 + im = Image.open(src_path) 15.33 + im.thumbnail((width, heigth), Image.ANTIALIAS) 15.34 + if im.info.has_key('transparency'): 15.35 + opts = {'transparency': im.info['transparency']} 15.36 + else: 15.37 + opts = {} 15.38 + im.save(dst_path, save_format, **opts) 15.39 + except IOError: 15.40 + return (False, '') 15.41 + 15.42 + return (True, save_mime) 15.43 +
16.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 16.2 +++ b/attachments/forms.py Tue Aug 03 14:39:36 2010 +0400 16.3 @@ -0,0 +1,28 @@ 16.4 +from django import forms 16.5 +from django.utils.translation import ugettext_lazy as _ 16.6 +from django.contrib.contenttypes.models import ContentType 16.7 +from attachments.models import Attachment 16.8 + 16.9 + 16.10 +class AttachmentForm(forms.ModelForm): 16.11 + file = forms.FileField() 16.12 + description = forms.CharField(required=False, widget=forms.Textarea) 16.13 + 16.14 + class Meta: 16.15 + model = Attachment 16.16 + fields = ('file', 'description') 16.17 + 16.18 + def save(self, request, obj, *args, **kwargs): 16.19 + self.instance.user = request.user 16.20 + self.instance.content_object = obj 16.21 + self.instance.set_file(self.cleaned_data['file']) 16.22 + super(AttachmentForm, self).save(*args, **kwargs) 16.23 + 16.24 + 16.25 +class AttachmentUpdateForm(forms.ModelForm): 16.26 + description = forms.CharField(required=False, widget=forms.Textarea) 16.27 + 16.28 + class Meta: 16.29 + model = Attachment 16.30 + fields = ('description', ) 16.31 +
17.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 17.2 +++ b/attachments/locale/ru/LC_MESSAGES/django.po Tue Aug 03 14:39:36 2010 +0400 17.3 @@ -0,0 +1,204 @@ 17.4 +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 17.5 +# This file is distributed under the same license as the PACKAGE package. 17.6 +# 17.7 +# , 2010. 17.8 +msgid "" 17.9 +msgstr "" 17.10 +"Project-Id-Version: \n" 17.11 +"Report-Msgid-Bugs-To: \n" 17.12 +"POT-Creation-Date: 2010-08-02 18:54+0400\n" 17.13 +"PO-Revision-Date: 2010-08-02 18:59+0400\n" 17.14 +"Last-Translator: \n" 17.15 +"Language-Team: Русский <kde-russian@lists.kde.ru>\n" 17.16 +"MIME-Version: 1.0\n" 17.17 +"Content-Type: text/plain; charset=UTF-8\n" 17.18 +"Content-Transfer-Encoding: 8bit\n" 17.19 +"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && " 17.20 +"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" 17.21 +"X-Generator: Lokalize 1.0\n" 17.22 + 17.23 +#: admin.py:24 17.24 +msgid "Relations" 17.25 +msgstr "Связи" 17.26 + 17.27 +#: admin.py:27 17.28 +msgid "Attachment" 17.29 +msgstr "Вложение" 17.30 + 17.31 +#: admin.py:30 17.32 +msgid "Antivirus" 17.33 +msgstr "Антивирус" 17.34 + 17.35 +#: admin.py:33 17.36 +msgid "Statistics" 17.37 +msgstr "Статистика" 17.38 + 17.39 +#: admin.py:36 17.40 +msgid "Extra" 17.41 +msgstr "Экстра" 17.42 + 17.43 +#: models.py:22 17.44 +msgid "Standing" 17.45 +msgstr "Ожидание" 17.46 + 17.47 +#: models.py:23 17.48 +msgid "Virus not found" 17.49 +msgstr "Вирус не найден" 17.50 + 17.51 +#: models.py:24 17.52 +msgid "Virus found" 17.53 +msgstr "Вирус найден" 17.54 + 17.55 +#: models.py:50 17.56 +msgid "Content Type" 17.57 +msgstr "Тип данных" 17.58 + 17.59 +#: models.py:50 17.60 +msgid "relation to model wich instance is own this attachment" 17.61 +msgstr "связь с моделью экземпляр которой владеет этим вложением" 17.62 + 17.63 +#: models.py:51 17.64 +msgid "Object ID" 17.65 +msgstr "Номер обьекта" 17.66 + 17.67 +#: models.py:51 17.68 +msgid "ID of object row" 17.69 +msgstr "номер записи обьекта" 17.70 + 17.71 +#: models.py:54 17.72 +msgid "search hash" 17.73 +msgstr "Поисковый хэш" 17.74 + 17.75 +#: models.py:55 17.76 +msgid "user" 17.77 +msgstr "Пользователь" 17.78 + 17.79 +#: models.py:56 17.80 +msgid "real file name" 17.81 +msgstr "Настоящее имя файла" 17.82 + 17.83 +#: models.py:57 17.84 +msgid "description" 17.85 +msgstr "Описание" 17.86 + 17.87 +#: models.py:57 17.88 +msgid "attachment description" 17.89 +msgstr "описание вложения" 17.90 + 17.91 +#: models.py:58 models.py:192 17.92 +msgid "MIME type" 17.93 +msgstr "тип MIME" 17.94 + 17.95 +#: models.py:59 models.py:72 models.py:188 17.96 +msgid "attachment" 17.97 +msgstr "вложение" 17.98 + 17.99 +#: models.py:60 17.100 +msgid "file size" 17.101 +msgstr "размер файла" 17.102 + 17.103 +#: models.py:61 17.104 +msgid "download count" 17.105 +msgstr "количество скачиваний" 17.106 + 17.107 +#: models.py:62 17.108 +msgid "Created" 17.109 +msgstr "Создано" 17.110 + 17.111 +#: models.py:64 17.112 +msgid "avntivirus state" 17.113 +msgstr "состояние антивирусной проверки" 17.114 + 17.115 +#: models.py:65 17.116 +msgid "antivirus report" 17.117 +msgstr "доклад антивируса" 17.118 + 17.119 +#: models.py:67 17.120 +msgid "checksum" 17.121 +msgstr "контрольная сумма" 17.122 + 17.123 +#: models.py:67 17.124 +msgid "SHA1 checksum" 17.125 +msgstr "контрольная сумма по алгоритму SHA1" 17.126 + 17.127 +#: models.py:68 17.128 +msgid "can generate thumbnails" 17.129 +msgstr "Можно создать миниатюры" 17.130 + 17.131 +#: models.py:73 17.132 +msgid "attachments" 17.133 +msgstr "вложения" 17.134 + 17.135 +#: models.py:187 17.136 +msgid "created" 17.137 +msgstr "создано" 17.138 + 17.139 +#: models.py:188 17.140 +msgid "thumbnail for this attachment" 17.141 +msgstr "миниатюра для этого вложения" 17.142 + 17.143 +#: models.py:189 17.144 +msgid "width" 17.145 +msgstr "ширина" 17.146 + 17.147 +#: models.py:190 17.148 +msgid "height" 17.149 +msgstr "высота" 17.150 + 17.151 +#: models.py:191 models.py:196 17.152 +msgid "thumbnail" 17.153 +msgstr "миниатюра" 17.154 + 17.155 +#: models.py:197 17.156 +msgid "thumbnails" 17.157 +msgstr "миниатюры" 17.158 + 17.159 +#: utils.py:24 17.160 +#, python-format 17.161 +msgid "%d B" 17.162 +msgstr "%d Б" 17.163 + 17.164 +#: utils.py:26 17.165 +#, python-format 17.166 +msgid "%d KiB" 17.167 +msgstr "%d КиБ" 17.168 + 17.169 +#: utils.py:28 17.170 +#, python-format 17.171 +msgid "%.2f MiB" 17.172 +msgstr "%.2f МиБ" 17.173 + 17.174 +#: utils.py:30 17.175 +#, python-format 17.176 +msgid "%.2f GiB" 17.177 +msgstr "%.2f ГиБ" 17.178 + 17.179 +#: utils.py:32 17.180 +#, python-format 17.181 +msgid "%.2f TiB" 17.182 +msgstr "%.2f ТиБ" 17.183 + 17.184 +#: views.py:46 17.185 +msgid "Your attachment was uploaded." 17.186 +msgstr "Ваше вложение загружено." 17.187 + 17.188 +#: views.py:69 17.189 +msgid "Your attachment was deleted." 17.190 +msgstr "Ваше вложение удалено." 17.191 + 17.192 +#: views.py:96 17.193 +msgid "Your attachment was updated." 17.194 +msgstr "Ваше вложение обновлено." 17.195 + 17.196 +#: templates/attachments/add_form.html:7 17.197 +msgid "Add attachment" 17.198 +msgstr "Добавить вложение" 17.199 + 17.200 +#: templates/attachments/delete_form.html:6 17.201 +msgid "Delete attachment" 17.202 +msgstr "Удалить вложение" 17.203 + 17.204 +#: templates/attachments/update_form.html:7 17.205 +msgid "Update attachment" 17.206 +msgstr "Добавить вложение" 17.207 +
18.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 18.2 +++ b/attachments/management/commands/attachments_antivirus.py Tue Aug 03 14:39:36 2010 +0400 18.3 @@ -0,0 +1,20 @@ 18.4 +# -*- coding: utf-8 -*- 18.5 + 18.6 +import os 18.7 +import sys 18.8 + 18.9 +from django.conf import settings 18.10 +from django.core.management.base import BaseCommand, CommandError 18.11 +from django.core.exceptions import ValidationError 18.12 +from attachments.models import Attachment 18.13 + 18.14 + 18.15 +class Command(BaseCommand): 18.16 + help = 'Recheck all attachments using antivirus' 18.17 + args = '' 18.18 + 18.19 + def handle(self, *args, **options): 18.20 + for att in Attachment.objects.all(): 18.21 + if att.make_check_av(): 18.22 + print u'{att.name} ({att.file.path}) infected!'.format(att=att) 18.23 + att.save()
19.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 19.2 +++ b/attachments/management/commands/attachments_guess_mime.py Tue Aug 03 14:39:36 2010 +0400 19.3 @@ -0,0 +1,31 @@ 19.4 +# -*- coding: utf-8 -*- 19.5 + 19.6 +import os 19.7 +import sys 19.8 +from optparse import make_option 19.9 + 19.10 +from django.conf import settings 19.11 +from django.core.management.base import BaseCommand, CommandError 19.12 +from django.core.exceptions import ValidationError 19.13 +from attachments.models import Attachment 19.14 + 19.15 + 19.16 +class Command(BaseCommand): 19.17 + option_list = BaseCommand.option_list + ( 19.18 + make_option('--all', '-a', default=False, help='all attachments'), 19.19 + ) 19.20 + help = 'Try to guess attachments mime types' 19.21 + args = '' 19.22 + 19.23 + def handle(self, *args, **options): 19.24 + all_rows = options['all'] 19.25 + 19.26 + if all_rows: 19.27 + query = Attachment.objects.all() 19.28 + else: 19.29 + query = Attachment.objects.filter(mime_type='application/octet-stream') 19.30 + 19.31 + for att in query: 19.32 + att.mime_type = att.detect_mime() 19.33 + att.save() 19.34 + att.file.close()
20.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 20.2 +++ b/attachments/management/commands/attachments_sha1sum.py Tue Aug 03 14:39:36 2010 +0400 20.3 @@ -0,0 +1,36 @@ 20.4 +# -*- coding: utf-8 -*- 20.5 + 20.6 +import os 20.7 +import sys 20.8 +from optparse import make_option 20.9 + 20.10 +from django.conf import settings 20.11 +from django.core.management.base import BaseCommand, CommandError 20.12 +from django.core.exceptions import ValidationError 20.13 +from attachments.models import Attachment 20.14 + 20.15 + 20.16 +class Command(BaseCommand): 20.17 + option_list = BaseCommand.option_list + ( 20.18 + make_option('--out', '-o', default=None, help='output file'), 20.19 + ) 20.20 + help = 'Generate sha1sum list for attachments' 20.21 + args = '' 20.22 + 20.23 + def handle(self, *args, **options): 20.24 + oflname = options['out'] 20.25 + if oflname: 20.26 + ofd = open(oflname, 'w') 20.27 + else: 20.28 + ofd = sys.stdout 20.29 + 20.30 + for att in Attachment.objects.all(): 20.31 + if not att.checksum or att.checksum == '!': 20.32 + att.make_checksum() 20.33 + att.save() 20.34 + 20.35 + ofd.write(att.checksum) 20.36 + ofd.write(' ') 20.37 + ofd.write(att.file.path) 20.38 + ofd.write('\n') 20.39 + att.file.close()
21.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 21.2 +++ b/attachments/models.py Tue Aug 03 14:39:36 2010 +0400 21.3 @@ -0,0 +1,209 @@ 21.4 +# -*- coding: utf-8 -*- 21.5 +import os 21.6 +import tempfile 21.7 +from django.db import models 21.8 +from django.utils.translation import ugettext_lazy as _ 21.9 +from django_extensions.db.models import CreationDateTimeField 21.10 +from django.contrib.contenttypes.models import ContentType 21.11 +from django.contrib.contenttypes import generic 21.12 +from django.utils.hashcompat import sha_constructor, md5_constructor 21.13 +from django.conf import settings 21.14 +from django.core.files.images import ImageFile 21.15 +from django.utils.encoding import smart_str 21.16 +from django.contrib.auth.models import User 21.17 +from attachments.utils import antivirus, detect_mime, create_thumbnail, humanize_filesize 21.18 + 21.19 + 21.20 +FILE_STANDING = 0 21.21 +FILE_VIRUS_NOT_FOUND = 1 21.22 +FILE_VIRUS_FOUND = 2 21.23 + 21.24 +FILE_STATUS_CHOICES = ( 21.25 + (FILE_STANDING, _('Standing')), 21.26 + (FILE_VIRUS_NOT_FOUND, _('Virus not found')), 21.27 + (FILE_VIRUS_FOUND, _('Virus found')), 21.28 +) 21.29 + 21.30 + 21.31 +class AttachmentManager(models.Manager): 21.32 + def attachments_for_object(self, obj): 21.33 + object_type = ContentType.objects.get_for_model(obj) 21.34 + return self.filter(content_type__pk=object_type.id, 21.35 + object_id=obj.id) 21.36 + 21.37 + 21.38 +class Attachment(models.Model): 21.39 + """ 21.40 + Attachment model 21.41 + """ 21.42 + def file_upload_to(instance, filename): 21.43 + if not instance.hash: 21.44 + instance.make_hash() 21.45 + return 'attachments/{hash_byte[0]}/{hash_byte[1]}/{hash_byte[2]}/{user_id}_{hash}'.format( 21.46 + hash=instance.hash, 21.47 + hash_byte=instance.get_hash_3(), 21.48 + user_id=instance.user.id, 21.49 + ) 21.50 + 21.51 + objects = AttachmentManager() 21.52 + 21.53 + content_type = models.ForeignKey(ContentType, verbose_name=_('Content Type'), help_text=_('relation to model wich instance is own this attachment')) 21.54 + object_id = models.PositiveIntegerField(_('Object ID'), help_text=_('ID of object row')) 21.55 + content_object = generic.GenericForeignKey('content_type', 'object_id') 21.56 + 21.57 + hash = models.CharField(_('search hash'), max_length=40, unique=True, db_index=True) 21.58 + user = models.ForeignKey(User, related_name='attachments', verbose_name=_('user')) 21.59 + name = models.CharField(_('real file name'), max_length=255) 21.60 + description = models.TextField(_('description'), blank=True, help_text=_('attachment description')) 21.61 + mime_type = models.CharField(_('MIME type'), max_length=128, default='application/octet-stream') 21.62 + file = models.FileField(_('attachment'), upload_to=file_upload_to) 21.63 + size = models.PositiveIntegerField(_('file size'), blank=True, default=0) 21.64 + download_count = models.PositiveIntegerField(_('download count'), default=0) 21.65 + created = CreationDateTimeField(_('Created'), editable=True) 21.66 + 21.67 + av_state = models.PositiveIntegerField(_('avntivirus state'), default=FILE_STANDING, choices=FILE_STATUS_CHOICES) 21.68 + av_result = models.TextField(_('antivirus report'), blank=True) 21.69 + 21.70 + checksum = models.CharField(_('checksum'), max_length=40, blank=True, help_text=_('SHA1 checksum')) 21.71 + thumbnail = models.BooleanField(_('can generate thumbnails'), default=True) 21.72 + 21.73 + class Meta: 21.74 + ordering = ['-created'] 21.75 + verbose_name = _('attachment') 21.76 + verbose_name_plural = _('attachments') 21.77 + permissions = ( 21.78 + ('delete_foreign_attachment', 'Can delete foreign attachment'), 21.79 + ('change_foreign_attachment', 'Can change foreign attachment'), 21.80 + ('download_attachment', 'Can download attachment'), 21.81 + ) 21.82 + 21.83 + 21.84 + class ChecksumDoesNotExist(Exception): 21.85 + pass 21.86 + 21.87 + 21.88 + def __unicode__(self): 21.89 + return u'{self.name} uloaded by {self.user.username}'.format(self=self) 21.90 + 21.91 + def save(self, *args, **kvargs): 21.92 + if not self.id: 21.93 + self.set_file(self.file) 21.94 + super(Attachment, self).save(*args, **kvargs) 21.95 + 21.96 + @models.permalink 21.97 + def get_absolute_url(self): 21.98 + return ('attachments:download', [self.id]) 21.99 + 21.100 + def make_hash(self): 21.101 + self.hash = md5_constructor(smart_str(self.name) + 21.102 + smart_str(self.created.strftime('%s')) + 21.103 + smart_str(self.user.username)).hexdigest() 21.104 + 21.105 + def get_hash_3(self): 21.106 + """return tuple of first three bytes of search hash""" 21.107 + return self.hash[0:2], self.hash[2:4], self.hash[4:6] 21.108 + 21.109 + def set_file(self, fl): 21.110 + self.name = os.path.basename(fl.name) 21.111 + self.size = fl.size 21.112 + self.file = fl 21.113 + self.make_hash() 21.114 + 21.115 + def make_checksum(self): 21.116 + self.checksum = sha_constructor(self.file.read()).hexdigest() 21.117 + 21.118 + def check_file(self): 21.119 + if self.checksum: 21.120 + csum = sha_constructor(self.file.read()).hexdigest() 21.121 + return self.checksum == csum 21.122 + else: 21.123 + raise self.ChecksumDoesNotExist('for row {self.id} file {self.file}'.format(self=self)) 21.124 + 21.125 + def make_check_av(self): 21.126 + res, val = antivirus.scan_file(self.file.path) 21.127 + if res: 21.128 + self.av_state = FILE_VIRUS_FOUND 21.129 + self.av_result = val 21.130 + else: 21.131 + self.av_state = FILE_VIRUS_NOT_FOUND 21.132 + return res 21.133 + 21.134 + def detect_mime(self): 21.135 + """detect mime using system `file` program""" 21.136 + return detect_mime(self.file.path) 21.137 + 21.138 + def get_size_display(self): 21.139 + return humanize_filesize(self.size) 21.140 + 21.141 + 21.142 +class ThumbnailManager(models.Manager): 21.143 + def get_or_create(self, attachment, width, height): 21.144 + try: 21.145 + th = self.get(attachment=attachment, width=width, height=height) 21.146 + except self.model.DoesNotExist: 21.147 + th = self.create_for_attachment(attachment, width, height) 21.148 + return th 21.149 + 21.150 + 21.151 + def create_for_attachment(self, attachment, width, height): 21.152 + """ 21.153 + Create Thumbnail object and save it if thumbnailer backend can generate thumbnail for this file. 21.154 + If not — return None. 21.155 + """ 21.156 + if not attachment.thumbnail: 21.157 + return None 21.158 + 21.159 + th = self.model(attachment=attachment, width=width, height=height) 21.160 + with tempfile.NamedTemporaryFile(suffix='.thumb') as tfd: 21.161 + tfd.file.close() 21.162 + status, mime_type = create_thumbnail(attachment.file.path, 21.163 + tfd.name, 21.164 + width, height, 21.165 + attachment.mime_type) 21.166 + if status: 21.167 + th.mime_type = mime_type 21.168 + th.file = ImageFile(open(tfd.name)) 21.169 + th.save() 21.170 + else: 21.171 + attachment.thumbnail = False 21.172 + attachment.save() 21.173 + return None 21.174 + 21.175 + return th 21.176 + 21.177 + 21.178 +class Thumbnail(models.Model): 21.179 + def file_upload_to(instance, filename): 21.180 + return 'attachments/thumb/{hash_byte[0]}/{hash_byte[1]}/{hash_byte[2]}/{user_id}_{width}x{height}_{hash}'.format( 21.181 + hash=instance.attachment.hash, 21.182 + hash_byte=instance.attachment.get_hash_3(), 21.183 + user_id=instance.attachment.user.id, 21.184 + width=instance.width, 21.185 + height=instance.height, 21.186 + ) 21.187 + 21.188 + objects = ThumbnailManager() 21.189 + 21.190 + created = CreationDateTimeField(_('created'), blank=True, editable=True) 21.191 + attachment = models.ForeignKey(Attachment, related_name='thumbnails', verbose_name=_('attachment'), help_text=_('thumbnail for this attachment')) 21.192 + width = models.PositiveIntegerField(_('width'), db_index=True) 21.193 + height = models.PositiveIntegerField(_('height'), db_index=True) 21.194 + file = models.ImageField(_('thumbnail'), upload_to=file_upload_to) 21.195 + mime_type = models.CharField(_('MIME type'), max_length=128, default='image/jpeg') 21.196 + 21.197 + class Meta: 21.198 + ordering = ('-created', 'attachment__id') 21.199 + verbose_name = _('thumbnail') 21.200 + verbose_name_plural = _('thumbnails') 21.201 + unique_together = ('attachment', 'width', 'height') 21.202 + 21.203 + def __unicode__(self): 21.204 + return u'for {self.attachment.name} {self.width}x{self.height}'.format(self=self) 21.205 + 21.206 + @models.permalink 21.207 + def get_absolute_url(self): 21.208 + return ('attachments:thumbnail', (self.attachment_id, self.width, self.height)) 21.209 + 21.210 + 21.211 +from attachments import signals 21.212 +
22.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 22.2 +++ b/attachments/settings.py Tue Aug 03 14:39:36 2010 +0400 22.3 @@ -0,0 +1,71 @@ 22.4 +# -*- coding: utf-8 -*- 22.5 + 22.6 +from django.conf import settings 22.7 + 22.8 +# 22.9 +# Antivirus 22.10 +# 22.11 + 22.12 +ANTIVIRUS_BACKEND = getattr(settings, 'ANTIVIRUS_BACKEND', 'attachments.backends.av_clamav') 22.13 + 22.14 +ANTIVIRUS_CLAMD_UNIX_SOCK = getattr(settings, 'ANTIVIRUS_CLAMD_UNIX_SOCK', '/var/run/clamav/clamd.ctl') 22.15 +ANTIVIRUS_CLAMD_HOST = getattr(settings, 'ANTIVIRUS_CLAMD_HOST', '127.0.0.1') 22.16 +ANTIVIRUS_CLAMD_PORT = getattr(settings, 'ANTIVIRUS_CLAMD_PORT', 3310) 22.17 + 22.18 +# 22.19 +# Thumbnail 22.20 +# 22.21 + 22.22 +THUMBNAIL_BACKENDS = getattr(settings, 'THUMBNAIL_BACKENDS', ('attachments.backends.th_pil',)) 22.23 + 22.24 +# 22.25 +# Serve file 22.26 +# 22.27 + 22.28 +SERVE_FILE_BACKEND = getattr(settings, 'SERVE_FILE_BACKEND', 'attachments.backends.dl_default') 22.29 + 22.30 +# 22.31 +# ACL callbacks 22.32 +# 22.33 + 22.34 +def check_perm_attach_factory(perm_mod, perm_user): 22.35 + """ 22.36 + Generates checker function that 22.37 + check's attachment permission 22.38 + that it can be updated/deleted/downloaded 22.39 + 22.40 + `perm_mod` -- global permission key 22.41 + 'perm_user` -- user permission key 22.42 + 22.43 + check_perm: 22.44 + `request` -- view request 22.45 + `attachment` -- attachment instance 22.46 + """ 22.47 + 22.48 + if perm_mod is not None: 22.49 + def check_perm(request, attachment): 22.50 + return request.user.has_perm(perm_mod) or \ 22.51 + (request.user == attachment.user and request.user.has_perm(perm_user)) 22.52 + else: 22.53 + def check_perm(request, attachment): 22.54 + return request.user.has_perm(perm_user) 22.55 + return check_perm 22.56 + 22.57 +def check_perm_object(request, object_): 22.58 + """ 22.59 + Check permission for `object_` that attachment can be added 22.60 + 22.61 + `request` -- view request 22.62 + `object_` -- object that handle new attachment 22.63 + """ 22.64 + return request.user.has_perm('add_attachment') 22.65 + 22.66 +DEFAULT_ATTACHMENTS_PERM_CALLBACKS = { 22.67 + 'add': check_perm_object, 22.68 + 'delete': check_perm_attach_factory('delete_foreign_attachment', 'delete_attachment'), 22.69 + 'update': check_perm_attach_factory('change_foreign_attachment', 'change_attachment'), 22.70 + 'download': check_perm_attach_factory(None, 'download_attachment'), 22.71 +} 22.72 + 22.73 +DEFAULT_ATTACHMENTS_PERM_CALLBACKS.update(getattr(settings, 'ATTACHMENTS_PERM_CALLBACKS', {})) 22.74 +
23.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 23.2 +++ b/attachments/signals.py Tue Aug 03 14:39:36 2010 +0400 23.3 @@ -0,0 +1,37 @@ 23.4 +# -*- coding: utf-8 -*- 23.5 + 23.6 +import os 23.7 +import models 23.8 +from lib.helpers import signals 23.9 + 23.10 +try: 23.11 + from dblogging import add_log 23.12 +except ImportError: 23.13 + def add_log(*args, **kvargs): pass 23.14 + 23.15 + 23.16 +@signals.post_save(sender=models.Attachment) 23.17 +def attachment_saved(sender, instance, created, **kvargs): 23.18 + if created: 23.19 + instance.mime_type = instance.detect_mime() 23.20 + instance.make_checksum() 23.21 + instance.make_check_av() 23.22 + instance.save() 23.23 + 23.24 + # deduplication step 23.25 + dupfirst = models.Attachment.objects.filter(checksum=instance.checksum).order_by('id')[0] 23.26 + if dupfirst.id < instance.id: 23.27 + path=instance.file.path 23.28 + try: 23.29 + os.unlink(path) 23.30 + os.link(dupfirst.file.path, path) 23.31 + add_log('DEDUPLICATION', 23.32 + item_id=instance.id, 23.33 + first_id=dupfirst.id) 23.34 + except IOError, e: 23.35 + add_log('DEDUPLICATION_ERROR', 23.36 + exception=e, 23.37 + item_id=instance.id) 23.38 + with open(path, 'w') as fd: 23.39 + fd.write(dupfirst.file.read()) 23.40 +
24.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 24.2 +++ b/attachments/templates/attachments/add_form.html Tue Aug 03 14:39:36 2010 +0400 24.3 @@ -0,0 +1,9 @@ 24.4 +{% load i18n %} 24.5 +<form method="post" enctype="multipart/form-data" action="{{ form_url }}" class="add-attachment"> 24.6 + {% csrf_token %} 24.7 + <input type="hidden" name="next" value="{{ next }}"/> 24.8 + {{ form.as_p }} 24.9 + <p class="submit"> 24.10 + <input type="submit" value="{% trans "Add attachment" %}"/> 24.11 + </p> 24.12 +</form>
25.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 25.2 +++ b/attachments/templates/attachments/delete_form.html Tue Aug 03 14:39:36 2010 +0400 25.3 @@ -0,0 +1,8 @@ 25.4 +{% load i18n %} 25.5 +<form method="post" enctype="multipart/form-data" action="{{ form_url }}" class="delete-attachment"> 25.6 + {% csrf_token %} 25.7 + <input type="hidden" name="next" value="{{ next }}"/> 25.8 + <p class="submit"> 25.9 + <input type="submit" value="{% trans "Delete attachment" %}"/> 25.10 + </p> 25.11 +</form>
26.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 26.2 +++ b/attachments/templates/attachments/update_form.html Tue Aug 03 14:39:36 2010 +0400 26.3 @@ -0,0 +1,9 @@ 26.4 +{% load i18n %} 26.5 +<form method="post" enctype="multipart/form-data" action="{{ form_url }}" class="update-attachment"> 26.6 + {% csrf_token %} 26.7 + <input type="hidden" name="next" value="{{ next }}"/> 26.8 + {{ form.as_p }} 26.9 + <p class="submit"> 26.10 + <input type="submit" value="{% trans "Update attachment" %}"/> 26.11 + </p> 26.12 +</form>
27.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 27.2 +++ b/attachments/tests.py Tue Aug 03 14:39:36 2010 +0400 27.3 @@ -0,0 +1,23 @@ 27.4 +""" 27.5 +This file demonstrates two different styles of tests (one doctest and one 27.6 +unittest). These will both pass when you run "manage.py test". 27.7 + 27.8 +Replace these with more appropriate tests for your application. 27.9 +""" 27.10 + 27.11 +from django.test import TestCase 27.12 + 27.13 +class SimpleTest(TestCase): 27.14 + def test_basic_addition(self): 27.15 + """ 27.16 + Tests that 1 + 1 always equals 2. 27.17 + """ 27.18 + self.failUnlessEqual(1 + 1, 2) 27.19 + 27.20 +__test__ = {"doctest": """ 27.21 +Another way to test that 1 + 1 is equal to 2. 27.22 + 27.23 +>>> 1 + 1 == 2 27.24 +True 27.25 +"""} 27.26 +
28.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 28.2 +++ b/attachments/urls.py Tue Aug 03 14:39:36 2010 +0400 28.3 @@ -0,0 +1,11 @@ 28.4 +from django.conf.urls.defaults import * 28.5 +from views import add, delete, update, download, thumbnail 28.6 + 28.7 +urlpatterns = patterns( 28.8 + '', 28.9 + url(r'^add/(?P<app_label>[\w\-]+)/(?P<module_name>[\w\-]+)/(?P<pk>\d+)/$', add, name='add'), 28.10 + url(r'^delete/(?P<pk>\d+)/$', delete, name='delete'), 28.11 + url(r'^update/(?P<pk>\d+)/$', update, name='update'), 28.12 + url(r'^download/(?P<pk>\d+)/$', download, name='download'), 28.13 + url(r'^thumbnail/(?P<attachment_pk>\d+)/(?P<width>\d+)x(?P<height>\d+)/$', thumbnail, name='thumbnail'), 28.14 +)
29.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 29.2 +++ b/attachments/utils.py Tue Aug 03 14:39:36 2010 +0400 29.3 @@ -0,0 +1,81 @@ 29.4 +# -*- coding: utf-8 -*- 29.5 + 29.6 +import os 29.7 +import subprocess 29.8 +from attachments.settings import ANTIVIRUS_BACKEND, \ 29.9 + THUMBNAIL_BACKENDS, SERVE_FILE_BACKEND 29.10 +from django.utils.importlib import import_module 29.11 +from django.utils.translation import ugettext_lazy as _ 29.12 + 29.13 + 29.14 +antivirus = getattr(import_module(ANTIVIRUS_BACKEND), 'Antivirus')() 29.15 +serve_file = getattr(import_module(SERVE_FILE_BACKEND), 'serve_file') 29.16 + 29.17 + 29.18 +def detect_mime(filepath): 29.19 + """detect mime using system `file` program""" 29.20 + proc = subprocess.Popen(['file', '--mime-type', '-b', filepath], 29.21 + stdout=subprocess.PIPE) 29.22 + out = proc.communicate() 29.23 + return out[0].strip() 29.24 + 29.25 + 29.26 +def humanize_filesize(size): 29.27 + if size < 1024: 29.28 + return _('%d B') % (size, ) 29.29 + elif size < 1024**2: 29.30 + return _('%d KiB') % (size/1024, ) 29.31 + elif size < 1024**3: 29.32 + return _('%.2f MiB') % (size/(1024.0**2), ) 29.33 + elif size < 1024**4: 29.34 + return _('%.2f GiB') % (size/(1024.0**3), ) 29.35 + else: 29.36 + return _('%.2f TiB') % (size/(1024.0**4), ) 29.37 + 29.38 + 29.39 +class ThumbnailGenerator(object): 29.40 + _thumbnailers = {} 29.41 + 29.42 + class ThumbnailerDoesNotExist(Exception): 29.43 + pass 29.44 + 29.45 + def __init__(self, module_name_list): 29.46 + self.load(module_name_list) 29.47 + 29.48 + def load(self, module_name_list): 29.49 + self._thumbnailers = {} 29.50 + for th_module in module_name_list: 29.51 + th = getattr(import_module(th_module), 'Thumbnailer')() 29.52 + for mime_type in th.mime_types: 29.53 + if self._thumbnailers.has_key(mime_type): 29.54 + self._thumbnailers[mime_type].append(th) 29.55 + else: 29.56 + self._thumbnailers[mime_type] = [th] 29.57 + 29.58 + def get_list(self, mime_type): 29.59 + th_list = [] 29.60 + file_class, format = mime_type.split('/', 1) 29.61 + if self._thumbnailers.has_key(mime_type): 29.62 + th_list += self._thumbnailers[mime_type] 29.63 + if _thumbnailers.has_key(klass+'/*'): 29.64 + th_list += _thumbnailers[klass+'/*'] 29.65 + if not th_list: 29.66 + raise self.ThumbnailerDoesNotExist(mime_type) 29.67 + return th_list 29.68 + 29.69 + def __call__(self, src_path, dst_path, width, heigth, mime_type): 29.70 + try: 29.71 + th_list = self.get_list(mime_type) 29.72 + except self.ThumbnailerDoesNotExist, e: 29.73 + return (False, 'Thumbnailer for %s does not exist' % e) 29.74 + 29.75 + for th in th_list: 29.76 + status, mime = th.thumbnail(src_path, dst_path, width, heigth, mime_type) 29.77 + if status: 29.78 + return (status, mime) 29.79 + 29.80 + return (False, 'All thumbnailers return false') 29.81 + 29.82 + 29.83 +create_thumbnail = ThumbnailGenerator(THUMBNAIL_BACKENDS) 29.84 +
30.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 30.2 +++ b/attachments/views.py Tue Aug 03 14:39:36 2010 +0400 30.3 @@ -0,0 +1,129 @@ 30.4 +from django.shortcuts import render_to_response, get_object_or_404 30.5 +from django.views.decorators.http import require_POST 30.6 +from django.http import HttpResponseRedirect, HttpResponseForbidden, Http404 30.7 +from django.db.models.loading import get_model 30.8 +from django.db.models import F 30.9 +from django.core.urlresolvers import reverse 30.10 +from django.utils.translation import ugettext, ugettext_lazy as _ 30.11 +from django.template.context import RequestContext 30.12 +from django.views.decorators.csrf import csrf_protect 30.13 +from attachments.models import Attachment, Thumbnail 30.14 +from attachments.utils import serve_file 30.15 +from attachments.forms import AttachmentForm, AttachmentUpdateForm 30.16 +from attachments.settings import DEFAULT_ATTACHMENTS_PERM_CALLBACKS as PERMS 30.17 + 30.18 + 30.19 +def add_url_for_obj(obj): 30.20 + return reverse('attachments:add', kwargs={ 30.21 + 'app_label': obj._meta.app_label, 30.22 + 'module_name': obj._meta.module_name, 30.23 + 'pk': obj.pk 30.24 + }) 30.25 + 30.26 + 30.27 +@csrf_protect 30.28 +def add(request, app_label, module_name, pk, 30.29 + template_name='attachments/add_form.html', 30.30 + perm_callback=PERMS['add']): 30.31 + 30.32 + # TODO: check for proper applabel/modelname 30.33 + model = get_model(app_label, module_name) 30.34 + obj = get_object_or_404(model, pk=pk) 30.35 + 30.36 + # TODO: check for valid next attribute 30.37 + next = request.REQUEST.get('next') or '/' 30.38 + 30.39 + if not perm_callback(request, obj): 30.40 + return HttpResponseForbidden('Did\'t allowed upload attachments for this item') 30.41 + 30.42 + if request.method == 'POST': 30.43 + form = AttachmentForm(request.POST, request.FILES) 30.44 + else: 30.45 + form = AttachmentForm() 30.46 + 30.47 + if form.is_valid(): 30.48 + form.save(request, obj) 30.49 + request.user.message_set.create(message=ugettext('Your attachment was uploaded.')) 30.50 + return HttpResponseRedirect(next) 30.51 + else: 30.52 + return render_to_response(template_name, { 30.53 + 'form': form, 30.54 + 'form_url': add_url_for_obj(obj), 30.55 + 'next': next, 30.56 + }, 30.57 + RequestContext(request)) 30.58 + 30.59 + 30.60 +@csrf_protect 30.61 +def delete(request, pk, 30.62 + template_name='attachments/delete_form.html', 30.63 + perm_callback=PERMS['delete']): 30.64 + attachment = get_object_or_404(Attachment, pk=pk) 30.65 + next = request.REQUEST.get('next') or '/' 30.66 + 30.67 + if not perm_callback(request, attachment): 30.68 + return HttpResponseForbidden('Delete not allowed.') 30.69 + 30.70 + if request.method == 'POST': 30.71 + attachment.delete() 30.72 + request.user.message_set.create(message=ugettext('Your attachment was deleted.')) 30.73 + return HttpResponseRedirect(next) 30.74 + else: 30.75 + return render_to_response(template_name, { 30.76 + 'form_url': reverse('attachments:delete', kwargs={'pk': pk}), 30.77 + 'next': next}, 30.78 + RequestContext(request)) 30.79 + 30.80 + 30.81 +@csrf_protect 30.82 +def update(request, pk, 30.83 + template_name='attachments/update_form.html', 30.84 + perm_callback=PERMS['update']): 30.85 + 30.86 + attachment = get_object_or_404(Attachment, pk=pk) 30.87 + next = request.REQUEST.get('next') or '/' 30.88 + 30.89 + if request.method == 'POST': 30.90 + form = AttachmentUpdateForm(request.POST, request.FILE, instance=attachment) 30.91 + else: 30.92 + form = AttachmentUpdateForm(instance=attachment) 30.93 + 30.94 + if not perm_callback(request, attachment): 30.95 + return HttpResponseForbidden('Update not allowed.') 30.96 + 30.97 + if form.is_valid(): 30.98 + form.save() 30.99 + request.user.message_set.create(message=ugettext('Your attachment was updated.')) 30.100 + return HttpResponseRedirect(next) 30.101 + else: 30.102 + return render_to_response(template_name, { 30.103 + 'form': form, 30.104 + 'form_url': reverse('attachments:update', kwargs={'pk': pk}), 30.105 + 'next': next}, 30.106 + RequestContext(request)) 30.107 + 30.108 + 30.109 +def download(request, pk, 30.110 + perm_callback=PERMS['download']): 30.111 + attachment = get_object_or_404(Attachment, pk=pk) 30.112 + 30.113 + if not perm_callback(request, attachment): 30.114 + return HttpResponseForbidden('Forbidden attachment.') 30.115 + 30.116 + attachment.download_count = F('download_count') + 1 30.117 + attachment.save() 30.118 + return serve_file(request, attachment.file, 30.119 + attachment.name, attachment.mime_type) 30.120 + 30.121 + 30.122 +def thumbnail(request, attachment_pk, width, height, 30.123 + perm_callback=PERMS['download']): 30.124 + attachment = get_object_or_404(Attachment, pk=attachment_pk) 30.125 + 30.126 + if not perm_callback(request, attachment): 30.127 + return HttpResponseForbidden('Forbidden attachment.') 30.128 + 30.129 + thumb = Thumbnail.objects.get_or_create(attachment, width, height) 30.130 + if thumb is None: 30.131 + raise Http404('Thumbnail can not be created') 30.132 + return serve_file(request, thumb.file, None, thumb.mime_type)
31.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 31.2 +++ b/setup.py Tue Aug 03 14:39:36 2010 +0400 31.3 @@ -0,0 +1,38 @@ 31.4 +from setuptools import setup, find_packages 31.5 +from os.path import join, dirname 31.6 +import attachments 31.7 + 31.8 +def desc(): 31.9 + return open(join(dirname(__file__), 'README.rst')).read() 31.10 + 31.11 +setup( 31.12 + name='django-extended-attachments', 31.13 + description='A generic Django application to attach Files (Attachments) to any model', 31.14 + long_description=desc(), 31.15 + license='BSD', 31.16 + version=attachments.__version__, 31.17 + author=attachments.__author__, 31.18 + author_email=attachments.__email__, 31.19 + url='http://vehq.ru/project/django-extended-attachments', 31.20 + download_url='http://hg.vehq.ru/django-extended-attachments/archive/%s.tar.bz2' % attachments.__version__, 31.21 + keywords=['Django', 'attachments', 'antivirus'], 31.22 + packages=find_packages(), 31.23 + classifiers=[ 31.24 + 'Development Status :: 4 - Beta', 31.25 + 'Environment :: Web Environment', 31.26 + 'Intended Audience :: Developers', 31.27 + 'License :: OSI Approved :: BSD License', 31.28 + 'Operating System :: OS Independent', 31.29 + 'Programming Language :: Python', 31.30 + 'Framework :: Django', 31.31 + ], 31.32 + package_data={ 31.33 + 'attachments': [ 31.34 + 'templates/attachments/*.html', 31.35 + 'locale/*/*/*', 31.36 + ] 31.37 + }, 31.38 + install_requires=['django_extensions', 'django>=1.2'], # python >= 2.6 31.39 + platforms='any', 31.40 + zip_safe=False, 31.41 +)
