commit
					5efd6261c8
				
				 35 changed files with 1203 additions and 0 deletions
			
			
		| @ -0,0 +1,31 @@ | |||||||
|  | NEXT | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | REVIEW | ||||||
|  | 
 | ||||||
|  | [] python-iiif https://github.com/zimeon/iiif | ||||||
|  | [] https://github.com/fish2000/instakit | ||||||
|  | [] http://django-daguerre.readthedocs.org/en/latest/guides/areas.html | ||||||
|  | [] https://github.com/codingjoe/django-stdimage | ||||||
|  | [] https://github.com/brmc/django-media-helper#general-info | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | POSTPONED | ||||||
|  | 
 | ||||||
|  | [] multipage-PDF | ||||||
|  | 
 | ||||||
|  | [] snapshots | ||||||
|  | 
 | ||||||
|  | [] manual upload | ||||||
|  | 	filesystem check schon, muss aber per sftp angelegt werden | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | MAYBE | ||||||
|  | 
 | ||||||
|  | [] Integrate django-image-crop | ||||||
|  | [] https://github.com/francescortiz/image/tree/master/image | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ISSUES | ||||||
|  | 
 | ||||||
|  | [] on save look for manual overrides of variants and ask to delete them | ||||||
| @ -0,0 +1,161 @@ | |||||||
|  | from copy import copy | ||||||
|  | from django.conf import settings | ||||||
|  | from django.core.files import File | ||||||
|  | from django.utils.functional import SimpleLazyObject | ||||||
|  | from imagekit.files import BaseIKFile | ||||||
|  | from imagekit.registry import generator_registry | ||||||
|  | from imagekit.signals import content_required, existence_required | ||||||
|  | from imagekit.utils import get_logger, get_singleton, generate, get_by_qname | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class MediaAssetCacheFile(BaseIKFile, File): | ||||||
|  |     """ | ||||||
|  |     A file that represents the result of a generator. Creating an instance of | ||||||
|  |     this class is not enough to trigger the generation of the file. In fact, | ||||||
|  |     one of the main points of this class is to allow the creation of the file | ||||||
|  |     to be deferred until the time that the cache file strategy requires it. | ||||||
|  | 
 | ||||||
|  |     """ | ||||||
|  | 
 | ||||||
|  |     def __init__(self, generator, name=None, storage=None, cachefile_backend=None, cachefile_strategy=None): | ||||||
|  |         """ | ||||||
|  |         :param generator: The object responsible for generating a new image. | ||||||
|  |         :param name: The filename | ||||||
|  |         :param storage: A Django storage object that will be used to save the | ||||||
|  |             file. | ||||||
|  |         :param cachefile_backend: The object responsible for managing the | ||||||
|  |             state of the file. | ||||||
|  |         :param cachefile_strategy: The object responsible for handling events | ||||||
|  |             for this file. | ||||||
|  | 
 | ||||||
|  |         """ | ||||||
|  |         self.generator = generator | ||||||
|  | 
 | ||||||
|  |         if not name: | ||||||
|  |             try: | ||||||
|  |                 name = generator.cachefile_name | ||||||
|  |             except AttributeError: | ||||||
|  |                 fn = get_by_qname(settings.IMAGEKIT_CACHEFILE_NAMER, 'namer') | ||||||
|  |                 name = fn(generator) | ||||||
|  |         self.name = name | ||||||
|  | 
 | ||||||
|  |         storage = storage or getattr(generator, 'cachefile_storage', | ||||||
|  |             None) or get_singleton(settings.IMAGEKIT_DEFAULT_FILE_STORAGE, | ||||||
|  |             'file storage backend') | ||||||
|  |         self.cachefile_backend = ( | ||||||
|  |             cachefile_backend | ||||||
|  |             or getattr(generator, 'cachefile_backend', None) | ||||||
|  |             or get_singleton(settings.IMAGEKIT_DEFAULT_CACHEFILE_BACKEND, | ||||||
|  |                              'cache file backend')) | ||||||
|  |         self.cachefile_strategy = ( | ||||||
|  |             cachefile_strategy | ||||||
|  |             or getattr(generator, 'cachefile_strategy', None) | ||||||
|  |             or get_singleton(settings.IMAGEKIT_DEFAULT_CACHEFILE_STRATEGY, | ||||||
|  |                              'cache file strategy') | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         super(MediaAssetCacheFile, self).__init__(storage=storage) | ||||||
|  | 
 | ||||||
|  |     def _require_file(self): | ||||||
|  |         if getattr(self, '_file', None) is None: | ||||||
|  |             content_required.send(sender=self, file=self) | ||||||
|  |             self._file = self.storage.open(self.name, 'rb') | ||||||
|  | 
 | ||||||
|  |     # The ``path`` and ``url`` properties are overridden so as to not call | ||||||
|  |     # ``_require_file``, which is only meant to be called when the file object | ||||||
|  |     # will be directly interacted with (e.g. when using ``read()``). These only | ||||||
|  |     # require the file to exist; they do not need its contents to work. This | ||||||
|  |     # distinction gives the user the flexibility to create a cache file | ||||||
|  |     # strategy that assumes the existence of a file, but can still make the file | ||||||
|  |     # available when its contents are required. | ||||||
|  | 
 | ||||||
|  |     def _storage_attr(self, attr): | ||||||
|  |         if getattr(self, '_file', None) is None: | ||||||
|  |             existence_required.send(sender=self, file=self) | ||||||
|  |         fn = getattr(self.storage, attr) | ||||||
|  |         return fn(self.name) | ||||||
|  | 
 | ||||||
|  |     @property | ||||||
|  |     def path(self): | ||||||
|  |         return self._storage_attr('path') | ||||||
|  | 
 | ||||||
|  |     @property | ||||||
|  |     def url(self): | ||||||
|  |         return self._storage_attr('url') | ||||||
|  | 
 | ||||||
|  |     def generate(self, force=False): | ||||||
|  |         """ | ||||||
|  |         Generate the file. If ``force`` is ``True``, the file will be generated | ||||||
|  |         whether the file already exists or not. | ||||||
|  | 
 | ||||||
|  |         """ | ||||||
|  |         if force or getattr(self, '_file', None) is None: | ||||||
|  |             self.cachefile_backend.generate(self, force) | ||||||
|  | 
 | ||||||
|  |     def _generate(self): | ||||||
|  |         # Generate the file | ||||||
|  |         content = generate(self.generator) | ||||||
|  | 
 | ||||||
|  |         actual_name = self.storage.save(self.name, content) | ||||||
|  | 
 | ||||||
|  |         # We're going to reuse the generated file, so we need to reset the pointer. | ||||||
|  |         content.seek(0) | ||||||
|  | 
 | ||||||
|  |         # Store the generated file. If we don't do this, the next time the | ||||||
|  |         # "file" attribute is accessed, it will result in a call to the storage | ||||||
|  |         # backend (in ``BaseIKFile._get_file``). Since we already have the | ||||||
|  |         # contents of the file, what would the point of that be? | ||||||
|  |         self.file = File(content) | ||||||
|  | 
 | ||||||
|  |         if actual_name != self.name: | ||||||
|  |             get_logger().warning( | ||||||
|  |                 'The storage backend %s did not save the file with the' | ||||||
|  |                 ' requested name ("%s") and instead used "%s". This may be' | ||||||
|  |                 ' because a file already existed with the requested name. If' | ||||||
|  |                 ' so, you may have meant to call generate() instead of' | ||||||
|  |                 ' generate(force=True), or there may be a race condition in the' | ||||||
|  |                 ' file backend %s. The saved file will not be used.' % ( | ||||||
|  |                     self.storage, | ||||||
|  |                     self.name, actual_name, | ||||||
|  |                     self.cachefile_backend | ||||||
|  |                 ) | ||||||
|  |             ) | ||||||
|  | 
 | ||||||
|  |     def __bool__(self): | ||||||
|  |         if not self.name: | ||||||
|  |             return False | ||||||
|  | 
 | ||||||
|  |         # Dispatch the existence_required signal before checking to see if the | ||||||
|  |         # file exists. This gives the strategy a chance to create the file. | ||||||
|  |         existence_required.send(sender=self, file=self) | ||||||
|  | 
 | ||||||
|  |         try: | ||||||
|  |             check = self.cachefile_strategy.should_verify_existence(self) | ||||||
|  |         except AttributeError: | ||||||
|  |             # All synchronous backends should have created the file as part of | ||||||
|  |             # `existence_required` if they wanted to. | ||||||
|  |             check = getattr(self.cachefile_backend, 'is_async', False) | ||||||
|  |         return self.cachefile_backend.exists(self) if check else True | ||||||
|  | 
 | ||||||
|  |     def __getstate__(self): | ||||||
|  |         state = copy(self.__dict__) | ||||||
|  | 
 | ||||||
|  |         # file is hidden link to "file" attribute | ||||||
|  |         state.pop('_file', None) | ||||||
|  | 
 | ||||||
|  |         return state | ||||||
|  | 
 | ||||||
|  |     def __nonzero__(self): | ||||||
|  |         # Python 2 compatibility | ||||||
|  |         return self.__bool__() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class LazyMediaAssetCacheFile(SimpleLazyObject): | ||||||
|  |     def __init__(self, generator_id, *args, **kwargs): | ||||||
|  |         def setup(): | ||||||
|  |             generator = generator_registry.get(generator_id, *args, **kwargs) | ||||||
|  |             return MediaAssetCacheFile(generator) | ||||||
|  |         super(LazyMediaAssetCacheFile, self).__init__(setup) | ||||||
|  | 
 | ||||||
|  |     def __repr__(self): | ||||||
|  |         return '<%s: %s>' % (self.__class__.__name__, str(self) or 'None') | ||||||
| @ -0,0 +1,148 @@ | |||||||
|  | # -*- coding: utf-8 -*- | ||||||
|  | from __future__ import unicode_literals | ||||||
|  | # Erik Stein <code@classlibrary.net>, 2016 | ||||||
|  | 
 | ||||||
|  | from ..utils import get_singleton, get_cache, sanitize_cache_key | ||||||
|  | import warnings | ||||||
|  | from copy import copy | ||||||
|  | from django.core.exceptions import ImproperlyConfigured | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | from imagekit.cachefiles.backends import CacheFileState, CachedFileBackend | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class MediaAssetCacheFileState(CacheFileState): | ||||||
|  |     """ | ||||||
|  |     Manual means that the user has manually added | ||||||
|  |     """ | ||||||
|  | 
 | ||||||
|  |     MANUAL = 'manual' | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class MediaAssetCachedFileBackend(CachedFileBackend): | ||||||
|  |     # As we will defer a lot to a celery queue, set the timeout higher | ||||||
|  |     existence_check_timeout = 20 | ||||||
|  |     """ | ||||||
|  |     The number of seconds to wait before rechecking to see if the file exists. | ||||||
|  |     If the image is found to exist, that information will be cached using the | ||||||
|  |     timeout specified in your CACHES setting (which should be very high). | ||||||
|  |     However, when the file does not exist, you probably want to check again | ||||||
|  |     in a relatively short amount of time. This attribute allows you to do that. | ||||||
|  | 
 | ||||||
|  |     """ | ||||||
|  | 
 | ||||||
|  |     def get_state(self, file, check_if_unknown=True): | ||||||
|  |         key = self.get_key(file) | ||||||
|  |         state = self.cache.get(key) | ||||||
|  |         if state is None and check_if_unknown: | ||||||
|  |             exists = self._exists(file) | ||||||
|  |             state = CacheFileState.EXISTS if exists else CacheFileState.DOES_NOT_EXIST | ||||||
|  |             self.set_state(file, state) | ||||||
|  |         return state | ||||||
|  | 
 | ||||||
|  |     def set_state(self, file, state): | ||||||
|  |         key = self.get_key(file) | ||||||
|  |         if state == CacheFileState.DOES_NOT_EXIST: | ||||||
|  |             self.cache.set(key, state, self.existence_check_timeout) | ||||||
|  |         else: | ||||||
|  |             self.cache.set(key, state) | ||||||
|  | 
 | ||||||
|  |     def __getstate__(self): | ||||||
|  |         state = copy(self.__dict__) | ||||||
|  |         # Don't include the cache when pickling. It'll be reconstituted based | ||||||
|  |         # on the settings. | ||||||
|  |         state.pop('_cache', None) | ||||||
|  |         return state | ||||||
|  | 
 | ||||||
|  |     def exists(self, file): | ||||||
|  |         return self.get_state(file) == CacheFileState.EXISTS | ||||||
|  | 
 | ||||||
|  |     def generate(self, file, force=False): | ||||||
|  |         raise NotImplementedError | ||||||
|  | 
 | ||||||
|  |     def generate_now(self, file, force=False): | ||||||
|  |         if force or self.get_state(file) not in (CacheFileState.GENERATING, CacheFileState.EXISTS): | ||||||
|  |             self.set_state(file, CacheFileState.GENERATING) | ||||||
|  |             file._generate() | ||||||
|  |             self.set_state(file, CacheFileState.EXISTS) | ||||||
|  |             file.close() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class Simple(MediaAssetCachedFileBackend): | ||||||
|  |     """ | ||||||
|  |     The most basic file backend. The storage is consulted to see if the file | ||||||
|  |     exists. Files are generated synchronously. | ||||||
|  | 
 | ||||||
|  |     """ | ||||||
|  | 
 | ||||||
|  |     def generate(self, file, force=False): | ||||||
|  |         # FIXME Don't try this for large files like movies or recordings | ||||||
|  |         self.generate_now(file, force=force) | ||||||
|  | 
 | ||||||
|  |     def _exists(self, file): | ||||||
|  |         """ | ||||||
|  |         The variant be either the automatically generated file or a | ||||||
|  |         user uploaded file. Check both and set the state to OVERRIDEN | ||||||
|  |         appropriately. | ||||||
|  |         """ | ||||||
|  |         return bool(getattr(file, '_file', None) | ||||||
|  |                     or file.storage.exists(file.name)) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def _generate_file(backend, file, force=False): | ||||||
|  |     backend.generate_now(file, force=force) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class BaseAsync(Simple): | ||||||
|  |     """ | ||||||
|  |     Base class for cache file backends that generate files asynchronously. | ||||||
|  |     """ | ||||||
|  |     is_async = True | ||||||
|  | 
 | ||||||
|  |     def generate(self, file, force=False): | ||||||
|  |         # Schedule the file for generation, unless we know for sure we don't | ||||||
|  |         # need to. If an already-generated file sneaks through, that's okay; | ||||||
|  |         # ``generate_now`` will catch it. We just want to make sure we don't | ||||||
|  |         # schedule anything we know is unnecessary--but we also don't want to | ||||||
|  |         # force a costly existence check. | ||||||
|  |         state = self.get_state(file, check_if_unknown=False) | ||||||
|  |         if state not in (CacheFileState.GENERATING, CacheFileState.EXISTS, CacheFileState.MANUAL): | ||||||
|  |             self.schedule_generation(file, force=force) | ||||||
|  | 
 | ||||||
|  |     def schedule_generation(self, file, force=False): | ||||||
|  |         # overwrite this to have the file generated in the background, | ||||||
|  |         # e. g. in a worker queue. | ||||||
|  |         raise NotImplementedError | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | try: | ||||||
|  |     from celery import task | ||||||
|  | except ImportError: | ||||||
|  |     pass | ||||||
|  | else: | ||||||
|  |     _celery_task = task(ignore_result=True, serializer='pickle')(_generate_file) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class Celery(BaseAsync): | ||||||
|  |     """ | ||||||
|  |     A backend that uses Celery to generate the images. | ||||||
|  |     """ | ||||||
|  |     def __init__(self, *args, **kwargs): | ||||||
|  |         try: | ||||||
|  |             import celery  # noqa | ||||||
|  |         except ImportError: | ||||||
|  |             raise ImproperlyConfigured('You must install celery to use' | ||||||
|  |                                        ' imagekit.cachefiles.backends.Celery.') | ||||||
|  |         super(Celery, self).__init__(*args, **kwargs) | ||||||
|  | 
 | ||||||
|  |     def schedule_generation(self, file, force=False): | ||||||
|  |         _celery_task.delay(self, file, force=force) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class Auto(BaseAsync): | ||||||
|  |     """ | ||||||
|  |     A backend that decides between immediately generating a variant | ||||||
|  |     or queuing with celery, depending on media type, file size and | ||||||
|  |     encoding. | ||||||
|  |     """ | ||||||
| @ -0,0 +1,46 @@ | |||||||
|  | """ | ||||||
|  | Functions responsible for returning filenames for the given image generator. | ||||||
|  | Users are free to define their own functions; these are just some some sensible | ||||||
|  | choices. | ||||||
|  | 
 | ||||||
|  | """ | ||||||
|  | 
 | ||||||
|  | from django.conf import settings | ||||||
|  | import os | ||||||
|  | from ..utils import suggest_extension | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def spec_slug_as_path(generator): | ||||||
|  |     """ | ||||||
|  |     A namer that, given the following source file name:: | ||||||
|  | 
 | ||||||
|  |         <asset_uuid>/original/bulldog.jpg | ||||||
|  | 
 | ||||||
|  |     will generate a relative name like this:: | ||||||
|  | 
 | ||||||
|  |             <spec_slug>/5ff3233527c5ac3e4b596343b440ff67.jpg | ||||||
|  | 
 | ||||||
|  |     which is meant to be added to both | ||||||
|  | 
 | ||||||
|  |         <asset_uuid>/cache/ | ||||||
|  |         <asset_uuid>/manual/ | ||||||
|  |     . | ||||||
|  | 
 | ||||||
|  |     """ | ||||||
|  |     source_filename = getattr(generator.source, 'name', None) | ||||||
|  |     spec_slug = getattr(generator, 'slug', None) | ||||||
|  | 
 | ||||||
|  |     ext = suggest_extension(source_filename or '', generator.format) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     if source_filename is None or os.path.isabs(source_filename): | ||||||
|  |         # Generally, we put the file right in the cache file directory. | ||||||
|  |         dir = settings.IMAGEKIT_CACHEFILE_DIR | ||||||
|  |     else: | ||||||
|  |         # For source files with relative names (like Django media files), | ||||||
|  |         # use the source's name to create the new filename. | ||||||
|  |         dir = os.path.join(settings.IMAGEKIT_CACHEFILE_DIR, | ||||||
|  |                            os.path.splitext(source_filename)[0]) | ||||||
|  | 
 | ||||||
|  |     return os.path.normpath(os.path.join(dir, | ||||||
|  |                                          '%s%s' % (generator.get_hash(), ext))) | ||||||
| @ -0,0 +1,22 @@ | |||||||
|  | # -*- coding: utf-8 -*- | ||||||
|  | from __future__ import unicode_literals | ||||||
|  | # Erik Stein <code@classlibrary.net>, 2016 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class Asynchronous(object): | ||||||
|  |     """ | ||||||
|  |     A strategy that queue's the generation right when the file is saved. | ||||||
|  | 
 | ||||||
|  |     """ | ||||||
|  | 
 | ||||||
|  |     def on_source_saved(self, file): | ||||||
|  |         file.generate() | ||||||
|  | 
 | ||||||
|  |     def should_verify_existence(self, file): | ||||||
|  |         return True | ||||||
|  | 
 | ||||||
|  |     # def on_existence_required(self, file): | ||||||
|  |     #     file.generate() | ||||||
|  | 
 | ||||||
|  |     # def on_content_required(self, file): | ||||||
|  |     #     file.generate() | ||||||
| @ -0,0 +1,4 @@ | |||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class ConversionNotApplicable(Exception): | ||||||
|  |     pass | ||||||
| @ -0,0 +1,77 @@ | |||||||
|  | # -*- coding: utf-8 -*- | ||||||
|  | from __future__ import unicode_literals | ||||||
|  | # Erik Stein <code@classlibrary.net>, 2016 | ||||||
|  | 
 | ||||||
|  | import os | ||||||
|  | import warnings | ||||||
|  | 
 | ||||||
|  | from django.core.files.storage import FileSystemStorage | ||||||
|  | from django.conf import settings | ||||||
|  | from django.utils._os import safe_join | ||||||
|  | from django.utils.deconstruct import deconstructible | ||||||
|  | from django.utils.encoding import filepath_to_uri | ||||||
|  | from django.utils.six.moves.urllib.parse import urljoin | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | __all__ = ('MediaAssetStorage',) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ORIGINAL_FILE_PREFIX = 'original' | ||||||
|  | CACHED_VARIANT_PREFIX = 'cache' | ||||||
|  | MANUAL_VARIANT_PREFIX = 'manual' | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | """ | ||||||
|  | Storage setup: | ||||||
|  | 
 | ||||||
|  |     /<asset_uuid>/original/filename.ext | ||||||
|  |                  /cache/<format_slug>/<hash>.<variant_ext> | ||||||
|  |                  /manual/<format_slug>/<hash>.<variant_ext> | ||||||
|  | 
 | ||||||
|  | TODO Add snapshot (versioning) to path after asset_uuid. | ||||||
|  | 
 | ||||||
|  | """ | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class BaseVariantSubStorage(FileSystemStorage): | ||||||
|  |     def delete(self, parent_storage, name): | ||||||
|  |         """ | ||||||
|  |         Deletes the whole storage path | ||||||
|  |         """ | ||||||
|  |         raise NotImplemented | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @deconstructible | ||||||
|  | class MediaAssetStorage(FileSystemStorage): | ||||||
|  |     """ | ||||||
|  |     Standard media assets filesystem storage | ||||||
|  |     """ | ||||||
|  | 
 | ||||||
|  |     def __init__(self, location=None, base_url=None, file_permissions_mode=None, | ||||||
|  |             directory_permissions_mode=None): | ||||||
|  |         if location is None: | ||||||
|  |             location = getattr(settings, 'MEDIA_ASSETS_ROOT', settings.MEDIA_ROOT) | ||||||
|  |         if base_url is None: | ||||||
|  |             base_url = getattr(settings, 'MEDIA_ASSETS_URL', settings.MEDIA_URL) | ||||||
|  |         if not base_url.endswith('/'): | ||||||
|  |             base_url += '/' | ||||||
|  |         super(MediaAssetStorage, self).__init__(self) | ||||||
|  | 
 | ||||||
|  |     def delete(self, name): | ||||||
|  |         super(MediaAssetStorage, self).delete(name) | ||||||
|  |         # FIXME Delete all cached files, too | ||||||
|  |         warnings.warn("Cached files for asset \"%s\" are not deleted." % name) | ||||||
|  | 
 | ||||||
|  |     def path(self, name): | ||||||
|  |         """ | ||||||
|  |         `name` must already contain the whole asset filesystem path. | ||||||
|  |         """ | ||||||
|  |         return safe_join(self.location, ORIGINAL_FILE_PREFIX, name) | ||||||
|  | 
 | ||||||
|  |     def size(self, name): | ||||||
|  |         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.") | ||||||
|  |         return urljoin(self.base_url, ORIGINAL_FILE_PREFIX, filepath_to_uri(name)) | ||||||
| @ -0,0 +1,91 @@ | |||||||
|  | # -*- coding: utf-8 -*- | ||||||
|  | from __future__ import unicode_literals | ||||||
|  | # Erik Stein <code@classlibrary.net>, 2016 | ||||||
|  | 
 | ||||||
|  | from django.db import models | ||||||
|  | from django.utils.translation import ugexttext_lazy as _ | ||||||
|  | 
 | ||||||
|  | from imagekit.models import ImageSpecField | ||||||
|  | from imagekit.processors import Adjust, Thumbnail, ResizeToFit | ||||||
|  | 
 | ||||||
|  | from ..exceptions import ConversionNotApplicable | ||||||
|  | from ..processors import Noop, Placeholder | ||||||
|  | 
 | ||||||
|  | from .fields import MediaAssetFileField | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | MEDIA_TYPE_CHOICES = ( | ||||||
|  |         (UNKNOWN,  _("unknown")), | ||||||
|  |         (IMAGE,    _("image")), | ||||||
|  |         (MOVIE,    _("movie")), | ||||||
|  |         (AUDIO,    _("audio")), | ||||||
|  |         (PDF,      _("pdf")), | ||||||
|  |         (SVG,      _("svg")), | ||||||
|  |         (VECTOR,   _("eps")), | ||||||
|  |         (DOCUMENT, _("document")), | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class ImageMediaHandler(object): | ||||||
|  |     # thumbnail = ImageSpecField(ThumbnailSpec) | ||||||
|  | 
 | ||||||
|  |     thumbnail = ImageSpecField(source='file', | ||||||
|  |             processors=[Adjust(contrast=1.2, sharpness=1.1), | ||||||
|  |                         Thumbnail(100, 50)], | ||||||
|  |             format='JPEG', options={'quality': 90}) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # class PDFFile(object) | ||||||
|  | #     def get_page(self, num): | ||||||
|  | #     def get_page_count(self): | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class PDFMediaHandler(object): | ||||||
|  |     # thumbnail = ImageSpec | ||||||
|  |     # small_downloadable = PDFSpecField | ||||||
|  | 
 | ||||||
|  |     def page_count(self): | ||||||
|  |         # self.original_file = PDFFile() | ||||||
|  |         return count(self.original_file.pages) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class MovieMediaHandler(object): | ||||||
|  |     # thumbnail = ImageSpec | ||||||
|  |     # desktop_webm = MovieSpecField | ||||||
|  |     # desktop_mp4 = MovieSpecField | ||||||
|  |     pass | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class MediaAsset(models.Model): | ||||||
|  |     """ | ||||||
|  |     TODO Django should have a internal=True parameter on FileFields, preventing URL based access to the files | ||||||
|  | 
 | ||||||
|  |     Depending on the type the Asset might have different | ||||||
|  |     """ | ||||||
|  |     # TODO Added uuid field | ||||||
|  |     original_file = MediaAssetFileField(_("original file"), metadata_field='metadata') | ||||||
|  |     original_mimetype = models.CharField(_("mimetype"), blank=True, max_length=50) | ||||||
|  |     metadata = models.JSONField() | ||||||
|  |     media_type = models.ChoiceField(_("media type"), blank=True, choices=MEDIA_TYPE_CHOICES, default=MEDIA_TYPE_CHOICES[0][0]) | ||||||
|  |     description = models.TextField(_("internal description"), null=True, blank=True) | ||||||
|  |     slug = SlugField(help_text=_("should contain only the most specific part of the description, e.g.\"poster\"; the whole filename will be automatically build from the object the asset is attached and ")) | ||||||
|  |     # copyright = models.TextField(_("copyright"), null=True, blank=True) | ||||||
|  |     # author = models.TextField(_("author"), null=True, blank=True) | ||||||
|  | 
 | ||||||
|  |     def save(self, *args, **kwargs): | ||||||
|  |         # TODO Fill media_type field | ||||||
|  |         # TODO Fill mimetype field | ||||||
|  |         super(MediaAsset, self).save(*args, **kwargs) | ||||||
|  | 
 | ||||||
|  |     @property | ||||||
|  |     def media_handler(self): | ||||||
|  |         """ | ||||||
|  |         Returns a media_handler instance depending on the type | ||||||
|  |         of the original file. | ||||||
|  | 
 | ||||||
|  |         >>> m = MediaAsset.objects.first() | ||||||
|  |         >>> m.original_file | ||||||
|  |         'media/0/reference.png' | ||||||
|  |         >>> m.media_handler.thumbnail | ||||||
|  |         """ | ||||||
|  | 
 | ||||||
| @ -0,0 +1,47 @@ | |||||||
|  | # -*- coding: utf-8 -*- | ||||||
|  | from __future__ import unicode_literals | ||||||
|  | # Erik Stein <code@classlibrary.net>, 2016 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | from imagekit.models.fields.utils import ImageSpecFileDescriptor | ||||||
|  | from imagekit.models.fields import ImageSpecField | ||||||
|  | from ..files.storage import MediaAssetFileSystemStorage | ||||||
|  | from ...cachefiles import MediaAssetCacheFile | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class MediaAssetCacheFileDescriptor(ImageSpecFileDescriptor): | ||||||
|  |     """ | ||||||
|  |     - Manages manually overriden cached files | ||||||
|  |     - knows to handle ConversionNotApplicable exceptions and | ||||||
|  |       provides svg-icons for this case | ||||||
|  | 
 | ||||||
|  |     """ | ||||||
|  | 
 | ||||||
|  |     def __init__(self, field, attname, source_field_name): | ||||||
|  |         self.attname = attname | ||||||
|  |         self.field = field | ||||||
|  |         self.source_field_name = source_field_name | ||||||
|  | 
 | ||||||
|  |     def __get__(self, instance, owner): | ||||||
|  |         if instance is None: | ||||||
|  |             return self.field | ||||||
|  |         else: | ||||||
|  |             source = getattr(instance, self.source_field_name) | ||||||
|  |             spec = self.field.get_spec(source=source) | ||||||
|  |             file = MediaAssetCacheFile(spec) | ||||||
|  |             instance.__dict__[self.attname] = file | ||||||
|  |             return file | ||||||
|  | 
 | ||||||
|  |     def __set__(self, instance, value): | ||||||
|  |         instance.__dict__[self.attname] = value | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class MediaAssetFileField(ImageSpecField): | ||||||
|  |     """ | ||||||
|  |     - default storage | ||||||
|  |     """ | ||||||
|  | 
 | ||||||
|  |     def __init__(self, storage=MediaAssetFileSystemStorage): | ||||||
|  |         pass | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| @ -0,0 +1,15 @@ | |||||||
|  | # -*- coding: utf-8 -*- | ||||||
|  | from __future__ import unicode_literals | ||||||
|  | # Erik Stein <code@classlibrary.net>, 2016 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | UNKNOWN = 'unknown' | ||||||
|  | IMAGE = 'image' | ||||||
|  | MOVIE = 'movie' | ||||||
|  | AUDIO = 'audio' | ||||||
|  | PDF = 'pdf' | ||||||
|  | DOCUMENT = 'document' | ||||||
|  | SVG = 'svg' | ||||||
|  | VECTOR = 'vector' | ||||||
|  | 
 | ||||||
|  | # TODO Implemen __all__ dynamically, made from MediaType classes carrying more information about the types | ||||||
| @ -0,0 +1,5 @@ | |||||||
|  | __title__ = 'django-assetkit' | ||||||
|  | __author__ = 'Erik Stein' | ||||||
|  | __version__ = '0.1' | ||||||
|  | __license__ = 'BSD' | ||||||
|  | __all__ = ['__title__', '__author__', '__version__', '__license__'] | ||||||
| @ -0,0 +1,20 @@ | |||||||
|  | # -*- coding: utf-8 -*- | ||||||
|  | from __future__ import unicode_literals | ||||||
|  | # Erik Stein <code@classlibrary.net>, 2016 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class Noop(object): | ||||||
|  |     def process(self, source): | ||||||
|  |         result = source | ||||||
|  |         return result | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class Placeholder(object): | ||||||
|  |     def __init__(self, label_text="placeholder"): | ||||||
|  |         # The label text is intentionally not translated | ||||||
|  |         self.label_text = label_text | ||||||
|  | 
 | ||||||
|  |     def process(self, source): | ||||||
|  |         # Ignore source | ||||||
|  |         raise NotImplemented | ||||||
|  | 
 | ||||||
| @ -0,0 +1,38 @@ | |||||||
|  | # -*- coding: utf-8 -*- | ||||||
|  | from __future__ import unicode_literals | ||||||
|  | # Erik Stein <code@classlibrary.net>, 2016 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class UniversalThumbnail(ImageSpec): | ||||||
|  |     """ | ||||||
|  |     Produces thumbnail images from any type of media, using | ||||||
|  |     per-type initial conversion to an image. | ||||||
|  |     """ | ||||||
|  |     slug = 'thumbnail' | ||||||
|  |     not_applicable_types = (UNKNOWN, AUDIO, DOCUMENT) | ||||||
|  |     base_conversion_map = { | ||||||
|  |         IMAGE: [], | ||||||
|  |         MOVIE: [], | ||||||
|  |     } | ||||||
|  |     _processors = [ResizeToFill(100, 50)] | ||||||
|  |     format = 'JPEG' | ||||||
|  |     options = {'quality': 60} | ||||||
|  | 
 | ||||||
|  |     @property | ||||||
|  |     def processors(self): | ||||||
|  |         instance, field_name = get_field_info(self.source) | ||||||
|  |         if instance.media_type in self.not_applicable_types: | ||||||
|  |             raise ConversionNotApplicable | ||||||
|  | 
 | ||||||
|  |         if instance.media_type == | ||||||
|  | 
 | ||||||
|  |         processors = self.base_conversion_map[instance.media_type] | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     """ | ||||||
|  |     Dynamic spec, depending on the source's media type | ||||||
|  |     e.g. first convert a movie to an image, depending on a time code | ||||||
|  |     for the movie poster | ||||||
|  | 
 | ||||||
|  |     see http://django-imagekit.readthedocs.org/en/develop/advanced_usage.html#specs-that-change | ||||||
|  |     """ | ||||||
| @ -0,0 +1,95 @@ | |||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | Hauptsächlich ist es ein mediakit; | ||||||
|  | d.h. imagekit erweitert auf alle arten von dateien (letztendlich inkl. document), | ||||||
|  | das eine grundsätzliche struktur bietet | ||||||
|  | 
 | ||||||
|  | 1. für automatische konvertierung in jeweils andere formate | ||||||
|  |    	denkbar eben auch bild -> pdf, film -> bild etc., pdf -> epub | ||||||
|  |    conversion-quere mit celery | ||||||
|  |    klarer state "noch nicht konvertiert" | ||||||
|  | 
 | ||||||
|  | 2. klare dateisystemverwaltung der originaldateien + automatische / manuelle konvertierungen | ||||||
|  | 	manuelle konvertierung heißt: die automatische konvertierung kann jederzeit überschrieben werden | ||||||
|  | 
 | ||||||
|  | 3. zugriffskontrolle: | ||||||
|  | 	grundsätzlich ist keine datei öffentlich zugänglich, | ||||||
|  | 	specs können mit zugriffskontrolle (anonym etc.) zugänglich gemacht werden, | ||||||
|  | 	dazu wird x-sendfile u.ä. verwendet | ||||||
|  | 
 | ||||||
|  | 4. benennung: | ||||||
|  | 	öffentlich zugänglich sollten alle dateien über generierte | ||||||
|  | 	namen sein, also slug + konvertierungstyp, so dass heruntergeladene dateien sinnvoll benannt sind | ||||||
|  | 
 | ||||||
|  | 5. umgang mit mehrseitigen dokumenten, also | ||||||
|  |    mehrseitige PDF-dokument in bilder zu verwandeln | ||||||
|  |    	über spezial-descriptor, der page_count und page[index] zurückgeben kann? | ||||||
|  |    	dann auch jeweils alle seiten im cache ablegen? | ||||||
|  | 
 | ||||||
|  | 6. jeder asset bekommt eine eindeutige globale uuid, für interoperabilität mit z.b. iiif | ||||||
|  | 
 | ||||||
|  | 7. chunk upload | ||||||
|  | 
 | ||||||
|  | 8. batch upload | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | imagekit | ||||||
|  | 	processors | ||||||
|  | 		pilkit-processors | ||||||
|  | 		neue processors: | ||||||
|  | 			movie | ||||||
|  | 			pdf | ||||||
|  | 		assure conversion processors: | ||||||
|  | 			assure_image | ||||||
|  | 				schaut, wie die ursprungsdatei (z.b. film, pdf) in ein bild konvertiert werden muss | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | brauche async-processors | ||||||
|  | 	die also zwangsläufig die konvertierung an einen anderen prozess auslagern (ffmpeg), | ||||||
|  | 	und darüber aber bescheid wissen; | ||||||
|  | 	vermutlich ist es richtig, ein lock-file zu schreiben mit z.b. eine pid; oder evtl. einem celery-task-id | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | cachefiles | ||||||
|  | 	Cachefile ist gleich konvertierte Datei; | ||||||
|  | 		Cachefile sollte auch immer das manual-pedant verwalten/checken; | ||||||
|  | 		s.a. CacheFileState: | ||||||
|  | 			MANUAL_OVERRIDE | ||||||
|  | 			cachefile backend schreiben, dass unsere dateisystemlayout implementiert | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | zugriffskontrolle | ||||||
|  | 	möglichst über extra modul realisieren; | ||||||
|  | 	permissions als zusätzl. config, nicht integriert in specs | ||||||
|  | 	zugriffspfad über urls.py/views | ||||||
|  | 	d.h. mediakit selbst liefert alles direkt aus dem filesystem aus | ||||||
|  | 	außer: originaldateien; diese sollten immer abseits gespeichert werden, damit alles öffentlich mindestens durch specs kontrollierbar ist | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | brauche eigene ImageKitConf | ||||||
|  | 
 | ||||||
|  | SourceGroup? | ||||||
|  | 
 | ||||||
|  | MovieSpec | ||||||
|  | 
 | ||||||
|  | PDFSpec | ||||||
|  | 
 | ||||||
|  | TextDocumentSpec | ||||||
|  | 	better: MarkupSpec? | ||||||
|  | 			TextFormatSpec? | ||||||
|  | 		html | ||||||
|  | 		doc | ||||||
|  | 		docx | ||||||
|  | 		odt | ||||||
|  | 		epub | ||||||
|  | 		markdown | ||||||
|  | 		rst | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | Admin/Upload per Rest-Interface, | ||||||
|  | aufwendigem Widget | ||||||
|  | 	d.h. in Django-Struktur ist nur das original_file-Feld | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| @ -0,0 +1,3 @@ | |||||||
|  | from django.contrib import admin | ||||||
|  | 
 | ||||||
|  | # Register your models here. | ||||||
| @ -0,0 +1,7 @@ | |||||||
|  | from __future__ import unicode_literals | ||||||
|  | 
 | ||||||
|  | from django.apps import AppConfig | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class MedialibraryConfig(AppConfig): | ||||||
|  |     name = 'medialibrary' | ||||||
| @ -0,0 +1,49 @@ | |||||||
|  | # -*- 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 | ||||||
|  | 
 | ||||||
|  | # TODO Define the central storage somewhere more central | ||||||
|  | originals_storage = ProtectedMediaAssetStorage() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # TODO Move this to special FileField class | ||||||
|  | def get_upload_path(instance, filename): | ||||||
|  |     """ | ||||||
|  |     Returns /<uuid_hex>/original/<slugified_filename.ext> | ||||||
|  |     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) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @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 | ||||||
|  |                                      ) | ||||||
|  |     name = models.CharField(_('name'), max_length=50) | ||||||
|  | 
 | ||||||
|  |     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) | ||||||
|  | 
 | ||||||
| @ -0,0 +1,3 @@ | |||||||
|  | from django.test import TestCase | ||||||
|  | 
 | ||||||
|  | # Create your tests here. | ||||||
| @ -0,0 +1,3 @@ | |||||||
|  | from django.shortcuts import render | ||||||
|  | 
 | ||||||
|  | # Create your views here. | ||||||
| @ -0,0 +1,145 @@ | |||||||
|  | # -*- coding: utf-8 -*- | ||||||
|  | from __future__ import unicode_literals | ||||||
|  | # Erik Stein <code@classlibrary.net>, 2015 | ||||||
|  | 
 | ||||||
|  | import os | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | PROJECT_DIR = os.path.dirname(os.path.abspath(__file__)) | ||||||
|  | BASE_DIR = os.path.dirname(PROJECT_DIR) | ||||||
|  | ENV_DIR = os.path.dirname(BASE_DIR) | ||||||
|  | 
 | ||||||
|  | # Quick-start development settings - unsuitable for production | ||||||
|  | # See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/ | ||||||
|  | 
 | ||||||
|  | # SECURITY WARNING: keep the secret key used in production secret! | ||||||
|  | SECRET_KEY = 'a+!qvbt4np4m$&@5s6=coc*yt4(mnrludzo(f=paaf&)-6zeuw' | ||||||
|  | 
 | ||||||
|  | # SECURITY WARNING: don't run with debug turned on in production! | ||||||
|  | DEBUG = True | ||||||
|  | 
 | ||||||
|  | ALLOWED_HOSTS = [] | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # Application definition | ||||||
|  | 
 | ||||||
|  | INSTALLED_APPS = [ | ||||||
|  |     'django.contrib.admin', | ||||||
|  |     'django.contrib.auth', | ||||||
|  |     'django.contrib.contenttypes', | ||||||
|  |     'django.contrib.sessions', | ||||||
|  |     'django.contrib.messages', | ||||||
|  |     'django.contrib.staticfiles', | ||||||
|  | 
 | ||||||
|  |     'imagekit', | ||||||
|  |     'main.medialibrary', | ||||||
|  |     'main', | ||||||
|  | ] | ||||||
|  | 
 | ||||||
|  | MIDDLEWARE_CLASSES = [ | ||||||
|  |     'django.middleware.security.SecurityMiddleware', | ||||||
|  |     'django.contrib.sessions.middleware.SessionMiddleware', | ||||||
|  |     'django.middleware.common.CommonMiddleware', | ||||||
|  |     'django.middleware.csrf.CsrfViewMiddleware', | ||||||
|  |     'django.contrib.auth.middleware.AuthenticationMiddleware', | ||||||
|  |     'django.contrib.auth.middleware.SessionAuthenticationMiddleware', | ||||||
|  |     'django.contrib.messages.middleware.MessageMiddleware', | ||||||
|  |     'django.middleware.clickjacking.XFrameOptionsMiddleware', | ||||||
|  | ] | ||||||
|  | 
 | ||||||
|  | ROOT_URLCONF = 'main.urls' | ||||||
|  | 
 | ||||||
|  | TEMPLATES = [ | ||||||
|  |     { | ||||||
|  |         'BACKEND': 'django.template.backends.django.DjangoTemplates', | ||||||
|  |         'DIRS': [ | ||||||
|  |             os.path.join(PROJECT_DIR, 'templates'), | ||||||
|  |         ], | ||||||
|  |         'APP_DIRS': True, | ||||||
|  |         'OPTIONS': { | ||||||
|  |             'context_processors': [ | ||||||
|  |                 'django.template.context_processors.debug', | ||||||
|  |                 'django.template.context_processors.request', | ||||||
|  |                 'django.contrib.auth.context_processors.auth', | ||||||
|  |                 'django.contrib.messages.context_processors.messages', | ||||||
|  |             ], | ||||||
|  |         }, | ||||||
|  |     }, | ||||||
|  | ] | ||||||
|  | 
 | ||||||
|  | WSGI_APPLICATION = 'main.wsgi.application' | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # Database | ||||||
|  | # https://docs.djangoproject.com/en/1.9/ref/settings/#databases | ||||||
|  | 
 | ||||||
|  | DATABASES = { | ||||||
|  |     'default': { | ||||||
|  |         'ENGINE': 'django.db.backends.sqlite3', | ||||||
|  |         'NAME': os.path.join(ENV_DIR, 'var', 'db.sqlite3'), | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # Password validation | ||||||
|  | # https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators | ||||||
|  | 
 | ||||||
|  | AUTH_PASSWORD_VALIDATORS = [ | ||||||
|  |     { | ||||||
|  |         'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |         'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |         'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |         'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', | ||||||
|  |     }, | ||||||
|  | ] | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # Internationalization | ||||||
|  | # https://docs.djangoproject.com/en/1.9/topics/i18n/ | ||||||
|  | 
 | ||||||
|  | LANGUAGE_CODE = 'en-us' | ||||||
|  | 
 | ||||||
|  | TIME_ZONE = 'UTC' | ||||||
|  | 
 | ||||||
|  | USE_I18N = True | ||||||
|  | 
 | ||||||
|  | USE_L10N = True | ||||||
|  | 
 | ||||||
|  | USE_TZ = True | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # Static files (CSS, JavaScript, Images) | ||||||
|  | # https://docs.djangoproject.com/en/1.9/howto/static-files/ | ||||||
|  | 
 | ||||||
|  | # MEDIA_ASSETS_URL = '/assets/' | ||||||
|  | # MEDIA_ASSETS_ROOT = os.path.join(BASE_DIR, 'media') | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage' | ||||||
|  | 
 | ||||||
|  | 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') | ||||||
|  | PROTECTED_MEDIA_ASSETS_ROOT = os.path.join(ENV_DIR, 'var', 'protected') | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # IMAGEKIT_DEFAULT_CACHEFILE_BACKEND = 'assetkit.cachefiles.backend.MediaAssetCacheBackend' | ||||||
|  | # IMAGEKIT_DEFAULT_FILE_STORAGE = 'private_media.storages.PrivateMediaStorage' | ||||||
|  | # ORIGINALFILE_ROOT = os.path.join(ENV_DIR, 'var', 'protected') | ||||||
|  | # IMAGEKIT_CACHEFILE_DIR = ''  # Directly in media root | ||||||
|  | 
 | ||||||
|  | # PRIVATE_MEDIA_URL = '/media-archive/' | ||||||
|  | # PRIVATE_MEDIA_INTERNAL_URL = '/media-archive-x-accel-redirect/' | ||||||
|  | # PRIVATE_MEDIA_ROOT = os.path.join(ROOT_DIR, 'var', 'media-private') | ||||||
|  | 
 | ||||||
|  | # PRIVATE_MEDIA_SERVER = 'private_media.servers.NginxXAccelRedirectServer' | ||||||
|  | # PRIVATE_MEDIA_PERMISSIONS = 'm1web.multimedia.permissions.AssetPermissions' | ||||||
|  | 
 | ||||||
| @ -0,0 +1,54 @@ | |||||||
|  | # -*- coding: utf-8 -*- | ||||||
|  | from __future__ import unicode_literals | ||||||
|  | 
 | ||||||
|  | import os | ||||||
|  | import warnings | ||||||
|  | from django.conf import settings | ||||||
|  | from django.core.files.storage import FileSystemStorage | ||||||
|  | from django.utils._os import safe_join | ||||||
|  | from django.utils.six.moves.urllib.parse import urljoin | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ORIGINAL_FILE_PREFIX = 'original' | ||||||
|  | CACHED_VARIANT_PREFIX = 'cache' | ||||||
|  | MANUAL_VARIANT_PREFIX = 'manual' | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # TODO ? @deconstructible | ||||||
|  | class ProtectedMediaAssetStorage(FileSystemStorage): | ||||||
|  |     """ | ||||||
|  |     Alllows uploads to /original/-subdirectories, but never provides URLs | ||||||
|  |     to those files, instead raising an error. | ||||||
|  |     """ | ||||||
|  | 
 | ||||||
|  |     ORIGINAL_FILE_PREFIX = ORIGINAL_FILE_PREFIX | ||||||
|  | 
 | ||||||
|  |     def __init__(self, location=None, file_permissions_mode=None, | ||||||
|  |             directory_permissions_mode=None): | ||||||
|  |         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) | ||||||
|  | 
 | ||||||
|  |     def delete(self, name): | ||||||
|  |         super(ProtectedMediaAssetStorage, self).delete(name) | ||||||
|  |         # FIXME Delete all cached files, too | ||||||
|  |         warnings.warn("Cached files for asset \"%s\" are not deleted." % name) | ||||||
|  | 
 | ||||||
|  |     def path(self, name): | ||||||
|  |         """ | ||||||
|  |         `name` must already contain the whole asset filesystem path. | ||||||
|  |         """ | ||||||
|  |         return safe_join(self.location, ORIGINAL_FILE_PREFIX, name) | ||||||
|  | 
 | ||||||
|  |     def size(self, name): | ||||||
|  |         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.") | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # TODO ? @deconstructible | ||||||
|  | class | ||||||
|  |         return urljoin(self.base_url, ORIGINAL_FILE_PREFIX, filepath_to_uri(name)) | ||||||
| @ -0,0 +1,19 @@ | |||||||
|  | # -*- coding: utf-8 -*- | ||||||
|  | from __future__ import unicode_literals | ||||||
|  | # Erik Stein <code@classlibrary.net>, 2015 | ||||||
|  | 
 | ||||||
|  | from django.conf import settings | ||||||
|  | from django.conf.urls import include, url | ||||||
|  | from django.contrib import admin | ||||||
|  | from django.conf.urls.static import static | ||||||
|  | from django.utils.translation import ugettext_lazy as _ | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | urlpatterns = [ | ||||||
|  |     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) | ||||||
|  | 
 | ||||||
| @ -0,0 +1,16 @@ | |||||||
|  | """ | ||||||
|  | WSGI config for example_site project. | ||||||
|  | 
 | ||||||
|  | It exposes the WSGI callable as a module-level variable named ``application``. | ||||||
|  | 
 | ||||||
|  | For more information on this file, see | ||||||
|  | https://docs.djangoproject.com/en/1.9/howto/deployment/wsgi/ | ||||||
|  | """ | ||||||
|  | 
 | ||||||
|  | import os | ||||||
|  | 
 | ||||||
|  | from django.core.wsgi import get_wsgi_application | ||||||
|  | 
 | ||||||
|  | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "main.settings") | ||||||
|  | 
 | ||||||
|  | application = get_wsgi_application() | ||||||
| @ -0,0 +1,10 @@ | |||||||
|  | #!/usr/bin/env python | ||||||
|  | import os | ||||||
|  | import sys | ||||||
|  | 
 | ||||||
|  | if __name__ == "__main__": | ||||||
|  |     os.environ.setdefault("DJANGO_SETTINGS_MODULE", "main.settings") | ||||||
|  | 
 | ||||||
|  |     from django.core.management import execute_from_command_line | ||||||
|  | 
 | ||||||
|  |     execute_from_command_line(sys.argv) | ||||||
| @ -0,0 +1,71 @@ | |||||||
|  | #/usr/bin/env python | ||||||
|  | import codecs | ||||||
|  | import os | ||||||
|  | from setuptools import setup, find_packages | ||||||
|  | import sys | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # Workaround for multiprocessing/nose issue. See http://bugs.python.org/msg170215 | ||||||
|  | try: | ||||||
|  |     import multiprocessing | ||||||
|  | except ImportError: | ||||||
|  |     pass | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | if 'publish' in sys.argv: | ||||||
|  |     os.system('python setup.py sdist upload') | ||||||
|  |     sys.exit() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | read = lambda filepath: codecs.open(filepath, 'r', 'utf-8').read() | ||||||
|  | 
 | ||||||
|  | def exec_file(filepath, globalz=None, localz=None): | ||||||
|  |         exec(read(filepath), globalz, localz) | ||||||
|  | 
 | ||||||
|  | # Load package meta from the pkgmeta module without loading assetkit. | ||||||
|  | pkgmeta = {} | ||||||
|  | exec_file(os.path.join(os.path.dirname(__file__), | ||||||
|  |          'assetkit', 'pkgmeta.py'), pkgmeta) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | setup( | ||||||
|  |     name='django-assetkit', | ||||||
|  |     version=pkgmeta['__version__'], | ||||||
|  |     description='Automated media asset processing for Django models.', | ||||||
|  |     long_description=read(os.path.join(os.path.dirname(__file__), 'README.rst')), | ||||||
|  |     author='Erik Stein', | ||||||
|  |     author_email='erik@classlibrary.net', | ||||||
|  |     license='BSD', | ||||||
|  |     url='http://github.com/sha-red/django-assetkit/', | ||||||
|  |     packages=find_packages(exclude=['*.tests', '*.tests.*', 'tests.*', 'tests']), | ||||||
|  |     zip_safe=False, | ||||||
|  |     include_package_data=True, | ||||||
|  |     tests_require=[ | ||||||
|  |         'Pillow', | ||||||
|  |     ], | ||||||
|  |     test_suite='testrunner.run_tests', | ||||||
|  |     install_requires=[ | ||||||
|  |         'django-appconf>=0.5', | ||||||
|  |         'django-imagekit>=3.0', | ||||||
|  |         'pilkit>=0.2.0', | ||||||
|  |         'six', | ||||||
|  |     ], | ||||||
|  |     extras_require={ | ||||||
|  |         'async': ['django-celery>=3.0'], | ||||||
|  |     }, | ||||||
|  |     classifiers=[ | ||||||
|  |         'Development Status :: 5 - Production/Stable', | ||||||
|  |         'Environment :: Web Environment', | ||||||
|  |         'Framework :: Django', | ||||||
|  |         'Intended Audience :: Developers', | ||||||
|  |         'License :: OSI Approved :: BSD License', | ||||||
|  |         'Operating System :: OS Independent', | ||||||
|  |         'Programming Language :: Python :: 2', | ||||||
|  |         'Programming Language :: Python :: 2.7', | ||||||
|  |         'Programming Language :: Python :: 3', | ||||||
|  |         'Programming Language :: Python :: 3.3', | ||||||
|  |         'Programming Language :: Python :: 3.4', | ||||||
|  |         'Programming Language :: Python :: 3.5', | ||||||
|  |         'Topic :: Utilities' | ||||||
|  |     ], | ||||||
|  | ) | ||||||
| @ -0,0 +1,19 @@ | |||||||
|  | # A wrapper for Django's test runner. | ||||||
|  | # See http://ericholscher.com/blog/2009/jun/29/enable-setuppy-test-your-django-apps/ | ||||||
|  | # and http://gremu.net/blog/2010/enable-setuppy-test-your-django-apps/ | ||||||
|  | import os | ||||||
|  | import sys | ||||||
|  | 
 | ||||||
|  | os.environ['DJANGO_SETTINGS_MODULE'] = 'tests.settings' | ||||||
|  | test_dir = os.path.dirname(__file__) | ||||||
|  | sys.path.insert(0, test_dir) | ||||||
|  | 
 | ||||||
|  | from django.test.utils import get_runner | ||||||
|  | from django.conf import settings | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def run_tests(): | ||||||
|  |     cls = get_runner(settings) | ||||||
|  |     runner = cls() | ||||||
|  |     failures = runner.run_tests(['tests']) | ||||||
|  |     sys.exit(failures) | ||||||
					Loading…
					
					
				
		Reference in new issue