Thumbnails for images and videos #87
@ -105,8 +105,7 @@ ABUSE_TYPE = Choices(
|
|||||||
# thumbnails of existing images must exist in MEDIA_ROOT before
|
# thumbnails of existing images must exist in MEDIA_ROOT before
|
||||||
# the code expecting the new dimensions can be deployed!
|
# the code expecting the new dimensions can be deployed!
|
||||||
THUMBNAIL_SIZE_DEFAULT = (1920, 1080)
|
THUMBNAIL_SIZE_DEFAULT = (1920, 1080)
|
||||||
THUMBNAIL_SIZE_M = (1280, 720)
|
|
||||||
THUMBNAIL_SIZE_S = (640, 360)
|
THUMBNAIL_SIZE_S = (640, 360)
|
||||||
THUMBNAIL_SIZES = [THUMBNAIL_SIZE_DEFAULT, THUMBNAIL_SIZE_M, THUMBNAIL_SIZE_S]
|
THUMBNAIL_SIZES = [THUMBNAIL_SIZE_DEFAULT, THUMBNAIL_SIZE_S]
|
||||||
THUMBNAIL_FORMAT = 'PNG'
|
THUMBNAIL_FORMAT = 'PNG'
|
||||||
THUMBNAIL_QUALITY = 83
|
THUMBNAIL_QUALITY = 83
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
|
from pathlib import Path
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from django.contrib.admin.models import LogEntry, DELETION
|
from django.contrib.admin.models import LogEntry, DELETION
|
||||||
from django.test import TestCase # , TransactionTestCase
|
from django.test import TestCase, override_settings
|
||||||
|
|
||||||
from common.tests.factories.extensions import create_approved_version, create_version
|
from common.tests.factories.extensions import create_approved_version, create_version
|
||||||
from common.tests.factories.files import FileFactory
|
from common.tests.factories.files import FileFactory
|
||||||
@ -10,7 +11,10 @@ import extensions.models
|
|||||||
import files.models
|
import files.models
|
||||||
import reviewers.models
|
import reviewers.models
|
||||||
|
|
||||||
|
TEST_MEDIA_DIR = Path(__file__).resolve().parent / 'media'
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(MEDIA_ROOT=TEST_MEDIA_DIR)
|
||||||
class DeleteTest(TestCase):
|
class DeleteTest(TestCase):
|
||||||
fixtures = ['dev', 'licenses']
|
fixtures = ['dev', 'licenses']
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ def scan_selected_files(self, request, queryset):
|
|||||||
|
|
||||||
def make_thumbnails(self, request, queryset):
|
def make_thumbnails(self, request, queryset):
|
||||||
"""Make thumbnails for selected files."""
|
"""Make thumbnails for selected files."""
|
||||||
for instance in queryset:
|
for instance in queryset.filter(type__in=(File.TYPES.IMAGE, File.TYPE.VIDEO)):
|
||||||
files.tasks.make_thumbnails.task_function(file_id=instance.pk)
|
files.tasks.make_thumbnails.task_function(file_id=instance.pk)
|
||||||
|
|
||||||
|
|
||||||
@ -44,6 +44,8 @@ class FileAdmin(admin.ModelAdmin):
|
|||||||
css = {'all': ('files/admin/file.css',)}
|
css = {'all': ('files/admin/file.css',)}
|
||||||
|
|
||||||
def thumbnails(self, obj):
|
def thumbnails(self, obj):
|
||||||
|
if not obj or not (obj.is_image or obj.is_video):
|
||||||
|
return ''
|
||||||
try:
|
try:
|
||||||
context = {
|
context = {
|
||||||
'MEDIA_URL': settings.MEDIA_URL,
|
'MEDIA_URL': settings.MEDIA_URL,
|
||||||
@ -58,6 +60,14 @@ class FileAdmin(admin.ModelAdmin):
|
|||||||
logger.exception('Failed to render thumbnails')
|
logger.exception('Failed to render thumbnails')
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
def get_form(self, request, obj=None, **kwargs):
|
||||||
|
# Only override if the obj exisits
|
||||||
|
if obj:
|
||||||
|
if obj.is_image or obj.is_video:
|
||||||
|
help_text = 'Additional information about the file, e.g. existing thumbnails.'
|
||||||
|
kwargs.update({'help_texts': {'metadata': help_text}})
|
||||||
|
return super().get_form(request, obj, **kwargs)
|
||||||
|
|
||||||
view_on_site = False
|
view_on_site = False
|
||||||
save_on_top = True
|
save_on_top = True
|
||||||
|
|
||||||
@ -79,8 +89,8 @@ class FileAdmin(admin.ModelAdmin):
|
|||||||
'date_approved',
|
'date_approved',
|
||||||
'date_status_changed',
|
'date_status_changed',
|
||||||
'size_bytes',
|
'size_bytes',
|
||||||
'thumbnail',
|
|
||||||
'thumbnails',
|
'thumbnails',
|
||||||
|
'thumbnail',
|
||||||
'type',
|
'type',
|
||||||
'user',
|
'user',
|
||||||
'original_hash',
|
'original_hash',
|
||||||
@ -101,7 +111,7 @@ class FileAdmin(admin.ModelAdmin):
|
|||||||
{
|
{
|
||||||
'fields': (
|
'fields': (
|
||||||
'id',
|
'id',
|
||||||
('source', 'thumbnail', 'thumbnails'),
|
('source', 'thumbnails', 'thumbnail'),
|
||||||
('type', 'content_type', 'original_name'),
|
('type', 'content_type', 'original_name'),
|
||||||
'status',
|
'status',
|
||||||
)
|
)
|
||||||
|
@ -51,7 +51,7 @@ def thumbnail_upload_to(instance, filename):
|
|||||||
|
|
||||||
|
|
||||||
class File(CreatedModifiedMixin, TrackChangesMixin, models.Model):
|
class File(CreatedModifiedMixin, TrackChangesMixin, models.Model):
|
||||||
track_changes_to_fields = {'status', 'size_bytes', 'hash', 'thumbnail'}
|
track_changes_to_fields = {'status', 'size_bytes', 'hash', 'thumbnail', 'metadata'}
|
||||||
|
|
||||||
TYPES = FILE_TYPE_CHOICES
|
TYPES = FILE_TYPE_CHOICES
|
||||||
STATUSES = FILE_STATUS_CHOICES
|
STATUSES = FILE_STATUS_CHOICES
|
||||||
@ -100,7 +100,7 @@ class File(CreatedModifiedMixin, TrackChangesMixin, models.Model):
|
|||||||
default=dict,
|
default=dict,
|
||||||
blank=True,
|
blank=True,
|
||||||
# TODO add link to the manifest file user manual page.
|
# TODO add link to the manifest file user manual page.
|
||||||
help_text=('Meta information that was parsed from the `manifest file.'),
|
help_text=('Meta information that was parsed from the manifest file.'),
|
||||||
)
|
)
|
||||||
|
|
||||||
objects = FileManager()
|
objects = FileManager()
|
||||||
|
@ -38,11 +38,10 @@ def make_thumbnails(file_id: int) -> None:
|
|||||||
|
|
||||||
# TODO: For an image, source of the thumbnail is file.source itself
|
# TODO: For an image, source of the thumbnail is file.source itself
|
||||||
if file.is_image:
|
if file.is_image:
|
||||||
image_field = file.thumbnail
|
thumbnail_field = file.thumbnail
|
||||||
if not image_field:
|
if not thumbnail_field:
|
||||||
# Use the source image if max-size thumbnail is not yet available
|
# Use the source image if max-size thumbnail is not yet available
|
||||||
image_field.name = file.source.name
|
thumbnail_field.name = file.source.name
|
||||||
# base_path = files.utils.get_base_path(image_field.name)
|
|
||||||
thumbnails_paths = file.thumbnails
|
thumbnails_paths = file.thumbnails
|
||||||
files.utils.make_thumbnails(
|
files.utils.make_thumbnails(
|
||||||
abs_path,
|
abs_path,
|
||||||
@ -52,8 +51,18 @@ def make_thumbnails(file_id: int) -> None:
|
|||||||
optimize=True,
|
optimize=True,
|
||||||
progressive=True,
|
progressive=True,
|
||||||
)
|
)
|
||||||
image_field.name = thumbnails_paths[THUMBNAIL_SIZE_DEFAULT]
|
thumbnail_default_path = thumbnails_paths[THUMBNAIL_SIZE_DEFAULT]
|
||||||
file.save(update_fields={'thumbnail'})
|
# Reverse to make JSON-serialisable
|
||||||
|
thumbnail_metadata = {path: size for size, path in thumbnails_paths.items()}
|
||||||
|
update_fields = {}
|
||||||
|
if thumbnail_default_path != thumbnail_field.name:
|
||||||
|
thumbnail_field.name = thumbnail_default_path
|
||||||
|
update_fields.add('thumbnail')
|
||||||
|
if file.metadata.get('thumbnails') != thumbnail_metadata:
|
||||||
|
file.metadata.update({'thumbnails': thumbnail_metadata})
|
||||||
|
update_fields.add('metadata')
|
||||||
|
if update_fields:
|
||||||
|
file.save(update_fields=update_fields)
|
||||||
# TODO: For a video, source of the thumbnail is some frame fetched with ffpeg
|
# TODO: For a video, source of the thumbnail is some frame fetched with ffpeg
|
||||||
elif file.is_video:
|
elif file.is_video:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
@ -42,9 +42,11 @@ class FileTest(TestCase):
|
|||||||
'new_state': {'status': 'Approved'},
|
'new_state': {'status': 'Approved'},
|
||||||
'object': '<File: test.zip (Approved)>',
|
'object': '<File: test.zip (Approved)>',
|
||||||
'old_state': {
|
'old_state': {
|
||||||
'status': 2,
|
|
||||||
'hash': 'foobar',
|
'hash': 'foobar',
|
||||||
|
'metadata': {},
|
||||||
'size_bytes': 7149,
|
'size_bytes': 7149,
|
||||||
|
'status': 2,
|
||||||
|
'thumbnail': '',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
Loading…
Reference in New Issue
Block a user