Galerie: začátek podpory jiných typů [WIP]

Co funguje: ~~nic, protože není vygenerovaná migrace~~ Nejspíš lecos,
ale nejde to otestovat. Speciálně, pokud zavolám funkce z `galerie.typy`
ručně ze shellu, tak se to popřeškáluje a výsledná URL jde skutečně
vyrenderovat na webu (resp. šlo v nějaké průběžné verzi před zavedením
templatetagu).

TODO:
- ve views a modelech se pořád ještě vyskytuje `obrazek_stredni` a `obrazek_velky`
- nevím, co dělá Admin a nahrávání obrázků
- chybí funkce na tipování typů nahraných obrázků (a její použití v kódu)
- podpora pro videjka zatím není vůbec (jen připravený kód)
- DummyBazmek nemá použitelné placeholdery (představuji si SVG s nějakým
  vykřičníkem a odkazem do Admina
This commit is contained in:
Pavel "LEdoian" Turinsky 2025-05-05 01:50:08 +02:00
parent b7498b42b2
commit b25d475148
6 changed files with 169 additions and 40 deletions

View file

@ -28,14 +28,14 @@ def prepnout_fotogalerii_do_org_rezimu(modeladmin, request, queryset):
class GalerieInline(admin.TabularInline): class GalerieInline(admin.TabularInline):
model = Obrazek model = Obrazek
fields = ['obrazek_velky', 'nazev', 'popis', 'poradi'] fields = ['soubor', 'nazev', 'popis', 'poradi']
readonly_fields = ['nazev'] readonly_fields = ['nazev']
formfield_overrides = { formfield_overrides = {
models.TextField: {'widget': forms.TextInput}, models.TextField: {'widget': forms.TextInput},
} }
class ObrazekAdmin(admin.ModelAdmin): class ObrazekAdmin(admin.ModelAdmin):
list_display = ('obrazek_velky', 'nazev', 'popis', 'obrazek_maly_tag', 'poradi') list_display = ('soubor', 'nazev', 'popis', 'poradi')
search_fields = ['nazev','popis'] search_fields = ['nazev','popis']
class GalerieAdmin(admin.ModelAdmin): class GalerieAdmin(admin.ModelAdmin):

View file

@ -22,10 +22,12 @@ def obrazek_filename_maly():
pass pass
def obrazek_filename_stredni(): def obrazek_filename_stredni():
pass pass
def obrazek_filename():
pass
def obrazek_filename_velky(): def obrazek_filename_velky():
pass pass
def obrazek_filename(self, filename): def galerie_filename(self, filename):
gal = self.galerie gal = self.galerie
cislo_gal = gal.pk cislo_gal = gal.pk
@ -42,31 +44,32 @@ def obrazek_filename(self, filename):
return os.path.join(*cesta) return os.path.join(*cesta)
# Technicky misnomer: takový `Obrazek` může být i videjko a výhledově i něco dalšího…
class Obrazek(models.Model): class Obrazek(models.Model):
# „originál“ (modulo max. velikost uploadu na web FIXME!) # „originál“ (modulo max. velikost uploadu na web FIXME!)
obrazek_velky = models.FileField(upload_to=obrazek_filename, soubor = models.FileField(upload_to=galerie_filename,
help_text = "Lze vložit libovolně velký obrázek. Ideální je, aby alespoň jeden rozměr měl alespoň 500px.") help_text = "Lze vložit libovolně velký obrázek. Ideální je, aby alespoň jeden rozměr měl alespoň 500px.")
# To, co se zobrazí v galerii jako hlavní obrázek (při prohlížení konkrétního obrázku a jako tittulní obrázek u galerií, které nemají vlastní obrázky (kupř. Vávrovka 2015)) class Typ(models.TextChoices):
# obrazek_stredni = ImageSpecField(source='obrazek_velky', OBRAZEK = 'obrazek', 'Obrázek'
# processors=[Transpose(Transpose.AUTO), ResizeToFit(900, 675, upscale=False)], VIDEO = 'video', 'Video'
# options={'quality': 95}) NEVIM = 'nevim', 'Neznámý typ'
# Zmenšené obrázky v přehledu obrázků a pod hlavním obrázkem (předchozí/následující) typ = models.CharField('Typ', max_length=16, blank=False, null=False, choices=Typ.choices, default=Typ.NEVIM)
# obrazek_maly = ImageSpecField(source='obrazek_velky', # Filename by default; slouží k řazení
# processors=[Transpose(Transpose.AUTO), ResizeToFit(167, 167, upscale=False)],
# options={'quality': 95})
nazev = models.CharField('Název', max_length=50, blank=True, null=True) nazev = models.CharField('Název', max_length=50, blank=True, null=True)
# ~~Rádoby~~ vtipný popisek od orgů
popis = models.TextField('Popis', blank=True, null=True) popis = models.TextField('Popis', blank=True, null=True)
datum_vlozeni = models.DateTimeField('Datum vložení', auto_now_add=True) datum_vlozeni = models.DateTimeField('Datum vložení', auto_now_add=True)
galerie = models.ForeignKey('Galerie', blank=True, null=True, on_delete=models.SET_NULL) galerie = models.ForeignKey('Galerie', blank=True, null=True, on_delete=models.SET_NULL)
# Primární klíč k řazení pro overridování řazení podle názvu
poradi = models.IntegerField('Pořadí', blank=True, null=True) poradi = models.IntegerField('Pořadí', blank=True, null=True)
def __str__(self): def __str__(self):
return self.obrazek_velky.name return f'Obrázek {self.nazev} ({self.soubor.name})'
class Meta: class Meta:
verbose_name = 'Obrázek' verbose_name = 'Obrázek'
verbose_name_plural = 'Obrázky' verbose_name_plural = 'Obrázky'
ordering = ['nazev'] ordering = ['nazev'] #FIXME: zohlednit pořadí a asi i příslušnost ke galerii
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
# obrázek potřebuje název, protože se z něj generuje cesta pro jeho uložení # obrázek potřebuje název, protože se z něj generuje cesta pro jeho uložení
@ -75,6 +78,17 @@ class Obrazek(models.Model):
self.nazev = os.path.basename(self.obrazek_velky.name) self.nazev = os.path.basename(self.obrazek_velky.name)
super(Obrazek, self).save(*args, **kwargs) super(Obrazek, self).save(*args, **kwargs)
def jako_bazmek(self):
"""
Hlavní metoda pro dělání `galerie.typy.ZobrazitelnyBazmek` z DB objektů
"""
import galerie.typy as typy # pozor, cyklí!
match self.typ:
case self.Typ.OBRAZEK: return typy.Obrazek(self)
case self.Typ.VIDEO: return typy.Video(self)
case self.Typ.NEVIM: return typy.DummyBazmek(self)
case _: raise ValueError("Neznámý typ obrázku, bug v kódu!")
class Galerie(models.Model): class Galerie(models.Model):
nazev = models.CharField('Název', max_length=100) nazev = models.CharField('Název', max_length=100)

View file

@ -1,5 +1,6 @@
{% extends "galerie/base.html" %} {% extends "galerie/base.html" %}
{% load bazmeky %}
{% block nadpis1a %} {% block nadpis1a %}
{{galerie.nazev}}: {{ obrazek.popis | default:"Fotka" }} {{galerie.nazev}}: {{ obrazek.popis | default:"Fotka" }}
@ -63,11 +64,12 @@
{% endwith %} {% endwith %}
{% endif%} {% endif%}
<span id="nahoru" class="kotva_obrazku"></span> <span id="nahoru" class="kotva_obrazku"></span>
<img src="{{obrazek.obrazek_stredni.url}}" {% zobrazit obrazek.jako_bazmek
height="{{vyska}}" height=vyska
width="{{sirka}}" width=sirka
alt="{{obrazek.popis}}" alt=obrazek.popis
class="obrazek"> title=obrazek.popis
class=obrazek" %}
{% if obrazky_dalsi %} {% if obrazky_dalsi %}
{% with obrazky_dalsi|first as dalsi_obrazek %} {% with obrazky_dalsi|first as dalsi_obrazek %}
@ -78,7 +80,7 @@
{% endif%} {% endif%}
</div> </div>
<!--<div>--> <!--<div>-->
<!--<a href="{{ obrazek.obrazek_velky.url }}">Obrázek v plné velikosti</a>--> <!--<a href="{{ obrazek.soubor.url }}">Obrázek v plné velikosti</a>-->
<!--</div>--> <!--</div>-->
{# Popisek fotky #} {# Popisek fotky #}
@ -109,21 +111,21 @@
{% endif %} {% endif %}
{# nahledy predchozich obrazku #} {# nahledy predchozich obrazku #}
{% for obrazek in obrazky_predchozi %} {% for obrazek in obrazky_predchozi %}
<a href="../{{obrazek.pk}}#nahoru"><img src="{{obrazek.obrazek_maly.url}}" height="100"></a> <a href="../{{obrazek.pk}}#nahoru">{% zmenseny_nahled obrazek.jako_bazmek height=100 %}</a>
{% endfor %} {% endfor %}
</div> </div>
<img src={{obrazek.obrazek_maly.url}} {% zmenseny_nahled obrazek.jako_bazmek
height="{{obrazek.obrazek_maly.height}}" alt=obrazek.popis
width="{{obrazek.obrazek_maly.width}}" class=obrazek
alt="{{obrazek.popis}}" id=prostredni %}
class="obrazek" {# height=obrazek.obrazek_maly.height
id="prostredni"> width=obrazek.obrazek_maly.width #}
<div class="navigace"> <div class="navigace">
{# nahledy nasledujicich obrazku #} {# nahledy nasledujicich obrazku #}
{% for obrazek in obrazky_dalsi %} {% for obrazek in obrazky_dalsi %}
<a href="../{{obrazek.pk}}#nahoru"><img src="{{obrazek.obrazek_maly.url}}" height="100"></a> <a href="../{{obrazek.pk}}#nahoru">{% zmenseny_nahled obrazek.jako_bazmek height=100 %}</a>
{% endfor %} {% endfor %}
{# odkaz na nasledujici galerii #} {# odkaz na nasledujici galerii #}
{% if nasledujici_galerie %} {% if nasledujici_galerie %}

View file

@ -1,5 +1,7 @@
{% extends "galerie/base.html" %} {% extends "galerie/base.html" %}
{% load bazmeky %}
{% block nadpis1a %} {% block nadpis1a %}
Galerie {{galerie.nazev}} Galerie {{galerie.nazev}}
{% endblock %} {% endblock %}
@ -23,7 +25,7 @@ Galerie {{galerie.nazev}}
{% if not obrazky %} {% if not obrazky %}
<div class="galerie_hlavicka"> <div class="galerie_hlavicka">
{% if galerie.titulni_obrazek %} {% if galerie.titulni_obrazek %}
<img src="{{ galerie.titulni_obrazek.obrazek_stredni.url }}" class="titulni_obrazek"> {% zobrazit galerie.titulni_obrazek.jako_bazmek class=titulni_obrazek %}
{% endif %} {% endif %}
</div> </div>
{% endif %} {% endif %}
@ -52,10 +54,7 @@ Galerie {{galerie.nazev}}
{% endif %} {% endif %}
class="podgalerie_nahled"> class="podgalerie_nahled">
{% if galerie.titulni_obrazek %} {% if galerie.titulni_obrazek %}
{% with galerie.titulni_obrazek.obrazek_maly as obrazek %} {% zmenseny_nahled galerie.titulni_obrazek.jako_bazmek %}
<img src="{{ obrazek.url }}"
/>
{% endwith %}
{% endif %} {% endif %}
<div class="nazev_galerie"> <div class="nazev_galerie">
{{ galerie|truncatechars:max_delka_nazvu }} {{ galerie|truncatechars:max_delka_nazvu }}
@ -87,13 +86,12 @@ Galerie {{galerie.nazev}}
{% if obrazek.popis %} {% if obrazek.popis %}
title="{{ obrazek.popis }}" title="{{ obrazek.popis }}"
{% endif %} {% endif %}
href="./{{obrazek.pk}}#nahoru" class="galerie_nahled"><span class="vystredeno"></span><img href="./{{obrazek.pk}}#nahoru" class="galerie_nahled">
src="{{obrazek.obrazek_maly.url}}" <span class="vystredeno"></span>
{% if obrazek.popis %} {% zmenseny_nahled obrazek.jako_bazmek %}
title="{{ obrazek.popis }}" {# title=obrazek.popis (a byl tu if, že se použil jen když existoval…)
{% endif %} width=obrazek.obrazek_maly.width
width="{{ obrazek.obrazek_maly.width }}" height=obrazek.obrazek_maly.height #}
height="{{ obrazek.obrazek_maly.height }}" />
</a> </a>
{% endfor %} {% endfor %}
<br> <br>

View file

@ -0,0 +1,13 @@
"""
Pomocné tagy pro zobrazování `galerie.typy.ZobrazitelnyBazmek`. Jen volají příslušnou metodu na bazmeku.
"""
from django import template
register = template.Library()
@register.simple_tag
def zobrazit(bazmek, /, **kwargs):
return bazmek.zobrazit(**kwargs)
@register.simple_tag
def zmenseny_nahled(bazmek, /, **kwargs):
return bazmek.zmenseny_nahled(**kwargs)

102
galerie/typy.py Normal file
View file

@ -0,0 +1,102 @@
"""
Naše typy objektů v galeriích.
V databázi jsou všechny v jedné tabulce, protože se liší jen prezentací navenek. Všechny implementují ~~rozhraní~~ ABC `ZobrazitelnyBazmek`.
Doporučené použití: TODO
"""
import abc
from datetime import datetime
from django.utils.safestring import mark_safe, SafeString
from django.utils.html import format_html, format_html_join
from imagekit import ImageSpec
from imagekit.cachefiles import ImageCacheFile
from imagekit.processors import ResizeToFit, Transpose
HTML = str | SafeString
from galerie.models import Obrazek as DbObrazek
class ZobrazitelnyBazmek(abc.ABC):
def __init__(self, db_objekt):
self.db_objekt = db_objekt
# zkratka
self.soubor = db_objekt.soubor
# Volá se z templatetagu
@abc.abstractmethod
def zobrazit(self, **kwargs) -> HTML:
"""To, co se zobrazí v galerii jako hlavní obrázek (při prohlížení konkrétního obrázku a jako tittulní obrázek u galerií, které nemají vlastní obrázky (kupř. Vávrovka 2015))"""
...
@abc.abstractmethod
def zmenseny_nahled(self, **kwargs) -> HTML:
"""Zmenšené obrázky v přehledu obrázků a pod hlavním obrázkem (předchozí/následující)"""
...
@property
#@abc.abstractmethod
def cas_porizeni(self) -> datetime | None:
# TODO: použít tohle na automatické řazení věcí. Má vytáhnout datum z metadat
raise NotImplementedError("Prosím, naimplementuj hledání času vzniku v metadatech (nebo vrať None).")
#TODO: nativní rozměry?
### Obrázky ###
# Odpovídá původnímu chování (bo se mi nechce vymýšlet novoty…
class ObrazekStredniSpec(ImageSpec):
"""Specifikace obrázku pro velké zobrazení"""
processors = [
Transpose(Transpose.AUTO), # Rotuj podle dat v EXIFu
ResizeToFit(900, 675, upscale=False),
]
format = 'JPEG'
options = {'quality': 95} # Proč tolik?
class ObrazekMalySpec(ImageSpec):
"""Specifikace obrázku pro náhledy (pod hlavním obrázkem nebo v přehledu galerie)"""
processors = [
Transpose(Transpose.AUTO),
ResizeToFit(167, 167, upscale=False),
]
format = 'JPEG'
options = {'quality': 95}
class Obrazek(ZobrazitelnyBazmek):
"""Obrázek pro zobrazení
Použije některý z ImageSpec-ů jako popis transformace a ImageCacheFile pro uložení výsledného obrázku.
Reference: https://django-imagekit.readthedocs.io/en/latest/#defining-specs-outside-of-models
Reference: https://django-imagekit.readthedocs.io/en/latest/caching.html
"""
def zobrazit(self, **kwargs):
# Jak se takový cachefile používá je potřeba vyčíst ze zdrojáků?
file = ImageCacheFile(ObrazekStredniSpec(source=self.soubor))
file.generate()
attrs = format_html_join(' ', r'{}="{}"', ((mark_safe(k), v) for k, v in kwargs.items()))
html = format_html(r'<img src="{}" {} />', file.url, attrs)
return html
def zmenseny_nahled(self, **kwargs):
file = ImageCacheFile(ObrazekMalySpec(source=self.soubor))
file.generate()
attrs = format_html_join(' ', r'{}="{}"', ((mark_safe(k), v) for k, v in kwargs.items()))
html = format_html(r'<img src="{}" {} />', file.url, attrs)
return html
class Video(ZobrazitelnyBazmek):
...
class DummyBazmek(ZobrazitelnyBazmek):
def zobrazit(self, **kwargs):
return r'<p>Tohle zobrazit neumím :-(</p>'
def zmenseny_nahled(self, **kwargs):
return r'<p>Tohle zobrazit neumím :-(</p>'
def tipniTyp(soubor) -> DbObrazek.Typ: ...