RFC: Videjka a další bazmeky do galerií #99

Open
ledoian wants to merge 18 commits from galerie_videjka into master
13 changed files with 375 additions and 99 deletions

View file

@ -1,13 +1,14 @@
from galerie.models import Obrazek, Galerie, VZDY, ORG, NIKDY, UCASTNIK
from galerie.models import Soubor, Galerie, VZDY, ORG, UCASTNIK
from django.contrib import admin
from django.http import HttpResponseRedirect
from django import forms
from django.db import models
# akction
def zverejnit_fotogalerii(modeladmin, request, queryset):
'''zverejni vybranou fotogalerii i jeji vsechny podgalerie'''
'''zveřejní vybranou fotogalerii i její všechny podgalerie'''
# TODO: rozbíjí práva. Čistší je mít separátně práva (zobrazit:
# VŽDY/ÚČASTNÍKŮM/ORGŮM) a úpravy (bool), přičemž během úprav se zobrazuje
# jen orgům?
queryset = queryset.filter(zobrazit=ORG)
for galerie in queryset:
galerie.zobrazit = VZDY
@ -18,7 +19,7 @@ def zverejnit_fotogalerii(modeladmin, request, queryset):
def prepnout_fotogalerii_do_org_rezimu(modeladmin, request, queryset):
'''zneverjni vybranou fotogalerii i jeji vsechny podgalerie'''
'''zneveřejni vybranou fotogalerii i její všechny podgalerie'''
queryset = queryset.filter(zobrazit=VZDY)
for galerie in queryset:
galerie.zobrazit = ORG
@ -29,15 +30,15 @@ def prepnout_fotogalerii_do_org_rezimu(modeladmin, request, queryset):
'Přepnout do režimu úprav (zneveřejní galerii)'
class GalerieInline(admin.TabularInline):
model = Obrazek
fields = ['obrazek_velky', 'nazev', 'popis', 'obrazek_maly_tag', 'poradi']
readonly_fields = ['nazev', 'obrazek_maly_tag']
model = Soubor
fields = ['soubor', 'nazev', 'popis', 'typ', 'poradi']
readonly_fields = ['nazev']
formfield_overrides = {
models.TextField: {'widget': forms.TextInput},
}
class ObrazekAdmin(admin.ModelAdmin):
list_display = ('obrazek_velky', 'nazev', 'popis', 'obrazek_maly_tag', 'poradi')
class SouborAdmin(admin.ModelAdmin):
list_display = ('soubor', 'nazev', 'popis', 'poradi')
search_fields = ['nazev','popis']
class GalerieAdmin(admin.ModelAdmin):
@ -50,5 +51,5 @@ class GalerieAdmin(admin.ModelAdmin):
save_on_top = True
ordering = ['galerie_up__nazev', 'poradi']
admin.site.register(Obrazek, ObrazekAdmin)
admin.site.register(Soubor, SouborAdmin)
admin.site.register(Galerie, GalerieAdmin)

View file

@ -1,8 +1,8 @@
from django import forms
class KomentarForm(forms.Form):
"""Formulář na přidání/úpravu popisku u souboru (obrázku)"""
komentar = forms.CharField(label = "Komentář:", max_length = 300, required=False)
class NewGalerieForm(forms.Form):
nazev = forms.CharField(label = "Název galerie", max_length = 100)
#popis = forms.CharField(label = "Popis", required = False, max_length = 2000, widget = forms.Textarea)

View file

@ -0,0 +1,33 @@
# Generated by Django 4.2.20 on 2025-05-05 00:21
from django.db import migrations, models
import galerie.models
def zatim_byly_jen_obrazky(apps, schema_editor):
Obrazek = apps.get_model("galerie", "Obrazek")
Obrazek.objects.all().update(typ='obrazek')
class Migration(migrations.Migration):
dependencies = [
('galerie', '0016_alter_obrazek_galerie'),
]
operations = [
migrations.RenameField(
model_name='obrazek',
old_name='obrazek_velky',
new_name='soubor',
),
migrations.AlterField(
model_name='obrazek',
name='soubor',
field=models.FileField(help_text='Lze vložit libovolně velký obrázek. Ideální je, aby alespoň jeden rozměr měl alespoň 500px.', upload_to=galerie.models.galerie_filename),
),
migrations.AddField(
model_name='obrazek',
name='typ',
field=models.CharField(choices=[('obrazek', 'Obrázek'), ('video', 'Video'), ('nevim', 'Neznámý typ')], default='nevim', max_length=16, verbose_name='Typ'),
),
migrations.RunPython(zatim_byly_jen_obrazky, migrations.RunPython.noop)
Review

Možná by směrem zpátky spíš mělo selhat, pokud v galerii existuje ne-obrázek…

Možná by směrem zpátky spíš mělo selhat, pokud v galerii existuje ne-obrázek…
]

View file

@ -6,16 +6,17 @@ from imagekit.processors import ResizeToFit, Transpose
import os
from soustredeni.models import Soustredeni
from galerie.utils import top_galerie
VZDY=0
ORG=1
NIKDY=2
# Pozor, diskontinuita, tady bylo `NIKDY`
UCASTNIK=3
# TODO: Enum!
VIDITELNOST = (
(VZDY, 'Vždy'),
(ORG, 'Organizátorům'),
(UCASTNIK, 'Účastníkům a orgům'),
(NIKDY, 'Nikdy'),
)
# tyhle funkce jsou tady jen kvůli starým migracím, které se na ně odkazují
@ -24,16 +25,17 @@ def obrazek_filename_maly():
pass
def obrazek_filename_stredni():
pass
def obrazek_filename():
pass
def obrazek_filename_velky():
pass
def obrazek_filename(self, filename):
def galerie_filename(self, filename):
gal = self.galerie
cislo_gal = gal.pk
# najdi kořenovou galerii
while (gal.galerie_up):
gal = gal.galerie_up
gal = top_galerie(gal)
# soustředění je v cestě jen pokud galerie pod nějaké patří
cesta = (
@ -44,50 +46,58 @@ def obrazek_filename(self, filename):
return os.path.join(*cesta)
class Obrazek(models.Model):
obrazek_velky = models.ImageField(upload_to=obrazek_filename,
help_text = "Lze vložit libovolně velký obrázek. Ideální je, aby alespoň jeden rozměr měl alespoň 500px.")
obrazek_stredni = ImageSpecField(source='obrazek_velky',
processors=[Transpose(Transpose.AUTO), ResizeToFit(900, 675, upscale=False)],
options={'quality': 95})
obrazek_maly = ImageSpecField(source='obrazek_velky',
processors=[Transpose(Transpose.AUTO), ResizeToFit(167, 167, upscale=False)],
options={'quality': 95})
nazev = models.CharField('Název', max_length=50, blank=True, null=True)
popis = models.TextField('Popis', blank=True, null=True)
class Soubor(models.Model):
# „originál“ (modulo max. velikost uploadu na web FIXME!)
soubor = models.FileField(upload_to=galerie_filename,
help_text = "Lze vložit libovolně velký obrázek/soubor. Ideální je, aby alespoň jeden rozměr měl alespoň 500px.")
class Typ(models.TextChoices):
OBRAZEK = 'obrazek', 'Obrázek'
VIDEO = 'video', 'Video'
NEVIM = 'nevim', 'Neznámý typ'
typ = models.CharField('Typ', max_length=16, blank=False, null=False, choices=Typ.choices, default=Typ.NEVIM)
# Filename by default; slouží k řazení
nazev = models.CharField('Jméno souboru', max_length=50, blank=True, null=True)
# ~~Rádoby~~ vtipný popisek od orgů
popis = models.TextField('Popisek', blank=True, null=True)
datum_vlozeni = models.DateTimeField('Datum vložení', auto_now_add=True)
galerie = models.ForeignKey('Galerie', blank=True, null=True, on_delete=models.CASCADE)
# Primární klíč k řazení pro overridování řazení podle názvu
poradi = models.IntegerField('Pořadí', blank=True, null=True)
def __str__(self):
return self.obrazek_velky.name
return f'Soubor {self.nazev} ({self.soubor.name})'
class Meta:
verbose_name = 'Obrázek'
verbose_name_plural = 'Obrázky'
ordering = ['nazev']
def obrazek_maly_tag(self):
if not self.obrazek_maly:
return ''
return u'<img src="{}">'.format(self.obrazek_maly.url)
obrazek_maly_tag.short_description = "Náhled"
obrazek_maly_tag.allow_tags = True
verbose_name = 'Soubor'
verbose_name_plural = 'Soubory'
db_table = 'galerie_obrazek' # FIXME: přejmenovat
ordering = ['galerie', 'poradi', 'nazev']
def save(self, *args, **kwargs):
# obrázek potřebuje název, protože se z něj generuje cesta pro jeho uložení
# (a pak se podle něj taky řadí)
if self.nazev is None:
self.nazev = os.path.basename(self.obrazek_velky.name)
super(Obrazek, self).save(*args, **kwargs)
self.nazev = os.path.basename(self.soubor.name)
super().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):
nazev = models.CharField('Název', max_length=100)
nazev = models.CharField('Název galerie', max_length=100, help_text='Např. "Soustředění Bratrouchov 2025" nebo "Pondělí", ukazuje se v cestičce')
datum_vytvoreni = models.DateTimeField('Datum vytvoření', auto_now_add = True)
datum_zmeny = models.DateTimeField('Datum poslední změny', auto_now = True)
popis = models.TextField('Popis', blank = True, null = True)
titulni_obrazek = models.ForeignKey(Obrazek, blank = True, null = True, related_name = "+", on_delete = models.SET_NULL)
poznamka = models.TextField('neveřejná poznámka', blank = True, null = True)
titulni_obrazek = models.ForeignKey(Soubor, blank = True, null = True, related_name = "+", on_delete = models.SET_NULL)
zobrazit = models.IntegerField('Zobrazit?', default = ORG, choices = VIDITELNOST)
galerie_up = models.ForeignKey('Galerie', blank = True, null = True,
on_delete=models.PROTECT)
@ -100,3 +110,4 @@ class Galerie(models.Model):
class Meta:
verbose_name = 'Galerie'
verbose_name_plural = 'Galerie'
db_table = 'galerie_galerie'

View file

@ -6,9 +6,10 @@
/* velká fotka */
/* zmenšování spolu s oknem prohlížeče */
.galerie .obrazek, .titulni_obrazek {
max-width: 100%;
max-width: max(100%, 900px);
zelvuska marked this conversation as resolved
Review

900 je nějak moc, ne? (Třeba když jsi na mobilu, tak to znamená, že uvidíš minimum z toho obrázku, ne?)

900 je nějak moc, ne? (Třeba když jsi na mobilu, tak to znamená, že uvidíš minimum z toho obrázku, ne?)
Review

To je vzaté z původních views.py, za to nemůžu já :-D Ale je to hodně, co třeba 600px? (Haluzím velmi náhodně a na monitoru, zas to nemůže být moc málo, jinak se to zmenší moc…)

To je vzaté z původních `views.py`, za to nemůžu já :-D Ale je to hodně, co třeba `600px`? (Haluzím velmi náhodně a na monitoru, zas to nemůže být moc málo, jinak se to zmenší moc…)
Review

Aha, tak to asi nechme…

Aha, tak to asi nechme…
max-height: 900px;
height: auto;
width: auto\9; /* ie8 */
width: auto;
}
.predchozi_obrazek{
@ -75,6 +76,8 @@
.galerie_nahledy img {
margin: 10px;
max-height: 100px;
max-width: 200px;
Review

Hodnoty random nastřelené… (asi nevadí)

Hodnoty random nastřelené… (asi nevadí)
}
.galerie_nahledy div.navigace {

View file

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="768"
height="576"
viewBox="0 0 768 576"
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="layer1">
<path
style="fill:#6f2509;stroke:#6f2509;stroke-width:24.4876;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1;paint-order:markers fill stroke;fill-opacity:1"
id="path1"
d="m 171.56486,156.34958 277.81099,481.18278 -555.62201,-2e-5 z"
transform="translate(212.43515,-108.94158)" />
<g
id="g4"
style="fill:#ff0000"
transform="matrix(0.63613865,0,0,0.62847938,383.26722,188.1938)">
<circle
style="fill:#ff0000;fill-opacity:1;stroke:none;stroke-width:32.6502;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1;paint-order:markers fill stroke"
id="path2"
cx="0"
cy="430.19247"
r="64.870293" />
<path
style="fill:#ff0000;fill-opacity:1;stroke:none;stroke-width:24.4876;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1;paint-order:markers fill stroke"
id="path3"
d="m -93.037653,283.56151 -174.608127,-302.430127 349.216236,-10e-6 z"
transform="matrix(0.69829007,0,0,1.0128734,64.967275,29.408685)" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="768"
height="576"
viewBox="0 0 768 576"
version="1.1"
id="svg1"
sodipodi:docname="video_placeholder.svg"
inkscape:version="1.4.1 (93de688d07, 2025-03-30)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#ffffff"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="1.1715686"
inkscape:cx="377.69874"
inkscape:cy="285.08787"
inkscape:window-width="1920"
inkscape:window-height="1164"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="svg1" />
<defs
id="defs1" />
<g
id="layer2"
style="stroke:#aaaaaa;stroke-opacity:1;fill:#cccccc;fill-opacity:1">
<g
id="rect1"
style="opacity:1;fill:#cccccc;fill-opacity:1"
transform="matrix(0.95921938,0,0,0.86504219,15.659757,38.867849)">
<path
style="baseline-shift:baseline;display:inline;overflow:visible;vector-effect:none;stroke:none;stroke-linecap:round;stroke-linejoin:round;paint-order:markers fill stroke;enable-background:accumulate;stop-color:#000000;stop-opacity:1;fill:#cccccc;fill-opacity:1"
d="M 0,-12.244141 C -6.7620501,-12.243653 -12.243653,-6.7620501 -12.244141,0 v 576 c 4.89e-4,6.76205 5.4820913,12.24365 12.244141,12.24414 h 768 c 6.76205,-4.9e-4 12.24365,-5.48209 12.24414,-12.24414 V 0 C 780.24365,-6.7620497 774.76205,-12.243652 768,-12.244141 Z"
id="path4" />
</g>
</g>
<g
id="layer1">
<g
id="path1"
style="opacity:1;fill:#ff0000;fill-opacity:0.771591"
transform="matrix(0.77572769,0,0,0.77572769,169.75532,213.5323)">
<path
style="baseline-shift:baseline;display:inline;overflow:visible;vector-effect:none;fill:#f5b34f;fill-opacity:1;stroke-linecap:round;stroke-linejoin:round;paint-order:markers fill stroke;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
d="m 54.501953,-169.58203 c -5.851459,1.03154 -10.117969,6.11495 -10.119141,12.05664 v 507.05078 c 8.82e-4,9.42554 10.204294,15.31604 18.367188,10.60352 L 501.86719,106.60352 c 8.16111,-4.71353 8.16111,-16.493503 0,-21.207036 L 62.75,-168.12891 c -2.493096,-1.43876 -5.413362,-1.95325 -8.248047,-1.45312 z"
id="path3" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View file

@ -1,5 +1,6 @@
{% extends "galerie/base.html" %}
{% load bazmeky %}
{% block nadpis1a %}
{{galerie.nazev}}: {{ obrazek.popis | default:"Fotka" }}
@ -64,11 +65,7 @@
{% endwith %}
{% endif%}
<span id="nahoru" class="kotva_obrazku"></span>
<img src="{{obrazek.obrazek_stredni.url}}"
height="{{vyska}}"
width="{{sirka}}"
alt="{{obrazek.popis}}"
class="obrazek">
{% zobrazit obrazek.jako_bazmek alt=obrazek.popis title=obrazek.popis class="obrazek" %}
{% if obrazky_dalsi %}
{% with obrazky_dalsi|first as dalsi_obrazek %}
@ -79,7 +76,7 @@
{% endif%}
</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>-->
{# Popisek fotky #}
@ -110,21 +107,14 @@
{% endif %}
{# nahledy predchozich obrazku #}
{% 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>
Review

zrušit height?

zrušit height?
{% endfor %}
</div>
<img src={{obrazek.obrazek_maly.url}}
height="{{obrazek.obrazek_maly.height}}"
width="{{obrazek.obrazek_maly.width}}"
alt="{{obrazek.popis}}"
class="obrazek"
id="prostredni">
{% zmenseny_nahled obrazek.jako_bazmek alt=obrazek.popis class="obrazek" id="prostredni" %}
<div class="navigace">
{# nahledy nasledujicich obrazku #}
{% 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>
Review

i tady zrušit height?

i tady zrušit height?
{% endfor %}
{# odkaz na nasledujici galerii #}
{% if nasledujici_galerie %}

View file

@ -1,5 +1,7 @@
{% extends "galerie/base.html" %}
{% load bazmeky %}
{% block nadpis1a %}
Galerie {{galerie.nazev}}
{% endblock %}
@ -26,7 +28,7 @@ Galerie {{galerie.nazev}}
{% if not obrazky %}
<div class="galerie_hlavicka">
{% 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 %}
</div>
{% endif %}
@ -55,10 +57,7 @@ Galerie {{galerie.nazev}}
{% endif %}
class="podgalerie_nahled {% if pgalerie.zobrazit == 1 or pgalerie.zobrazit == 2 %}mam-org-only{% endif %}{% if pgalerie.zobrazit == 3 %}mam-resitel-only{% endif %}">
{% if pgalerie.titulni_obrazek %}
{% with pgalerie.titulni_obrazek.obrazek_maly as obrazek %}
<img src="{{ obrazek.url }}"
/>
{% endwith %}
{% zmenseny_nahled pgalerie.titulni_obrazek.jako_bazmek class="" %}
Review

Má mít class nebo tam ten atribut nemá být vůbec

Má mít class nebo tam ten atribut nemá být vůbec
{% endif %}
<div class="nazev_galerie">
{{ pgalerie|truncatechars:max_delka_nazvu }}
@ -96,13 +95,10 @@ Galerie {{galerie.nazev}}
{% if obrazek.popis %}
title="{{ obrazek.popis }}"
{% endif %}
href="./{{obrazek.pk}}#nahoru" class="galerie_nahled"><span class="vystredeno"></span><img
src="{{obrazek.obrazek_maly.url}}"
{% if obrazek.popis %}
title="{{ obrazek.popis }}"
{% endif %}
width="{{ obrazek.obrazek_maly.width }}"
height="{{ obrazek.obrazek_maly.height }}" />
href="./{{obrazek.pk}}#nahoru" class="galerie_nahled">
<span class="vystredeno"></span>
{% zmenseny_nahled obrazek.jako_bazmek %}
{# title=obrazek.popis (a byl tu if, že se použil jen když existoval…) width=obrazek.obrazek_maly.width height=obrazek.obrazek_maly.height #}
Review

smazat? title by se měl použít z vnějšího tagu, či?

smazat? title by se měl použít z vnějšího tagu, či?
</a>
{% endfor %}
<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)

140
galerie/typy.py Normal file
View file

@ -0,0 +1,140 @@
"""
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
import logging
logger = logging.getLogger(__name__)
from django.contrib.staticfiles.finders import find
# FIXME: static fily na lokálním webu??
find = lambda x: '/static/'+x
Review

FIXME

FIXME
from django.utils.safestring import mark_safe, SafeString
from django.utils.html import format_html, format_html_join
from django.urls import reverse
from imagekit import ImageSpec
from imagekit.cachefiles import ImageCacheFile
from imagekit.processors import ResizeToFit, Transpose
HTML = str | SafeString
Review

Komentář, že v Py3.12 má být type HTML = ...

Komentář, že v Py3.12 má být `type HTML = ...`
from galerie.models import Soubor
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}
def _fmt_attrs(attrs):
return format_html_join(' ', r'{}="{}"', ((mark_safe(k), v) for k, v in attrs.items()))
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 = _fmt_attrs(kwargs)
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 = _fmt_attrs(kwargs)
html = format_html(r'<img src="{}" {} />', file.url, attrs)
return html
class Video(ZobrazitelnyBazmek):
def __init__(self, *a, **kwa):
super().__init__(*a, **kwa)
self.placeholder = find('galerie/video_placeholder.svg')
def zobrazit(self, **kwargs):
attrs = _fmt_attrs(kwargs)
# Atributy specifické pro video musíme vesměs vyřešit tady… (šlo by to {% if %}-ovat podle typu, ale to je spíš haluz.
html = format_html(r'<video src="{}" preload="metadata" controls {} />', self.soubor.url, attrs)
return html
def zmenseny_nahled(self, **kwargs):
attrs = _fmt_attrs(kwargs)
return format_html(r'<img src="{}" {} />', self.placeholder, attrs)
class DummyBazmek(ZobrazitelnyBazmek):
def __init__(self, *a, **kwa):
super().__init__(*a, **kwa)
self.placeholder = find('galerie/neznamy_placeholder.svg')
def zobrazit(self, **kwargs):
attrs = _fmt_attrs(kwargs)
# Stavíme HTML ad-hoc, co se může rozbít :'-)
obrazek = format_html(r'<img src="{}"/>', self.placeholder, attrs)
popisek = format_html(r'<p style="text-align: center; font-size: 1.1em;">Prosím oprav typ aktuálního obrázku v <a href="{}">adminu</a>!</p>', reverse('admin:galerie_soubor_change', args=(self.db_objekt.id,)))
return format_html(r'<div {}>{}{}</div>', attrs, obrazek, popisek)
def zmenseny_nahled(self, **kwargs):
attrs = _fmt_attrs(kwargs)
return format_html(r'<img src="{}" {} />', self.placeholder, attrs)
def tipniTyp(soubor) -> Soubor.Typ:
from PIL import Image, UnidentifiedImageError
try:
Image.open(soubor)
return Soubor.Typ.OBRAZEK
except UnidentifiedImageError:
return Soubor.Typ.NEVIM
logger.warning("Nepodařilo se tipnout typ nečekaným způsobem!")
return Soubor.Typ.NEVIM

View file

@ -1,4 +1,8 @@
from galerie.models import Galerie
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from galerie.models import Galerie
# Miluju pythoní typing :-P
def top_galerie(g: Galerie) -> Galerie:
while g.galerie_up is not None:

View file

@ -8,7 +8,7 @@ from datetime import datetime
from galerie.utils import top_galerie
from personalni.utils import resitel_uzivatele
from galerie.models import Obrazek, Galerie, VZDY, ORG, UCASTNIK, NIKDY
from galerie.models import Soubor, Galerie, VZDY, ORG, UCASTNIK
from soustredeni.models import Soustredeni
from galerie.forms import KomentarForm, NewGalerieForm
@ -16,7 +16,7 @@ import logging
logger = logging.getLogger(__name__)
def galerie_ke_zobrazeni(soustredeni: Soustredeni | None, request: HttpRequest) -> tuple[int]:
if request.user.is_superuser: return (VZDY, ORG, UCASTNIK, NIKDY)
if request.user.is_superuser: return (VZDY, ORG, UCASTNIK)
if request.user.je_org: return (VZDY, ORG, UCASTNIK)
if request.user.is_anonymous: return (VZDY,)
if soustredeni is None: return (VZDY,)
@ -35,7 +35,7 @@ def zobrazit(galerie: Galerie, request: HttpRequest) -> bool:
def dovolit_upravy_popisku(galerie: Galerie, request: HttpRequest) -> bool:
# FIXME: Dočasné: úpravy jen když je to v org-only stavu. (Odpovídá předchozímu chování)
return request.user.je_org and galerie.zobrazit in (ORG, NIKDY)
return request.user.je_org and galerie.zobrazit in (ORG,)
def cesta_od_korene(g):
@ -56,7 +56,7 @@ def nahled(request, pk, soustredeni):
podgalerie = Galerie.objects.filter(galerie_up = galerie).order_by('poradi')
podgalerie = podgalerie.filter(zobrazit__in=galerie_ke_zobrazeni(soustredeni, request))
obrazky = galerie.obrazek_set.all().order_by('poradi', 'nazev')
obrazky = galerie.soubor_set.all().order_by('poradi', 'nazev')
ma_se_zobrazit = zobrazit(galerie, request)
if not ma_se_zobrazit: raise Http404("Galerie sice existuje, ale my se tváříme, že ne :-D")
@ -89,20 +89,16 @@ def nahled(request, pk, soustredeni):
def detail(request, pk, fotka, soustredeni):
"""Zobrazeni nahledu fotky s id 'fotka'."""
MAX_VYSKA = 900
MAX_SIRKA = 900
MAX_VYSKA_MALA = 100
MAX_SIRKA_MALA = 200
NAHLEDU = 1
galerie = get_object_or_404(Galerie, pk=pk)
soustredeni = top_galerie(galerie).soustredeni
ma_se_zobrazit = zobrazit(galerie, request)
if not ma_se_zobrazit: raise Http404("Obrázek neukážu!")
obrazek = get_object_or_404(Obrazek, pk=fotka)
obrazek = get_object_or_404(Soubor, pk=fotka)
# Pořadí není povinné. FIXME: `nazev` je zavádějící… Ale tohle je kanonické pořadí obrázků v galerii…
obrazky = galerie.obrazek_set.all().order_by('poradi', 'nazev')
obrazky = galerie.soubor_set.all().order_by('poradi', 'nazev')
obrazky = list(obrazky)
index_obrazku = obrazky.index(obrazek)
# Podle mě se nemůže stát, že by volání výš selhalo, kdyžtak shodí web. (původně to byl explicitně ošetřený stav dávající 404)
@ -141,23 +137,11 @@ def detail(request, pk, fotka, soustredeni):
else:
form = KomentarForm({'komentar': obrazek.popis})
# Preskalovani obrazku do vybraneho prostoru.
vyska = obrazek.obrazek_stredni.height
sirka = obrazek.obrazek_stredni.width
if vyska > MAX_VYSKA:
sirka = sirka * MAX_VYSKA / vyska
vyska = MAX_VYSKA
if sirka > MAX_SIRKA:
vyska = vyska * MAX_SIRKA / sirka
sirka = MAX_SIRKA
return render(request, 'galerie/Galerie.html',
{'galerie' : galerie,
'predchozi_galerie' : predchozi_galerie,
'nasledujici_galerie' : nasledujici_galerie,
'obrazek' : obrazek,
'vyska' : vyska,
'sirka' : sirka,
'obrazky_predchozi' : predchozi_obrazky,
'obrazky_dalsi' : nasledujici_obrazky,
'upravy_popisku' : dovolit_upravy_popisku(galerie, request),
@ -200,9 +184,11 @@ def new_galerie(request, galerie, soustredeni):
gal.save()
# zpracovani obrazku v galerii
from galerie.typy import tipniTyp
for obr in request.FILES.getlist('obr'):
o = Obrazek()
o.obrazek_velky = obr
o = Soubor()
o.soubor = obr
o.typ = tipniTyp(obr)
o.nazev = str(obr)
o.galerie = gal
o.save()