Thumbnails for images and videos #87

Merged
Anna Sirota merged 28 commits from thumbnails into main 2024-04-25 17:50:58 +02:00
8 changed files with 55 additions and 47 deletions
Showing only changes of commit 363d6a9516 - Show all commits

View File

@ -6,7 +6,7 @@ from mdgen import MarkdownPostProvider
import factory import factory
import factory.fuzzy import factory.fuzzy
from extensions.models import Extension, Version, Tag from extensions.models import Extension, Version, Tag, Preview
from ratings.models import Rating from ratings.models import Rating
fake_markdown = Faker() fake_markdown = Faker()
@ -35,7 +35,7 @@ class ExtensionFactory(DjangoModelFactory):
if extracted: if extracted:
for _ in extracted: for _ in extracted:
_.extension_preview.create(caption='Media Caption', extension=self) Preview.objects.create(file=_, caption='Media Caption', extension=self)
@factory.post_generation @factory.post_generation
def process_extension_id(self, created, extracted, **kwargs): def process_extension_id(self, created, extracted, **kwargs):

View File

@ -66,24 +66,14 @@ class AddPreviewFileForm(forms.ModelForm):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
"""Save Preview from the cleaned form data.""" """Save Preview from the cleaned form data."""
# If file with this hash was already uploaded by the same user, return it
hash_ = self.instance.generate_hash(self.instance.source)
model = self.instance.__class__
existing_image = model.objects.filter(original_hash=hash_, user=self.request.user).first()
if (
existing_image
and not existing_image.extension_preview.filter(extension_id=self.extension.id).count()
):
logger.warning('Found an existing %s pk=%s', model, existing_image.pk)
self.instance = existing_image
# Fill in missing fields from request and the source file # Fill in missing fields from request and the source file
self.instance.user = self.request.user self.instance.user = self.request.user
instance = super().save(*args, **kwargs) instance = super().save(*args, **kwargs)
# Create extension preview and save caption to it # Create extension preview and save caption to it
instance.extension_preview.create( extensions.models.Preview.objects.create(
file=instance,
caption=self.cleaned_data['caption'], caption=self.cleaned_data['caption'],
extension=self.extension, extension=self.extension,
) )

View File

@ -0,0 +1,20 @@
# Generated by Django 4.2.11 on 2024-04-23 11:56
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('files', '0007_alter_file_status'),
('extensions', '0026_remove_extension_date_deleted_and_more'),
]
operations = [
migrations.AlterField(
model_name='preview',
name='file',
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='files.file'),
),
]

View File

@ -277,7 +277,7 @@ class Extension(CreatedModifiedMixin, RatingMixin, TrackChangesMixin, models.Mod
TODO: Might be better to query Previews directly instead of going TODO: Might be better to query Previews directly instead of going
for the reverse relationship. for the reverse relationship.
""" """
return self.previews.listed.order_by('extension_preview__position') return self.previews.listed.order_by('preview__position')
@property @property
def valid_file_statuses(self) -> List[int]: def valid_file_statuses(self) -> List[int]:
@ -653,9 +653,7 @@ class Maintainer(CreatedModifiedMixin, models.Model):
class Preview(CreatedModifiedMixin, RecordDeletionMixin, models.Model): class Preview(CreatedModifiedMixin, RecordDeletionMixin, models.Model):
extension = models.ForeignKey(Extension, on_delete=models.CASCADE) extension = models.ForeignKey(Extension, on_delete=models.CASCADE)
file = models.ForeignKey( file = models.OneToOneField('files.File', on_delete=models.CASCADE)
'files.File', related_name='extension_preview', on_delete=models.CASCADE
)
caption = models.CharField(max_length=255, default='', null=False, blank=True) caption = models.CharField(max_length=255, default='', null=False, blank=True)
position = models.IntegerField(default=0) position = models.IntegerField(default=0)

View File

@ -11,7 +11,7 @@
data-galleria-content-type="{{ preview.content_type }}" data-galleria-content-type="{{ preview.content_type }}"
data-galleria-index="{{ forloop.counter }}"> data-galleria-index="{{ forloop.counter }}">
<img src="{{ thumbnail_l_url }}" alt="{{ preview.extension_preview.first.caption }}"> <img src="{{ thumbnail_l_url }}" alt="{{ preview.preview.caption }}">
</a> </a>
{% endwith %} {% endwith %}
{% endfor %} {% endfor %}

View File

@ -14,6 +14,7 @@ import reviewers.models
TEST_MEDIA_DIR = Path(__file__).resolve().parent / 'media' TEST_MEDIA_DIR = Path(__file__).resolve().parent / 'media'
# Media file are physically deleted when files records are deleted, hence the override
@override_settings(MEDIA_ROOT=TEST_MEDIA_DIR) @override_settings(MEDIA_ROOT=TEST_MEDIA_DIR)
class DeleteTest(TestCase): class DeleteTest(TestCase):
fixtures = ['dev', 'licenses'] fixtures = ['dev', 'licenses']
@ -58,7 +59,7 @@ class DeleteTest(TestCase):
file_validation, file_validation,
extension, extension,
approval_activity, approval_activity,
preview_file.extension_preview.first(), preview_file.preview,
version, version,
], ],
) )

View File

@ -74,7 +74,7 @@ class UpdateTest(TestCase):
self.assertEqual(File.objects.filter(type=File.TYPES.IMAGE).count(), 1) self.assertEqual(File.objects.filter(type=File.TYPES.IMAGE).count(), 1)
self.assertEqual(extension.previews.count(), 1) self.assertEqual(extension.previews.count(), 1)
file1 = extension.previews.all()[0] file1 = extension.previews.all()[0]
self.assertEqual(file1.extension_preview.first().caption, 'First Preview Caption Text') self.assertEqual(file1.preview.caption, 'First Preview Caption Text')
self.assertEqual( self.assertEqual(
file1.original_hash, file1.original_hash,
'sha256:643e15eb6c4831173bbcf71b8c85efc70cf3437321bf2559b39aa5e9acfd5340', 'sha256:643e15eb6c4831173bbcf71b8c85efc70cf3437321bf2559b39aa5e9acfd5340',
@ -123,8 +123,8 @@ class UpdateTest(TestCase):
self.assertEqual(extension.previews.count(), 2) self.assertEqual(extension.previews.count(), 2)
file1 = extension.previews.all()[0] file1 = extension.previews.all()[0]
file2 = extension.previews.all()[1] file2 = extension.previews.all()[1]
self.assertEqual(file1.extension_preview.first().caption, 'First Preview Caption Text') self.assertEqual(file1.preview.caption, 'First Preview Caption Text')
self.assertEqual(file2.extension_preview.first().caption, 'Second Preview Caption Text') self.assertEqual(file2.preview.caption, 'Second Preview Caption Text')
self.assertEqual( self.assertEqual(
file1.original_hash, file1.original_hash,
'sha256:643e15eb6c4831173bbcf71b8c85efc70cf3437321bf2559b39aa5e9acfd5340', 'sha256:643e15eb6c4831173bbcf71b8c85efc70cf3437321bf2559b39aa5e9acfd5340',

View File

@ -180,6 +180,28 @@ def run_clamdscan(abs_path: str) -> tuple:
return result return result
def delete_file_in_storage(file_name: str) -> None:
"""Delete file from disk or whatever other default storage."""
if not file_name:
return
if not default_storage.exists(file_name):
logger.warning("%s doesn't exist in storage, nothing to delete", file_name)
else:
logger.info('Deleting %s from storage', file_name)
default_storage.delete(file_name)
def delete_thumbnails(file_metadata: dict) -> None:
"""Read thumbnail paths from given metadata and delete them from storage."""
thumbnails = file_metadata.get('thumbnails', {})
for _, thumb in thumbnails.items:
path = thumb.get('path', '')
if not path:
continue
delete_file_in_storage(path)
def get_base_path(file_path: str) -> str: def get_base_path(file_path: str) -> str:
"""Return the given path without its extension.""" """Return the given path without its extension."""
base_path, _ = os.path.splitext(file_path) base_path, _ = os.path.splitext(file_path)
@ -194,8 +216,7 @@ def _resize(image: Image, size: tuple, output, output_format: str = 'PNG', **out
def make_thumbnails(image_field, output_paths: dict, output_format: str = 'PNG', **output_params): def make_thumbnails(image_field, output_paths: dict, output_format: str = 'PNG', **output_params):
""" """Generate thumbnail files for given models.ImageField and list of dimensions.
Generate thumbnail files for given models.ImageField and list of dimensions.
Currently only intended to be used manually from shell, e.g.: Currently only intended to be used manually from shell, e.g.:
@ -262,25 +283,3 @@ def extract_frame(source_path: str, output_path: str):
except (FFmpegError, FFmpegFileNotFound, FFmpegInvalidCommand) as e: except (FFmpegError, FFmpegFileNotFound, FFmpegInvalidCommand) as e:
logger.exception(f'Failed to extract a frame: {e.message}, {" ".join(ffmpeg.arguments)}') logger.exception(f'Failed to extract a frame: {e.message}, {" ".join(ffmpeg.arguments)}')
raise raise
def delete_file_in_storage(file_name: str) -> None:
"""Delete file from disk or whatever other default storage."""
if not file_name:
return
if not default_storage.exists(file_name):
logger.warning("%s doesn't exist in storage, nothing to delete", file_name)
else:
logger.info('Deleting %s from storage', file_name)
default_storage.delete(file_name)
def delete_thumbnails(file_metadata: dict) -> None:
"""Read thumbnail paths from given metadata and delete them from storage."""
thumbnails = file_metadata.get('thumbnails', {})
for _, thumb in thumbnails.items:
path = thumb.get('path', '')
if not path:
continue
delete_file_in_storage(path)