django-extended-attachments

changeset 0:b244a4b6f89c

initial import from robobb
author Vladimir Ermakov <vooon341@gmail.com>
date Tue Aug 03 14:39:36 2010 +0400 (21 months ago)
parents
children 312b1b3cfa7f
files .hgignore AUTHORS LICENSE MANIFEST.in README.rst attachments/__init__.py attachments/admin.py attachments/backends/__init__.py attachments/backends/av_clamav.py attachments/backends/av_clamd.py attachments/backends/dl_default.py attachments/backends/dl_xsendfile.py attachments/backends/th_graphicsmagick.py attachments/backends/th_pdf.py attachments/backends/th_pil.py attachments/forms.py attachments/locale/ru/LC_MESSAGES/django.po attachments/management/__init__.py attachments/management/commands/__init__.py attachments/management/commands/attachments_antivirus.py attachments/management/commands/attachments_guess_mime.py attachments/management/commands/attachments_sha1sum.py attachments/models.py attachments/settings.py attachments/signals.py attachments/templates/attachments/add_form.html attachments/templates/attachments/delete_form.html attachments/templates/attachments/update_form.html attachments/tests.py attachments/urls.py attachments/utils.py attachments/views.py setup.py
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 +)
Repositories maintained by Vladimir Ermakov