From 5e86ffb7b41f45fac86ee49bd9be39da864c27d9 Mon Sep 17 00:00:00 2001 From: Erik Stein Date: Fri, 10 Jun 2016 12:36:44 +0200 Subject: [PATCH] ... --- README.rst | 0 assetkit/pkgmeta.py | 0 example_site/main/medialibrary/admin.py | 17 ++- example_site/main/medialibrary/apps.py | 2 +- example_site/main/medialibrary/fields.py | 110 ++++++++++++++++++ example_site/main/medialibrary/forms.py | 8 ++ .../medialibrary/migrations/0001_initial.py | 27 +++++ .../migrations/0002_mediaasset_simple_file.py | 20 ++++ .../migrations/0003_auto_20160425_0811.py | 21 ++++ example_site/main/medialibrary/models.py | 44 +++---- example_site/main/medialibrary/utils.py | 25 ++++ example_site/main/settings.py | 8 +- example_site/main/storages.py | 14 +-- example_site/main/urls.py | 5 +- 14 files changed, 256 insertions(+), 45 deletions(-) create mode 100644 README.rst create mode 100644 assetkit/pkgmeta.py create mode 100644 example_site/main/medialibrary/fields.py create mode 100644 example_site/main/medialibrary/forms.py create mode 100644 example_site/main/medialibrary/migrations/0001_initial.py create mode 100644 example_site/main/medialibrary/migrations/0002_mediaasset_simple_file.py create mode 100644 example_site/main/medialibrary/migrations/0003_auto_20160425_0811.py create mode 100644 example_site/main/medialibrary/utils.py diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..e69de29 diff --git a/assetkit/pkgmeta.py b/assetkit/pkgmeta.py new file mode 100644 index 0000000..e69de29 diff --git a/example_site/main/medialibrary/admin.py b/example_site/main/medialibrary/admin.py index 8c38f3f..aa9a000 100644 --- a/example_site/main/medialibrary/admin.py +++ b/example_site/main/medialibrary/admin.py @@ -1,3 +1,18 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals +# Erik Stein , 2016 + from django.contrib import admin +from django.db import models +from django.utils.translation import ugettext_lazy as _ + +from .models import MediaAsset + + +@admin.register(MediaAsset) +class MediaAssetAdmin(admin.ModelAdmin): + list_display = ['name', 'get_original_path'] -# Register your models here. + def get_original_path(self, obj): + return obj.original_file.name + get_original_path.short_description = _("original path") diff --git a/example_site/main/medialibrary/apps.py b/example_site/main/medialibrary/apps.py index f79a626..02af3e0 100644 --- a/example_site/main/medialibrary/apps.py +++ b/example_site/main/medialibrary/apps.py @@ -4,4 +4,4 @@ from django.apps import AppConfig class MedialibraryConfig(AppConfig): - name = 'medialibrary' + name = 'main.medialibrary' diff --git a/example_site/main/medialibrary/fields.py b/example_site/main/medialibrary/fields.py new file mode 100644 index 0000000..a3c11cf --- /dev/null +++ b/example_site/main/medialibrary/fields.py @@ -0,0 +1,110 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals +# Erik Stein , 2016 + +import os +from django import forms +from django.contrib.admin.widgets import AdminFileWidget +from django.db import models +from django.db.models.fields.files import FileDescriptor +from django.utils.html import conditional_escape +from django.utils.text import slugify + +from ..storages import ProtectedMediaAssetStorage + + +# TODO Define the central storage somewhere more central +ORIGINALS_STORAGE = ProtectedMediaAssetStorage() +# TODO Move filename max length to storage +FILENAME_MAX_LENGTH = 255 + + +# TOD Move get_upload_path to utils +def get_upload_path(instance, filename): + """ + Returns //original/ + where + - uuid is taken from instance, + - filename is slugified and shortened to a max length including the extension. + """ + name, ext = os.path.splitext(filename) + name = slugify(name) + name = name[:(FILENAME_MAX_LENGTH - len(ext))] + filename = "%s%s" % (name, ext) + return os.path.join( + instance.get_uuid(), + instance.STORAGE.ORIGINAL_FILE_PREFIX, + filename + ) + + +DEFAULT_UPLOAD_TO = get_upload_path + + +class MediaAssetFileWidget(AdminFileWidget): + """ + Widget which understands ProtectedMediaAssetStorage + (knows that the url property is not relevant), also + does not provide a link to the original file. + # TODO Add admin access to the original file, if permissions apply + # TODO Add permission "allowed to view original file" + """ + + template_with_initial = ( + '%(initial_text)s: %(initial)s ' + '%(clear_template)s
%(input_text)s: %(input)s' + ) + + def is_initial(self, value): + # Checks for 'name' instead of 'url' property + return bool(value and hasattr(value, 'name')) + + def get_template_substitution_values(self, value): + # Does not use value.url + return { + 'initial': conditional_escape(value), + } + + +class MediaAssetFileDescriptor(FileDescriptor): + # TODO Assign metadata fields like width/height + + def __set__(self, instance, value): + previous_file = instance.__dict__.get(self.field.name) + super(MediaAssetFileDescriptor, self).__set__(instance, value) + + # TODO Check if previous_file is actually None in our case; remove comment + # To prevent recalculating image dimensions when we are instantiating + # an object from the database (bug #11084), only update dimensions if + # the field had a value before this assignment. Since the default + # value for FileField subclasses is an instance of field.attr_class, + # previous_file will only be None when we are called from + # Model.__init__(). The ImageField.update_dimension_fields method + # hooked up to the post_init signal handles the Model.__init__() cases. + # Assignment happening outside of Model.__init__() will trigger the + # update right here. + if previous_file is not None: + pass # self.field.update_dimension_fields(instance, force=True) + + +class MediaAssetFormField(forms.FileField): + widget = MediaAssetFileWidget + + +class MediaAssetField(models.FileField): + # The descriptor to use for accessing the attribute off of the class. + description_class = MediaAssetFileDescriptor + + def _init__(self, verbose_name=None, name=None, upload_to='', storage=None, *args, **kwargs): + kwargs['max_length'] = kwargs.get('max_length', FILENAME_MAX_LENGTH) + kwargs['upload_to'] = kwargs.get('upload_to', DEFAULT_UPLOAD_TO) + kwargs['storage'] = kwargs.get('storage', ORIGINALS_STORAGE) + super(MediaAssetField, self).__init__(verbose_name, name, **kwargs) + + def formfield(self, **kwargs): + defaults = { + 'form_class': MediaAssetFormField, + 'max_length': self.max_length + } + defaults.update(kwargs) + return super(MediaAssetField, self).formfield(**defaults) diff --git a/example_site/main/medialibrary/forms.py b/example_site/main/medialibrary/forms.py new file mode 100644 index 0000000..8b5a776 --- /dev/null +++ b/example_site/main/medialibrary/forms.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals +# Erik Stein , 2016 + + +class FileField(Field): + widget = ClearableFileInput + diff --git a/example_site/main/medialibrary/migrations/0001_initial.py b/example_site/main/medialibrary/migrations/0001_initial.py new file mode 100644 index 0000000..365eaf2 --- /dev/null +++ b/example_site/main/medialibrary/migrations/0001_initial.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.4 on 2016-03-24 12:34 +from __future__ import unicode_literals + +from django.db import migrations, models +import main.medialibrary.models +import main.storages + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='MediaAsset', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('uuid_hex', models.CharField(editable=False, max_length=32)), + ('original_file', models.FileField(storage=main.storages.ProtectedMediaAssetStorage(), upload_to=main.medialibrary.fields.get_upload_path)), + ('name', models.CharField(max_length=50, verbose_name='name')), + ], + ), + ] diff --git a/example_site/main/medialibrary/migrations/0002_mediaasset_simple_file.py b/example_site/main/medialibrary/migrations/0002_mediaasset_simple_file.py new file mode 100644 index 0000000..33c2210 --- /dev/null +++ b/example_site/main/medialibrary/migrations/0002_mediaasset_simple_file.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.4 on 2016-04-24 10:33 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('medialibrary', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='mediaasset', + name='simple_file', + field=models.FileField(blank=True, null=True, upload_to=b''), + ), + ] diff --git a/example_site/main/medialibrary/migrations/0003_auto_20160425_0811.py b/example_site/main/medialibrary/migrations/0003_auto_20160425_0811.py new file mode 100644 index 0000000..9c49112 --- /dev/null +++ b/example_site/main/medialibrary/migrations/0003_auto_20160425_0811.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.4 on 2016-04-25 08:11 +from __future__ import unicode_literals + +from django.db import migrations +import main.medialibrary.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('medialibrary', '0002_mediaasset_simple_file'), + ] + + operations = [ + migrations.AlterField( + model_name='mediaasset', + name='original_file', + field=main.medialibrary.fields.MediaAssetField(upload_to=b'', verbose_name='original file'), + ), + ] diff --git a/example_site/main/medialibrary/models.py b/example_site/main/medialibrary/models.py index 85b3ea8..87d5b45 100644 --- a/example_site/main/medialibrary/models.py +++ b/example_site/main/medialibrary/models.py @@ -1,49 +1,33 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -import os import uuid from django.db import models from django.utils.encoding import python_2_unicode_compatible -from django.utils.text import slugify from django.utils.translation import ugettext_lazy as _ -from ..storages import ProtectedMediaAssetStorage +from ..medialibrary.fields import MediaAssetField -# TODO Define the central storage somewhere more central -originals_storage = ProtectedMediaAssetStorage() +class UUIDMixin(models.Model): + uuid_hex = models.CharField(max_length=32, null=False, editable=False) + + class Meta: + abstract = True -# TODO Move this to special FileField class -def get_upload_path(instance, filename): - """ - Returns //original/ - where - - uuid is taken from instance, - - filename is slugified and shortened to a max length of 255 characters including the extension. - """ - MAX_LENGTH = 255 - name, ext = os.path.splitext(filename) - name = slugify(name) - name = name[:(MAX_LENGTH - len(ext))] - filename = "%s%s" % (name, ext) - return os.path.join(instance.get_uuid(), instance.storage.ORIGINAL_FILE_PREFIX, filename) + # TODO Make auto-initializing UUID-field + def get_uuid(self): + if not self.uuid_hex: + self.uuid_hex = uuid.uuid4().hex + return str(uuid.UUID(self.uuid_hex)) @python_2_unicode_compatible -class MediaAsset(models.Model): - uuid_hex = models.CharField(max_length=32, null=False, editable=False) - original_file = models.FileField(upload_to=get_upload_path, - storage=originals_storage - ) +class MediaAsset(UUIDMixin, models.Model): + original_file = MediaAssetField(_("original file")) name = models.CharField(_('name'), max_length=50) + simple_file = models.FileField(null=True, blank=True) def __str__(self): return self.name - # TODO Make auto-initialized UUID-field - def get_uuid(self): - if not self.uuid_hex: - self.uuid_hex = uuid.uuid4().hex - return uuid.UUID(self.uuid_hex) - diff --git a/example_site/main/medialibrary/utils.py b/example_site/main/medialibrary/utils.py new file mode 100644 index 0000000..6d2db66 --- /dev/null +++ b/example_site/main/medialibrary/utils.py @@ -0,0 +1,25 @@ +# # -*- coding: utf-8 -*- +# from __future__ import unicode_literals +# # Erik Stein , 2016 + +# import os +# from django.utils.text import slugify + + +# def get_upload_path(instance, filename): +# """ +# Returns //original/ +# where +# - uuid is taken from instance, +# - filename is slugified and shortened to a max length including the extension. +# """ +# name, ext = os.path.splitext(filename) +# name = slugify(name) +# name = name[:(FILENAME_MAX_LENGTH - len(ext))] +# filename = "%s%s" % (name, ext) +# return os.path.join( +# instance.get_uuid(), +# instance.STORAGE.ORIGINAL_FILE_PREFIX, +# filename +# ) + diff --git a/example_site/main/settings.py b/example_site/main/settings.py index 5182a9f..2f1193a 100644 --- a/example_site/main/settings.py +++ b/example_site/main/settings.py @@ -32,8 +32,10 @@ INSTALLED_APPS = [ 'django.contrib.staticfiles', 'imagekit', - 'main.medialibrary', - 'main', + 'main.medialibrary.apps.MedialibraryConfig', + # 'main', + + 'django_extensions', ] MIDDLEWARE_CLASSES = [ @@ -127,7 +129,7 @@ STATIC_URL = '/static/' MEDIA_URL = '/media/' STATIC_ROOT = os.path.join(ENV_DIR, 'var', 'static') -# All media uploads should be protected: MEDIA_ROOT = os.path.join(ENV_DIR, 'var', 'media') +MEDIA_ROOT = os.path.join(ENV_DIR, 'var', 'public') PROTECTED_MEDIA_ASSETS_ROOT = os.path.join(ENV_DIR, 'var', 'protected') diff --git a/example_site/main/storages.py b/example_site/main/storages.py index d6693a4..883df06 100644 --- a/example_site/main/storages.py +++ b/example_site/main/storages.py @@ -23,12 +23,11 @@ class ProtectedMediaAssetStorage(FileSystemStorage): ORIGINAL_FILE_PREFIX = ORIGINAL_FILE_PREFIX - def __init__(self, location=None, file_permissions_mode=None, - directory_permissions_mode=None): + def __init__(self, location=None, base_url=None, **kwargs): if location is None: location = getattr(settings, 'PROTECTED_MEDIA_ASSETS_ROOT', settings.PROTECTED_MEDIA_ASSETS_ROOT) - super(ProtectedMediaAssetStorage, self).__init__(self, location=location, file_permissions_mode=None, - directory_permissions_mode=None) + super(ProtectedMediaAssetStorage, self).__init__(location=location, base_url=None, **kwargs) + self.base_url = None def delete(self, name): super(ProtectedMediaAssetStorage, self).delete(name) @@ -45,10 +44,9 @@ class ProtectedMediaAssetStorage(FileSystemStorage): return os.path.getsize(self.path(name)) def url(self, name): - if self.base_url is None: - raise ValueError("This file is not accessible via a URL.") + raise ValueError("This file storage is not accessible via a URL.") # TODO ? @deconstructible -class - return urljoin(self.base_url, ORIGINAL_FILE_PREFIX, filepath_to_uri(name)) +# class +# return urljoin(self.base_url, ORIGINAL_FILE_PREFIX, filepath_to_uri(name)) diff --git a/example_site/main/urls.py b/example_site/main/urls.py index 9c687fe..6f18904 100644 --- a/example_site/main/urls.py +++ b/example_site/main/urls.py @@ -10,10 +10,11 @@ from django.utils.translation import ugettext_lazy as _ urlpatterns = [ - url(r'^admin/', include(admin.site.urls)), + url(r'^admin/', include(admin.site.urls)), # url(r'^', include('private_media.urls')), ] -# Never used: urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) +# For unprotected uploads: +urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)