Thumbnails for images and videos #87
@ -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):
|
||||
|
@ -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,
|
||||
)
|
||||
|
20
extensions/migrations/0027_unique_preview_files.py
Normal file
20
extensions/migrations/0027_unique_preview_files.py
Normal 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'),
|
||||
),
|
||||
]
|
@ -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)
|
||||
|
||||
|
@ -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 %}
|
||||
|
@ -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,
|
||||
],
|
||||
)
|
||||
|
@ -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',
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user