diff --git a/photo21/settings.py b/photo21/settings.py
index baca374..24a6874 100644
--- a/photo21/settings.py
+++ b/photo21/settings.py
@@ -209,7 +209,7 @@ AUTH_PASSWORD_VALIDATORS = [
# Internationalization
# https://docs.djangoproject.com/en/2.2/topics/i18n/
-TIME_ZONE = "Europe/Paris"
+TIME_ZONE = "UTC"
USE_TZ = True
diff --git a/photologue/admin.py b/photologue/admin.py
index 6a5456d..c516bf3 100644
--- a/photologue/admin.py
+++ b/photologue/admin.py
@@ -45,20 +45,14 @@ class MediaAdmin(admin.ModelAdmin):
def get_queryset(self, request):
return super().get_queryset(request).prefetch_related("owner", "galleries")
- def get_date_source(self, obj):
- return obj.get_date_source_display() or "-"
-
get_owner.admin_order_field = "owner"
get_owner.short_description = _("owner")
get_galleries.short_description = _("Gallery")
- get_date_source.admin_order_field = "date_source"
- get_date_source.short_description = _("date source")
class PhotoAdmin(MediaAdmin):
- list_display = ("title", "date_taken", "get_date_source", "date_added", "is_public", "view_count", "admin_thumbnail", "get_owner", "get_galleries")
- # The capture date is editable so that a camera set to the wrong time can be
- # corrected; an edited date is then marked as manual and never overwritten.
+ list_display = ("title", "date_taken", "date_added", "is_public", "view_count", "admin_thumbnail", "get_owner", "get_galleries")
+ readonly_fields = ("date_taken",)
model = Photo
@@ -70,7 +64,7 @@ class TagAdmin(admin.ModelAdmin):
class VideoAdmin(MediaAdmin):
- list_display = ("title", "date_taken", "get_date_source", "date_added", "is_public", "get_owner", "get_galleries")
+ list_display = ("title", "date_added", "is_public", "get_owner", "get_galleries")
model = Video
diff --git a/photologue/forms.py b/photologue/forms.py
index 6f48485..10a757d 100644
--- a/photologue/forms.py
+++ b/photologue/forms.py
@@ -43,9 +43,6 @@ class MultipleFileField(forms.FileField):
class UploadForm(forms.Form):
file_field = MultipleFileField(label="")
- # File.lastModified, in milliseconds, sent by upload.js. An upload is the last
- # chance to catch it: the date of the file itself does not survive HTTP.
- file_modified = forms.IntegerField(required=False, widget=forms.HiddenInput)
gallery = forms.ModelChoiceField(
Gallery.objects.all(),
label=_("Gallery"),
diff --git a/photologue/management/commands/backfill_capture_dates.py b/photologue/management/commands/backfill_capture_dates.py
deleted file mode 100644
index ae6f703..0000000
--- a/photologue/management/commands/backfill_capture_dates.py
+++ /dev/null
@@ -1,77 +0,0 @@
-# This file is part of photo21
-# Copyright (C) 2022 Amicale des élèves de l'ENS Paris-Saclay
-# SPDX-License-Identifier: GPL-3.0-or-later
-
-from django.core.management.base import BaseCommand
-
-from photologue.models import DATE_FROM_EXIF, DATE_FROM_VIDEO, DATE_MANUAL, Photo, Video
-from photologue.utils import capture_date_from_exif, capture_date_from_video
-
-
-class Command(BaseCommand):
- help = (
- "Read the capture date of existing media from their metadata. Media whose "
- "date was set by hand are never touched. By default only media without a "
- "date, and media imported before dates were tracked, are read."
- )
-
- def add_arguments(self, parser):
- parser.add_argument(
- "--force",
- action="store_true",
- help="Re-read every media, even those with a known date source.",
- )
- parser.add_argument(
- "--dry-run",
- action="store_true",
- help="Report what would change without writing anything.",
- )
-
- def handle(self, *args, **options):
- for model, reader in ((Photo, self.photo_date), (Video, self.video_date)):
- queryset = model.objects.exclude(date_source=DATE_MANUAL)
- if not options["force"]:
- # An empty source means the media predates this field, and its date
- # may have been stored in the wrong time zone.
- queryset = queryset.filter(date_taken__isnull=True) | queryset.filter(date_source="")
- self.process(model, queryset.distinct(), reader, options)
-
- def process(self, model, queryset, reader, options):
- name = model._meta.verbose_name_plural
- found = unchanged = missing = 0
- for media in queryset.iterator():
- taken, source = reader(media)
- if taken is None:
- missing += 1
- self.stdout.write(f" no date in {media.title}")
- continue
- if taken == media.date_taken and source == media.date_source:
- unchanged += 1
- continue
- found += 1
- self.stdout.write(f" {media.title}: {media.date_taken} -> {taken} ({source})")
- if not options["dry_run"]:
- media.set_capture_date(taken, source)
- media.save(update_fields=["date_taken", "date_source"])
- done = "would be updated" if options["dry_run"] else "updated"
- self.stdout.write(
- self.style.SUCCESS(f"{name}: {found} {done}, {unchanged} already correct, {missing} without metadata")
- )
-
- @staticmethod
- def photo_date(photo):
- if not photo.image:
- return None, None
- try:
- exif_date = photo.exif(photo.image.file).get("EXIF DateTimeOriginal", None)
- except Exception:
- return None, None
- if exif_date is None:
- return None, None
- return capture_date_from_exif(exif_date.values), DATE_FROM_EXIF
-
- @staticmethod
- def video_date(video):
- if not video.file:
- return None, None
- return capture_date_from_video(video.file.path), DATE_FROM_VIDEO
diff --git a/photologue/migrations/0011_photo_date_source_video_date_source_video_date_taken_and_more.py b/photologue/migrations/0011_photo_date_source_video_date_source_video_date_taken_and_more.py
deleted file mode 100644
index 7724cbb..0000000
--- a/photologue/migrations/0011_photo_date_source_video_date_source_video_date_taken_and_more.py
+++ /dev/null
@@ -1,33 +0,0 @@
-# Generated by Django 6.0.4 on 2026-09-24 09:12
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('photologue', '0010_video'),
- ]
-
- operations = [
- migrations.AddField(
- model_name='photo',
- name='date_source',
- field=models.CharField(blank=True, choices=[('exif', 'EXIF metadata'), ('video', 'video metadata'), ('file', 'file date at upload'), ('manual', 'set by hand')], editable=False, help_text='Where the capture date comes from.', max_length=10, verbose_name='date source'),
- ),
- migrations.AddField(
- model_name='video',
- name='date_source',
- field=models.CharField(blank=True, choices=[('exif', 'EXIF metadata'), ('video', 'video metadata'), ('file', 'file date at upload'), ('manual', 'set by hand')], editable=False, help_text='Where the capture date comes from.', max_length=10, verbose_name='date source'),
- ),
- migrations.AddField(
- model_name='video',
- name='date_taken',
- field=models.DateTimeField(blank=True, help_text='Date the media was captured; read from its metadata.', null=True, verbose_name='date taken'),
- ),
- migrations.AlterField(
- model_name='photo',
- name='date_taken',
- field=models.DateTimeField(blank=True, help_text='Date the media was captured; read from its metadata.', null=True, verbose_name='date taken'),
- ),
- ]
diff --git a/photologue/models.py b/photologue/models.py
index 240f72b..782f9dc 100644
--- a/photologue/models.py
+++ b/photologue/models.py
@@ -4,6 +4,7 @@
import logging
import os
import unicodedata
+from datetime import datetime
from functools import partial
from importlib import import_module
from inspect import isclass
@@ -38,19 +39,6 @@ LATEST_LIMIT = getattr(settings, "PHOTOLOGUE_GALLERY_LATEST_LIMIT", None)
# max_length setting for the ImageModel ImageField
IMAGE_FIELD_MAX_LENGTH = getattr(settings, "PHOTOLOGUE_IMAGE_FIELD_MAX_LENGTH", 100)
-# Where a capture date comes from, from the most to the least trustworthy. An
-# empty value means the date was never determined.
-DATE_FROM_EXIF = "exif"
-DATE_FROM_VIDEO = "video"
-DATE_FROM_FILE = "file"
-DATE_MANUAL = "manual"
-DATE_SOURCE_CHOICES = (
- (DATE_FROM_EXIF, _("EXIF metadata")),
- (DATE_FROM_VIDEO, _("video metadata")),
- (DATE_FROM_FILE, _("file date at upload")),
- (DATE_MANUAL, _("set by hand")),
-)
-
# Modify image file buffer size.
ImageFile.MAXBLOCK = getattr(settings, "PHOTOLOGUE_MAXBLOCK", 256 * 2**10)
@@ -277,20 +265,6 @@ class MediaModel(models.Model):
)
thumbnail_width = models.PositiveIntegerField(editable=False, null=True)
thumbnail_height = models.PositiveIntegerField(editable=False, null=True)
- date_taken = models.DateTimeField(
- _("date taken"),
- null=True,
- blank=True,
- help_text=_("Date the media was captured; read from its metadata."),
- )
- date_source = models.CharField(
- _("date source"),
- max_length=10,
- blank=True,
- editable=False,
- choices=DATE_SOURCE_CHOICES,
- help_text=_("Where the capture date comes from."),
- )
is_video = False
@@ -314,32 +288,9 @@ class MediaModel(models.Model):
def get_download_url(self):
raise NotImplementedError
- @property
- def date_is_approximate(self):
- """True when the date was deduced rather than read from the capture itself."""
- return self.date_source == DATE_FROM_FILE
-
- def set_capture_date(self, value, source):
- """Record a capture date read from the file, and where it comes from."""
- self.date_taken = value
- self.date_source = source
- self._date_read_from_file = True
-
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.title)
- if self.pk and not getattr(self, "_date_read_from_file", False):
- # A date changed by anything but our own readers was set by a human,
- # and must survive a later re-read of the metadata.
- stored = (
- type(self)
- .objects.filter(pk=self.pk)
- .values_list("date_taken", flat=True)
- .first()
- )
- if stored != self.date_taken:
- self.date_source = DATE_MANUAL
- self._date_read_from_file = False
super().save(*args, **kwargs)
@@ -351,6 +302,12 @@ class Photo(MediaModel):
width_field="thumbnail_width",
height_field="thumbnail_height",
)
+ date_taken = models.DateTimeField(
+ _("date taken"),
+ null=True,
+ blank=True,
+ help_text=_("Date image was taken; is obtained from the image EXIF data."),
+ )
view_count = models.PositiveIntegerField(_("view count"), default=0, editable=False)
crop_from = models.CharField(
_("crop from"),
@@ -620,15 +577,21 @@ class Photo(MediaModel):
if (self.date_taken is None or image_has_changed) and self.image:
# Attempt to get the date the photo was taken from the EXIF data.
try:
- from .utils import capture_date_from_exif
-
exif_date = self.exif(self.image.file).get(
"EXIF DateTimeOriginal", None
)
if exif_date is not None:
- taken = capture_date_from_exif(exif_date.values)
- if taken is not None:
- self.set_capture_date(taken, DATE_FROM_EXIF)
+ d, t = exif_date.values.split()
+ year, month, day = d.split(":")
+ hour, minute, second = t.split(":")
+ self.date_taken = datetime(
+ int(year),
+ int(month),
+ int(day),
+ int(hour),
+ int(minute),
+ int(second),
+ )
except Exception:
logger.error("Failed to read EXIF DateTimeOriginal", exc_info=True)
# If crop_from property has been changed on existing image,
diff --git a/photologue/static/gallery_detail.js b/photologue/static/gallery_detail.js
index 56cbb94..ed345d6 100644
--- a/photologue/static/gallery_detail.js
+++ b/photologue/static/gallery_detail.js
@@ -5,7 +5,7 @@
// Init gallery
-const lgInstance = lightGallery(document.getElementById('lightgallery'), {
+lightGallery(document.getElementById('lightgallery'), {
selector: 'a.photo-item',
plugins: [lgAdmin, lgHash, lgThumbnail, lgZoom, lgVideo],
autoplayFirstVideo: true,
@@ -26,41 +26,6 @@ const lgInstance = lightGallery(document.getElementById('lightgallery'), {
//Add download warning code
const lgContainer = document.getElementById('lightgallery');
-// Change photo with a two-finger horizontal swipe on a trackpad. Without this the
-// browser reads the gesture as "go back" and leaves the gallery.
-const SWIPE_DISTANCE = 80; // horizontal pixels before switching photo
-const SWIPE_COOLDOWN = 400; // ms, so the inertia of one swipe moves one photo
-const SWIPE_GAP = 200; // ms of quiet that starts a new gesture
-let swipeOuter = null;
-let swipeAmount = 0;
-let swipeLastWheel = 0;
-let swipeLastMove = 0;
-
-function onTrackpadSwipe(e) {
- // Leave vertical scrolling, and zoomed in photos, alone.
- if (Math.abs(e.deltaX) <= Math.abs(e.deltaY)) return;
- e.preventDefault();
- if (swipeOuter.classList.contains('lg-zoomed')) return;
-
- const now = performance.now();
- if (now - swipeLastWheel > SWIPE_GAP) swipeAmount = 0;
- swipeLastWheel = now;
- if (now - swipeLastMove < SWIPE_COOLDOWN) return;
-
- swipeAmount += e.deltaX;
- if (Math.abs(swipeAmount) < SWIPE_DISTANCE) return;
- if (swipeAmount > 0) lgInstance.goToNextSlide();
- else lgInstance.goToPrevSlide();
- swipeAmount = 0;
- swipeLastMove = now;
-}
-
-lgContainer.addEventListener('lgAfterOpen', () => {
- swipeOuter = document.querySelector('.lg-outer');
- swipeAmount = 0;
- swipeOuter.addEventListener('wheel', onTrackpadSwipe, { passive: false });
-});
-
// On attend que la galerie soit ouverte pour cibler le bouton
lgContainer.addEventListener('lgAfterOpen', () => {
const downloadBtn = document.querySelector('.lg-download');
diff --git a/photologue/static/gallery_justified.js b/photologue/static/gallery_justified.js
index c4958c7..cec2070 100644
--- a/photologue/static/gallery_justified.js
+++ b/photologue/static/gallery_justified.js
@@ -3,25 +3,6 @@
(function () {
const SPACING = 3;
const PADDING = 3;
- // Photos that all share the same aspect ratio give rows of identical height,
- // which reads as a contact sheet. Some photos are therefore displayed with a
- // slightly different ratio than their own: `object-fit: cover` trims the
- // edges, and the justified rows become irregular on their own.
- const CROP_STEPS = [0.8, 1, 1, 1.25]; // display ratio multipliers, drawn per photo
-
- // id -> multiplier, drawn once per page load and kept on resize (and when a
- // photo is deleted) so the gallery does not reshuffle while it is looked at.
- const cropPlan = new Map();
-
- function displayRatio(item, index) {
- const w = parseInt(item.dataset.width) || 1;
- const h = parseInt(item.dataset.height) || 1;
- const id = item.dataset.slideName || 'i' + index;
- if (!cropPlan.has(id)) {
- cropPlan.set(id, CROP_STEPS[Math.floor(Math.random() * CROP_STEPS.length)]);
- }
- return (w / h) * cropPlan.get(id);
- }
function applyLayout() {
const container = document.getElementById('lightgallery');
@@ -32,7 +13,11 @@
const containerWidth = container.offsetWidth - PADDING * 2;
const TARGET_HEIGHT = Math.min(220, Math.floor(containerWidth * 0.4));
- const ratios = items.map(displayRatio);
+ const ratios = items.map(a => {
+ const w = parseInt(a.dataset.width) || 1;
+ const h = parseInt(a.dataset.height) || 1;
+ return w / h;
+ });
// Group items into rows
const rows = [];
@@ -80,6 +65,8 @@
container.style.height = (top - SPACING + PADDING) + 'px';
items.forEach(item => {
item.style.visibility = 'visible';
+ const img = item.querySelector('img[data-lazy]');
+ if (img) img.src = img.dataset.lazy;
});
}
diff --git a/photologue/static/upload.js b/photologue/static/upload.js
index f741085..584ec42 100644
--- a/photologue/static/upload.js
+++ b/photologue/static/upload.js
@@ -99,8 +99,6 @@ async function uploadFile(file, galleryID, csrfvalue, ownerID) {
const sendform = new FormData();
sendform.append('csrfmiddlewaretoken', csrfvalue);
sendform.append('file_field', file);
- // The capture date is often missing from the metadata but present here.
- if (file.lastModified) sendform.append('file_modified', file.lastModified);
sendform.append('gallery', galleryID);
if (ownerID) sendform.append('owner', ownerID);
const start = performance.now();
diff --git a/photologue/templates/photologue/gallery_detail.html b/photologue/templates/photologue/gallery_detail.html
index c9a85d6..2ebf304 100755
--- a/photologue/templates/photologue/gallery_detail.html
+++ b/photologue/templates/photologue/gallery_detail.html
@@ -112,9 +112,9 @@ SPDX-License-Identifier: GPL-3.0-or-later
data-uncensor-url="{% url 'photologue:pl-media-uncensor' item.model_name item.id %}"
data-width="{{ item.thumbnail_width|default:1 }}"
data-height="{{ item.thumbnail_height|default:1 }}">
-
+ alt="{{ item.title }}{% if not item.is_video and item.date_taken %} - {{ item.date_taken|date }} {{ item.date_taken|time }}{% endif %} - {{ item.owner.first_name|default:item.owner.username }}{% if not item.is_video and item.license %} - {{ item.license }}{% endif %}{% if not item.is_public %} - !PRIVATE!{% endif %}">
{% endfor %}
diff --git a/photologue/templates/photologue/photo_detail.html b/photologue/templates/photologue/photo_detail.html
index df4d246..0c40697 100644
--- a/photologue/templates/photologue/photo_detail.html
+++ b/photologue/templates/photologue/photo_detail.html
@@ -12,10 +12,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
- {% if object.date_taken %}{% trans "Taken" %} {{ object.date_taken }}{% if object.date_is_approximate %} ({% trans "approximate date" %}){% endif %} · {% endif %} - {% trans "Published" %} {{ object.date_added }} -
+{% trans "Published" %} {{ object.date_added }}
{{ object.caption }}
{% endif %} diff --git a/photologue/utils.py b/photologue/utils.py index a697eb1..d3ff68b 100644 --- a/photologue/utils.py +++ b/photologue/utils.py @@ -5,12 +5,7 @@ import logging import os -from datetime import datetime, timezone as dt_timezone - -from django.conf import settings from django.core.files.base import ContentFile -from django.utils import timezone -from django.utils.dateparse import parse_datetime from io import BytesIO from PIL import Image import av @@ -66,89 +61,3 @@ def generate_video_thumbnail(video): video.thumbnail.save(thumb_name, ContentFile(buffer.getvalue()), save=True) except Exception: logger.error("Failed to save video thumbnail", exc_info=True) - - -def capture_date_from_exif(exif_value): - """Turn an EXIF "YYYY:MM:DD HH:MM:SS" value into an aware datetime. - - EXIF dates carry no time zone: they are local time as read by the camera, so - they are interpreted in the site time zone. - """ - try: - day, hour = str(exif_value).split() - year, month, dayofmonth = (int(part) for part in day.split(":")) - hours, minutes, seconds = (int(part) for part in hour.split(":")) - naive = datetime(year, month, dayofmonth, hours, minutes, seconds) - except (AttributeError, TypeError, ValueError): - logger.error("Unreadable EXIF date %r", exif_value) - return None - return timezone.make_aware(naive) if settings.USE_TZ else naive - - -def capture_date_from_video(path): - """Read the capture date from a video container, or None.""" - try: - container = av.open(path) - raw = container.metadata.get("creation_time") - container.close() - except Exception: - logger.error("Failed to read video metadata from %s", path, exc_info=True) - return None - if not raw: - return None - parsed = parse_datetime(raw) - if parsed is None: - return None - if settings.USE_TZ and timezone.is_naive(parsed): - # Containers usually store UTC; a naive value is the camera's local time. - parsed = timezone.make_aware(parsed) - return parsed - - -def capture_date_from_upload(last_modified_ms): - """Turn a browser File.lastModified value (ms since epoch) into a datetime.""" - try: - stamp = int(last_modified_ms) / 1000 - except (TypeError, ValueError): - return None - if not stamp: - return None - return datetime.fromtimestamp(stamp, tz=dt_timezone.utc) - - -def order_media_by_capture(items): - """Sort media in capture order, and fill in the dates that are missing. - - A file name is a reliable chronological clue inside one photographer's batch - (most cameras number their files in order) and meaningless between two of - them. So media without a capture date borrow the date of the closest dated - file of the same owner, which puts them back in the middle of the sequence - instead of exiling them at the end of the gallery. Only media whose owner has - no dated file at all end up at the end, in file name order. - - Every item is annotated with `capture_date` and `capture_date_is_guessed`. - """ - by_owner = {} - for item in items: - item.capture_date = item.date_taken - item.capture_date_is_guessed = False - by_owner.setdefault(item.owner_id, []).append(item) - - dated, undated = [], [] - for batch in by_owner.values(): - batch.sort(key=lambda item: (item.title, item.pk)) - # Forwards, then backwards for the files that come before the first date. - for sequence in (batch, batch[::-1]): - known = None - for item in sequence: - if item.capture_date is not None: - known = item.capture_date - elif known is not None: - item.capture_date = known - item.capture_date_is_guessed = True - for item in batch: - (dated if item.capture_date is not None else undated).append(item) - - dated.sort(key=lambda item: (item.capture_date, item.owner_id, item.title, item.pk)) - undated.sort(key=lambda item: (item.owner_id, item.title, item.pk)) - return dated + undated diff --git a/photologue/views.py b/photologue/views.py index 21b4643..93116d2 100644 --- a/photologue/views.py +++ b/photologue/views.py @@ -26,15 +26,8 @@ from django.contrib.auth import get_user_model from django.db.models import F, Q from .forms import UploadForm -from .models import DATE_FROM_FILE, DATE_FROM_VIDEO, Gallery, Photo, Tag, Video -from .utils import ( - capture_date_from_upload, - capture_date_from_video, - generate_video_thumbnail, - is_photo, - is_video, - order_media_by_capture, -) +from .models import Gallery, Photo, Tag, Video +from .utils import generate_video_thumbnail, is_photo, is_video MEDIA_MODELS = {"photo": Photo, "video": Video} @@ -236,9 +229,9 @@ class GalleryDetailView(DetailView): context["photos"] = context["photos"].filter(owner__id=self.kwargs["owner"]) context["videos"] = context["videos"].filter(owner__id=self.kwargs["owner"]) - # Combine photos and videos into a single list, in capture order - context["media_items"] = order_media_by_capture( - [*context["photos"], *context["videos"]] + # Combine photos and videos into a single sorted list for the template + context["media_items"] = sorted( + [*context["photos"], *context["videos"]], key=lambda x: x.title ) # Increment the photo view count @@ -311,25 +304,7 @@ class GalleryUpload(PermissionRequiredMixin, FormView): kwargs["is_staff"] = self.request.user.is_staff return kwargs - @staticmethod - def _complete_capture_date(obj, modified_ms): - """ - Fill in the capture date that the file itself did not carry: photos get - theirs from EXIF when saved, videos from their container, and anything - still undated falls back on the file date sent by the browser. - """ - taken = source = None - if obj.date_taken is None and obj.is_video: - taken, source = capture_date_from_video(obj.file_path), DATE_FROM_VIDEO - if taken is None and obj.date_taken is None and modified_ms: - taken, source = capture_date_from_upload(modified_ms), DATE_FROM_FILE - if taken is None: - return - obj.set_capture_date(taken, source) - obj.save(update_fields=["date_taken", "date_source"]) - - def _upload_media(self, model, file_field, file_obj, gallery, gallery_dir, owner, - post_save=None, modified_ms=None): + def _upload_media(self, model, file_field, file_obj, gallery, gallery_dir, owner, post_save=None): """ Create a media object, save it to the DB, schedule file save on commit. Returns True if uploaded, False if already exists. @@ -342,12 +317,11 @@ class GalleryUpload(PermissionRequiredMixin, FormView): with transaction.atomic(): obj.save() obj.galleries.set([gallery]) - def _save(o=obj, fp=file_path, f=file_obj, ff=file_field, ps=post_save, ms=modified_ms): + def _save(o=obj, fp=file_path, f=file_obj, ff=file_field, ps=post_save): f.seek(0) getattr(o, ff).save(fp, f) if ps: ps(o) - self._complete_capture_date(o, ms) transaction.on_commit(_save) return True @@ -385,14 +359,11 @@ class GalleryUpload(PermissionRequiredMixin, FormView): uploaded_photo_name = [] already_exists = 0 files = form.cleaned_data["file_field"] - modified_ms = form.cleaned_data.get("file_modified") for photo_file in files: if is_photo(photo_file): - uploaded = self._upload_media(Photo, "image", photo_file, gallery, gallery_dir, owner, - modified_ms=modified_ms) + uploaded = self._upload_media(Photo, "image", photo_file, gallery, gallery_dir, owner) elif is_video(photo_file): - uploaded = self._upload_media(Video, "file", photo_file, gallery, gallery_dir, owner, - post_save=generate_video_thumbnail, modified_ms=modified_ms) + uploaded = self._upload_media(Video, "file", photo_file, gallery, gallery_dir, owner, post_save=generate_video_thumbnail) else: messages.error(self.request, f"{photo_file.name} is not a recognized image or video") jsondata["code"] = 400