Files
Axia4/django_app/club/models.py

145 lines
5.4 KiB
Python

import re
from io import BytesIO
from pathlib import Path
from django.core.files.base import ContentFile
from django.db import models
from core.models import ClubEvent as CoreClubEvent
# Pillow
from PIL import ExifTags, Image, ImageOps
def _safe_segment(value: str) -> str:
return re.sub(r"[^A-Za-z0-9._-]", "", (value or ""))
def club_event_picture_original_upload_to(instance, filename: str) -> str:
ext = Path(filename or "").suffix.lower() or ".bin"
date_ref = _safe_segment(getattr(instance.event, "date_ref", "")) or "event"
person = _safe_segment(instance.person_name) or "persona"
stem = _safe_segment(Path(filename or "image").stem) or "image"
return f"club/events/{date_ref}/{person}/original/{stem}{ext}"
def club_event_picture_compressed_upload_to(instance, filename: str) -> str:
date_ref = _safe_segment(getattr(instance.event, "date_ref", "")) or "event"
person = _safe_segment(instance.person_name) or "persona"
stem = _safe_segment(Path(filename or "image").stem) or "image"
return f"club/events/{date_ref}/{person}/compressed/{stem}.jpg"
def _to_degrees(value):
if isinstance(value, tuple) and len(value) == 2:
return float(value[0]) / float(value[1] or 1)
return float(value)
def _dms_to_decimal(dms, ref):
try:
deg = _to_degrees(dms[0])
minute = _to_degrees(dms[1])
second = _to_degrees(dms[2])
decimal = deg + (minute / 60.0) + (second / 3600.0)
if ref in {"S", "W"}:
decimal = -decimal
return decimal
except Exception:
return None
class ClubEventPicture(models.Model):
event = models.ForeignKey(CoreClubEvent, on_delete=models.CASCADE, related_name="pictures")
person_name = models.TextField(default="")
location = models.TextField(default="", blank=True)
title = models.TextField(default="", blank=True)
hidden = models.BooleanField(default=False)
original_image = models.FileField(upload_to=club_event_picture_original_upload_to)
compressed_image = models.FileField(upload_to=club_event_picture_compressed_upload_to, blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = "club_event_pictures"
def __str__(self):
return f"{self.event.date_ref} - {self.title or self.original_image.name}"
def _extract_exif_location(self):
try:
if not self.original_image:
return ""
self.original_image.seek(0)
image = Image.open(self.original_image)
exif = image.getexif()
gps_info = {}
gps_tag_id = None
for key, value in ExifTags.TAGS.items():
if value == "GPSInfo":
gps_tag_id = key
break
if gps_tag_id is None or gps_tag_id not in exif:
return ""
raw_gps = exif.get(gps_tag_id, {})
for key, val in raw_gps.items():
tag_name = ExifTags.GPSTAGS.get(key, key)
gps_info[tag_name] = val
lat = _dms_to_decimal(gps_info.get("GPSLatitude"), gps_info.get("GPSLatitudeRef"))
lon = _dms_to_decimal(gps_info.get("GPSLongitude"), gps_info.get("GPSLongitudeRef"))
if lat is None or lon is None:
return ""
return f"{lat:.6f}, {lon:.6f}"
except Exception:
return ""
def _build_compressed_content(self):
self.original_image.seek(0)
image = Image.open(self.original_image)
image = ImageOps.exif_transpose(image)
# JPEG no soporta alpha; cuando hay transparencia la aplanamos en blanco.
if image.mode in ("RGBA", "LA") or (image.mode == "P" and "transparency" in image.info):
alpha_image = image.convert("RGBA")
background = Image.new("RGB", alpha_image.size, (255, 255, 255))
background.paste(alpha_image, mask=alpha_image.getchannel("A"))
image = background
elif image.mode != "RGB":
image = image.convert("RGB")
image.thumbnail((800, 800))
buffer = BytesIO()
image.save(buffer, format="JPEG", quality=60, optimize=True)
return ContentFile(buffer.getvalue())
def _original_image_changed(self) -> bool:
if not self.original_image:
return False
if not self.pk:
return True
previous = type(self).objects.filter(pk=self.pk).values_list("original_image", flat=True).first()
return str(previous or "") != str(self.original_image.name or "")
def save(self, *args, **kwargs):
should_refresh_assets = self._original_image_changed()
if not self.title and self.original_image:
self.title = Path(self.original_image.name).stem
if should_refresh_assets and self.original_image:
if not self.location:
self.location = self._extract_exif_location()
compressed_content = self._build_compressed_content()
compressed_name = Path(self.original_image.name).stem + ".jpg"
self.compressed_image.save(compressed_name, compressed_content, save=False)
super().save(*args, **kwargs)
class ClubEvent(CoreClubEvent):
class Meta:
proxy = True
app_label = "club"
verbose_name = "Evento"
verbose_name_plural = "Eventos"