Compare commits

..

17 commits

Author SHA1 Message Date
Pavel "LEdoian" Turinsky
d6653cf9dc Merge branch 'galerie_videjka' into videjka_a_zvyrazneni 2025-05-05 17:15:15 +02:00
Pavel "LEdoian" Turinsky
d07d4daf04 Moc věcí se jmenuje obrázek… 2025-05-05 17:14:54 +02:00
Pavel "LEdoian" Turinsky
63d1d4afeb Merge branch 'odevzdavatko_zvyrazneni_mych_problemu' into videjka_a_zvyrazneni 2025-05-05 16:57:21 +02:00
Pavel "LEdoian" Turinsky
e0939e0e03 Hezčí video placeholder 2025-05-05 16:48:18 +02:00
Pavel "LEdoian" Turinsky
96b04b6a09 Placeholdery vol 2: hezké chování 2025-05-05 16:44:09 +02:00
Pavel "LEdoian" Turinsky
289e09f558 Placeholdery, vol 1 2025-05-05 16:25:51 +02:00
Pavel "LEdoian" Turinsky
5d67227bfe Videjkaaa 2025-05-05 15:33:48 +02:00
Pavel "LEdoian" Turinsky
cbffa32887 Tipování typů + drobné úpravy Admina 2025-05-05 15:17:35 +02:00
Pavel "LEdoian" Turinsky
b001b79349 Náhodné opravy 2025-05-05 04:50:06 +02:00
Pavel "LEdoian" Turinsky
372e9dcada Tagy nemůžou být víceřádkové :-/ 2025-05-05 04:24:46 +02:00
Pavel "LEdoian" Turinsky
a7f8fa0600 Zrušeny další obrazek_* 2025-05-05 04:17:39 +02:00
Pavel "LEdoian" Turinsky
fa4729ad45 Migrace (ručně editovaná, ale vypadá, že funguje…) 2025-05-05 02:29:21 +02:00
Pavel "LEdoian" Turinsky
9786c42c4a Merge branch 'master' into galerie_videjka 2025-05-05 02:13:27 +02:00
Pavel "LEdoian" Turinsky
b25d475148 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
2025-05-05 01:50:08 +02:00
Pavel "LEdoian" Turinsky
b7498b42b2 Galerie: z obrazek_velký FileField, ostatní dočasně zrušené [WIP!]
Plán: udělat z `obrazek_velky` `soubor`, přidat `typ` (`OBRAZEK`,
`VIDEO`, `NEVIM`) a ImageKit použít jen na vyrábění zmenšených obrázků
(pro `VIDEO` použít `<video>` přímo; pro `NEVIM` nějaký generický
placeholder a pokyn orgovi, ať to opraví v Adminu.

Ref: https://django-imagekit.readthedocs.io/en/latest/#defining-specs-outside-of-models
2025-05-01 00:20:51 +02:00
Pavel "LEdoian" Turinsky
e835d0ab48 Galerie: Co je který obrázek_velikost 2025-05-01 00:20:23 +02:00
Pavel "LEdoian" Turinsky
def6c0ede7 Galerie: Zrušení obrazek_maly_tag
už od Dj 2.0 nefunkční, kdyžtak se přidá zpět později…
2025-05-01 00:18:44 +02:00
35 changed files with 1101 additions and 1126 deletions

View file

@ -91,7 +91,7 @@ TODO: tabulka není úplná. Pokud na něco narazíte, tak ji prosím doplňte.
Ubuntu 22.10, ??, ``python3-venv``, ``python3-dev``, ``libpq-dev``, "Je potřeba zapnout zdroj ``universe`` a nainstalovat kompilátor C (``gcc``)?" Ubuntu 22.10, ??, ``python3-venv``, ``python3-dev``, ``libpq-dev``, "Je potřeba zapnout zdroj ``universe`` a nainstalovat kompilátor C (``gcc``)?"
Linux Mint 21, ??, ``python3-venv``, ``python3-dev``, ``libpq-dev``, "" Linux Mint 21, ??, ``python3-venv``, ``python3-dev``, ``libpq-dev``, ""
Archlinux 2022.11.01, AUR, vestavěný, vestavěné, ``postgresql-libs``, "Je potřeba céčkový kompilátor (``gcc``); nezapomenout vygenerovat locale ``cs_CZ.UTF-8``" Archlinux 2022.11.01, AUR, vestavěný, vestavěné, ``postgresql-libs``, "Je potřeba céčkový kompilátor (``gcc``)"
openSUSE Leap 15.4, oficiální (``python39``), předinstalovaný?, ``python39-devel``, ??FIXME!!, "Výchozí verze pythonu je 3.6 a ta je moc stará, potřeba instalovat ``gcc``. Nevím jak sehnat pg_config." openSUSE Leap 15.4, oficiální (``python39``), předinstalovaný?, ``python39-devel``, ??FIXME!!, "Výchozí verze pythonu je 3.6 a ta je moc stará, potřeba instalovat ``gcc``. Nevím jak sehnat pg_config."
Debian 11, "oficiální, výchozí", ??, ??, ??, "Určitě to tam rozběhat jde, protože Gimli. Nejspíš bude relativně podobné Ubuntu." Debian 11, "oficiální, výchozí", ??, ??, ??, "Určitě to tam rozběhat jde, protože Gimli. Nejspíš bude relativně podobné Ubuntu."

View file

@ -30,14 +30,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', 'obrazek_maly_tag', 'poradi'] fields = ['soubor', 'nazev', 'popis', 'typ', 'poradi']
readonly_fields = ['nazev', 'obrazek_maly_tag'] 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

@ -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)
]

View file

@ -24,10 +24,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
@ -44,43 +46,51 @@ 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):
obrazek_velky = models.ImageField(upload_to=obrazek_filename, # „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. 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.")
obrazek_stredni = ImageSpecField(source='obrazek_velky', class Typ(models.TextChoices):
processors=[Transpose(Transpose.AUTO), ResizeToFit(900, 675, upscale=False)], OBRAZEK = 'obrazek', 'Obrázek'
options={'quality': 95}) VIDEO = 'video', 'Video'
obrazek_maly = ImageSpecField(source='obrazek_velky', NEVIM = 'nevim', 'Neznámý typ'
processors=[Transpose(Transpose.AUTO), ResizeToFit(167, 167, upscale=False)], typ = models.CharField('Typ', max_length=16, blank=False, null=False, choices=Typ.choices, default=Typ.NEVIM)
options={'quality': 95}) # Filename by default; slouží k řazení
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.CASCADE) 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) 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 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
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í
# (a pak se podle něj taky řadí) # (a pak se podle něj taky řadí)
if self.nazev is None: if self.nazev is None:
self.nazev = os.path.basename(self.obrazek_velky.name) self.nazev = os.path.basename(self.soubor.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

@ -6,9 +6,10 @@
/* velká fotka */ /* velká fotka */
/* zmenšování spolu s oknem prohlížeče */ /* zmenšování spolu s oknem prohlížeče */
.galerie .obrazek, .titulni_obrazek { .galerie .obrazek, .titulni_obrazek {
max-width: 100%; max-width: max(100%, 900px);
max-height: 900px;
height: auto; height: auto;
width: auto\9; /* ie8 */ width: auto;
} }
.predchozi_obrazek{ .predchozi_obrazek{
@ -75,6 +76,8 @@
.galerie_nahledy img { .galerie_nahledy img {
margin: 10px; margin: 10px;
max-height: 100px;
max-width: 200px;
} }
.galerie_nahledy div.navigace { .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" %} {% extends "galerie/base.html" %}
{% load bazmeky %}
{% block nadpis1a %} {% block nadpis1a %}
{{galerie.nazev}}: {{ obrazek.popis | default:"Fotka" }} {{galerie.nazev}}: {{ obrazek.popis | default:"Fotka" }}
@ -64,11 +65,7 @@
{% 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 alt=obrazek.popis title=obrazek.popis class="obrazek" %}
height="{{vyska}}"
width="{{sirka}}"
alt="{{obrazek.popis}}"
class="obrazek">
{% if obrazky_dalsi %} {% if obrazky_dalsi %}
{% with obrazky_dalsi|first as dalsi_obrazek %} {% with obrazky_dalsi|first as dalsi_obrazek %}
@ -79,7 +76,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 #}
@ -110,21 +107,14 @@
{% 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>
<img src={{obrazek.obrazek_maly.url}} {% zmenseny_nahled obrazek.jako_bazmek alt=obrazek.popis class="obrazek" id="prostredni" %}
height="{{obrazek.obrazek_maly.height}}"
width="{{obrazek.obrazek_maly.width}}"
alt="{{obrazek.popis}}"
class="obrazek"
id="prostredni">
<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 %}
@ -26,7 +28,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 %}
@ -55,10 +57,7 @@ Galerie {{galerie.nazev}}
{% endif %} {% endif %}
class="podgalerie_nahled {% if pgalerie.zobrazit == 1 or pgalerie.zobrazit == 2 %}mam-org-only{% endif %}{% if pgalerie.zobrazit == 3 %}mam-resitel-only{% 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 %} {% if pgalerie.titulni_obrazek %}
{% with pgalerie.titulni_obrazek.obrazek_maly as obrazek %} {% zmenseny_nahled pgalerie.titulni_obrazek.jako_bazmek class="" %}
<img src="{{ obrazek.url }}"
/>
{% endwith %}
{% endif %} {% endif %}
<div class="nazev_galerie"> <div class="nazev_galerie">
{{ pgalerie|truncatechars:max_delka_nazvu }} {{ pgalerie|truncatechars:max_delka_nazvu }}
@ -96,13 +95,10 @@ 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…) width=obrazek.obrazek_maly.width height=obrazek.obrazek_maly.height #}
{% endif %}
width="{{ obrazek.obrazek_maly.width }}"
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)

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
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
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}
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_obrazek_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) -> DbObrazek.Typ:
from PIL import Image, UnidentifiedImageError
try:
Image.open(soubor)
return DbObrazek.Typ.OBRAZEK
except UnidentifiedImageError:
return DbObrazek.Typ.NEVIM
logger.warning("Nepodařilo se tipnout typ nečekaným způsobem!")
return DbObrazek.Typ.NEVIM

View file

@ -89,10 +89,6 @@ def nahled(request, pk, soustredeni):
def detail(request, pk, fotka, soustredeni): def detail(request, pk, fotka, soustredeni):
"""Zobrazeni nahledu fotky s id 'fotka'.""" """Zobrazeni nahledu fotky s id 'fotka'."""
MAX_VYSKA = 900
MAX_SIRKA = 900
MAX_VYSKA_MALA = 100
MAX_SIRKA_MALA = 200
NAHLEDU = 1 NAHLEDU = 1
galerie = get_object_or_404(Galerie, pk=pk) galerie = get_object_or_404(Galerie, pk=pk)
@ -141,23 +137,11 @@ def detail(request, pk, fotka, soustredeni):
else: else:
form = KomentarForm({'komentar': obrazek.popis}) 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', return render(request, 'galerie/Galerie.html',
{'galerie' : galerie, {'galerie' : galerie,
'predchozi_galerie' : predchozi_galerie, 'predchozi_galerie' : predchozi_galerie,
'nasledujici_galerie' : nasledujici_galerie, 'nasledujici_galerie' : nasledujici_galerie,
'obrazek' : obrazek, 'obrazek' : obrazek,
'vyska' : vyska,
'sirka' : sirka,
'obrazky_predchozi' : predchozi_obrazky, 'obrazky_predchozi' : predchozi_obrazky,
'obrazky_dalsi' : nasledujici_obrazky, 'obrazky_dalsi' : nasledujici_obrazky,
'upravy_popisku' : dovolit_upravy_popisku(galerie, request), 'upravy_popisku' : dovolit_upravy_popisku(galerie, request),
@ -200,9 +184,11 @@ def new_galerie(request, galerie, soustredeni):
gal.save() gal.save()
# zpracovani obrazku v galerii # zpracovani obrazku v galerii
from galerie.typy import tipniTyp
for obr in request.FILES.getlist('obr'): for obr in request.FILES.getlist('obr'):
o = Obrazek() o = Obrazek()
o.obrazek_velky = obr o.soubor = obr
o.typ = tipniTyp(obr)
o.nazev = str(obr) o.nazev = str(obr)
o.galerie = gal o.galerie = gal
o.save() o.save()

View file

@ -1,19 +1,19 @@
.textzanaseni { display:none; } .textzanaseni { display:none; }
.textzastarale { display:none; } .textzastarale { display:none; }
#prekomentar, #prekorektura, #prepointer { display: none; } #prekomentar, #preoprava, #prepointer { display: none; }
body { body {
&[data-stav_pdf="pridavani"] { &[data-status="pridavani"] {
background: #f3f3f3; background: #f3f3f3;
} }
&[data-stav_pdf="zanaseni"] { &[data-status="zanaseni"] {
background: yellow; background: yellow;
.textzanaseni { display: unset; } .textzanaseni { display: unset; }
} }
&[data-stav_pdf="zastarale"] { &[data-status="zastarale"] {
background: red; background: red;
.textzastarale { display: unset; } .textzastarale { display: unset; }
@ -28,25 +28,25 @@ body {
img{background:white;} img{background:white;}
/* Barvy korektur */ /* Barvy korektur */
[data-stav_korektury="k_oprave"] { [data-opravastatus="k_oprave"] {
--rgb: 255, 0, 0; --rgb: 255, 0, 0;
[value="k_oprave"] { display: none } [value="k_oprave"] { display: none }
.komentovat_disabled { display: none } .komentovat_disabled { display: none }
} }
[data-stav_korektury="opraveno"] { [data-opravastatus="opraveno"] {
--rgb: 0, 0, 255; --rgb: 0, 0, 255;
[value="opraveno"] { display: none } [value="opraveno"] { display: none }
.komentovat { display: none } .komentovat { display: none }
} }
[data-stav_korektury="neni_chyba"] { [data-opravastatus="neni_chyba"] {
--rgb: 128, 128, 128; --rgb: 128, 128, 128;
[value="neni_chyba"] { display: none } [value="neni_chyba"] { display: none }
.komentovat { display: none } .komentovat { display: none }
} }
[data-stav_korektury="k_zaneseni"] { [data-opravastatus="k_zaneseni"] {
--rgb: 0, 255, 0; --rgb: 0, 255, 0;
[value="k_zaneseni"] { display: none } [value="k_zaneseni"] { display: none }
@ -54,16 +54,10 @@ img{background:white;}
} }
/* Skrývání korektur */ /* Skrývání korektur */
[data-korektura_sbalena="true"] { [data-opravazobrazit="false"] {
.korektura-telo { display: none; } .corr-body { display: none; }
.korektura-tlacitka { display: none; } .corr-buttons { display: none; }
.sbal-rozbal-img { transform: rotate(180deg); } .toggle-button { transform: rotate(180deg); }
}
/* Skrývání komentářů */
[data-komentar_sbalen="true"] {
.sbal-rozbal-img { transform: rotate(180deg); }
.uprav-komentar { display: none; }
.komtext { display: none; }
} }
@ -78,14 +72,14 @@ img{background:white;}
--alpha: 0.35; --alpha: 0.35;
/* Zvýraznění čáry při najetí na korekturu */ /* Zvýraznění čáry při najetí na korekturu */
&[data-hover="true"] { &[data-highlight="true"] {
border-width: 3px; border-width: 3px;
--alpha: 1; --alpha: 1;
} }
} }
/* Korektura samotná */ /* Korektura samotná */
.korektura { .oprava {
margin: 1px; margin: 1px;
background-color: white; background-color: white;
width: 300px; width: 300px;
@ -112,11 +106,11 @@ img{background:white;}
button img { pointer-events: none; } button img { pointer-events: none; }
.hlavicka-komentare { .corr-header {
overflow: auto; overflow: auto;
} }
.autor { .author {
font-weight: bold; font-weight: bold;
float: left; float: left;
margin-top: 3px; margin-top: 3px;
@ -139,7 +133,7 @@ form {
} }
/* Přidávání korektury / úprava komentáře */ /* Přidávání korektury / úprava komentáře */
#korekturovaci-formular-div { #commform-div {
position: absolute; position: absolute;
background-color: white; background-color: white;
padding: 3px; padding: 3px;
@ -154,8 +148,8 @@ form {
margin: 2px; margin: 2px;
padding: 2px; padding: 2px;
&[data-vybran="false"] { background: unset !important; } &[data-selected="false"] { background: unset !important; }
/*&[data-vybran="true"] { border-color: unset !important; }*/ /*&[data-selected="true"] { border-color: unset !important; }*/
} }
/* Šipky na posouvání korektur */ /* Šipky na posouvání korektur */

View file

@ -0,0 +1,46 @@
const W_SKIP = 10;
const H_SKIP = 5;
const POINTER_MIN_H = 30;
function place_comments_one_div(img_id, comments)
{
const img = document.getElementById("img-"+img_id);
if( img == null ) return;
const comments_sorted = comments.sort((a, b) => a.y - b.y);
const par = img.parentNode;
const w = img.clientWidth;
let bott_max = 0;
for (const oprava of comments_sorted) {
const x = oprava.x;
const y = oprava.y;
const htmlElement = oprava.htmlElement;
const pointer = oprava.pointer;
par.appendChild(pointer);
par.appendChild(htmlElement);
const delta_y = (y > bott_max) ? 0: bott_max - y + H_SKIP;
pointer.style.left = x;
pointer.style.top = y;
pointer.style.width = w - x + W_SKIP;
pointer.style.height = POINTER_MIN_H + delta_y;
htmlElement.style.left = w + W_SKIP;
htmlElement.style.top = y + delta_y;
bott_max = Math.max(bott_max, htmlElement.offsetTop + htmlElement.offsetHeight + H_SKIP); // FIXME nemám páru, proč +H_SKIP funguje, ale opravuje to bug, že nově vytvořené korektury za sebou neměly mezeru
}
if (par.offsetHeight < bott_max) par.style.height = bott_max;
}
function place_comments() {
for (let [img_id, opravy] of Object.entries(comments)) {
place_comments_one_div(img_id, opravy)
}
}

View file

@ -0,0 +1,76 @@
{% load static %}
<div id="korektury-sipky">
<button type='button' id="predchozi-korektura" title='Předchozí korektura'>
<img class='toggle-button' src='{% static "korektury/imgs/hide.png" %}' alt='⬆'/>
</button>
<button type='button' id="predchozi-korektura-k-oprave" title='Předchozí korektura k opravě'>
<img class='toggle-button' src='{% static "korektury/imgs/hide.png" %}' alt='⬆'/>
</button>
<button type='button' id="predchozi-korektura-k-zaneseni" title='Předchozí korektura k zaneseni'>
<img class='toggle-button' src='{% static "korektury/imgs/hide.png" %}' alt='⬆'/>
</button>
<br>
<button type='button' id="dalsi-korektura" title='Další korektura'>
<img class='toggle-button' src='{% static "korektury/imgs/hide.png" %}' alt='⬇' style="transform: rotate(180deg);"/>
</button>
<button type='button' id="dalsi-korektura-k-oprave" title='Další korektura k opravě'>
<img class='toggle-button' src='{% static "korektury/imgs/hide.png" %}' alt='⬇' style="transform: rotate(180deg);"/>
</button>
<button type='button' id="dalsi-korektura-k-zaneseni" title='Další korektura k zaneseni'>
<img class='toggle-button' src='{% static "korektury/imgs/hide.png" %}' alt='⬇' style="transform: rotate(180deg);"/>
</button>
<button type='button' id='korektury-aktualizace'
title='Aktualizuj korektury
Nemusíš mačkat, pokud ti stačí, že se korektury aktualizují samy každé 2 minuty a při každém přidání korektury/komentáře.'
>
<img class='toggle-button' src='{% static "korektury/imgs/reload.svg" %}' alt='↻' style="width: 15px"/>
</button>
</div>
<script>
const predchozi_k = document.getElementById('predchozi-korektura');
const dalsi_k = document.getElementById('dalsi-korektura');
const predchozi_k_o = document.getElementById('predchozi-korektura-k-oprave');
const dalsi_k_o = document.getElementById('dalsi-korektura-k-oprave');
const predchozi_k_z = document.getElementById('predchozi-korektura-k-zaneseni');
const dalsi_k_z = document.getElementById('dalsi-korektura-k-zaneseni');
function dalsi_nebo_predchozi_korektura(dalsi=true, stav=null) {
let predchozi = null;
for (let [_, opravy] of Object.entries(comments)) {
for (const oprava of opravy) {
if (stav == null || oprava.status === stav) {
const y = oprava.htmlElement.getBoundingClientRect().y;
if (y >= -1) {
if (dalsi) {
if (y > 1) {
oprava.htmlElement.scrollIntoView();
return;
}
} else {
if (predchozi !== null) predchozi.htmlElement.scrollIntoView(); else alert("Výše už není žádná taková korektura.");
return;
}
}
predchozi = oprava;
}
}
}
if (!dalsi && predchozi !== null) {
predchozi.htmlElement.scrollIntoView();
return;
}
alert("Žádná další korektura.");
}
predchozi_k.addEventListener('click', _ => { dalsi_nebo_predchozi_korektura(false) });
dalsi_k.addEventListener('click', _ => { dalsi_nebo_predchozi_korektura(true) });
predchozi_k_o.addEventListener('click', _ => { dalsi_nebo_predchozi_korektura(false, "k_oprave") });
dalsi_k_o.addEventListener('click', _ => { dalsi_nebo_predchozi_korektura(true, "k_oprave") });
predchozi_k_z.addEventListener('click', _ => { dalsi_nebo_predchozi_korektura(false, "k_zaneseni") });
dalsi_k_z.addEventListener('click', _ => { dalsi_nebo_predchozi_korektura(true, "k_zaneseni") });
// FIXME není mi jasné, zda v {} nemá být `cache: "no-store"`, aby prohlížeč necachoval GET.
document.getElementById("korektury-aktualizace").addEventListener("click", _ => update_all({}, false));
</script>

View file

@ -0,0 +1,111 @@
<div id="commform-div" style="display: none">
<input size="24" name="au" value="{{user.first_name}} {{user.last_name}}" readonly/>
<button type="button" id="commform-submit">Oprav!</button>
<button type="button" id="commform-close">Zavřít</button>
<br/>
<textarea id="commform-text" cols=40 rows=10 name="txt"></textarea>
<br/>
<div id="commform-tagy-info">Úprava tagů celé korektury:</div>
<div id="commform-tagy">
{% for tag in tagy %}
<button type="button" class="korektury-tag" value="{{tag.id}}" data-selected="false" style="background: {{ tag.barva }}; border-color: {{ tag.barva }};">{{tag.nazev}}</button>
{% endfor %}
</div>
</div>
<script>
class _CommForm {
constructor() {
this.div = document.getElementById('commform-div');
this.text = document.getElementById('commform-text');
this.submit_button = document.getElementById('commform-submit');
const close_button = document.getElementById('commform-close');
this.tagy = document.getElementById('commform-tagy');
this.tagy_info = document.getElementById('commform-tagy-info');
// ctrl-enter submits form
this.text.addEventListener("keydown", ev => {
if (ev.code === "Enter" && ev.ctrlKey) this.submit();
});
close_button.addEventListener("click", _ => { this.close(); });
this.submit_button.addEventListener("click", _ => { this.submit(); });
for (const tag of this.tagy.getElementsByTagName("button")) tag.addEventListener("click", event => { this.toggle_tag(event); });
this.reset_tags_every_open = true;
}
toggle_tag(event) {
const button = event.target;
button.dataset.selected = String(button.dataset.selected === "false");
}
reset_tags() { for (const tag of this.tagy.getElementsByTagName("button")) tag.dataset.selected = "false"; }
// schová commform
close() { this.div.style.display = 'none'; }
// zobrazí commform (bez vyplňování)
_show(img_id, x, y) {
this.submit_button.disabled = false;
this.div.style.display = 'block';
this.div.style.left = x;
this.div.style.top = y;
const img = document.getElementById("img-" + img_id);
img.parentNode.appendChild(commform.div);
this.text.focus();
}
// fill up comment form and show him
show(img_id, x, y, text, oprava_id=-1, komentar_id=-1) {
if (this.div.style.display !== 'none' && this.text.value !== "" && !confirm("Zavřít předchozí okénko přidávání korektury / editace komentáře?")) return;
// set hidden values
this.x = x;
this.y = y;
this.imgID = img_id;
this.oprava_id = oprava_id;
this.komentar_id = komentar_id;
this.text.value = text;
// show form
if (oprava_id === -1 && komentar_id === -1) {
if (this.reset_tags_every_open) this.reset_tags();
this.tagy_info.style.display = 'none';
} else {
const oprava = opravy[oprava_id];
this.tagy_info.style.display = 'unset';
for (const tag of this.tagy.getElementsByTagName("button"))
tag.dataset.selected = String(oprava.tagy.has(parseInt(tag.value)));
}
this._show(img_id, x, y);
}
submit() {
this.submit_button.disabled = true;
const data = new FormData(CSRF_FORM);
data.append('x', this.x);
data.append('y', this.y);
data.append('img_id', this.imgID);
data.append('oprava_id', this.oprava_id);
data.append('komentar_id', this.komentar_id);
const tagy = [];
for (const tag of this.tagy.getElementsByTagName("button")) {
if (tag.dataset.selected !== "false") tagy.push(tag.value);
}
data.append('tagy', String(tagy));
data.append('text', this.text.value);
update_all({method: 'POST', body: data}, true, () => {this.close(); this.submit_button.disabled = false;});
}
}
const commform = new _CommForm();
</script>

View file

@ -0,0 +1,106 @@
{% load static %}
<div class='comment' id='prekomentar' {# id='k{{k.id}}' #}>
<div class='corr-header'>
<div class='author'>{# {{k.autor}} #}</div>
<div class='float-right'>
<button type='button' style='display: none' class='del-comment' title='Smaž komentář'>
<img src='{% static "korektury/imgs/delete.png" %}' alt='del'/>
</button>
<button type='button' class='update-comment' title='Uprav komentář'>
<img src='{% static "korektury/imgs/edit.png"%}' alt='edit'/>
</button>
</div>
</div>
<div class='komtext'>{# {{k.text|linebreaks}} #}</div>
<hr>
</div>
<script>
const prekomentar = document.getElementById('prekomentar');
const komentare = {};
class Komentar {
static update_or_create(komentar_data, oprava) {
const id = komentar_data['id'];
if (id in komentare) komentare[id].update(komentar_data);
else new Komentar(komentar_data, oprava);
}
#autor; #text;
htmlElement;
id; oprava; {# komentar_data; #}
autor;
/**
*
* @param komentar_data
* @param {Oprava} oprava
*/
constructor(komentar_data, oprava) {
this.htmlElement = prekomentar.cloneNode(true);
this.#autor = this.htmlElement.getElementsByClassName('author')[0];
this.#text = this.htmlElement.getElementsByClassName('komtext')[0];
this.id = komentar_data['id'];
this.htmlElement.id = 'k' + this.id;
this.oprava = oprava;
this.oprava.add_komentar_htmlElement(this.htmlElement);
this.update(komentar_data);
this.htmlElement.getElementsByClassName('update-comment')[0].addEventListener('click', _ => this.#update_comment());
this.htmlElement.getElementsByClassName('del-comment')[0].addEventListener('click', _ => this.#delete_comment());
komentare[this.id] = this;
}
update(komentar_data) {
{# this.komentar_data = komentar_data; #}
this.set_autor(komentar_data['autor']);
this.set_text(komentar_data['text']);
};
set_autor(autor) {
this.#autor.textContent=autor;
this.autor = autor;
};
set_text(text) {
this.#text.innerHTML=text;
};
// show comment form when 'update-comment' button pressed
#update_comment() {
return commform.show(this.oprava.img_id, this.oprava.x, this.oprava.y, this.#text.textContent, this.oprava.id, this.id);
}
#delete_comment() {
if (confirm('Opravdu smazat komentář?')) {
const data = new FormData(CSRF_FORM);
data.append('komentar_id', this.id);
fetch('{% url "korektury_api_komentar_smaz" %}', {method: 'POST', body: data})
.then(response => {
if (!response.ok) {alert('Něco se nepovedlo:' + response.statusText);}
this.smaz_pouze_na_strance();
place_comments();
})
.catch(error => {alert('Něco se nepovedlo:' + error);});
}
}
smaz_pouze_na_strance() {
delete komentare[this.id];
this.htmlElement.remove();
}
}
</script>

View file

@ -0,0 +1,189 @@
{% load static %}
<div id='prepointer' {# id='op{{o.id}}-pointer' #}
class='pointer'
data-highlight='false'
{# data-opravastatus='{{o.status}}' #}
></div>
<div id='preoprava' {# name='op{{o.id}}' id='op{{o.id}}' #}
class='oprava'
{# data-opravastatus='{{o.status}}' #}
data-opravazobrazit='true'
>
<div class='corr-tagy'>
{# {% for tag in o.tagy %} <span style="background:{{ tag.barva }}>{{ tag.text }}<span/> #}
</div>
<div class='corr-body'>
{# {% for k in o.komentare %} {% include "korektury/korekturovatko/__komentar.html" %} {% endfor %} #}
</div>
<div class='corr-header'>
<span class='float-right'>
<span class='corr-buttons'>
<button type='button' style='display: none' class='del' title='Smaž opravu'>
<img src='{% static "korektury/imgs/delete.png"%}' alt='🗑️'/>
</button>
<button type='button' class='action' value='k_oprave' title='Označ jako neopravené'>
<img src='{% static "korektury/imgs/undo.png"%}' alt='↪'/>
</button>
<button type='button' class='action' value='opraveno' title='Označ jako opravené'>
<img src='{% static "korektury/imgs/check.png"%}' alt='✔️'/>
</button>
<button type='button' class='action' value='neni_chyba' title='Označ, že se nebude měnit'>
<img src='{% static "korektury/imgs/cross.png" %}' alt='❌'/>
</button>
<button type='button' class='action' value='k_zaneseni' title='Označ jako připraveno k zanesení'>
<img src='{% static "korektury/imgs/tex.png" %}' alt='TeX'/>
</button>
<a href='{% url "admin:korektury_oprava_change" -1 %}' class='edit' title='Uprav korekturu jako takovou.' style="text-decoration: none;"> {# FIXME Udělat z toho tlačítko? #}
<img src='{% static "korektury/imgs/edit.png"%}' alt='✏️' style="opacity: 0.5;"/> {# FIXME Odlišit jinak než pomocí opacity? #}
</a>
<button type='button' class='komentovat_disabled' title='Korekturu nelze komentovat, protože už je uzavřená' disabled=''>
<img src='{% static "korektury/imgs/comment-gr.png" %}' alt='💭'/>
</button>
<button type='button' class='komentovat' title='Komentovat'>
<img src='{% static "korektury/imgs/comment.png" %}' alt='💭'/>
</button>
</span>
<button type='button' class='toggle-vis' title='Skrýt/Zobrazit'>
<img class='toggle-button' src='{% static "korektury/imgs/hide.png" %}' alt='⬆'/>
</button>
</span>
</div>
</div>
<script>
const preoprava = document.getElementById('preoprava');
const prepointer = document.getElementById('prepointer');
const opravy = {};
class Oprava {
static update_or_create(oprava_data) {
const id = oprava_data['id'];
if (id in opravy) return opravy[id].update(oprava_data);
else return new Oprava(oprava_data);
}
#komentare; #tagy;
htmlElement; pointer;
id; x; y; img_id; status; zobrazit = true; {# oprava_data; #}
tagy;
constructor(oprava_data) {
this.htmlElement = preoprava.cloneNode(true);
this.pointer = prepointer.cloneNode(true);
this.#komentare = this.htmlElement.getElementsByClassName('corr-body')[0];
this.#tagy = this.htmlElement.getElementsByClassName('corr-tagy')[0];
this.id = oprava_data['id'];
this.htmlElement.id = 'op' + this.id;
this.pointer.id = 'op' + this.id + '-pointer';
this.x = oprava_data['x'];
this.y = oprava_data['y'];
this.img_id = oprava_data['strana'];
this.update(oprava_data);
this.htmlElement.getElementsByClassName('toggle-vis')[0].addEventListener('click', _ => this.#toggle_visibility());
for (const button of this.htmlElement.getElementsByClassName('action'))
button.addEventListener('click', async event => this.#zmenStavKorektury(event));
this.htmlElement.getElementsByClassName('komentovat')[0].addEventListener('click', _ => this.#comment())
this.htmlElement.getElementsByClassName('del')[0].addEventListener('click', _ => this.#delete());
const odkaz_editace = this.htmlElement.getElementsByClassName('edit')[0];
odkaz_editace.href = odkaz_editace.href.replace("-1", this.id);
odkaz_editace.onclick = ev => { if (!confirm("Editace korektury je velmi pokročilá featura umožňující přesouvat korekturu nebo přidávat informované orgy, opravdu chceš pokračovat do adminu?")) ev.preventDefault(); };
this.htmlElement.addEventListener('mouseover', _ => this.pointer.dataset.highlight = 'true');
this.htmlElement.addEventListener('mouseout', _ => this.pointer.dataset.highlight = 'false');
opravy[this.id] = this;
if (this.img_id in comments) comments[this.img_id].push(this); else alert("Někdo korekturoval stranu, která neexistuje. Dejte vědět webařům :)");
}
update(oprava_data) {
{# this.oprava_data = oprava_data; #}
this.set_status(oprava_data['status']);
this.#tagy.innerHTML = "";
this.tagy = new Set();
for (const tag of oprava_data["tagy"]) {
this.tagy.add(tag["id"]);
const span = document.createElement("span");
span.innerHTML = tag["nazev"];
span.classList.add("korektury-tag");
span.style.backgroundColor = tag["barva"];
this.#tagy.appendChild(span);
}
return this;
};
set_status(status) {
this.status = status;
this.htmlElement.dataset.opravastatus=status;
this.pointer.dataset.opravastatus=status;
};
add_komentar_htmlElement(htmlElement) { this.#komentare.appendChild(htmlElement); }
// hide or show text of correction
toggle_visibility() {
this.zobrazit = !this.zobrazit;
this.htmlElement.dataset.opravazobrazit = String(this.zobrazit);
}
#toggle_visibility(){
this.toggle_visibility();
place_comments()
}
// show comment form, when 'comment' button pressed
#comment() { commform.show(this.img_id, this.x, this.y, "", this.id); }
#zmenStavKorektury(event) {
const data = new FormData(CSRF_FORM);
data.append('id', this.id);
data.append('action', event.target.value);
fetch('{% url "korektury_api_oprava_stav" %}', {method: 'POST', body: data})
.then(response => {
if (!response.ok) {alert('Něco se nepovedlo:' + response.statusText);}
else response.json().then(data => {
this.set_status(data['status']);
updatuj_pocty_stavu();
});
})
.catch(error => {alert('Něco se nepovedlo:' + error);});
}
#delete() {
if (confirm('Opravdu smazat korekturu?')) {
const data = new FormData(CSRF_FORM);
data.append('oprava_id', this.id);
fetch('{% url "korektury_api_oprava_smaz" %}', {method: 'POST', body: data})
.then(response => {
if (!response.ok) {alert('Něco se nepovedlo:' + response.statusText);}
this.#smaz_pouze_na_strance()
updatuj_pocty_stavu();
updatuj_pocty_zasluh();
place_comments();
})
.catch(error => {alert('Něco se nepovedlo:' + error);});
}
}
#smaz_pouze_na_strance() {
comments[this.img_id].splice(comments[this.img_id].indexOf(this), 1);
delete opravy[this.id];
for (const komentar of Object.values(komentare)) if (komentar.oprava === this) komentar.smaz_pouze_na_strance();
this.htmlElement.remove();
this.pointer.remove();
}
}
</script>

View file

@ -0,0 +1,54 @@
{% for i in img_indexes %}
<div class='imgdiv'>
<img
id='img-{{i}}'
width='1021' height='1448'
src='/media/korektury/img/{{korekturovanepdf.get_prefix}}-{{i}}.png'
alt='Strana {{ i|add:1 }}'
class="strana"
/>
</div>
<hr/>
{% endfor %}
<script>
// Mapování stránka -> korektury
/**
* @type {Object.<number, Array<Oprava>>}
*/
const comments = {
{% for s in img_indexes %}
{{s}}: []{% if not forloop.last %},{% endif %}
{% endfor %}
};
// show comment form, when clicked to image
for (const image of document.getElementsByClassName('strana')) {
image.addEventListener('click', ev => {
switch (document.body.dataset.status) {
case 'zanaseni':
if (!confirm('Právě jsou zanášeny korektury, opravdu chcete přidat novou?'))
return;
break;
case 'zastarale':
if (!confirm('Toto PDF je již zastaralé, opravdu chcete vytvořit korekturu?'))
return;
break;
}
let dx, dy;
const par = image.parentNode;
if (ev.pageX != null) {
dx = ev.pageX - par.offsetLeft;
dy = ev.pageY - par.offsetTop;
} else { //IE a další
dx = ev.offsetX;
dy = ev.offsetY;
}
const img_id = image.id.substring(4);
commform.show(img_id, dx, dy, '');
console.log("Pro přesun korektur: strana = " + img_id + ", x = " + dx + ", y = " + dy);
});
}
</script>

View file

@ -0,0 +1,57 @@
{% include "korektury/korekturovatko/__edit_komentar.html" %}
{% include "korektury/korekturovatko/__stranky.html" %}
{# {% for o in opravy %} {% include "korektury/korekturovatko/__oprava.html" %} {% endfor %} #}
{% include "korektury/korekturovatko/__oprava.html" %}
{% include "korektury/korekturovatko/__komentar.html" %}
{% include "korektury/korekturovatko/__dalsi_korektura.html" %}
<script>
/**
*
* @param {RequestInit} data
* @param {Boolean} catchError
* @param pri_uspechu Akce, která se má provést při úspěchu (speciálně zavřít formulář)
*/
function update_all(data={}, catchError=true, pri_uspechu=null) { // FIXME není mi jasné, zda v {} nemá být `cache: "no-store"`, aby prohlížeč necachoval GET.
fetch('{% url "korektury_api_opravy_a_komentare" korekturovanepdf.id %}', data)
.then(response => {
if (!response.ok && catchError) {alert('Něco se nepovedlo:' + response.statusText);}
else response.json().then(data => {
for (const oprava_data of data["context"]) {
const oprava = Oprava.update_or_create(oprava_data);
for (const komentar_data of oprava_data["komentare"]) {
Komentar.update_or_create(komentar_data, oprava);
}
}
updatuj_pocty_stavu();
updatuj_pocty_zasluh();
place_comments();
if (pri_uspechu) pri_uspechu();
});
})
.catch(error => {if (catchError) alert('Něco se nepovedlo:' + error);});
}
window.addEventListener("load", _ => {
update_all({}, true, _ => {
if (location.hash !== "") { // Po rozházení korektur sescrollujeme na kotvu v URL
const h = location.hash.substring(1);
location.hash = "HACK";
location.hash = h;
}
});
});
// FIXME není mi jasné, zda v {} nemá být `cache: "no-store"`, aby prohlížeč necachoval GET.
setInterval(() => update_all({}, false), 120000); // Každý dvě minuty fetchni korektury
</script>
<form id='CSRF_form' style='display: none'>{% csrf_token %}</form>
<script>
const CSRF_FORM = document.getElementById('CSRF_form');
</script>

View file

@ -0,0 +1,83 @@
Zobrazit:
<input type="checkbox"
id="k_oprave_checkbox"
name="k_oprave_checkbox"
onchange="toggle_corrections('k_oprave')" checked>
<label for="k_oprave_checkbox">K opravě (<span id="k_oprave_pocet"></span>)</label>
<input type="checkbox"
id="opraveno_checkbox"
name="opraveno_checkbox"
onchange="toggle_corrections('opraveno')" checked>
<label for="opraveno_checkbox">Opraveno (<span id="opraveno_pocet"></span>)</label>
<input type="checkbox"
id="neni_chyba_checkbox"
name="neni_chyba_checkbox"
onchange="toggle_corrections('neni_chyba')" checked>
<label for="neni_chyba_checkbox">Není chyba (<span id="neni_chyba_pocet"></span>)</label>
<input type="checkbox"
id="k_zaneseni_checkbox"
name="k_zaneseni_checkbox"
onchange="toggle_corrections('k_zaneseni')" checked>
<label for="k_zaneseni_checkbox">K zanesení (<span id="k_zaneseni_pocet"></span>)</label>
<button type="button" id="sbal-korektury">Sbal korektury</button>
<button type="button" id="rozbal-korektury">Rozbal korektury</button>
<hr/>
<script>
const spany_s_pocty_stavu = {
'k_oprave': document.getElementById('k_oprave_pocet'),
'opraveno': document.getElementById('opraveno_pocet'),
'neni_chyba': document.getElementById('neni_chyba_pocet'),
'k_zaneseni': document.getElementById('k_zaneseni_pocet'),
}
function toggle_corrections(aclass)
{
const stylesheets = document.styleSheets;
let ssheet = null;
for (let i=0; i<stylesheets.length; i++){
if (stylesheets[i].title === "opraf-css"){
ssheet = stylesheets[i];
break;
}
}
if (! ssheet){
return;
}
for (let i=0; i<ssheet.cssRules.length; i++){
const rule = ssheet.cssRules[i];
if (rule.selectorText === '[data-opravastatus="'+aclass+'"]'){
if (rule.style.display === ""){
rule.style.display = "none";
} else {
rule.style.display = "";
}
}
}
place_comments();
}
function updatuj_pocty_stavu() {
const pocty_stavu = {};
for (const stav of Object.keys(spany_s_pocty_stavu)) pocty_stavu[stav] = 0;
for (const oprava of Object.values(opravy)) {
if (!(oprava.status in pocty_stavu)) pocty_stavu[oprava.status] = 0;
pocty_stavu[oprava.status] += 1;
}
for (let [stav, pocet] of Object.entries(pocty_stavu)) spany_s_pocty_stavu[stav].innerText = pocet;
}
document.getElementById("sbal-korektury").addEventListener("click", () => {
for (const oprava of Object.values(opravy))
if (oprava.zobrazit) oprava.toggle_visibility();
place_comments();
})
document.getElementById("rozbal-korektury").addEventListener("click", () => {
for (const oprava of Object.values(opravy))
if (!oprava.zobrazit) oprava.toggle_visibility();
place_comments();
})
</script>

View file

@ -1,5 +1,5 @@
{# Template starající se o formulář na změnu stavu PDF (včetně jeho odeslání) #} <h4>Změnit stav PDF:</h4>
<b>Změnit stav PDF:</b> <i>Aktuální: {{korekturovanepdf.status}}</i>
<br> <br>
<form method="post" id="PDFSTAV_FORM"> <form method="post" id="PDFSTAV_FORM">
{% csrf_token %} {% csrf_token %}
@ -13,24 +13,19 @@
</form> </form>
<script> <script>
/**
* Formulář měnící stav korekturovaného PDF
* @type {HTMLFormElement}
*/
const pdfstav_form = document.getElementById('PDFSTAV_FORM'); const pdfstav_form = document.getElementById('PDFSTAV_FORM');
/** /**
* Fetchne stav korekturovaného PDF a změní ho na dané stránce. *
* FIXME: nemění, který radio-button je vybrán. * @param {RequestInit} data
* @param {RequestInit} data FormData a jiné náležitosti (method: POST) posílané při změně stavu korekturovaného PDF * @param {Boolean} catchError
* @param {Boolean} catchError jestli padat hlasitě (pokud se aktualizuje automaticky a spadne to např. na nepřítomnost sítě, pak není třeba informovat uživatele)
*/ */
function fetchStav(data, catchError=true) { function fetchStav(data, catchError=true) {
fetch("{% url 'korektury_api_pdf_stav' korekturovanepdf.id %}", data fetch("{% url 'korektury_api_pdf_stav' korekturovanepdf.id %}", data
) )
.then(response => { .then(response => {
if (!response.ok) { if (catchError) alert("Něco se nepovedlo:" + response.statusText);} if (!response.ok) { if (catchError) alert("Něco se nepovedlo:" + response.statusText);}
else response.json().then(data => document.body.dataset.stav_pdf = data["status"]); else response.json().then(data => document.body.dataset.status = data["status"]);
}) })
.catch(error => {if (catchError) alert("Něco se nepovedlo:" + error);}); .catch(error => {if (catchError) alert("Něco se nepovedlo:" + error);});
} }

View file

@ -1,66 +0,0 @@
{# Část korekturovátka, která obsahuje všechno okolo korektur #}
{% include "korektury/korekturovatko/moduly/schovani_korektur.html" %}
{% include "korektury/korekturovatko/moduly/edit_komentar.html" %}
{% include "korektury/korekturovatko/moduly/stranky_pdfka.html" %}
{# {% for k in korektury %} {% include "korektury/korekturovatko/korektura.html" %} {% endfor %} #}
{% include "korektury/korekturovatko/moduly/korektura.html" %}
{% include "korektury/korekturovatko/moduly/komentar.html" %}
{% include "korektury/korekturovatko/moduly/dalsi_korektura.html" %}
<script>
/**
* Fetchne korektury a komentáře a na základě toho aktualizuje všechno
* (korektury, komentáře, zásluhy, počty korektur v daných stavech, umístění korektur)
* @param {RequestInit} data FormData a jiné náležitosti (method: POST) posílané při přidání/úpravě korektury/komentáře
* @param {Boolean} catchError jestli padat hlasitě (pokud se aktualizuje automaticky a spadne to např. na nepřítomnost sítě, pak není třeba informovat uživatele)
* @param {(() => *)?} pri_uspechu akce, která se má provést při úspěchu (speciálně zavřít formulář)
*/
function aktualizuj_vse(data={}, catchError=true, pri_uspechu=null) { // FIXME není mi jasné, zda v {} nemá být `cache: "no-store"`, aby prohlížeč necachoval GET.
fetch('{% url "korektury_api_opravy_a_komentare" korekturovanepdf.id %}', data)
.then(response => {
if (!response.ok && catchError) {alert('Něco se nepovedlo:' + response.statusText);}
else response.json().then(data => {
for (const korektura_data of data["context"]) {
const korektura = Korektura.aktualizuj_nebo_vytvor(korektura_data);
for (const komentar_data of korektura_data["komentare"]) {
Komentar.aktualizuj_nebo_vytvor(komentar_data, korektura);
}
}
aktualizuj_pocty_stavu();
aktualizuj_pocty_zasluh();
umisti_korektury();
if (pri_uspechu) pri_uspechu();
});
})
.catch(error => {if (catchError) alert('Něco se nepovedlo:' + error);});
}
window.addEventListener("load", _ => {
aktualizuj_vse({}, true, () => {
if (location.hash !== "") { // Po rozházení korektur sescrollujeme na kotvu v URL
const h = location.hash.substring(1);
location.hash = "HACK";
location.hash = h;
}
});
});
// FIXME není mi jasné, zda v {} nemá být `cache: "no-store"`, aby prohlížeč necachoval GET.
setInterval(() => aktualizuj_vse({}, false), 120000); // Každý dvě minuty fetchni korektury
</script>
{# Formulář, který mouhou použít tlačítka bez svého formuláře k vytvoření POST requestu, viz CSRF_FORM níže #}
<form id='CSRF_form' style='display: none'>{% csrf_token %}</form>
<script>
/**
* Formulář, který mouhou použít tlačítka bez svého formuláře k vytvoření POST requestu
* @type {HTMLFormElement}
*/
const CSRF_FORM = document.getElementById('CSRF_form');
</script>

View file

@ -1,14 +1,14 @@
{# Okolí samotného hlavni_cast_korekturovatka.html, tedy „povinné HTML věci“, informace o korekturovaném PDF a starání se o stav PDF #}
{% load static %} {% load static %}
<html lang='cs'> <html>
<head> <head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<link rel="stylesheet" title="opraf-css" type="text/css" media="screen, projection" href="{% static "korektury/opraf.css"%}?version=3" /> <link rel="stylesheet" title="opraf-css" type="text/css" media="screen, projection" href="{% static "korektury/opraf.css"%}?version=2" />
<link href="{% static 'css/rozliseni.css' %}?version=3" rel="stylesheet"> <link href="{% static 'css/rozliseni.css' %}?version=2" rel="stylesheet">
<script src="{% static "korektury/opraf.js"%}?version=2"></script>
<title>Korektury {{korekturovanepdf.nazev}}</title> <title>Korektury {{korekturovanepdf.nazev}}</title>
</head> </head>
<body class="{{ LOCAL_TEST_PROD }}web" data-stav_pdf="{{ korekturovanepdf.status }}"> <body class="{{ LOCAL_TEST_PROD }}web" data-status="{{ korekturovanepdf.status }}">
<h1>Korektury {{korekturovanepdf.nazev}}</h1> <h1>Korektury {{korekturovanepdf.nazev}}</h1>
@ -27,9 +27,11 @@
<a href="https://mam.mff.cuni.cz/wiki">wiki</a> | <a href="https://mam.mff.cuni.cz/wiki">wiki</a> |
<hr /> <hr />
{% include "korektury/korekturovatko/hlavni_cast_korekturovatka.html" %} {% include "korektury/korekturovatko/_schovani_korektur.html" %}
{% include "korektury/korekturovatko/_main.html" %}
{% include "korektury/korekturovatko/zmena_stavu_pdf.html" %} {% include "korektury/korekturovatko/_zmena_stavu.html" %}
<hr/> <hr/>
<p> <p>
@ -37,14 +39,9 @@
<hr> <hr>
<script> <script>
/**
* HTML prvek, kam se zapíší (pomocí .innerHTML) počty korektur jednotlivých autorů
* @type {HTMLElement}
*/
const span_s_pocty_autoru = document.getElementById("pocty_autoru") const span_s_pocty_autoru = document.getElementById("pocty_autoru")
/** Aktualizuje, kolik který autor má komentářů u daného korekturovaného PDF. */ function updatuj_pocty_zasluh() {
function aktualizuj_pocty_zasluh() {
const pocty_autoru = {}; const pocty_autoru = {};
for (let komentar of Object.values(komentare)) { for (let komentar of Object.values(komentare)) {
if (!(komentar.autor in pocty_autoru)) pocty_autoru[komentar.autor] = 0; if (!(komentar.autor in pocty_autoru)) pocty_autoru[komentar.autor] = 0;

View file

@ -1,77 +0,0 @@
{# Template starající se o tlačítka v levém dolním rohu, především skákající na další/předchozí korekturu. #}
{% load static %}
<div id="korektury-sipky">
<button type='button' id="predchozi-korektura" title='Předchozí korektura'>
<img src='{% static "korektury/imgs/hide.png" %}' alt='⬆'/>
</button>
<button type='button' id="predchozi-korektura-k-oprave" title='Předchozí korektura k opravě'>
<img src='{% static "korektury/imgs/hide.png" %}' alt='⬆'/>
</button>
<button type='button' id="predchozi-korektura-k-zaneseni" title='Předchozí korektura k zaneseni'>
<img src='{% static "korektury/imgs/hide.png" %}' alt='⬆'/>
</button>
<br>
<button type='button' id="dalsi-korektura" title='Další korektura'>
<img src='{% static "korektury/imgs/hide.png" %}' alt='⬇' style="transform: rotate(180deg);"/>
</button>
<button type='button' id="dalsi-korektura-k-oprave" title='Další korektura k opravě'>
<img src='{% static "korektury/imgs/hide.png" %}' alt='⬇' style="transform: rotate(180deg);"/>
</button>
<button type='button' id="dalsi-korektura-k-zaneseni" title='Další korektura k zaneseni'>
<img src='{% static "korektury/imgs/hide.png" %}' alt='⬇' style="transform: rotate(180deg);"/>
</button>
<button type='button' id='korektury-aktualizace'
title='Aktualizuj korektury
Nemusíš mačkat, pokud ti stačí, že se korektury aktualizují samy každé 2 minuty a při každém přidání korektury/komentáře.'
>
<img src='{% static "korektury/imgs/reload.svg" %}' alt='↻' style="width: 15px"/>
</button>
</div>
<script>
document.getElementById('predchozi-korektura').addEventListener('click', _ => { dalsi_nebo_predchozi_korektura(false) });
document.getElementById('dalsi-korektura').addEventListener('click', _ => { dalsi_nebo_predchozi_korektura(true) });
document.getElementById('predchozi-korektura-k-oprave').addEventListener('click', _ => { dalsi_nebo_predchozi_korektura(false, "k_oprave") });
document.getElementById('dalsi-korektura-k-oprave').addEventListener('click', _ => { dalsi_nebo_predchozi_korektura(true, "k_oprave") });
document.getElementById('predchozi-korektura-k-zaneseni').addEventListener('click', _ => { dalsi_nebo_predchozi_korektura(false, "k_zaneseni") });
document.getElementById('dalsi-korektura-k-zaneseni').addEventListener('click', _ => { dalsi_nebo_predchozi_korektura(true, "k_zaneseni") });
// FIXME není mi jasné, zda v {} nemá být `cache: "no-store"`, aby prohlížeč necachoval GET.
document.getElementById("korektury-aktualizace").addEventListener("click", _ => aktualizuj_vse({}, false));
/**
* Sescrolluje na další nebo předchozí (vůči hornímu okraji okna) korekturu (v daném stavu).
* V případě neexistence takové korektury vyhodí alert.
* @param {boolean} dalsi reprezentuje, zda chceme další nebo předchozí korekturu
* @param {?string} stav pokud je nenullový, tak ignoruje korektury v jiném stavu
*/
function dalsi_nebo_predchozi_korektura(dalsi=true, stav=null) {
let predchozi = null;
for (const strana of setrizene_strany) {
// strana.setrid_korektury(); // Nemělo by být potřeba, protože se volá vždy, když se renderují korektury.
for (const korektura of strana.korektury) {
if (stav == null || korektura.stav === stav) {
const y = korektura.htmlElement.getBoundingClientRect().y;
if (y >= -1) {
if (dalsi) {
if (y > 1) {
korektura.htmlElement.scrollIntoView();
return;
}
} else {
if (predchozi !== null) predchozi.htmlElement.scrollIntoView(); else alert("Výše už není žádná taková korektura.");
return;
}
}
predchozi = korektura;
}
}
}
if (!dalsi && predchozi !== null) {
predchozi.htmlElement.scrollIntoView();
return;
}
alert("Žádná další korektura.");
}
</script>

View file

@ -1,166 +0,0 @@
{# Template starající se o editační/přidávací formulář. #}
<div id="korekturovaci-formular-div" style="display: none">
<input size="24" name="au" value="{{user.osoba}}" readonly/>
<button type="button" id="korekturovaci-formular-odesli">Oprav!</button>
<button type="button" id="korekturovaci-formular-zavri">Zavřít</button>
<br/>
<textarea id="korekturovaci-formular-text" cols=40 rows=10 name="txt"></textarea>
<br/>
<div id="korekturovaci-formular-tagy-info">Úprava tagů celé korektury:</div>
<div id="korekturovaci-formular-tagy">
{% for tag in tagy %}
<button type="button" class="korektury-tag" value="{{tag.id}}" data-vybran="false" style="background: {{ tag.barva }}; border-color: {{ tag.barva }};">{{tag.nazev}}</button>
{% endfor %}
</div>
</div>
<script>
/** V podstatě singleton (viz korekturovaci_formular) starající se o editační/přidávací formulář. */
class _KorekturovaciFormular {
/**
* <div> obsahující celý formulář.
* @type {HTMLElement}
*/
div;
/**
* Políčko, kam uživatel vyplňuje text.
* @type {HTMLElement}
*/
text;
/**
* Tlačítko odeslat. Často ho chceme disablenout.
* @type {HTMLElement}
*/
odesilaci_button;
/**
* <div> obsahující všechny tagy, pomocí tagy.getElementsByTagName("button") umíme dělat operace nad všemi tagy.
* @type {HTMLElement}
*/
tagy;
/**
* Text upozorňující na to, že tagy nepřidáváme, ale editujeme. (Tj. chceme ho schovat, když vytváříme novou korekturu.)
* @type {HTMLElement}
*/
tagy_info;
/**
* zda při přidávání nové korektury mají být všechny tagy odvybrané, nebo mají kopírovat předchozí nastavení
* @type {boolean}
*/
pri_otevreni_odvyber_tagy;
constructor() {
this.div = document.getElementById('korekturovaci-formular-div');
this.text = document.getElementById('korekturovaci-formular-text');
this.odesilaci_button = document.getElementById('korekturovaci-formular-odesli');
const zaviraci_button = document.getElementById('korekturovaci-formular-zavri');
this.tagy = document.getElementById('korekturovaci-formular-tagy');
this.tagy_info = document.getElementById('korekturovaci-formular-tagy-info');
// ctrl-enter odešle formulář
this.text.addEventListener("keydown", ev => {
if (ev.code === "Enter" && ev.ctrlKey) this.odesli_formular();
});
zaviraci_button.addEventListener("click", _ => { this.schovej(); });
this.odesilaci_button.addEventListener("click", _ => { this.odesli_formular(); });
for (const tag of this.tagy.getElementsByTagName("button")) tag.addEventListener("click", event => { this.vyber_nebo_odvyber_tag(event); });
this.pri_otevreni_odvyber_tagy = true;
}
/**
* Přepne tag na vybraný/nevybraný (v závislosti na tom, zda byl nevybrán/vybrán)
* @param {MouseEvent} event vyvolaný kliknutím na daný tag (musí mít za event.target daný tag)
*/
vyber_nebo_odvyber_tag(event) {
const button = event.target;
button.dataset.vybran = String(button.dataset.vybran === "false");
}
/** Nastaví všechny tagy na nevybrané. */
odvyber_tagy() { for (const tag of this.tagy.getElementsByTagName("button")) tag.dataset.vybran = "false"; }
/** Schová (zavře) korekturovací formulář */
schovej() { this.div.style.display = 'none'; }
/**
* Zobrazí/otevře korekturovací formulář (bez toho, aby v něm cokoliv měnil).
* @param {Strana} strana (na které straně se má zobrazit)
* @param {number} x
* @param {number} y
*/
_zobraz(strana, x, y) {
this.odesilaci_button.disabled = false;
this.div.style.display = 'block';
this.div.style.left = x;
this.div.style.top = y;
strana.htmlElement_div.appendChild(korekturovaci_formular.div);
this.text.focus();
}
/**
* Předvyplní správně korekturovací formulář a zobrazí/otevře ho
* @param {Strana} strana (na které straně se má zobrazit)
* @param {Number} x
* @param {Number} y
* @param {string} text (text k předvyplněný, místo null chceš psáť "")
* @param {Number} komentar_id (!= -1 znamená úprava komentáře, -1 znamená přidávání korektury/komentáře)
* @param {Number} korektura_id (v případě komentar_id != -1 znamená: -1 je nová korektura, ne-1 je nový komentář)
*/
zobraz(strana, x, y, text, korektura_id=-1, komentar_id=-1) {
if (this.div.style.display !== 'none' && this.text.value !== "" && !confirm("Zavřít předchozí okénko přidávání korektury / editace komentáře?")) return;
// set hidden values
this.x = x;
this.y = y;
this.strana = strana;
this.korektura_id = korektura_id;
this.komentar_id = komentar_id;
this.text.value = text;
// show form
if (korektura_id === -1 && komentar_id === -1) {
if (this.pri_otevreni_odvyber_tagy) this.odvyber_tagy();
this.tagy_info.style.display = 'none';
} else {
const korektura = korektury[korektura_id];
this.tagy_info.style.display = 'unset';
for (const tag of this.tagy.getElementsByTagName("button"))
tag.dataset.vybran = String(korektura.tagy.has(parseInt(tag.value)));
}
this._zobraz(strana, x, y);
}
/** Shrábne data a pošle daný požadavek, čímž kromě vyřízení dané věci aktualizuje korektury+komentáře. */
odesli_formular() {
this.odesilaci_button.disabled = true;
const data = new FormData(CSRF_FORM);
data.append('x', this.x);
data.append('y', this.y);
data.append('img_id', this.strana.id);
data.append('oprava_id', this.korektura_id);
data.append('komentar_id', this.komentar_id);
const tagy = [];
for (const tag of this.tagy.getElementsByTagName("button")) {
if (tag.dataset.vybran !== "false") tagy.push(tag.value);
}
data.append('tagy', String(tagy));
data.append('text', this.text.value);
aktualizuj_vse({method: 'POST', body: data}, true, () => {this.schovej(); this.odesilaci_button.disabled = false;});
}
}
/**
* Objekt starající se o editační/přidávací formulář (jeho předvyplňování, zobrazování a posílání).
* @type {_KorekturovaciFormular}
*/
const korekturovaci_formular = new _KorekturovaciFormular();
</script>

View file

@ -1,168 +0,0 @@
{# Template starající se o jeden každý komentář u korektury. #}
{% load static %}
<div class='comment' id='prekomentar' {# id='k{{k.id}}' #}>
<div class='hlavicka-komentare'>
<div class='autor'>{# {{k.autor}} #}</div>
<div class='float-right'>
<button type='button' style='display: none' class="smaz-komentar" title='Smaž komentář'>
<img src='{% static "korektury/imgs/delete.png" %}' alt='del'/>
</button>
<button type='button' class="uprav-komentar" title='Uprav komentář'>
<img src='{% static "korektury/imgs/edit.png" %}' alt='edit'/>
</button>
<button type='button' class='sbal-rozbal' title='Skrýt/Zobrazit'>
<img class='sbal-rozbal-img' src='{% static "korektury/imgs/hide.png" %}' alt='⬆'/>
</button>
</div>
</div>
<div class='komtext'>{# {{k.text|linebreaks}} #}</div>
<hr>
</div>
<script>
/**
* Prototyp komentáře, ze kterého se vygeneruje každý komentář (resp. jeho HTML reprezentace) v dokumentu
* @type {HTMLElement}
*/
const prekomentar = document.getElementById('prekomentar');
/**
* Mapování ID |-> komentář
* @type {Object.<Number, Komentar>}
*/
const komentare = {};
/** Třída reprezentující jeden komentář (a starající se o vytvoření a updatování jeho HTML reprezentace) */
class Komentar {
/**
* Z dat aktualizuje (v případě, že korektura s daným ID existuje) nebo vytvoří Komentar
* @param {Object.<string, ?>} komentar_data „Slovník“ obsahující data daného komentáře
* @param {Korektura} korektura ke které se komentář má připojit
*/
static aktualizuj_nebo_vytvor(komentar_data, korektura) {
const id = komentar_data['id'];
if (id in komentare) komentare[id].aktualizuj(komentar_data);
else new Komentar(komentar_data, korektura);
}
/**
* <div> se jménem autora komentáře
* @type {HTMLElement}
*/
#autor;
/**
* <div> obsahující text komentáře
* @type {HTMLElement}
*/
#text;
/**
* <div> reprezentující celý komentář
* @type {HTMLElement}
*/
htmlElement;
/** @type {Number} */
id;
/** @type{Korektura} */
korektura;
/** @type{string} */
autor;
/** @type {boolean} */
sbalen = false;
/**
* Vytvoří HTML reprezentaci, připojí komentář pod korekturu, nastaví event-listenery, uloží si data
* @param {Object.<string, ?>} komentar_data „Slovník“ obsahující data daného komentáře
* @param {Korektura} korektura korektura ke které se komentář má připojit
*/
constructor(komentar_data, korektura) {
this.htmlElement = prekomentar.cloneNode(true);
this.#autor = this.htmlElement.getElementsByClassName('autor')[0];
this.#text = this.htmlElement.getElementsByClassName('komtext')[0];
this.id = komentar_data['id'];
this.htmlElement.id = 'k' + this.id;
this.korektura = korektura;
this.korektura.pridej_htmlElement_komentare(this.htmlElement);
this.aktualizuj(komentar_data);
this.htmlElement.getElementsByClassName('sbal-rozbal')[0].addEventListener('click', _ => this.#sbal_nebo_rozbal());
this.htmlElement.getElementsByClassName('uprav-komentar')[0].addEventListener('click', _ => this.#uprav_komentar());
this.htmlElement.getElementsByClassName('smaz-komentar')[0].addEventListener('click', _ => this.#smaz_komentar());
komentare[this.id] = this;
}
/**
* Aktualizuje/nastaví JS data i HTML reprezentaci komentáře
* @param {Object.<string, ?>} komentar_data „Slovník“ obsahující data daného komentáře
*/
aktualizuj(komentar_data) {
this.set_autor(komentar_data['autor']);
this.set_text(komentar_data['text']);
};
/**
* Aktualizuje/nastaví JS data i HTML reprezentaci autora komentáře
* @param {String} autor
*/
set_autor(autor) {
this.#autor.textContent=autor;
this.autor = autor;
};
/**
* @param {String} text
*/
set_text(text) {
this.#text.innerHTML=text;
};
/** Sbalí/rozbalí (podle toho, zda byl rozbalený/sbalený) komentář, ale nezmění pozice korektur (je třeba později zavolat umisti_korektury()) */
sbal_nebo_rozbal() {
this.sbalen = !this.sbalen;
this.htmlElement.dataset.komentar_sbalen = String(this.sbalen);
}
/** Doplněk sbal_nebo_rozbal, který i přeskládá korektury. */
#sbal_nebo_rozbal(){
this.sbal_nebo_rozbal();
umisti_korektury();
}
/** Ukáže formulář na editaci komentáře (když je zmáčknuto „uprav-komentar“) */
#uprav_komentar() {
return korekturovaci_formular.zobraz(this.korektura.strana, this.korektura.x, this.korektura.y, this.#text.textContent, this.korektura.id, this.id);
}
/** Smaže komentář (když je zmáčknuto „smaz-komentar“) */
#smaz_komentar() {
if (confirm('Opravdu smazat komentář?')) {
const data = new FormData(CSRF_FORM);
data.append('komentar_id', this.id);
fetch('{% url "korektury_api_komentar_smaz" %}', {method: 'POST', body: data})
.then(response => {
if (!response.ok) {alert('Něco se nepovedlo:' + response.statusText);}
this.smaz_pouze_na_strance();
umisti_korektury();
})
.catch(error => {alert('Něco se nepovedlo:' + error);});
}
}
/** Smaže div komentáře (ne databázový záznam!), používá se, když je smazán komentář nebo jeho nadřazená korektura */
smaz_pouze_na_strance() {
delete komentare[this.id];
this.htmlElement.remove();
}
}
</script>

View file

@ -1,271 +0,0 @@
{% load static %}
<div id='prepointer' {# id='kor{{k.id}}-pointer' #}
class='pointer'
data-hover='false'
{# data-stav_korektury='{{k.status}}' #}
></div>
<div id='prekorektura' {# name='kor{{k.id}}' id='kor{{k.id}}' #}
class='korektura'
{# data-stav_korektury='{{k.status}}' #}
data-korektura_sbalena='false'
>
<div class="korektura-tagy">
{# {% for tag in k.tagy %} <span style="background:{{ tag.barva }}>{{ tag.text }}<span/> #}
</div>
<div class='korektura-telo'>
{# {% for k in k.komentare %} {% include "korektury/korekturovatko/komentar.html" %} {% endfor %} #}
</div>
<div class='hlavicka-komentare'>
<span class='float-right'>
<span class='korektura-tlacitka'>
<button type='button' style='display: none' class="smaz-korekturu" title='Smaž korekturu'>
<img src='{% static "korektury/imgs/delete.png" %}' alt='🗑️'/>
</button>
<button type='button' class='action' value='k_oprave' title='Označ jako neopravené'>
<img src='{% static "korektury/imgs/undo.png" %}' alt='↪'/>
</button>
<button type='button' class='action' value='opraveno' title='Označ jako opravené'>
<img src='{% static "korektury/imgs/check.png" %}' alt='✔️'/>
</button>
<button type='button' class='action' value='neni_chyba' title='Označ, že se nebude měnit'>
<img src='{% static "korektury/imgs/cross.png" %}' alt='❌'/>
</button>
<button type='button' class='action' value='k_zaneseni' title='Označ jako připraveno k zanesení'>
<img src='{% static "korektury/imgs/tex.png" %}' alt='TeX'/>
</button>
<a href='{% url "admin:korektury_oprava_change" -1 %}' class='edit' title='Uprav korekturu jako takovou.' style="text-decoration: none;"> {# FIXME Udělat z toho tlačítko? #}
<img src='{% static "korektury/imgs/edit.png" %}' alt='✏️' style="opacity: 0.5;"/> {# FIXME Odlišit jinak než pomocí opacity? #}
</a>
<button type='button' class='komentovat_disabled' title='Korekturu nelze komentovat, protože už je uzavřená' disabled=''>
<img src='{% static "korektury/imgs/comment-gr.png" %}' alt='💭'/>
</button>
<button type='button' class='komentovat' title='Komentovat'>
<img src='{% static "korektury/imgs/comment.png" %}' alt='💭'/>
</button>
</span>
<button type='button' class='sbal-rozbal' title='Skrýt/Zobrazit'>
<img class='sbal-rozbal-img' src='{% static "korektury/imgs/hide.png" %}' alt='⬆'/>
</button>
</span>
</div>
</div>
<script>
/**
* Prototyp korektury, ze kterého se vygeneruje každý komentář (resp. jeho HTML reprezentace) v dokumentu
* @type {HTMLElement}
*/
const prekorektura = document.getElementById('prekorektura');
/**
* Prototyp pointeru (té lomené čáry od korektury)
* @type {HTMLElement}
*/
const prepointer = document.getElementById('prepointer');
/**
* Mapování ID |-> korektura
* @type {Object.<Number, Korektura>}
*/
const korektury = {};
/** Třída reprezentující jednu korekturu (a starající se o vytvoření a updatování její HTML reprezentace) */
class Korektura {
/**
* Z dat aktualizuje (v případě, že korektura s daným ID existuje) nebo vytvoří Korekturu
* @param {Object.<string, ?>} korektura_data „Slovník“ obsahující data dané korektury
* @returns {Korektura} vytvořená/aktualizovaná Korektura (pro použití při vytváření/aktualizaci komentářů)
*/
static aktualizuj_nebo_vytvor(korektura_data) {
const id = korektura_data['id'];
if (id in korektury) return korektury[id].aktualizuj(korektura_data);
else return new Korektura(korektura_data);
}
/**
* <div> obsahující <div>y komentářů
* @type {HTMLElement}
*/
#komentare;
/**
* <div> obsahující tagy
* @type {HTMLElement}
*/
#tagy;
/**
* <div> reprezentující celý korekturu
* @type {HTMLElement}
*/
htmlElement;
/**
* <div> reprezentující pointer (tu lomenou čáru od korektury)
* @type {HTMLElement}
*/
pointer;
/** @type {Number} */
id;
/** @type {Number} */
x;
/** @type {Number} */
y;
/** @type {Strana} */
strana;
/** @type {string} */
stav;
/** @type {boolean} */
sbalena = false;
/** @type Set<Number> */
tagy;
/**
* Vytvoří HTML reprezentaci, připojí korekturu pod stranu (ale neumístí ji), nastaví event-listenery, uloží si data
* @param {Object.<string, ?>} korektura_data „Slovník“ obsahující data dané korektury
*/
constructor(korektura_data) {
this.htmlElement = prekorektura.cloneNode(true);
this.pointer = prepointer.cloneNode(true);
this.#komentare = this.htmlElement.getElementsByClassName('korektura-telo')[0];
this.#tagy = this.htmlElement.getElementsByClassName('korektura-tagy')[0];
this.id = korektura_data['id'];
this.htmlElement.id = 'kor' + this.id;
this.pointer.id = 'kor' + this.id + '-pointer';
this.x = korektura_data['x'];
this.y = korektura_data['y'];
this.aktualizuj(korektura_data);
this.htmlElement.getElementsByClassName('sbal-rozbal')[0].addEventListener('click', _ => this.#sbal_nebo_rozbal());
for (const button of this.htmlElement.getElementsByClassName('action'))
button.addEventListener('click', async event => this.#zmen_stav_korektury(event));
this.htmlElement.getElementsByClassName('komentovat')[0].addEventListener('click', _ => this.#komentuj())
this.htmlElement.getElementsByClassName('smaz-korekturu')[0].addEventListener('click', _ => this.#smaz_korekturu());
const odkaz_editace = this.htmlElement.getElementsByClassName('edit')[0];
odkaz_editace.href = odkaz_editace.href.replace("-1", this.id);
odkaz_editace.onclick = ev => { if (!confirm("Editace korektury je velmi pokročilá featura umožňující přesouvat korekturu nebo přidávat informované orgy, opravdu chceš pokračovat do adminu?")) ev.preventDefault(); };
this.htmlElement.addEventListener('mouseover', _ => this.pointer.dataset.hover = 'true');
this.htmlElement.addEventListener('mouseout', _ => this.pointer.dataset.hover = 'false');
const cislo_strany = korektura_data['strana'];
if (cislo_strany in strany) {
this.strana = strany[cislo_strany];
this.strana.korektury.push(this);
} else alert("Někdo korekturoval stranu, která neexistuje. Dejte vědět webařům :)");
korektury[this.id] = this;
}
/**
* Aktualizuje/nastaví JS data i HTML reprezentaci korektury
* @param {Object.<string, ?>} korektura_data „Slovník“ obsahující data dané korektury
* @returns {Korektura} pro jednodušší implementaci aktualizuj_nebo_vytvor vracíme this
*/
aktualizuj(korektura_data) {
this.set_stav(korektura_data['status']);
this.set_tagy(korektura_data["tagy"]);
return this;
};
/**
* Aktualizuje/nastaví JS data i HTML reprezentaci tagů korektury
* @param {Object.<string, ?>[]} tagy
*/
set_tagy(tagy) {
this.#tagy.innerHTML = "";
this.tagy = new Set();
for (const tag of tagy) {
this.tagy.add(tag["id"]);
const span = document.createElement("span");
span.innerHTML = tag["nazev"];
span.classList.add("korektury-tag");
span.style.backgroundColor = tag["barva"];
this.#tagy.appendChild(span);
}
}
/**
* Aktualizuje/nastaví JS data i HTML reprezentaci stavu korektury
* @param {String} stav
*/
set_stav(stav) {
this.stav = stav;
this.htmlElement.dataset.stav_korektury=stav;
this.pointer.dataset.stav_korektury=stav;
};
/**
* Přidá HTML reprezentaci komentáře pod tuto korekturu
* @param {HTMLElement} htmlElement přidávaný komentář (jako HTML prvek)
*/
pridej_htmlElement_komentare(htmlElement) { this.#komentare.appendChild(htmlElement); }
/** Sbalí/rozbalí (podle toho, zda byla rozbalená/sbalená) korekturu, ale nezmění pozice korektur (je třeba později zavolat umisti_korektury()) */
sbal_nebo_rozbal() {
this.sbalena = !this.sbalena;
this.htmlElement.dataset.korektura_sbalena = String(this.sbalena);
}
/** Doplněk sbal_nebo_rozbal, který i přeskládá korektury. */
#sbal_nebo_rozbal(){
this.sbal_nebo_rozbal();
umisti_korektury();
}
/** Ukaž komentovací formulář (když je zmáčknuto komentovat) */
#komentuj() { korekturovaci_formular.zobraz(this.strana, this.x, this.y, "", this.id); }
/**
* Změní stav (když je zmáčknuto tlačítko daného stavu)
* @param {MouseEvent} event který vyvolal danou změnu (event.target.value musí být chtěný stav)
*/
#zmen_stav_korektury(event) {
const data = new FormData(CSRF_FORM);
data.append('id', this.id);
data.append('action', event.target.value);
fetch('{% url "korektury_api_oprava_stav" %}', {method: 'POST', body: data})
.then(response => {
if (!response.ok) {alert('Něco se nepovedlo:' + response.statusText);}
else response.json().then(data => {
this.set_stav(data['status']);
aktualizuj_pocty_stavu();
});
})
.catch(error => {alert('Něco se nepovedlo:' + error);});
}
/** Smaže korekturu (když je zmáčknuto „smaz-korekturu“) */
#smaz_korekturu() {
if (confirm('Opravdu smazat korekturu?')) {
const data = new FormData(CSRF_FORM);
data.append('oprava_id', this.id);
fetch('{% url "korektury_api_oprava_smaz" %}', {method: 'POST', body: data})
.then(response => {
if (!response.ok) {alert('Něco se nepovedlo:' + response.statusText);}
this.#smaz_pouze_na_strance()
aktualizuj_pocty_stavu();
aktualizuj_pocty_zasluh();
umisti_korektury();
})
.catch(error => {alert('Něco se nepovedlo:' + error);});
}
}
/** Smaže div korektury (včetně všech komentářů; ne databázový záznam!) */
#smaz_pouze_na_strance() {
this.strana.korektury.splice(this.strana.korektury.indexOf(this), 1);
delete korektury[this.id];
for (const komentar of Object.values(komentare)) if (komentar.korektura === this) komentar.smaz_pouze_na_strance();
this.htmlElement.remove();
this.pointer.remove();
}
}
</script>

View file

@ -1,86 +0,0 @@
{# Template starající se o tlačítkovou lištu nahoře, tj. hlavně o hromadné schovávání korektur. #}
Zobrazit:
<input type="checkbox" id="k_oprave_checkbox" checked>
<label for="k_oprave_checkbox">K opravě (<span id="k_oprave_pocet"></span>)</label>
<input type="checkbox" id="opraveno_checkbox" checked>
<label for="opraveno_checkbox">Opraveno (<span id="opraveno_pocet"></span>)</label>
<input type="checkbox" id="neni_chyba_checkbox" checked>
<label for="neni_chyba_checkbox">Není chyba (<span id="neni_chyba_pocet"></span>)</label>
<input type="checkbox" id="k_zaneseni_checkbox" checked>
<label for="k_zaneseni_checkbox">K zanesení (<span id="k_zaneseni_pocet"></span>)</label>
<button type="button" id="sbal-korektury">Sbal korektury</button>
<button type="button" id="rozbal-korektury">Rozbal korektury</button>
<hr/>
<script>
document.getElementById('k_oprave_checkbox').addEventListener('change', () => skryj_nebo_zobraz_korektury('k_oprave'));
document.getElementById('opraveno_checkbox').addEventListener('change', () => skryj_nebo_zobraz_korektury('opraveno'));
document.getElementById('neni_chyba_checkbox').addEventListener('change', () => skryj_nebo_zobraz_korektury('neni_chyba'));
document.getElementById('k_zaneseni_checkbox').addEventListener('change', () => skryj_nebo_zobraz_korektury('k_zaneseni'));
document.getElementById("sbal-korektury").addEventListener("click", () => {
for (const korektura of Object.values(korektury))
if (!korektura.sbalena) korektura.sbal_nebo_rozbal();
umisti_korektury();
})
document.getElementById("rozbal-korektury").addEventListener("click", () => {
for (const korektura of Object.values(korektury))
if (korektura.sbalena) korektura.sbal_nebo_rozbal();
umisti_korektury();
})
/**
* Změní CSS tak, aby se korektury příslušného stavu nezobrazovali/zobrazovali (v závislosti na tom, jestli byly zobrazené/nezobrazené)
* @param {string} aclass stav korektur, které mají být skryty/zobrazeny
*/
function skryj_nebo_zobraz_korektury(aclass)
{
const stylesheets = document.styleSheets;
let ssheet = null;
for (let i=0; i<stylesheets.length; i++){
if (stylesheets[i].title === "opraf-css"){
ssheet = stylesheets[i];
break;
}
}
if (! ssheet){
return;
}
for (let i=0; i<ssheet.cssRules.length; i++){
const rule = ssheet.cssRules[i];
if (rule.selectorText === '[data-stav_korektury="'+aclass+'"]'){
if (rule.style.display === ""){
rule.style.display = "none";
} else {
rule.style.display = "";
}
}
}
umisti_korektury();
}
/**
* Mapování stav korektur |-> span, kde se píše, kolik je korektur toho stavu.
* Používané v následující funcki
* @type {Object.<string, HTMLElement>}
*/
const spany_s_pocty_stavu_korektur = {
'k_oprave': document.getElementById('k_oprave_pocet'),
'opraveno': document.getElementById('opraveno_pocet'),
'neni_chyba': document.getElementById('neni_chyba_pocet'),
'k_zaneseni': document.getElementById('k_zaneseni_pocet'),
}
/** Aktualizuje počty korektur jednotlivých stavů */
function aktualizuj_pocty_stavu() {
const pocty_stavu_korektur = {};
for (const stav_korektury of Object.keys(spany_s_pocty_stavu_korektur)) pocty_stavu_korektur[stav_korektury] = 0;
for (const korektura of Object.values(korektury)) {
if (!(korektura.stav in pocty_stavu_korektur)) pocty_stavu_korektur[korektura.stav] = 0;
pocty_stavu_korektur[korektura.stav] += 1;
}
for (let [stav, pocet] of Object.entries(pocty_stavu_korektur)) spany_s_pocty_stavu_korektur[stav].innerText = pocet;
}
</script>

View file

@ -1,148 +0,0 @@
{# Template starající se o zobrazení PDF stran a o umístění korektur na ně. (O samotné korektury se stará `./korektura.html`.) #}
{% for i in indexy_stran %}
<div class='imgdiv'>
<img
id='img-{{i}}'
width='1021' height='1448'
src='/media/korektury/img/{{korekturovanepdf.get_prefix}}-{{i}}.png'
alt='Strana {{ i|add:1 }}'
class="strana"
/>
</div>
<hr/>
{% endfor %}
<script>
// Pro umisťování korektur
const HORIZONTALNI_MEZERA = 10;
const VERTIKALNI_MEZERA = 5;
const MINIMALNI_VYSKA_POINTERU = 30;
/**
* Mapování index_strany |-> strana
* @type {Object.<int, Strana>}
*/
const strany = {};
/** Třída spravující jednu stranu PDF a umisťující na ni příslušné korektury. */
class Strana {
/**
* <img> příslušící straně
* @type {HTMLElement}
*/
htmlElement_img;
/**
* <div> obalující stranu, do něj se umisťují korektury
* @type {HTMLElement}
*/
htmlElement_div;
/**
* Index strany (používá se při ukládání korektury (a načítání <img>))
* @type {Number}
*/
id;
/**
* Korektury na příslušné straně (BÚNO setříděné podle vertikálního umístění)
* @type {Korektura[]}
*/
korektury;
/**
* Uloží si data (včetně pointrů na správné části HTML DOMu) a nastaví event-listener
* @param {HTMLElement} htmlElement_img
*/
constructor(htmlElement_img) {
this.htmlElement_img = htmlElement_img;
this.htmlElement_div = this.htmlElement_img.parentNode;
this.id = parseInt(this.htmlElement_img.id.substring(4));
this.korektury = []
this.htmlElement_img.addEventListener('click', event => this.#korekturuj(event));
strany[this.id] = this;
}
/**
* Otevře korekturovací formulář pro přidání korektury v daném místě
* @param {MouseEvent} event
*/
#korekturuj(event) {
switch (document.body.dataset.stav_pdf) {
case 'zanaseni':
if (!confirm('Právě jsou zanášeny korektury, opravdu chcete přidat novou?')) return;
break;
case 'zastarale':
if (!confirm('Toto PDF je již zastaralé, opravdu chcete vytvořit korekturu?')) return;
break;
}
let dx, dy;
if (event.pageX != null) {
dx = event.pageX - this.htmlElement_div.offsetLeft;
dy = event.pageY - this.htmlElement_div.offsetTop;
} else { //IE a další
dx = event.offsetX;
dy = event.offsetY;
}
korekturovaci_formular.zobraz(this, dx, dy, '');
console.log("Pro přesun korektur: strana = " + this.id + ", x = " + dx + ", y = " + dy);
}
/** Setřídí seznam korektur příslušný dané straně */
setrid_korektury() { this.korektury.sort((a, b) => a.y - b.y); }
/** Zobrazí korektury a jejich pointry (a umístí je správně pod sebe) na dané straně */
umisti_korektury() {
this.setrid_korektury()
const w = this.htmlElement_img.clientWidth;
let spodek_posledni_korektury = 0;
for (const korektura of this.korektury) {
const x = korektura.x;
const y = korektura.y;
const pointer = korektura.pointer;
this.htmlElement_div.appendChild(pointer);
this.htmlElement_div.appendChild(korektura.htmlElement);
const delta_y = (y > spodek_posledni_korektury) ? 0: spodek_posledni_korektury - y + VERTIKALNI_MEZERA;
pointer.style.left = x;
pointer.style.top = y;
pointer.style.width = w - x + HORIZONTALNI_MEZERA;
pointer.style.height = MINIMALNI_VYSKA_POINTERU + delta_y;
korektura.htmlElement.style.left = w + HORIZONTALNI_MEZERA;
korektura.htmlElement.style.top = y + delta_y;
spodek_posledni_korektury = Math.max(
spodek_posledni_korektury,
korektura.htmlElement.offsetTop + korektura.htmlElement.offsetHeight + VERTIKALNI_MEZERA
); // FIXME nemám páru, proč +VERTIKALNI_MEZERA funguje, ale opravuje to bug, že nově vytvořené korektury za sebou neměly mezeru
}
this.htmlElement_div.style.height = "unset";
if (this.htmlElement_div.offsetHeight < spodek_posledni_korektury)
this.htmlElement_div.style.height = spodek_posledni_korektury;
}
}
// Vytvoření objektu Strana pro každou stranu
for (const strana_img of document.getElementsByClassName('strana'))
new Strana(strana_img);
/**
* Seznam stran setřízený podle toho, jak jdou po sobě (aby se dali korektury prohledávat od první na HTML stránce po poslední)
* @type {Strana[]}
*/
const setrizene_strany = Object.values(strany);
setrizene_strany.sort((a, b) => a.htmlElement_img.offsetTop - b.htmlElement_img.offsetTop);
/** Zobrazí korektury a jejich pointry (a umístí je správně pod sebe) na všech stranách */
function umisti_korektury() { for (const strana of Object.values(strany)) strana.umisti_korektury(); }
</script>

View file

@ -14,7 +14,7 @@ def send_email_notification_komentar(oprava: Oprava, autor: Organizator, request
# parametry e-mailu # parametry e-mailu
#odkaz = "https://mam.mff.cuni.cz/korektury/{}/".format(oprava.pdf.pk) #odkaz = "https://mam.mff.cuni.cz/korektury/{}/".format(oprava.pdf.pk)
odkaz = request.build_absolute_uri(reverse('korektury', kwargs={'pdf': oprava.pdf.pk})) odkaz = request.build_absolute_uri(reverse('korektury', kwargs={'pdf': oprava.pdf.pk}))
odkaz = f"{odkaz}#kor{oprava.id}-pointer" odkaz = f"{odkaz}#op{oprava.id}-pointer"
from_email = 'korekturovatko@mam.mff.cuni.cz' from_email = 'korekturovatko@mam.mff.cuni.cz'
subject = 'Nová korektura od {} v {}'.format(autor, oprava.pdf.nazev) subject = 'Nová korektura od {} v {}'.format(autor, oprava.pdf.nazev)
texty = [] texty = []

View file

@ -49,11 +49,11 @@ class KorekturySeskupeneListView(KorekturyAktualniListView):
class KorekturyView(generic.DetailView): class KorekturyView(generic.DetailView):
model = KorekturovanePDF model = KorekturovanePDF
pk_url_kwarg = "pdf" pk_url_kwarg = "pdf"
template_name = 'korektury/korekturovatko/html_obal.html' template_name = 'korektury/korekturovatko/htmlstrana.html'
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['indexy_stran'] = range(self.object.stran) context['img_indexes'] = range(self.object.stran)
context['tagy'] = KorekturaTag.objects.all() context['tagy'] = KorekturaTag.objects.all()
return context return context

View file

@ -8,7 +8,3 @@
color: #aaa; color: #aaa;
} }
} }
.hodnoceni.zvyraznene {
background-color: var(--svetla-oranzova);
}

View file

@ -1,4 +1,4 @@
{% extends "odevzdavatko/base.html" %} {% extends "base.html" %}
{% load static %} {% load static %}
{% load deadliny %} {% load deadliny %}
{% load mail %} {% load mail %}
@ -7,7 +7,6 @@
{# Přišlo mi to hezčí, než psát všude if. #} {# Přišlo mi to hezčí, než psát všude if. #}
{% block custom_css %} {% block custom_css %}
{{ block.super }}
{% if object.resitele.count == 1 %} {% if object.resitele.count == 1 %}
<style>.teamovaCast {display: none}</style> <style>.teamovaCast {display: none}</style>
{% endif %} {% endif %}
@ -113,7 +112,7 @@
<tr><th>Problém</th><th>{# 📖 #}🧍</th><th>{# 🔵 #}🧍∑</th><th class="teamovaCast">{# 💪 #}🧑‍🤝‍🧑</th><th class="teamovaCast">{# ❤ #}🧑‍🤝‍🧑∑</th><th>Deadline pro body</th><th>Zpětná vazba pro řešitele</th></tr> <tr><th>Problém</th><th>{# 📖 #}🧍</th><th>{# 🔵 #}🧍∑</th><th class="teamovaCast">{# 💪 #}🧑‍🤝‍🧑</th><th class="teamovaCast">{# ❤ #}🧑‍🤝‍🧑∑</th><th>Deadline pro body</th><th>Zpětná vazba pro řešitele</th></tr>
{% for subform in form %} {% for subform in form %}
<tbody> <tbody>
<tr class="hodnoceni{% if subform.problem.initial|ma_opravovatele:user %} zvyraznene{% endif %}"> <tr class="hodnoceni" {% if subform.problem.initial|ma_opravovatele:user %} style="background-color: var(--svetla-oranzova);"{% endif %}>
<td>{{ subform.problem }}</td> <td>{{ subform.problem }}</td>
<td class="bodovani">{{ subform.body }}</td> <td class="bodovani">{{ subform.body }}</td>
<td class="bodovani">{{ subform.body_celkem }}</td> <td class="bodovani">{{ subform.body_celkem }}</td>

View file

@ -12,22 +12,6 @@ POZOR! Kolize jmen! Dva řešitelé mají stejné makro!
{% autoescape off %} {% autoescape off %}
{% load tex %} {% load tex %}
\ExplSyntaxOn
\char_set_catcode_other:n{32}% Odsud dál do \char_set_catcode_space:n{32} nesmí být za žádnou cenu jiná mezera (tj. i zlom řádku) než ty mezi jménem a příjmením
\prop_const_from_keyval:Nn\g_tituly%
{%
{% for r in resitele %}{{r|sloz}}={% if r.titul == '' %}{}{% else %}{\titul{{r.titul|sloz}}}{% endif %},%
{% endfor %}}%
\char_set_catcode_space:n{32}
\DeclareDocumentCommand\Titul{mO{#1}}{%
\prop_if_in:NnTF\g_tituly{#1}%
{\prop_item:Nn\g_tituly{#1}}%
{\ClassError{mam}{Titul pro #1 nenalezen!}{}}%
#2%
}
\ExplSyntaxOff
{% for r in resitele %} {% for r in resitele %}
{% if r.titul == '' %} {% if r.titul == '' %}
{% spaceless %} {% spaceless %}