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

View File

@ -66,24 +66,14 @@ class AddPreviewFileForm(forms.ModelForm):
def save(self, *args, **kwargs):
"""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
self.instance.user = self.request.user
instance = super().save(*args, **kwargs)
# Create extension preview and save caption to it
instance.extension_preview.create(
extensions.models.Preview.objects.create(
file=instance,
caption=self.cleaned_data['caption'],
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
for the reverse relationship.
"""
return self.previews.listed.order_by('extension_preview__position')
return self.previews.listed.order_by('preview__position')
@property
def valid_file_statuses(self) -> List[int]:
@ -653,9 +653,7 @@ class Maintainer(CreatedModifiedMixin, models.Model):
class Preview(CreatedModifiedMixin, RecordDeletionMixin, models.Model):
extension = models.ForeignKey(Extension, on_delete=models.CASCADE)
file = models.ForeignKey(
'files.File', related_name='extension_preview', on_delete=models.CASCADE
)
file = models.OneToOneField('files.File', on_delete=models.CASCADE)
caption = models.CharField(max_length=255, default='', null=False, blank=True)
position = models.IntegerField(default=0)

View File

@ -11,7 +11,7 @@
data-galleria-content-type="{{ preview.content_type }}"
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>
{% endwith %}
{% endfor %}

View File

@ -14,6 +14,7 @@ import reviewers.models
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)
class DeleteTest(TestCase):
fixtures = ['dev', 'licenses']
@ -58,7 +59,7 @@ class DeleteTest(TestCase):
file_validation,
extension,
approval_activity,
preview_file.extension_preview.first(),
preview_file.preview,
version,
],
)

View File

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

View File

@ -180,6 +180,28 @@ def run_clamdscan(abs_path: str) -> tuple:
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:
"""Return the given path without its extension."""
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):
"""
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.:
@ -262,25 +283,3 @@ def extract_frame(source_path: str, output_path: str):
except (FFmpegError, FFmpegFileNotFound, FFmpegInvalidCommand) as e:
logger.exception(f'Failed to extract a frame: {e.message}, {" ".join(ffmpeg.arguments)}')
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)