Compare commits

..

18 commits

Author SHA1 Message Date
LEdoian
541ea5f737 WIP: refactoring, možná to teď nefunguje… 2025-11-25 03:02:44 +00:00
Pavel "LEdoian" Turinsky
d45b970f77 fixup! Galerie: Obrázek přejmenován na Soubor 2025-11-18 23:27:18 +01:00
Pavel "LEdoian" Turinsky
3e18f51550 Galerie: Obrázek přejmenován na Soubor 2025-11-18 18:42:27 +01:00
Pavel "LEdoian" Turinsky
d07d4daf04 Moc věcí se jmenuje obrázek… 2025-05-05 17:14:54 +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
47 changed files with 1142 additions and 1298 deletions

View file

@ -1,36 +0,0 @@
FROM python:3.9.18-slim-bullseye
# set work directory
WORKDIR /usr/mamweb-docker
# set environment variables
ENV PIP_DISABLE_PIP_VERSION_CHECK 1
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
# install dependencies
RUN apt-get update && apt-get install -y \
libpq-dev \
gcc \
locales \
imagemagick \
netcat \
postgresql-client
RUN pip install --upgrade pip
COPY ./requirements.txt .
COPY ./constraints.txt .
RUN pip install -r requirements.txt
# allow correct locales
RUN sed -i '/cs_CZ.UTF-8/s/^# //g' /etc/locale.gen && \
locale-gen
ENV LANG cs_CZ.UTF-8
ENV LANGUAGE en_US:en
ENV LC_ALL cs_CZ.UTF-8
# copy project
COPY . .
# create test data once db is ready
RUN chmod +x /usr/mamweb-docker/docker_entrypoint.sh
ENTRYPOINT ["/usr/mamweb-docker/docker_entrypoint.sh"]

File diff suppressed because one or more lines are too long

View file

@ -1,23 +0,0 @@
services:
web:
build: .
command: python manage.py runserver 0.0.0.0:8000
volumes:
- .:/usr/mamweb-docker
ports:
- 8000:8000
depends_on:
- db
db:
image: postgres:13-bullseye
volumes:
- postgres_data:/var/lib/postgresql/data/
environment:
- POSTGRES_USER=mam-web
- POSTGRES_PASSWORD=RoEGG5g7&b # Random generated string corresponding with Django settings
- POSTGRES_DB=mam_docker
volumes:
postgres_data:

View file

@ -1,19 +0,0 @@
#!/bin/sh
echo "Waiting for Postgres..."
while ! nc -z "db" "5432"; do
sleep 0.1
done
echo "PostgreSQL started"
PGPASSWORD="RoEGG5g7&b" # Random generated, corresponds to the one in docker-compose.yml
if psql "postgresql://mam-web:$PGPASSWORD@db:5432/mam_docker" -t -c '\dt' | cut -d \| -f 2 | grep -qw "seminar_cisla"; then
echo "\nExistuje tabulka 'seminar_cisla' v db, testdata pravděpodobně byla vygenerována.\n"
else
python ./manage.py testdata
python ./manage.py loaddata data/*
fi
exec "$@"

View file

@ -1,15 +0,0 @@
Lokální vývoj pomocí dockeru
============================
Přijde vám standardní zprovoznění painful? Docker comes to the rescue. Stačí mít nainstalovaný ``docker`` a spustit ``docker compose up``.
Co se děje under the hood
-------------------------
- ``docker-compose.yml`` specifikuje, že chceme kontejner pro web, který je závislý na kontejneru s PostgreSQL db
- ``web`` se buildí z ``Dockerfilu``, mountuje si kořen repa jako svůj volume (takže vidí změny), na konci spouští webserver a vysatvuje ho na port 8000 localhosta
- ``db`` je z ``postgres:13-bullseye`` a má nastavené nějaké parametry, svoje data ukládá jako docker volume
- ``Dockerfile`` staví ``web`` na pythonu3.9 a debianu bullseye (mělo by odpovídat gimlimu) - nainstaluje dependencies, nastaví locale a entrypoint (co se má vykonat při spouštění kontejneru)
- ``docker_entrypoint.sh`` počká na Postgres ready v ``db``, podívá se jestli jsou v něm testdata a když ne, tak je vygeneruje, pak spustí command z compose (i.e. webserver)
- ``mamweb/settings.py`` vybere django settings podle cesty (jako doteď), vybere ``mamweb/settings_docker.py``, které importuje všechno z ``mamweb/settings_local.py``, přepíše jen ``DATABASES`` a ``SECRET_KEY``, aby se jako DB Engine používal Postgres ve vedlejším kontejneru, aby se celý web choval stejně jako ``_test`` a ``_prod`` (``_local`` aktuálně používá DB Engine sqlite3, které se v něčem chová trochu rozdílně)
- kontejner ``web`` se musí nějak připojovat k postgresu v ``db``, k tomu slouží user ``mam-web``, náhodně vygenerované heslo (v ``docker-compose.yml`` a ``docker_entrypoint.sh``, musí se shodovat), db se jmenuje ``mam_docker``

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``)?"
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."
Debian 11, "oficiální, výchozí", ??, ??, ??, "Určitě to tam rozběhat jde, protože Gimli. Nejspíš bude relativně podobné Ubuntu."

View file

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

View file

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

View file

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

View file

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

View file

@ -6,9 +6,10 @@
/* velká fotka */
/* zmenšování spolu s oknem prohlížeče */
.galerie .obrazek, .titulni_obrazek {
max-width: 100%;
max-width: max(100%, 900px);
max-height: 900px;
height: auto;
width: auto\9; /* ie8 */
width: auto;
}
.predchozi_obrazek{
@ -75,6 +76,8 @@
.galerie_nahledy img {
margin: 10px;
max-height: 100px;
max-width: 200px;
}
.galerie_nahledy div.navigace {

View file

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

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

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

After

Width:  |  Height:  |  Size: 2.7 KiB

View file

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

View file

@ -1,5 +1,7 @@
{% extends "galerie/base.html" %}
{% load bazmeky %}
{% block nadpis1a %}
Galerie {{galerie.nazev}}
{% endblock %}
@ -26,7 +28,7 @@ Galerie {{galerie.nazev}}
{% if not obrazky %}
<div class="galerie_hlavicka">
{% if galerie.titulni_obrazek %}
<img src="{{ galerie.titulni_obrazek.obrazek_stredni.url }}" class="titulni_obrazek">
{% zobrazit galerie.titulni_obrazek.jako_bazmek class=titulni_obrazek %}
{% endif %}
</div>
{% endif %}
@ -55,10 +57,7 @@ Galerie {{galerie.nazev}}
{% endif %}
class="podgalerie_nahled {% if pgalerie.zobrazit == 1 or pgalerie.zobrazit == 2 %}mam-org-only{% endif %}{% if pgalerie.zobrazit == 3 %}mam-resitel-only{% endif %}">
{% if pgalerie.titulni_obrazek %}
{% with pgalerie.titulni_obrazek.obrazek_maly as obrazek %}
<img src="{{ obrazek.url }}"
/>
{% endwith %}
{% zmenseny_nahled pgalerie.titulni_obrazek.jako_bazmek class="" %}
{% endif %}
<div class="nazev_galerie">
{{ pgalerie|truncatechars:max_delka_nazvu }}
@ -96,13 +95,10 @@ Galerie {{galerie.nazev}}
{% if obrazek.popis %}
title="{{ obrazek.popis }}"
{% endif %}
href="./{{obrazek.pk}}#nahoru" class="galerie_nahled"><span class="vystredeno"></span><img
src="{{obrazek.obrazek_maly.url}}"
{% if obrazek.popis %}
title="{{ obrazek.popis }}"
{% endif %}
width="{{ obrazek.obrazek_maly.width }}"
height="{{ obrazek.obrazek_maly.height }}" />
href="./{{obrazek.pk}}#nahoru" class="galerie_nahled">
<span class="vystredeno"></span>
{% zmenseny_nahled obrazek.jako_bazmek %}
{# title=obrazek.popis (a byl tu if, že se použil jen když existoval…) width=obrazek.obrazek_maly.width height=obrazek.obrazek_maly.height #}
</a>
{% endfor %}
<br>

View file

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

140
galerie/typy.py Normal file
View file

@ -0,0 +1,140 @@
"""
Naše typy objektů v galeriích.
V databázi jsou všechny v jedné tabulce, protože se liší jen prezentací navenek. Všechny implementují ~~rozhraní~~ ABC `ZobrazitelnyBazmek`.
Doporučené použití: TODO
"""
import abc
from datetime import datetime
import logging
logger = logging.getLogger(__name__)
from django.contrib.staticfiles.finders import find
# FIXME: static fily na lokálním webu??
find = lambda x: '/static/'+x
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 Soubor
class ZobrazitelnyBazmek(abc.ABC):
def __init__(self, db_objekt):
self.db_objekt = db_objekt
# zkratka
self.soubor = db_objekt.soubor
# Volá se z templatetagu
@abc.abstractmethod
def zobrazit(self, **kwargs) -> HTML:
"""To, co se zobrazí v galerii jako hlavní obrázek (při prohlížení konkrétního obrázku a jako tittulní obrázek u galerií, které nemají vlastní obrázky (kupř. Vávrovka 2015))"""
...
@abc.abstractmethod
def zmenseny_nahled(self, **kwargs) -> HTML:
"""Zmenšené obrázky v přehledu obrázků a pod hlavním obrázkem (předchozí/následující)"""
...
@property
#@abc.abstractmethod
def cas_porizeni(self) -> datetime | None:
# TODO: použít tohle na automatické řazení věcí. Má vytáhnout datum z metadat
raise NotImplementedError("Prosím, naimplementuj hledání času vzniku v metadatech (nebo vrať None).")
#TODO: nativní rozměry?
### Obrázky ###
# Odpovídá původnímu chování (bo se mi nechce vymýšlet novoty…
class ObrazekStredniSpec(ImageSpec):
"""Specifikace obrázku pro velké zobrazení"""
processors = [
Transpose(Transpose.AUTO), # Rotuj podle dat v EXIFu
ResizeToFit(900, 675, upscale=False),
]
format = 'JPEG'
options = {'quality': 95} # Proč tolik?
class ObrazekMalySpec(ImageSpec):
"""Specifikace obrázku pro náhledy (pod hlavním obrázkem nebo v přehledu galerie)"""
processors = [
Transpose(Transpose.AUTO),
ResizeToFit(167, 167, upscale=False),
]
format = 'JPEG'
options = {'quality': 95}
def _fmt_attrs(attrs):
return format_html_join(' ', r'{}="{}"', ((mark_safe(k), v) for k, v in attrs.items()))
class Obrazek(ZobrazitelnyBazmek):
"""Obrázek pro zobrazení
Použije některý z ImageSpec-ů jako popis transformace a ImageCacheFile pro uložení výsledného obrázku.
Reference: https://django-imagekit.readthedocs.io/en/latest/#defining-specs-outside-of-models
Reference: https://django-imagekit.readthedocs.io/en/latest/caching.html
"""
def zobrazit(self, **kwargs):
# Jak se takový cachefile používá je potřeba vyčíst ze zdrojáků?
file = ImageCacheFile(ObrazekStredniSpec(source=self.soubor))
file.generate()
attrs = _fmt_attrs(kwargs)
html = format_html(r'<img src="{}" {} />', file.url, attrs)
return html
def zmenseny_nahled(self, **kwargs):
file = ImageCacheFile(ObrazekMalySpec(source=self.soubor))
file.generate()
attrs = _fmt_attrs(kwargs)
html = format_html(r'<img src="{}" {} />', file.url, attrs)
return html
class Video(ZobrazitelnyBazmek):
def __init__(self, *a, **kwa):
super().__init__(*a, **kwa)
self.placeholder = find('galerie/video_placeholder.svg')
def zobrazit(self, **kwargs):
attrs = _fmt_attrs(kwargs)
# Atributy specifické pro video musíme vesměs vyřešit tady… (šlo by to {% if %}-ovat podle typu, ale to je spíš haluz.
html = format_html(r'<video src="{}" preload="metadata" controls {} />', self.soubor.url, attrs)
return html
def zmenseny_nahled(self, **kwargs):
attrs = _fmt_attrs(kwargs)
return format_html(r'<img src="{}" {} />', self.placeholder, attrs)
class DummyBazmek(ZobrazitelnyBazmek):
def __init__(self, *a, **kwa):
super().__init__(*a, **kwa)
self.placeholder = find('galerie/neznamy_placeholder.svg')
def zobrazit(self, **kwargs):
attrs = _fmt_attrs(kwargs)
# Stavíme HTML ad-hoc, co se může rozbít :'-)
obrazek = format_html(r'<img src="{}"/>', self.placeholder, attrs)
popisek = format_html(r'<p style="text-align: center; font-size: 1.1em;">Prosím oprav typ aktuálního obrázku v <a href="{}">adminu</a>!</p>', reverse('admin:galerie_soubor_change', args=(self.db_objekt.id,)))
return format_html(r'<div {}>{}{}</div>', attrs, obrazek, popisek)
def zmenseny_nahled(self, **kwargs):
attrs = _fmt_attrs(kwargs)
return format_html(r'<img src="{}" {} />', self.placeholder, attrs)
def tipniTyp(soubor) -> Soubor.Typ:
from PIL import Image, UnidentifiedImageError
try:
Image.open(soubor)
return Soubor.Typ.OBRAZEK
except UnidentifiedImageError:
return Soubor.Typ.NEVIM
logger.warning("Nepodařilo se tipnout typ nečekaným způsobem!")
return Soubor.Typ.NEVIM

View file

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

View file

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

View file

@ -1,19 +1,19 @@
.textzanaseni { display:none; }
.textzastarale { display:none; }
#prekomentar, #prekorektura, #prepointer { display: none; }
#prekomentar, #preoprava, #prepointer { display: none; }
body {
&[data-stav_pdf="pridavani"] {
&[data-status="pridavani"] {
background: #f3f3f3;
}
&[data-stav_pdf="zanaseni"] {
&[data-status="zanaseni"] {
background: yellow;
.textzanaseni { display: unset; }
}
&[data-stav_pdf="zastarale"] {
&[data-status="zastarale"] {
background: red;
.textzastarale { display: unset; }
@ -28,25 +28,25 @@ body {
img{background:white;}
/* Barvy korektur */
[data-stav_korektury="k_oprave"] {
[data-opravastatus="k_oprave"] {
--rgb: 255, 0, 0;
[value="k_oprave"] { display: none }
.komentovat_disabled { display: none }
}
[data-stav_korektury="opraveno"] {
[data-opravastatus="opraveno"] {
--rgb: 0, 0, 255;
[value="opraveno"] { display: none }
.komentovat { display: none }
}
[data-stav_korektury="neni_chyba"] {
[data-opravastatus="neni_chyba"] {
--rgb: 128, 128, 128;
[value="neni_chyba"] { display: none }
.komentovat { display: none }
}
[data-stav_korektury="k_zaneseni"] {
[data-opravastatus="k_zaneseni"] {
--rgb: 0, 255, 0;
[value="k_zaneseni"] { display: none }
@ -54,16 +54,10 @@ img{background:white;}
}
/* Skrývání korektur */
[data-korektura_sbalena="true"] {
.korektura-telo { display: none; }
.korektura-tlacitka { display: none; }
.sbal-rozbal-img { transform: rotate(180deg); }
}
/* Skrývání komentářů */
[data-komentar_sbalen="true"] {
.sbal-rozbal-img { transform: rotate(180deg); }
.uprav-komentar { display: none; }
.komtext { display: none; }
[data-opravazobrazit="false"] {
.corr-body { display: none; }
.corr-buttons { display: none; }
.toggle-button { transform: rotate(180deg); }
}
@ -78,14 +72,14 @@ img{background:white;}
--alpha: 0.35;
/* Zvýraznění čáry při najetí na korekturu */
&[data-hover="true"] {
&[data-highlight="true"] {
border-width: 3px;
--alpha: 1;
}
}
/* Korektura samotná */
.korektura {
.oprava {
margin: 1px;
background-color: white;
width: 300px;
@ -112,11 +106,11 @@ img{background:white;}
button img { pointer-events: none; }
.hlavicka-komentare {
.corr-header {
overflow: auto;
}
.autor {
.author {
font-weight: bold;
float: left;
margin-top: 3px;
@ -139,7 +133,7 @@ form {
}
/* Přidávání korektury / úprava komentáře */
#korekturovaci-formular-div {
#commform-div {
position: absolute;
background-color: white;
padding: 3px;
@ -154,8 +148,8 @@ form {
margin: 2px;
padding: 2px;
&[data-vybran="false"] { background: unset !important; }
/*&[data-vybran="true"] { border-color: unset !important; }*/
&[data-selected="false"] { background: unset !important; }
/*&[data-selected="true"] { border-color: unset !important; }*/
}
/* Š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í) #}
<b>Změnit stav PDF:</b>
<h4>Změnit stav PDF:</h4>
<i>Aktuální: {{korekturovanepdf.status}}</i>
<br>
<form method="post" id="PDFSTAV_FORM">
{% csrf_token %}
@ -13,24 +13,19 @@
</form>
<script>
/**
* Formulář měnící stav korekturovaného PDF
* @type {HTMLFormElement}
*/
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 FormData a jiné náležitosti (method: POST) posílané při změně stavu korekturovaného PDF
* @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 {RequestInit} data
* @param {Boolean} catchError
*/
function fetchStav(data, catchError=true) {
fetch("{% url 'korektury_api_pdf_stav' korekturovanepdf.id %}", data
)
.then(response => {
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);});
}

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 %}
<html lang='cs'>
<html>
<head>
<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 href="{% static 'css/rozliseni.css' %}?version=3" rel="stylesheet">
<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=2" rel="stylesheet">
<script src="{% static "korektury/opraf.js"%}?version=2"></script>
<title>Korektury {{korekturovanepdf.nazev}}</title>
</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>
@ -19,17 +19,19 @@
<br>
<i>Klikni na chybu, napiš komentář</i> |
<a href="{{korekturovanepdf.pdf.url}}">stáhnout PDF (bez korektur)</a> |
<a href="../">seznam souborů</a> |
<a href="/admin/korektury/korekturovanepdf/">Spravovat PDF</a> |
<a href="../help">nápověda</a> |
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;|
<a href="../">🔙 seznam korekturovaných PDF</a> |
<a href="/">🏠 hlavní stránka</a> |
<a href="/">hlavní stránka</a> |
<a href="https://mam.mff.cuni.cz/wiki">wiki</a> |
<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/>
<p>
@ -37,14 +39,9 @@
<hr>
<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")
/** Aktualizuje, kolik který autor má komentářů u daného korekturovaného PDF. */
function aktualizuj_pocty_zasluh() {
function updatuj_pocty_zasluh() {
const pocty_autoru = {};
for (let komentar of Object.values(komentare)) {
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
#odkaz = "https://mam.mff.cuni.cz/korektury/{}/".format(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'
subject = 'Nová korektura od {} v {}'.format(autor, oprava.pdf.nazev)
texty = []

View file

@ -49,11 +49,11 @@ class KorekturySeskupeneListView(KorekturyAktualniListView):
class KorekturyView(generic.DetailView):
model = KorekturovanePDF
pk_url_kwarg = "pdf"
template_name = 'korektury/korekturovatko/html_obal.html'
template_name = 'korektury/korekturovatko/htmlstrana.html'
def get_context_data(self, **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()
return context

View file

@ -14,9 +14,6 @@ if "mamweb-test" in os.path.abspath(__file__):
elif "mamweb-prod" in os.path.abspath(__file__):
from .settings_prod import *
elif "mamweb-docker" in os.path.abspath(__file__):
from .settings_docker import *
else:
from .settings_local import *

View file

@ -1,33 +0,0 @@
# -*- coding: utf-8 -*-
#
# Docker nastaveni settings.py
#
# Pro vyber tohoto nastaveni muzete pouzit tez:
# DJANGO_SETTINGS_MODULE=mamweb.settings_docker ./manage.py ...
#
# Import common settings
from .settings_common import * # zatim nutne, casem snad vyresime # noqa
from mamweb.settings_local import * # Import all the settings for local development
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'EZfSzeuDCycKr5ZjiCQ^45ZqFU@8Ke#YDwn9ThqerfEpu^yV#p'
# Database
# https://docs.djangoproject.com/en/1.7/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': 'mam_docker',
'USER': 'mam-web',
'PASSWORD': 'RoEGG5g7&b', # Random generated string corresponding with docker-compose
'TEST': {
'NAME': 'mam-docker-testdb',
},
"HOST": "db",
"PORT": "5432",
},
}

View file

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

View file

@ -1,13 +1,11 @@
{% extends "odevzdavatko/base.html" %}
{% extends "base.html" %}
{% load static %}
{% load deadliny %}
{% load mail %}
{% load jmena %}
{% load orgove %}
{# Přišlo mi to hezčí, než psát všude if. #}
{% block custom_css %}
{{ block.super }}
{% if object.resitele.count == 1 %}
<style>.teamovaCast {display: none}</style>
{% endif %}
@ -113,7 +111,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>
{% for subform in form %}
<tbody>
<tr class="hodnoceni{% if subform.problem.initial|ma_opravovatele:user %} zvyraznene{% endif %}">
<tr class="hodnoceni">
<td>{{ subform.problem }}</td>
<td class="bodovani">{{ subform.body }}</td>
<td class="bodovani">{{ subform.body_celkem }}</td>

View file

@ -30,14 +30,10 @@ def gen_reseni_ulohy(rnd, cisla, uloha, pocet_resitelu, poradi_cisla, resitele_c
res.resitele.set(res_vyber)
res.save()
deadlines = cisla[poradi_cisla -1 ].deadline_v_cisle.all()
dline = random.choice(deadlines)
# Vytvoření hodnocení.
hod = Hodnoceni.objects.create(
body=rnd.randint(0, uloha.max_body),
deadline_body = dline,
cislo_body=cisla[poradi_cisla - 1],
reseni=res,
problem=uloha
)

View file

@ -12,22 +12,6 @@ POZOR! Kolize jmen! Dva řešitelé mají stejné makro!
{% autoescape off %}
{% 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 %}
{% if r.titul == '' %}
{% spaceless %}

View file

@ -52,7 +52,6 @@
</tbody>
</table>
<p>Tabulka je scrollovatelná. Je v ní {{ vysledkovka.radky_vysledkovky|length }} řešitelů.</p>
<p>Po kliknutí na políčko v záhlaví tabulky se u daného problému zobrazí (/skryje) detailní rozpis, za které podproblémy řešitelé dostali body.</p>
{# TODELETE #}

View file

@ -32,5 +32,3 @@
{% endfor %}
<tbody>
</table>
<p>Tabulka je scrollovatelná. Je v ní {{ vysledkovka.radky_vysledkovky|length }} řešitelů. </p>