Browse Source

Merge remote-tracking branch 'gitea/master' into zadavatko_problemu

zadavatko_problemu
Pavel "LEdoian" Turinsky 2 years ago
parent
commit
9c06bc6f62
  1. 24
      .editorconfig
  2. 1
      api/urls.py
  3. 15
      api/views/autocomplete.py
  4. 3
      mamweb/settings_common.py
  5. 1
      mamweb/settings_local.py
  6. 4
      mamweb/static/css/mamweb.css
  7. 2
      mamweb/templates/admin/base_site.html
  8. 6
      mamweb/templates/menu_mobile.html
  9. 12
      odevzdavatko/forms.py
  10. 6
      odevzdavatko/templates/odevzdavatko/detail.html
  11. 3
      odevzdavatko/templates/odevzdavatko/detail_resitele.html
  12. 46
      odevzdavatko/views.py
  13. 4
      prednasky/urls.py
  14. 2
      requirements.txt
  15. 18
      seminar/migrations/0108_nastaveni_cena_sous.py
  16. 18
      seminar/migrations/0109_hodnoceni_feedback.py
  17. 2
      seminar/models/odevzdavatko.py
  18. 4
      seminar/models/tvorba.py
  19. 25
      seminar/static/seminar/stvrzenky.tex
  20. 3
      seminar/templates/seminar/archiv/cislo.html
  21. 33
      seminar/templates/seminar/archiv/cislo_obalkovani.html
  22. 20
      seminar/templates/seminar/archiv/odmeny.html
  23. 30
      seminar/templates/seminar/org/obalkovani.html
  24. 3
      seminar/templates/seminar/zadani/AktualniVysledkovka.html
  25. 17
      seminar/testutils.py
  26. 6
      seminar/urls.py
  27. 56
      seminar/views/views_all.py
  28. 37
      soustredeni/templates/soustredeni/stvrzenky.tex
  29. 5
      soustredeni/templates/soustredeni/ucastnici.tex
  30. 26
      soustredeni/views.py
  31. 2
      various/autentizace/templates/autentizace/login.html
  32. 32
      vysledkovky/templates/vysledkovky/vysledkovka_cisla.html
  33. 24
      vysledkovky/templates/vysledkovky/vysledkovka_rocnik.html
  34. 5
      vysledkovky/utils.py

24
.editorconfig

@ -0,0 +1,24 @@
# Univerzální popis našich konvencí pro nastavení editorů.
# Vizte https://editorconfig.org pro detaily
root = true
[*]
charset = utf-8
# Unixové řádky
end_of_line = lf
insert_final_newline = true
[*.{py,css}]
indent_style = tab
# Nenařizujeme konkrétní šířku tabulátoru
indent_size = unset
# Automaticky generované migrace dodržují PEP-8, nemá smysl s tím moc bojovat.
[*/migrations/*.py]
indent_style = space
indent_size = 4
[*.html]
indent_style = space
indent_size = 2

1
api/urls.py

@ -23,6 +23,7 @@ urlpatterns = [
path('api/autocomplete/skola/', views.SkolaAutocomplete.as_view(), name='autocomplete_skola'), path('api/autocomplete/skola/', views.SkolaAutocomplete.as_view(), name='autocomplete_skola'),
path('api/autocomplete/resitel/', org_required(views.ResitelAutocomplete.as_view()), name='autocomplete_resitel'), path('api/autocomplete/resitel/', org_required(views.ResitelAutocomplete.as_view()), name='autocomplete_resitel'),
path('api/autocomplete/problem/odevzdatelny', views.OdevzdatelnyProblemAutocomplete.as_view(), name='autocomplete_problem_odevzdatelny'), path('api/autocomplete/problem/odevzdatelny', views.OdevzdatelnyProblemAutocomplete.as_view(), name='autocomplete_problem_odevzdatelny'),
path('api/autocomplete/problem/vsechny', views.ProblemAutocomplete.as_view(), name='autocomplete_problem'),
# Ceka na autocomplete v3 # Ceka na autocomplete v3
# path('autocomplete/organizatori/', # path('autocomplete/organizatori/',

15
api/views/autocomplete.py

@ -66,6 +66,21 @@ class OdevzdatelnyProblemAutocomplete(autocomplete.Select2QuerySetView):
Q(nazev__icontains=self.q)) Q(nazev__icontains=self.q))
return qs return qs
class ProblemAutocomplete(autocomplete.Select2QuerySetView):
""" View k :mod:`dal.autocomplete` pro vyhledávání problémů především v odevzdávátku. """
def get_queryset(self):
# FIXME i starší úlohy
nastaveni = get_object_or_404(m.Nastaveni)
rocnik = nastaveni.aktualni_rocnik
temaQ = Q(Tema___rocnik = rocnik)
ulohaQ = Q(Uloha___cislo_zadani__rocnik=rocnik)
clanekQ = Q(Clanek___cislo__rocnik=rocnik)
qs = m.Problem.objects.filter(temaQ | ulohaQ | clanekQ).order_by("-stav", "nazev")
if self.q:
qs = qs.filter(
Q(nazev__icontains=self.q))
return qs
# Ceka na autocomplete v3 # Ceka na autocomplete v3
# class OrganizatorAutocomplete(autocomplete.Select2QuerySetView): # class OrganizatorAutocomplete(autocomplete.Select2QuerySetView):
# def get_queryset(self): # def get_queryset(self):

3
mamweb/settings_common.py

@ -160,6 +160,9 @@ INSTALLED_APPS = (
# 'admin_tools.menu', # 'admin_tools.menu',
# 'admin_tools.dashboard', # 'admin_tools.dashboard',
'django.contrib.admin', 'django.contrib.admin',
# Nechat na konci (INSTALLED_APPS je uspořádané):
'django_cleanup.apps.CleanupConfig', # Uklízí media/
) )
DEBUG_TOOLBAR_CONFIG = { DEBUG_TOOLBAR_CONFIG = {

1
mamweb/settings_local.py

@ -31,6 +31,7 @@ TEMPLATES[0]['OPTIONS']['debug'] = True
from ipaddress import ip_network from ipaddress import ip_network
ALLOWED_HOSTS = [str(ip) for ip in ip_network('192.168.0.0/16')] ALLOWED_HOSTS = [str(ip) for ip in ip_network('192.168.0.0/16')]
ALLOWED_HOSTS.append('127.0.0.1') ALLOWED_HOSTS.append('127.0.0.1')
ALLOWED_HOSTS.append('localhost')
# Database # Database
# https://docs.djangoproject.com/en/1.7/ref/settings/#databases # https://docs.djangoproject.com/en/1.7/ref/settings/#databases

4
mamweb/static/css/mamweb.css

@ -421,6 +421,10 @@ input {
margin: 5px; margin: 5px;
} }
textarea.feedback {
margin: 5px;
}
/* titulni stranka */ /* titulni stranka */

2
mamweb/templates/admin/base_site.html

@ -10,5 +10,5 @@
{% block bodyclass %}{{ LOCAL_TEST_PROD }}web{% endblock %} {% block bodyclass %}{{ LOCAL_TEST_PROD }}web{% endblock %}
{% block branding %} {% block branding %}
<h1 id="site-name"><a href="/"> M&M GWP web </a></h1> <h1 id="site-name"><a href="/"> M&M web </a></h1>
{% endblock %} {% endblock %}

6
mamweb/templates/menu_mobile.html

@ -3,11 +3,9 @@
{% autoescape off %} {% autoescape off %}
<ul class="menu_mobile"> <ul class="menu_mobile">
{% for item in sitetree_items %} {% for item in sitetree_items %}
<li class="{% if item.has_children %}dropdown{% endif %} {% if item.is_current or item.in_current_branch %}active{% endif %}" <li class="{% if item.has_children %}dropdown{% endif %} {% if item.is_current or item.in_current_branch %}active{% endif %}">
style="{% if item.title == "HIDDEN" %}display:none{% endif %}"
>
<a href="{% if item.has_children %}#{% else %}{% sitetree_url for item %}{% endif %}" {% if item.has_children %}class="dropdown-toggle" data-toggle="dropdown"{% endif %}> <a href="{% if item.has_children %}#{% else %}{% sitetree_url for item %}{% endif %}" {% if item.has_children %}class="dropdown-toggle" data-toggle="dropdown"{% endif %}>
{{ item.title_resolved }} {% if item.title == "HIDDEN" %}Korektury{% else %}{{ item.title_resolved }}{% endif %}
</a> </a>
<div class="submenu_mobile {% if item.is_current or item.in_current_branch %}active{% endif %}"> <div class="submenu_mobile {% if item.is_current or item.in_current_branch %}active{% endif %}">
{% if item.has_children %} {% if item.has_children %}

12
odevzdavatko/forms.py

@ -88,11 +88,12 @@ ReseniSPrilohamiFormSet = inlineformset_factory(m.Reseni,m.PrilohaReseni,
class JednoHodnoceniForm(forms.ModelForm): class JednoHodnoceniForm(forms.ModelForm):
class Meta: class Meta:
model = m.Hodnoceni model = m.Hodnoceni
fields = ('problem', 'body', 'deadline_body') fields = ('problem', 'body', 'deadline_body', 'feedback',)
widgets = { widgets = {
'problem': autocomplete.ModelSelect2( 'problem': autocomplete.ModelSelect2(
url='autocomplete_problem_odevzdatelny', # FIXME: Dovolit i starší? url='autocomplete_problem',
) ),
'feedback': forms.Textarea(attrs={'rows': 1, 'cols': 30, 'class': 'feedback'}),
} }
OhodnoceniReseniFormSet = formset_factory(JednoHodnoceniForm, OhodnoceniReseniFormSet = formset_factory(JednoHodnoceniForm,
@ -155,10 +156,7 @@ class OdevzdavatkoTabulkaFiltrForm(forms.Form):
result = [] result = []
date_str = strftime(DATE_FORMAT, datetime.date.min.timetuple()) result.append(("0001-01-01", f"Odjakživa"))
if date_str[0] == '1': # Někde očividně vrací strftime rok bez nul.
date_str = "000" + date_str
result.append((date_str, f"Odjakživa"))
for deadline in m.Deadline.objects.filter( for deadline in m.Deadline.objects.filter(
deadline__lte=timezone.now(), deadline__lte=timezone.now(),

6
odevzdavatko/templates/odevzdavatko/detail.html

@ -91,7 +91,7 @@ $(document).ready(function(){
<form method=post onsubmit="return zkontroluj_hodnoceni();"> <form method=post onsubmit="return zkontroluj_hodnoceni();">
{# Poznámka #} {# Poznámka #}
<h3>Poznámka:</h3> <h3>Neveřejná poznámka:</h3>
<p>{{ poznamka_form.poznamka }}</p> <p>{{ poznamka_form.poznamka }}</p>
{# Hodnocení: #} {# Hodnocení: #}
@ -101,13 +101,14 @@ $(document).ready(function(){
{{ form.management_form }} {{ form.management_form }}
</table> </table>
<table id="form_set"> <table id="form_set">
<tr><th>Problém</th><th>Body</th><th>Deadline pro body</th></tr> <tr><th>Problém</th><th>Body</th><th>Deadline pro body</th><th>Zpětná vazba pro řešitele</th></tr>
{% for subform in form %} {% for subform in form %}
<tbody> <tbody>
<tr class="hodnoceni"> <tr class="hodnoceni">
<td>{{ subform.problem }}</td> <td>{{ subform.problem }}</td>
<td>{{ subform.body }}</td> <td>{{ subform.body }}</td>
<td>{{ subform.deadline_body }}</td> <td>{{ subform.deadline_body }}</td>
<td>{{ subform.feedback }}</td>
<td><a href="#" class="smazat_hodnoceni" id="id_{{subform.prefix}}-jsremove"><img src="{% static "odevzdavatko/cross.png" %}" alt="Smazat"></a></td> <td><a href="#" class="smazat_hodnoceni" id="id_{{subform.prefix}}-jsremove"><img src="{% static "odevzdavatko/cross.png" %}" alt="Smazat"></a></td>
</tr> </tr>
</tbody> </tbody>
@ -123,6 +124,7 @@ $(document).ready(function(){
<td>{{ form.empty_form.problem }}</td> <td>{{ form.empty_form.problem }}</td>
<td>{{ form.empty_form.body }}</td> <td>{{ form.empty_form.body }}</td>
<td>{{ form.empty_form.deadline_body }}</td> <td>{{ form.empty_form.deadline_body }}</td>
<td>{{ form.empty_form.feedback }}</td>
<td><a href="#" class="smazat_hodnoceni" id="id_{{form.empty_form.prefix}}-jsremove"><img src="{% static "odevzdavatko/cross.png" %}" alt="Smazat"></a></td> <td><a href="#" class="smazat_hodnoceni" id="id_{{form.empty_form.prefix}}-jsremove"><img src="{% static "odevzdavatko/cross.png" %}" alt="Smazat"></a></td>
</tr> </tr>
</table> </table>

3
odevzdavatko/templates/odevzdavatko/detail_resitele.html

@ -37,11 +37,12 @@
{# Hodnocení: #} {# Hodnocení: #}
<h3>Hodnocení:</h3> <h3>Hodnocení:</h3>
<table id="form_set" class="dosla_reseni"> <table id="form_set" class="dosla_reseni">
<tr><th>Problém</th><th>Body</th>{# <th>Deadline pro body</th> #}</tr> <tr><th>Problém</th><th>Body</th><th>Zpětná vazba od opravovatele</th>{# <th>Deadline pro body</th> #}</tr>
{% for h in hodnoceni %} {% for h in hodnoceni %}
<tr class="hodnoceni"> <tr class="hodnoceni">
<td>{{ h.problem }}</td> <td>{{ h.problem }}</td>
<td>{{ h.body }}</td> <td>{{ h.body }}</td>
<td>{{ h.feedback }}</td>
{# <td>{{ h.deadline_body }}</td>#} {# <td>{{ h.deadline_body }}</td>#}
</tr> </tr>
{% endfor %} {% endfor %}

46
odevzdavatko/views.py

@ -218,10 +218,11 @@ class DetailReseniView(DetailView):
self.reseni = get_object_or_404(m.Reseni, id=self.kwargs['pk']) self.reseni = get_object_or_404(m.Reseni, id=self.kwargs['pk'])
result = [] # Slovníky s klíči problem, body, deadline_body -- initial data pro f.OhodnoceniReseniFormSet result = [] # Slovníky s klíči problem, body, deadline_body -- initial data pro f.OhodnoceniReseniFormSet
for hodn in m.Hodnoceni.objects.filter(reseni=self.reseni): for hodn in m.Hodnoceni.objects.filter(reseni=self.reseni):
result.append( result.append({
{"problem": hodn.problem, "problem": hodn.problem,
"body": hodn.body, "body": hodn.body,
"deadline_body": hodn.deadline_body, "deadline_body": hodn.deadline_body,
"feedback": hodn.feedback,
}) })
return result return result
@ -245,30 +246,26 @@ def hodnoceniReseniView(request, pk, *args, **kwargs):
formset = f.OhodnoceniReseniFormSet(request.POST) formset = f.OhodnoceniReseniFormSet(request.POST)
poznamka_form = f.PoznamkaReseniForm(request.POST, instance=reseni) poznamka_form = f.PoznamkaReseniForm(request.POST, instance=reseni)
# TODO: Napsat validaci formuláře a formsetu # TODO: Napsat validaci formuláře a formsetu
# TODO: Implementovat větev, kdy formulář validní není. if not (formset.is_valid() and poznamka_form.is_valid()):
if formset.is_valid() and poznamka_form.is_valid(): raise ValueError(formset.errors, poznamka_form.errors)
with transaction.atomic():
# Poznámka je jednoduchá na zpracování: with transaction.atomic():
poznamka_form.save() # Poznámka je jednoduchá na zpracování:
poznamka_form.save()
# Smažeme všechna dosavadní hodnocení tohoto řešení
qs = m.Hodnoceni.objects.filter(reseni=reseni) # Smažeme všechna dosavadní hodnocení tohoto řešení
logger.info(f"Will delete {qs.count()} objects: {qs}") qs = m.Hodnoceni.objects.filter(reseni=reseni)
qs.delete() logger.info(f"Will delete {qs.count()} objects: {qs}")
qs.delete()
# Vyrobíme nová podle formsetu
for form in formset: # Vyrobíme nová podle formsetu
problem = form.cleaned_data['problem'] for form in formset:
body = form.cleaned_data['body'] hodnoceni = m.Hodnoceni(
deadline_body = form.cleaned_data['deadline_body']
hodnoceni = m.Hodnoceni(
problem=problem,
body=body,
deadline_body=deadline_body,
reseni=reseni, reseni=reseni,
**form.cleaned_data,
) )
logger.info(f"Creating Hodnoceni: {hodnoceni}") logger.info(f"Creating Hodnoceni: {hodnoceni}")
hodnoceni.save() hodnoceni.save()
return redirect(success_url) return redirect(success_url)
@ -285,6 +282,7 @@ class ResitelReseniView(DetailView):
{ {
"problem": hodn.problem, "problem": hodn.problem,
"body": hodn.body, "body": hodn.body,
"feedback": hodn.feedback,
# "deadline_body": hodn.deadline_body, # "deadline_body": hodn.deadline_body,
} }
) )

4
prednasky/urls.py

@ -8,13 +8,13 @@ Soubor sloužící jako „router“, tj. zde se definují url adresy a na co uk
- ``prednasky/seznam_prednasek/<int:seznam>/`` (seznam-list) :class:`~prednasky.views.SeznamListView` - ``prednasky/seznam_prednasek/<int:seznam>/`` (seznam-list) :class:`~prednasky.views.SeznamListView`
""" """
from django.urls import path from django.urls import path
from seminar.utils import org_required, resitel_required from seminar.utils import org_required, resitel_or_org_required
from . import views from . import views
urlpatterns = [ urlpatterns = [
path( path(
'prednasky/', 'prednasky/',
resitel_required(views.newPrednaska) resitel_or_org_required(views.newPrednaska)
), ),
path('prednasky/hotovo', views.Prednaska_hotovo), path('prednasky/hotovo', views.Prednaska_hotovo),
path( path(

2
requirements.txt

@ -21,6 +21,7 @@ django-sekizai
django-countries django-countries
django-solo django-solo
django-ckeditor django-ckeditor
django-cleanup # Uklízí media/ od smazaných „databázových“ souborů
django-flat-theme django-flat-theme
django-taggit django-taggit
django-autocomplete-light>=3.9.0rc1 django-autocomplete-light>=3.9.0rc1
@ -62,3 +63,4 @@ lorem
sphinx sphinx
sphinx_rtd_theme sphinx_rtd_theme
myst_parser

18
seminar/migrations/0108_nastaveni_cena_sous.py

@ -0,0 +1,18 @@
# Generated by Django 3.2.16 on 2022-11-14 20:51
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('seminar', '0107_zmrazenavysledkovka'),
]
operations = [
migrations.AddField(
model_name='nastaveni',
name='cena_sous',
field=models.IntegerField(default=1000, verbose_name='Účastnický poplatek za soustředění'),
),
]

18
seminar/migrations/0109_hodnoceni_feedback.py

@ -0,0 +1,18 @@
# Generated by Django 3.2.16 on 2022-11-14 19:30
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('seminar', '0108_nastaveni_cena_sous'),
]
operations = [
migrations.AddField(
model_name='hodnoceni',
name='feedback',
field=models.TextField(blank=True, default='', help_text='Zpětná vazba řešiteli (plain text)', verbose_name='zpětná vazba'),
),
]

2
seminar/models/odevzdavatko.py

@ -113,6 +113,8 @@ class Hodnoceni(bm.SeminarModelBase):
problem = models.ForeignKey(am.Problem, verbose_name='problém', problem = models.ForeignKey(am.Problem, verbose_name='problém',
related_name='hodnoceni', on_delete=models.PROTECT) related_name='hodnoceni', on_delete=models.PROTECT)
feedback = models.TextField('zpětná vazba', blank=True, default='', help_text='Zpětná vazba řešiteli (plain text)')
def __str__(self): def __str__(self):
return "{}, {}, {}".format(self.problem, self.reseni, self.body) return "{}, {}, {}".format(self.problem, self.reseni, self.body)

4
seminar/models/tvorba.py

@ -718,6 +718,10 @@ class Nastaveni(SingletonModel):
aktualni_cislo = models.ForeignKey(Cislo, verbose_name='Aktuální číslo', aktualni_cislo = models.ForeignKey(Cislo, verbose_name='Aktuální číslo',
null=False, on_delete=models.PROTECT) null=False, on_delete=models.PROTECT)
cena_sous = models.IntegerField(null=False,
verbose_name="Účastnický poplatek za soustředění",
default=1000)
@property @property
def aktualni_rocnik(self): def aktualni_rocnik(self):
return self.aktualni_cislo.rocnik return self.aktualni_cislo.rocnik

25
seminar/static/seminar/stvrzenky.tex

@ -1,25 +0,0 @@
\input opmac
\chyph
\nopagenumbers
\parindent=0pt
\def\castka{1000}
\newread\data
\openin\data=/dev/stdin
\read\data to\termin
\read\data to\misto
\loop
\read\data to\ucastnik
\unless\ifeof\data
\vbox{\picw=2cm\inspic logomm.pdf \smallskip\hrule\medskip
Potvrzujeme, že \ucastnik se zúčastnil(a) soustředění Korespondenčního semináře M\&M konaného % \ucastnik má na konci mezeru
v~termínu \termin a že zaplatil(a) účastnický poplatek ve výši $\sim$\castka$\sim$. % \termin též
\bigskip
\the\day.~\the\month.~\the\year, \misto\hfill Přijal(a): \hbox to 4cm{\hrulefill}
\bigskip
}
\repeat
\bye

3
seminar/templates/seminar/archiv/cislo.html

@ -40,8 +40,7 @@
<li><a href="obalky.pdf">Obálky (PDF)</a></li> <li><a href="obalky.pdf">Obálky (PDF)</a></li>
<li><a href="tituly.tex" download>Tituly (TeX)</a></li> <li><a href="tituly.tex" download>Tituly (TeX)</a></li>
<li><a href="vysledkovka.tex" download>Výsledkovka (TeX)</a></li> <li><a href="vysledkovka.tex" download>Výsledkovka (TeX)</a></li>
<li><a href="obalkovani">Obálkování</a></li> <li><a href="odmeny/{{prevcislo.rocnik.rocnik}}.{{prevcislo.poradi}}/">Odměny</a></li>
<li><a href="odmeny/{{prevcislo.rocnik.rocnik}}.{{prevcislo.poradi}}/">Odměny</a></li>
</ul> </ul>
</div> </div>
{% endif %} {% endif %}

33
seminar/templates/seminar/archiv/cislo_obalkovani.html

@ -1,33 +0,0 @@
{% extends "base.html" %}
{% block content %}
<h1>
{% block nadpis1a %}
Obálkování {{ cislo }}
{% endblock %}
</h1>
Obálkovat se budou tyto problémy:
<ul>
{% for p in problemy %}
<li> {{ p.kod_v_rocniku }} {{ p }}
{% endfor %}
</ul>
{% for r in reseni %}
{% ifchanged r.resitel %}
{% if not forloop.first %}
</ul>
{% endif %}
<h4>{{ r.resitel }}</h4>
<ul>
{% endifchanged %}
<li>
{{ r.problem.kod_v_rocniku }} {{ r.problem.nazev }} ({{ r.body }})
{% endfor %}
</ul>
{% endblock content %}

20
seminar/templates/seminar/archiv/odmeny.html

@ -7,23 +7,11 @@
{% endblock %} {% endblock %}
</h1> </h1>
<h2>Od prvního deadlinu {{ from_cislo }} do prvního deadlinu {{ to_cislo }}</h2> <h2>Od čísla {{ from_cislo }} do čísla {{ to_cislo }}</h2>
(Od „{{ from_deadline }}“ (vyjma) do „{{ to_deadline }}“ (včetně)) <br/>
<div class="field-error"><b>Posílat použe po opravení všeho v čísle {{ to_cislo }}!</b></div>
<ul> <ul>
{% for z in zmeny_prvni_prvni %} {% for z in zmeny %}
<li> {{z.jmeno}}: {{z.ftitul}} &rarr; {{z.ttitul}}</li>
{% endfor %}
</ul>
<h2>Od {{ from_cislo }} do prvního deadlinu {{ to_cislo }} (pro první číslo)</h2>
<ul>
{% for z in zmeny_posledni_prvni %}
<li> {{z.jmeno}}: {{z.ftitul}} &rarr; {{z.ttitul}}</li>
{% endfor %}
</ul>
<h2>Od prvního deadlinu {{ from_cislo }} do {{ to_cislo }} (pro poslední číslo)</h2>
<ul>
{% for z in zmeny_prvni_posledni %}
<li> {{z.jmeno}}: {{z.ftitul}} &rarr; {{z.ttitul}}</li> <li> {{z.jmeno}}: {{z.ftitul}} &rarr; {{z.ttitul}}</li>
{% endfor %} {% endfor %}
</ul> </ul>

30
seminar/templates/seminar/org/obalkovani.html

@ -1,30 +0,0 @@
{% extends "base.html" %}
{% block content %}
<h1>
{% block nadpis1a %}
Obálkování {{ cislo }}
{% endblock %}
</h1>
<ul>
{% for reseni in object_list %}
{% ifchanged reseni.resitele %}
{% if not forloop.first %}
</ul>
{% endif %}
<h4>{% for resitel in reseni.resitele.all %}{{resitel.osoba}},{% endfor %}</h4>
<ul>
{% endifchanged %}
<li>Celkem {{reseni.hodnoceni__body__sum}} bodů z {{reseni.hodnoceni__count}} hodnocení
<ul>
{% for h in reseni.hodnoceni_set.all %}
<li> {{ h.problem }}: {{ h.body }}b </li>
{% endfor %}
</ul>
</li>
{% endfor %}
</ul>
{% endblock content %}

3
seminar/templates/seminar/zadani/AktualniVysledkovka.html

@ -4,7 +4,8 @@
<h1> <h1>
{% block nadpis1a %} {% block nadpis1a %}
Průběžné výsledky {{ rocnik.rocnik }}.&nbsp;ročníku Výsledky {{ rocnik.rocnik }}.&nbsp;ročníku
{% if vysledkovka.do_deadlinu %}k datu {{ vysledkovka.do_deadlinu.deadline.date }}{% endif %}
{% endblock %} {% endblock %}
</h1> </h1>

17
seminar/testutils.py

@ -13,7 +13,7 @@ import unidecode
import logging import logging
from korektury.testutils import create_test_pdf from korektury.testutils import create_test_pdf
from seminar.models import Skola, Resitel, Rocnik, Cislo, Problem, Reseni, PrilohaReseni, Nastaveni, Soustredeni, Soustredeni_Ucastnici, Soustredeni_Organizatori, Osoba, Organizator, Prijemce, Tema, Uloha, Konfera, TextNode, UlohaVzorakNode, RocnikNode, CisloNode, TemaVCisleNode, Text, Hodnoceni, UlohaZadaniNode, Novinky, TreeNode from seminar.models import Skola, Resitel, Rocnik, Cislo, Deadline, Problem, Reseni, PrilohaReseni, Nastaveni, Soustredeni, Soustredeni_Ucastnici, Soustredeni_Organizatori, Osoba, Organizator, Prijemce, Tema, Uloha, Konfera, TextNode, UlohaVzorakNode, RocnikNode, CisloNode, TemaVCisleNode, Text, Hodnoceni, UlohaZadaniNode, Novinky, TreeNode
import seminar.models as m import seminar.models as m
from django.contrib.flatpages.models import FlatPage from django.contrib.flatpages.models import FlatPage
@ -299,7 +299,7 @@ def gen_reseni_ulohy(rnd, cisla, uloha, pocet_resitelu, poradi_cisla, resitele_c
# Vytvoření řešení. # Vytvoření řešení.
if uloha.cislo_zadani.zlomovy_deadline_pro_papirove_cislo() is not None: if uloha.cislo_zadani.zlomovy_deadline_pro_papirove_cislo() is not None:
# combine, abychom dostali plný čas a ne jen datum # combine, abychom dostali plný čas a ne jen datum
cas_doruceni = datetime.datetime.combine(uloha.cislo_zadani.datum_deadline, datetime.datetime.min.time()) - datetime.timedelta(days=random.randint(0, 40)) - datetime.timedelta(minutes=random.randint(0, 60*24)) cas_doruceni = uloha.cislo_zadani.deadline_v_cisle.first().deadline - datetime.timedelta(days=random.randint(0, 40)) - datetime.timedelta(minutes=random.randint(0, 60*24))
# astimezone, protože jinak vyhazuje warning o nenastavené TZ # astimezone, protože jinak vyhazuje warning o nenastavené TZ
res = Reseni.objects.create(forma=rnd.choice(Reseni.FORMA_CHOICES)[0], cas_doruceni=cas_doruceni.astimezone(datetime.timezone.utc)) res = Reseni.objects.create(forma=rnd.choice(Reseni.FORMA_CHOICES)[0], cas_doruceni=cas_doruceni.astimezone(datetime.timezone.utc))
else: else:
@ -436,22 +436,23 @@ def gen_cisla(rnd, rocniky):
(mesic_vydani + 1) % 12 + 1, (mesic_vydani + 1) % 12 + 1,
rnd.randint(1, 28)) rnd.randint(1, 28))
# posledni 2 cisla v rocniku nemaji deadline
if (ci + 2 > cisel):
deadline = None
cislo = Cislo.objects.create( cislo = Cislo.objects.create(
rocnik = rocnik, rocnik = rocnik,
poradi = str(ci), poradi = str(ci),
datum_vydani=vydano, datum_vydani=vydano,
datum_deadline=deadline,
verejne_db=True, verejne_db=True,
verejna_vysledkovka=True
) )
node2 = CisloNode.objects.get(cislo = cislo) node2 = CisloNode.objects.get(cislo = cislo)
node2.succ = node node2.succ = node
node2.root = rocnik.rocniknode node2.root = rocnik.rocniknode
cislo.save() cislo.save()
deadline = Deadline.objects.create(
cislo=cislo,
deadline=deadline,
typ=Deadline.TYP_CISLA,
verejna_vysledkovka=True,
)
deadline.save()
node = node2 node = node2
if otec: if otec:
otec = False otec = False

6
seminar/urls.py

@ -23,7 +23,6 @@ Soubor sloužící jako „router“, tj. zde se definují url adresy a na co uk
- ``cislo/<int:rocnik>.<str:cislo>/obalky.pdf`` (seminar_cislo_obalky) :func:`~seminar.views.views_all.cisloObalkyView` - ``cislo/<int:rocnik>.<str:cislo>/obalky.pdf`` (seminar_cislo_obalky) :func:`~seminar.views.views_all.cisloObalkyView`
- ``cislo/<int:rocnik>.<str:cislo>/tituly.tex`` (seminar_cislo_titul) :func:`~seminar.views.views_all.TitulyView` - ``cislo/<int:rocnik>.<str:cislo>/tituly.tex`` (seminar_cislo_titul) :func:`~seminar.views.views_all.TitulyView`
- ``stav`` (stav_databaze) :func:`~seminar.views.views_all.StavDatabazeView` - ``stav`` (stav_databaze) :func:`~seminar.views.views_all.StavDatabazeView`
- ``cislo/<int:rocnik>.<str:cislo>/obalkovani`` (seminar_cislo_resitel_obalkovani) :class:`~seminar.views.views_all.ObalkovaniView`
- ``cislo/<int:trocnik>.<str:tcislo>/odmeny/<int:frocnik>.<str:fcislo>/`` (seminar_archiv_odmeny) :class:`~seminar.views.views_all.OdmenyView` - ``cislo/<int:trocnik>.<str:tcislo>/odmeny/<int:frocnik>.<str:fcislo>/`` (seminar_archiv_odmeny) :class:`~seminar.views.views_all.OdmenyView`
- Další - Další
- `` `` (titulni_strana) :class:`~seminar.views.views_all.TitulniStranaView` - `` `` (titulni_strana) :class:`~seminar.views.views_all.TitulniStranaView`
@ -102,11 +101,6 @@ urlpatterns = [
org_required(views.StavDatabazeView), org_required(views.StavDatabazeView),
name='stav_databaze' name='stav_databaze'
), ),
path(
'cislo/<int:rocnik>.<str:cislo>/obalkovani',
org_required(views.ObalkovaniView.as_view()),
name='seminar_cislo_resitel_obalkovani'
),
path( path(
'cislo/<int:trocnik>.<str:tcislo>/odmeny/<int:frocnik>.<str:fcislo>/', 'cislo/<int:trocnik>.<str:tcislo>/odmeny/<int:frocnik>.<str:fcislo>/',
org_required(views.OdmenyView.as_view()), org_required(views.OdmenyView.as_view()),

56
seminar/views/views_all.py

@ -53,23 +53,6 @@ logger = logging.getLogger(__name__)
def get_problemy_k_tematu(tema): def get_problemy_k_tematu(tema):
return Problem.objects.filter(nadproblem = tema) return Problem.objects.filter(nadproblem = tema)
class ObalkovaniView(generic.ListView):
template_name = 'seminar/org/obalkovani.html'
def get_queryset(self):
rocnik = get_object_or_404(Rocnik,rocnik=self.kwargs['rocnik'])
cislo = get_object_or_404(Cislo,rocnik=rocnik,poradi=self.kwargs['cislo'])
self.cislo = cislo
self.hodnoceni = s.Hodnoceni.objects.filter(cislo_body=cislo)
self.reseni = Reseni.objects.filter(hodnoceni__in = self.hodnoceni).annotate(Sum('hodnoceni__body')).annotate(Count('hodnoceni')).order_by('resitele__osoba')
return self.reseni
def get_context_data(self, **kwargs):
context = super(ObalkovaniView, self).get_context_data(**kwargs)
print(self.cislo)
context['cislo'] = self.cislo
return context
# FIXME: Pozor, níž je ještě jeden ProblemView! # FIXME: Pozor, níž je ještě jeden ProblemView!
#class ProblemView(generic.DetailView): #class ProblemView(generic.DetailView):
@ -484,18 +467,11 @@ class OdmenyView(generic.TemplateView):
context["from_cislo"] = fromcislo context["from_cislo"] = fromcislo
context["to_cislo"] = tocislo context["to_cislo"] = tocislo
context["zmeny_prvni_prvni"] = get_diff( from_deadline = posledni_deadline_oprava(fromcislo)
fromcislo.zlomovy_deadline_pro_papirove_cislo(), to_deadline = posledni_deadline_oprava(tocislo)
tocislo.zlomovy_deadline_pro_papirove_cislo() context["from_deadline"] = from_deadline
) context["to_deadline"] = to_deadline
context["zmeny_prvni_posledni"] = get_diff( context["zmeny"] = get_diff(from_deadline, to_deadline)
fromcislo.zlomovy_deadline_pro_papirove_cislo(),
posledni_deadline_oprava(tocislo)
)
context["zmeny_posledni_prvni"] = get_diff(
posledni_deadline_oprava(fromcislo),
tocislo.zlomovy_deadline_pro_papirove_cislo()
)
return context return context
@ -597,28 +573,6 @@ def obalkyView(request, resitele):
return response return response
def oldObalkovaniView(request, rocnik, cislo):
rocnik = Rocnik.objects.get(rocnik=rocnik)
cislo = Cislo.objects.get(rocnik=rocnik, cislo=cislo)
reseni = (
Reseni.objects.filter(cislo_body=cislo)
.order_by(
'resitel__prijmeni',
'resitel__jmeno',
'problem__typ',
'problem__kod'
)
)
problemy = sorted(set(r.problem for r in reseni), key=lambda p: (p.typ, p.kod))
return render(
request,
'seminar/archiv/cislo_obalkovani.html',
{'cislo': cislo, 'problemy': problemy, 'reseni': reseni}
)
### Tituly ### Tituly
def TitulyViewRocnik(request, rocnik): def TitulyViewRocnik(request, rocnik):
return TitulyView(request, rocnik, None) return TitulyView(request, rocnik, None)

37
soustredeni/templates/soustredeni/stvrzenky.tex

@ -0,0 +1,37 @@
{% autoescape off %}
{% load static %}
{% load tex %}
\documentclass[11pt,a4paper]{article}
\usepackage[left=0.75in, right=0.75in,top=0.5in,bottom=0.5in]{geometry}
\usepackage[T1]{fontenc}
\usepackage[utf8]{inputenc}
\usepackage[czech]{babel}
\usepackage{graphicx}
\begin{document}
\pagenumbering{gobble}
\parindent=0pt
\def\stvrzenka#1#2{
\vbox{%
\includegraphics[width=2cm]{logomm.pdf}
\smallskip\hrule\medskip
{% with soustredeni as s %}
Potvrzujeme, že #1 #2 se zúčastnil(a) soustředění Korespondenčního semináře M\&M konaného
v~termínu {{s.datum_zacatku|date:"j.~n.~Y"|sloz}} --
{{s.datum_konce|date:"j.~n.~Y"|sloz}} a~že zaplatil(a) účastnický poplatek ve
výši $\sim${{castka|sloz}}$\sim$.
\bigskip
{{s.datum_zacatku|date:"j.~n.~Y"|sloz}}, {{s.misto|sloz}} \hfill Přijal(a): \hbox to 4cm{\hrulefill}
\bigskip
}
{% endwith %}
}
{% for u in ucastnici %}
{% with o=u.osoba %}
\stvrzenka{{o.jmeno|sloz}}{{o.prijmeni|sloz}}
{% endwith %}
{% endfor %}
\end{document}
{% endautoescape %}

5
soustredeni/templates/soustredeni/ucastnici.tex

@ -1,5 +0,0 @@
{% load tex %}
\newcommand{\datum}{{datum|date:"j. n. Y"|sloz}}
{% for u in ucastnici %}
\stvrzenka{{u.cislo_stvrzenky|sloz}}{{u.jmeno|sloz}}{{u.prijmeni|sloz}}{{u.ulice|sloz}}{{u.psc|sloz}}{{u.mesto|sloz}}
{% endfor %}

26
soustredeni/views.py

@ -1,8 +1,9 @@
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404, render
from django.http import HttpResponse from django.http import HttpResponse
from django.views import generic from django.views import generic
from django.conf import settings from django.conf import settings
from seminar.models import Soustredeni, Resitel, Soustredeni_Ucastnici # Tohle je stare a chceme se toho zbavit. Pouzivejte s.ToCoChci from django.contrib.staticfiles.finders import find
from seminar.models import Soustredeni, Resitel, Soustredeni_Ucastnici, Nastaveni # Tohle je stare a chceme se toho zbavit. Pouzivejte s.ToCoChci
import csv import csv
import tempfile import tempfile
import shutil import shutil
@ -62,20 +63,17 @@ def soustredeniUcastniciExportView(request, soustredeni):
def soustredeniStvrzenkyView(request, soustredeni): def soustredeniStvrzenkyView(request, soustredeni):
soustredeni = get_object_or_404(Soustredeni, id=soustredeni) soustredeni = get_object_or_404(Soustredeni, id=soustredeni)
ucastnici = Resitel.objects.filter(soustredeni=soustredeni) ucastnici = Resitel.objects.filter(soustredeni=soustredeni)
castka = Nastaveni.get_solo().cena_sous
tex = render(request, 'soustredeni/stvrzenky.tex', {'ucastnici': ucastnici, 'soustredeni': soustredeni, 'castka': castka}).content
static = Path(settings.STATIC_ROOT)
tempdir = Path(tempfile.mkdtemp()) tempdir = Path(tempfile.mkdtemp())
shutil.copy(static / 'images/logomm.pdf', tempdir) with open(tempdir / "stvrzenky.tex", "w") as texfile:
shutil.copy(static / 'seminar/stvrzenky.tex', tempdir) texfile.write(tex.decode())
subprocess.run(
['pdfcsplain', 'stvrzenky.tex'], shutil.copy(find('images/logomm.pdf'), tempdir)
cwd=tempdir, subprocess.call(["pdflatex", "stvrzenky.tex"], cwd = tempdir, stdout=subprocess.DEVNULL)
encoding='utf-8',
input=f'{soustredeni.datum_zacatku.strftime("%-d.~%-m.~%Y")} -- {soustredeni.datum_konce.strftime("%-d.~%-m.~%Y")}\n{soustredeni.misto}\n' with open(tempdir / "stvrzenky.pdf", "rb") as pdffile:
+ '\n'.join([f'{u.osoba.jmeno} {u.osoba.prijmeni}' for u in ucastnici])
)
with open(tempdir / 'stvrzenky.pdf', 'rb') as pdffile:
response = HttpResponse(pdffile.read(), content_type='application/pdf') response = HttpResponse(pdffile.read(), content_type='application/pdf')
shutil.rmtree(tempdir) shutil.rmtree(tempdir)
return response return response

2
various/autentizace/templates/autentizace/login.html

@ -24,7 +24,7 @@
<br> <br>
<p><a href="{% url 'reset_password' %}"> <p><a href="{% url 'reset_password' %}">
Zapomněl jsem heslo Zapomněl jsem heslo nebo uživatelské jméno
</a></p> </a></p>
<hr> <hr>

32
vysledkovky/templates/vysledkovky/vysledkovka_cisla.html

@ -1,49 +1,49 @@
<div style="overflow-x: auto;"> <div style="overflow-x: auto;">
<table class='vysledkovka'> <table class='vysledkovka'>
<tr class='border-b'> <tr class='border-b'>
<th class='border-r'># <th class='border-r'>#</th>
<th class='border-r'>Jméno <th class='border-r'>Jméno</th>
{% for p in vysledkovka.temata_a_spol%} {% for p in vysledkovka.temata_a_spol%}
<th class='border-r' id="problem{{ oznaceni_vysledkovky }}_{{ forloop.counter0 }}">{# <a href="{{ p.verejne_url }}"> #}{{ p.kod_v_rocniku }}{# </a> #} <th class='border-r' id="problem{{ oznaceni_vysledkovky }}_{{ forloop.counter0 }}">{# <a href="{{ p.verejne_url }}"> #}{{ p.kod_v_rocniku }}{# </a> #}</th>
{# TODELETE #} {# TODELETE #}
{% for podproblemy in vysledkovka.podproblemy_iter.next %} {% for podproblemy in vysledkovka.podproblemy_iter.next %}
<th class='border-r podproblem{{ oznaceni_vysledkovky }}_{{ forloop.parentloop.counter0 }} podproblem'>{# <a href="{{ podproblemy.verejne_url }}"> #}{{ podproblemy.kod_v_rocniku }}{# </a> #} <th class='border-r podproblem{{ oznaceni_vysledkovky }}_{{ forloop.parentloop.counter0 }} podproblem'>{# <a href="{{ podproblemy.verejne_url }}"> #}{{ podproblemy.kod_v_rocniku }}{# </a> #}</th>
{% endfor %} {% endfor %}
{# TODELETE #} {# TODELETE #}
{% endfor %} {% endfor %}
{% if vysledkovka.je_nejake_ostatni %}<th class='border-r' id='problem{{ oznaceni_vysledkovky }}_{{ vysledkovka.temata_a_spol| length }}'>Ostatní {% endif %} {% if vysledkovka.je_nejake_ostatni %}<th class='border-r' id='problem{{ oznaceni_vysledkovky }}_{{ vysledkovka.temata_a_spol| length }}'>Ostatní</th>{% endif %}
{# TODELETE #} {# TODELETE #}
{% for podproblemy in vysledkovka.podproblemy_iter.next %} {% for podproblemy in vysledkovka.podproblemy_iter.next %}
<th class='border-r podproblem{{ oznaceni_vysledkovky }}_{{ vysledkovka.temata_a_spol| length }} podproblem'>{# <a href="{{ podproblemy.verejne_url }}"> #}{{ podproblemy.kod_v_rocniku }}{# </a> #} <th class='border-r podproblem{{ oznaceni_vysledkovky }}_{{ vysledkovka.temata_a_spol| length }} podproblem'>{# <a href="{{ podproblemy.verejne_url }}"> #}{{ podproblemy.kod_v_rocniku }}{# </a> #}</th>
{% endfor %} {% endfor %}
{# TODELETE #} {# TODELETE #}
<th class='border-r'>Za číslo <th class='border-r'>Za číslo</th>
<th class='border-r'>Za ročník <th class='border-r'>Za ročník</th>
<th class='border-r'>Odjakživa <th class='border-r'>Odjakživa</th>
{% for rv in vysledkovka.radky_vysledkovky %} {% for rv in vysledkovka.radky_vysledkovky %}
<tr> <tr>
<td class='border-r'>{% autoescape off %}{{ rv.poradi }}{% endautoescape %} <td class='border-r'>{% autoescape off %}{{ rv.poradi }}{% endautoescape %}</td>
<th class='border-r'> <th class='border-r'>
{% if rv.titul %} {% if rv.titul %}
{{ rv.titul }}<sup>MM</sup> {{ rv.titul }}<sup>MM</sup>
{% endif %} {% endif %}
{{ rv.resitel.osoba.plne_jmeno }} {{ rv.resitel.osoba.plne_jmeno }}</th>
{% for b in rv.body_za_temata_seznam %} {% for b in rv.body_za_temata_seznam %}
<td class='border-r'>{{ b }} <td class='border-r'>{{ b }}</td>
{% for body_podproblemu in rv.body_podproblemy_iter.next %} {% for body_podproblemu in rv.body_podproblemy_iter.next %}
<td class='border-r podproblem{{ oznaceni_vysledkovky }}_{{ forloop.parentloop.counter0 }} podproblem'>{{ body_podproblemu }} <td class='border-r podproblem{{ oznaceni_vysledkovky }}_{{ forloop.parentloop.counter0 }} podproblem'>{{ body_podproblemu }}</td>
{% endfor %} {% endfor %}
{% endfor %} {% endfor %}
<td class='border-r'>{{ rv.body_cislo }} <td class='border-r'>{{ rv.body_cislo }}</td>
<td class='border-r'><b>{{ rv.body_rocnik }}</b> <td class='border-r'><b>{{ rv.body_rocnik }}</b></td>
<td class='border-r'>{{ rv.body_celkem_odjakziva }} <td class='border-r'>{{ rv.body_celkem_odjakziva }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>

24
vysledkovky/templates/vysledkovky/vysledkovka_rocnik.html

@ -1,29 +1,29 @@
<table class='vysledkovka'> <table class='vysledkovka'>
<tr class='border-b'> <tr class='border-b'>
<th class='border-r'># <th class='border-r'>#</th>
<th class='border-r'>Jméno <th class='border-r'>Jméno</th>
<th class='border-r'>R. <th class='border-r'>R.</th>
<th class='border-r'>Odjakživa <th class='border-r'>Odjakživa</th>
{% for c in vysledkovka.cisla_rocniku %} {% for c in vysledkovka.cisla_rocniku %}
<th class='border-r'><a href="{{ c.verejne_url }}"> <th class='border-r'><a href="{{ c.verejne_url }}">
{{c.rocnik.rocnik}}.{{ c.poradi }}</a> {{c.rocnik.rocnik}}.{{ c.poradi }}</a></th>
{% endfor %} {% endfor %}
<th class='border-r'>Celkem <th class='border-r'>Celkem</th>
{% for rv in vysledkovka.radky_vysledkovky %} {% for rv in vysledkovka.radky_vysledkovky %}
<tr> <tr>
<td class='border-r'>{% autoescape off %}{{ rv.poradi }}{% endautoescape %} <td class='border-r'>{% autoescape off %}{{ rv.poradi }}{% endautoescape %}</td>
<th class='border-r'> <th class='border-r'>
{% if rv.titul %} {% if rv.titul %}
{{ rv.titul }}<sup>MM</sup> {{ rv.titul }}<sup>MM</sup>
{% endif %} {% endif %}
{{ rv.resitel.osoba.plne_jmeno }} {{ rv.resitel.osoba.plne_jmeno }}</th>
<td class='border-r'>{{ rv.rocnik_resitele }} <td class='border-r'>{{ rv.rocnik_resitele }}</td>
<td class='border-r'>{{ rv.body_celkem_odjakziva }} <td class='border-r'>{{ rv.body_celkem_odjakziva }}</td>
{% for b in rv.body_cisla_seznam %} {% for b in rv.body_cisla_seznam %}
<td class='border-r'>{{ b }} <td class='border-r'>{{ b }}</td>
{% endfor %} {% endfor %}
<td class='border-r'><b>{{ rv.body_rocnik }}</b> <td class='border-r'><b>{{ rv.body_rocnik }}</b></td>
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>

5
vysledkovky/utils.py

@ -146,7 +146,10 @@ class VysledkovkaRocniku(Vysledkovka):
def __init__(self, rocnik: m.Rocnik, jen_verejne: bool = True): def __init__(self, rocnik: m.Rocnik, jen_verejne: bool = True):
self.rocnik = rocnik self.rocnik = rocnik
self.jen_verejne = jen_verejne self.jen_verejne = jen_verejne
self.do_deadlinu = m.Deadline.objects.filter(cislo__rocnik=rocnik).last() deadliny = m.Deadline.objects.filter(cislo__rocnik=rocnik)
if jen_verejne:
deadliny = deadliny.filter(verejna_vysledkovka=True)
self.do_deadlinu = deadliny.order_by("deadline").last()
@cached_property @cached_property
def aktivni_resitele(self) -> list[m.Resitel]: def aktivni_resitele(self) -> list[m.Resitel]:

Loading…
Cancel
Save