Upgrade odevzdavatka #30

Merged
zelvuska merged 37 commits from upgrade_odevzdavatka into master 2023-06-19 20:48:30 +02:00
15 changed files with 205 additions and 49 deletions

View file

@ -70,23 +70,17 @@ class PublicResitelAutocomplete(LoginRequiredAjaxMixin, autocomplete.Select2Quer
class OdevzdatelnyProblemAutocomplete(autocomplete.Select2QuerySetView): class OdevzdatelnyProblemAutocomplete(autocomplete.Select2QuerySetView):
""" View k :mod:`dal.autocomplete` pro vyhledávání problémů především v odevzdávátku. """ """ View k :mod:`dal.autocomplete` pro vyhledávání problémů především v odevzdávátku. """
def get_queryset(self): def get_queryset(self):
nastaveni = get_object_or_404(m.Nastaveni) qs = m.Problem.objects.filter(stav=m.Problem.STAV_ZADANY)
rocnik = nastaveni.aktualni_rocnik
# Od tohoto místa dál jsem zkoušel spoustu variací podle https://django-polymorphic.readthedocs.io/en/stable/advanced.html
temaQ = Q(Tema___rocnik = rocnik, stav=m.Problem.STAV_ZADANY)
ulohaQ = Q(Uloha___cislo_zadani__rocnik = rocnik, stav=m.Problem.STAV_ZADANY)
clanekQ = Q(Clanek___cislo__rocnik = rocnik, stav=m.Problem.STAV_ZADANY)
qs = m.Problem.objects.filter(temaQ | ulohaQ | clanekQ)
#print(temata, ulohy, clanky)
#ulohy.union(temata, all=True)
#print(ulohy)
#ulohy.union(clanky, all=True)
#print(ulohy)
#qs = ulohy
print(qs)
if self.q: if self.q:
qs = qs.filter( qs = qs.filter(
Q(nazev__icontains=self.q)) Q(nazev__icontains=self.q))
nadproblem_id = int(self.forwarded.get("nadproblem_id", -1))
if nadproblem_id != -1:
# Seřadíme tak, aby ty s nadproblem==None byly dole (větší motivace tam naklikat konkrétní úlohy) a pak nějak rozumně.
# Tohle je řazení pro odevzdávátko, kde je definován nadproblém, proto je to v tomto ifu. (Jinde si to netroufám řadit)
qs = qs.order_by("nadproblem", "kod", "nazev")
qs = list(filter(lambda problem: problem.hlavni_problem.id == nadproblem_id, qs))
zelvuska marked this conversation as resolved Outdated

Tohle je možná lepší psát nějakým filterem (ať už QuerySetovým, nebo čistě pythonovým list(filter(lambda: ..., qs)) – není to úprava dat, jen filtrování, tak ať je to názornější.

(A přirozeně, pokud by to šlo tím QuerySetovým, tak to může být rychlejší, protože databáze není napsaná v Pythonu :-))

Tohle je možná lepší psát nějakým filterem (ať už QuerySetovým, nebo čistě pythonovým `list(filter(lambda: ..., qs))` – není to úprava dat, jen filtrování, tak ať je to názornější. (A přirozeně, pokud by to šlo tím QuerySetovým, tak to může být rychlejší, protože databáze není napsaná v Pythonu :-))

No QuerySetovým to nejde, protože hlavni_problem není databázová věc… Nebo to jde nějak obejít?

No QuerySetovým to nejde, protože hlavni_problem není databázová věc… Nebo to jde nějak obejít?

Já si úplně nepamatuju, jestli to je nebo není DB věc :-) Takže holt klasickým pythoním filterem.

Já si úplně nepamatuju, jestli to je nebo není DB věc :-) Takže holt klasickým pythoním filterem.
return qs return qs
class ProblemAutocomplete(autocomplete.Select2QuerySetView): class ProblemAutocomplete(autocomplete.Select2QuerySetView):

View file

@ -437,7 +437,7 @@
"insitetree": true, "insitetree": true,
"parent": 21, "parent": 21,
"sort_order": 36, "sort_order": 36,
"title": "Poslat řešení", "title": "Nahrát řešení",
"tree": 1, "tree": 1,
"url": "seminar_nahraj_reseni", "url": "seminar_nahraj_reseni",
"urlaspattern": true "urlaspattern": true
@ -719,7 +719,7 @@
"insitetree": true, "insitetree": true,
"parent": 21, "parent": 21,
"sort_order": 36, "sort_order": 36,
"title": "Nahrát řešení", "title": "Vložit řešení",
"tree": 1, "tree": 1,
"url": "seminar_vloz_reseni", "url": "seminar_vloz_reseni",
"urlaspattern": true "urlaspattern": true
@ -1026,6 +1026,36 @@
"model": "sitetree.treeitem", "model": "sitetree.treeitem",
"pk": 51 "pk": 51
}, },
{
"fields": {
"access_guest": false,
"access_loggedin": false,
"access_perm_type": 1,
"access_permissions": [
[
"resitel",
"auth",
"user"
]
],
"access_restricted": true,
"alias": null,
"description": "",
"hidden": false,
"hint": "",
"inbreadcrumbs": true,
"inmenu": true,
"insitetree": true,
"parent": 23,
"sort_order": 52,
"title": "Nahrát řešení k nadproblému {{nadproblem_id}}",
zelvuska marked this conversation as resolved
Review

Zatím jsem neviděl realizaci, ale pojem „nadproblém“ bych nepsal nikam, kde ho můžou vidět řešitelé. Přijde mi dost matoucí i pro orgy, ale ti se aspoň mají koho zeptat :-)

Zatím jsem neviděl realizaci, ale pojem „nadproblém“ bych nepsal nikam, kde ho můžou vidět řešitelé. Přijde mi dost matoucí i pro orgy, ale ti se aspoň mají koho zeptat :-)
Review

Tohle políčkoje skryté, takže ho vidí jen pokud budou prohlížet html. To mi přijde dostatečně „ne můžou vidět“.

Tohle políčkoje skryté, takže ho vidí jen pokud budou prohlížet html. To mi přijde dostatečně „ne můžou vidět“.
"tree": 1,
"url": "seminar_nahraj_reseni nadproblem_id",
"urlaspattern": true
},
"model": "sitetree.treeitem",
"pk": 52
},
{ {
"fields": { "fields": {
"access_guest": false, "access_guest": false,
@ -1041,13 +1071,13 @@
"inmenu": true, "inmenu": true,
"insitetree": true, "insitetree": true,
"parent": 28, "parent": 28,
"sort_order": 52, "sort_order": 53,
"title": "Přidat PDF", "title": "Přidat PDF",
"tree": 1, "tree": 1,
"url": "/admin/korektury/korekturovanepdf/add/", "url": "/admin/korektury/korekturovanepdf/add/",
"urlaspattern": false "urlaspattern": false
}, },
"model": "sitetree.treeitem", "model": "sitetree.treeitem",
"pk": 52 "pk": 53
} }
] ]

View file

@ -28,7 +28,7 @@ Generuje se za pomocí::
nebo (v případě meníčka):: nebo (v případě meníčka)::
./manage.py dumpdata sitetree --natrual-foreign > data/sitetree_new.json ./manage.py dumpdata sitetree --natural-foreign > data/sitetree_new.json
./fix_json.py data/sitetree_new.json data/sitetree.json ./fix_json.py data/sitetree_new.json data/sitetree.json
deploy_v2 deploy_v2

View file

@ -1260,3 +1260,22 @@ label[for=id_skola] {
.bodovani>input { .bodovani>input {
width: 4em; width: 4em;
} }
/* Select2 používaný hlavně multiple selectem. Přidání checkboxů a změna barvy. */
/* Podle https://stackoverflow.com/a/48290544 */
/* U autocomplete.ModelSelect2Multiple vyžaduje 'data-dropdown-css-class': 's2m-se-zaskrtavatky' */
.s2m-se-zaskrtavatky .select2-results__option[aria-selected=true]:before {
content: '☑ ';
padding: 0 0 0 8px;
}
.s2m-se-zaskrtavatky .select2-results__option[aria-selected=false]:before {
content: '◻ ';
padding: 0 0 0 8px;
}
/* Oranžové zvýraznění v Select2 */
.select2-results__option--highlighted {
background-color: #e84e10 !important;
zelvuska marked this conversation as resolved
Review

Přemýšlím, kde všude jsou Select2Multiple, jestli je to nerozbíjí. Ale snad ne.

Přemýšlím, kde všude jsou Select2Multiple, jestli je to nerozbíjí. Ale snad ne.
Review

Tak tohle je jen tam, kam jsme explicitně přidali 'data-dropdown-css-class': 'select2multiple'. Spíš si teď naopak říkám, že ta barva možná měla být bez toho .select2multiple

Tak tohle je jen tam, kam jsme explicitně přidali `'data-dropdown-css-class': 'select2multiple'`. Spíš si teď naopak říkám, že ta barva možná měla být bez toho `.select2multiple`
Review

Měl jsem na mysli obecně všechny Select2Multiple, nemělo se to týkat jen barvy…

Barva asi může být všude (v Adminu tyhle styly stejně nepoužíváme, ne?)

Měl jsem na mysli obecně všechny Select2Multiple, nemělo se to týkat jen barvy… Barva asi může být všude (v Adminu tyhle styly stejně nepoužíváme, ne?)
Review

No ale tohle neovlivňuje všechny selecty. Tohle ovlivňuje jen ty, kterým tu třídu předhodíme…

No ale tohle neovlivňuje všechny selecty. Tohle ovlivňuje jen ty, kterým tu třídu předhodíme…
Review

A barvu bych tedy dal všude…

A barvu bych tedy dal všude…
Review

Třída .select2multiple je naše? Tak bych ji tím pádem pojmenoval nějak aspoň parciálně česky, myslel jsem, že hackujeme nějakou vestavěnou. (Uznávám, nečetl jsem kód moc pozorně…)

Možná .s2m-se-zaskrtavatky?

Třída `.select2multiple` je naše? Tak bych ji tím pádem pojmenoval nějak aspoň parciálně česky, myslel jsem, že hackujeme nějakou vestavěnou. (Uznávám, nečetl jsem kód moc pozorně…) Možná `.s2m-se-zaskrtavatky`?
}

View file

@ -4,8 +4,8 @@ Obsahuje vše, co se týká odevzdávání (+ nahrávání) a opravování řeš
Slovníček: Slovníček:
Moje řešení = Přehled řešení = Řešení, která odevzdal aktuálního uživatel sám. Moje řešení = Přehled řešení = Řešení, která odevzdal aktuálního uživatel sám.
Došlá řešení = Tabulka + seznam + detail + ... = Řešení, která poslal někdo jiný. Došlá řešení = Tabulka + seznam + detail + ... = Řešení, která poslal někdo jiný.
Poslat řešení = Odevdat řešení. (Tj. řešení se vztahem k aktuálnímu uživateli.) Nahrát řešení = Odevdat řešení. (Tj. řešení se vztahem k aktuálnímu uživateli.)
Nahrát řešení = Nahrání řešení bez vztahu k aktuálnímu uživateli. Vlož řešení = Vložit řešení bez vztahu k aktuálnímu uživateli.
TODO: Místo vložit řešení v nahrávání a posílání řešení dát něco jiného? TODO: Místo vložit řešení v nahrávání a posílání řešení dát něco jiného?
""" """

View file

@ -29,6 +29,8 @@ class PosliReseniForm(forms.Form):
attrs={ attrs={
'data-placeholder--id': '-1', 'data-placeholder--id': '-1',
'data-placeholder--text': '---', 'data-placeholder--text': '---',
'data-close-on-select': 'false',
'data-dropdown-css-class': 's2m-se-zaskrtavatky',
'data-allow-clear': 'true' 'data-allow-clear': 'true'
}, },
), ),
@ -43,6 +45,8 @@ class PosliReseniForm(forms.Form):
url='autocomplete_resitel', url='autocomplete_resitel',
attrs = {'data-placeholder--id': '-1', attrs = {'data-placeholder--id': '-1',
'data-placeholder--text' : '---', 'data-placeholder--text' : '---',
'data-close-on-select': 'false',
'data-dropdown-css-class': 's2m-se-zaskrtavatky',
'data-allow-clear': 'true'}) 'data-allow-clear': 'true'})
) )
@ -62,12 +66,6 @@ class PosliReseniForm(forms.Form):
#poznamka = models.TextField('neveřejná poznámka', blank=True, #poznamka = models.TextField('neveřejná poznámka', blank=True,
# help_text='Neveřejná poznámka k řešení (plain text)') # help_text='Neveřejná poznámka k řešení (plain text)')
#TODO body do cisla
#TODO prilohy
##def __init__(self, *args, **kwargs):
## super().__init__(*args, **kwargs)
## #self.fields['favorite_color'] = forms.ChoiceField(choices=[(color.id, color.name) for color in Resitel.objects.all()])
class NahrajReseniForm(forms.ModelForm): class NahrajReseniForm(forms.ModelForm):
class Meta: class Meta:
@ -80,23 +78,40 @@ class NahrajReseniForm(forms.ModelForm):
url='autocomplete_problem_odevzdatelny', url='autocomplete_problem_odevzdatelny',
attrs = {'data-placeholder--id': '-1', attrs = {'data-placeholder--id': '-1',
'data-placeholder--text' : '---', 'data-placeholder--text' : '---',
'data-close-on-select': 'false',
'data-dropdown-css-class': 's2m-se-zaskrtavatky',
'data-allow-clear': 'true'}, 'data-allow-clear': 'true'},
forward=["nadproblem_id"],
), ),
'resitele': 'resitele':
autocomplete.ModelSelect2Multiple( autocomplete.ModelSelect2Multiple(
url='autocomplete_resitel_public', url='autocomplete_resitel_public',
attrs = {'data-placeholder--id': '-1', attrs = {'data-placeholder--id': '-1',
'data-placeholder--text' : '---', 'data-placeholder--text' : '---',
'data-close-on-select': 'false',
'data-dropdown-css-class': 's2m-se-zaskrtavatky',
'data-allow-clear': 'true'}, 'data-allow-clear': 'true'},
) )
} }
nadproblem_id = forms.IntegerField(required=False, disabled=True, widget=forms.HiddenInput())
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
# FIXME Z nějakého důvodu se do této třídy dostaneme i bez resitele # FIXME Z nějakého důvodu se do této třídy dostaneme i bez resitele
if 'resitele' in self.fields: if 'resitele' in self.fields:
# FIXME Mnohem hezčí by to bylo u definice resitele výše, ale nepodařilo se mi to. # FIXME Mnohem hezčí by to bylo u definice resitele výše, ale nepodařilo se mi to.
self.fields['resitele'].required = False self.fields['resitele'].required = False
self.fields['resitele'].label = "Další autoři"
if 'problem' in self.fields:
self.fields['problem'].label = "Všechny řešené problémy"
def clean_problem(self):
problem = self.cleaned_data.get('problem')
for p in problem:
if p.stav != m.Problem.STAV_ZADANY:
raise forms.ValidationError("Problém " + str(p) + " již nelze řešit!")
return problem
ReseniSPrilohamiFormSet = inlineformset_factory(m.Reseni,m.PrilohaReseni, ReseniSPrilohamiFormSet = inlineformset_factory(m.Reseni,m.PrilohaReseni,
form = NahrajReseniForm, form = NahrajReseniForm,

View file

@ -7,19 +7,20 @@
{% block content %} {% block content %}
<h1> <h1>
{% block nadpis1a %} {% block nadpis1a %}
Vložit řešení Nahrát řešení
{% endblock %} {% endblock %}
</h1> </h1>
<p style="text-align: justify">Když řešení různých témátek vložíš každé zvlášť, lépe se v nich vyznáme a&nbsp;třeba ti je i&nbsp;rychleji opravíme.</p> <form enctype="multipart/form-data" action="{% url 'seminar_nahraj_reseni' nadproblem_id %}" method="post" onsubmit="return zkontroluj_prilohy();">
<p>Pokud řešíte ve více lidech, je <strong>nutné</strong> přidat tyto lidi jako „Autory řešení“. V tomto poli se vyhledává podle přezdívek, které si lze nastavit v „Osobní údaje“. Sebe vyplňovat nemusíte a za skupinu odevzdávejte pouze <strong>jednou</strong> (ne každý sám).</p>
<form enctype="multipart/form-data" action="{% url 'seminar_nahraj_reseni' %}" method="post" onsubmit="return zkontroluj_prilohy();">
{% csrf_token %} {% csrf_token %}
<table class='form' id="reseni"> <table class='form'>
<tr>
<td><label class="field-label field-required" for="tema">Téma:</label></td>
<td><input id="tema" disabled="" type="text" value="{{ nadproblem }}"></td>
</tr>
{% with field=form.problem %}
<tr> <tr>
{% for field in form %}
<td> <td>
<label class="field-label{% if field.field.required %} field-required{% endif %}" for="{{ field.id_for_label }}"> <label class="field-label{% if field.field.required %} field-required{% endif %}" for="{{ field.id_for_label }}">
{{ field.label }}: {{ field.label }}:
@ -28,15 +29,54 @@
<td> <td>
{{ field }} {{ field }}
</td> </td>
{% endfor %}
</tr> </tr>
{% if field.errors %}
<tr>
<td colspan="2"><span class="field-error">{{ field.errors }}</span></td>
</tr>
{% endif %}
{% endwith %}
</table> </table>
{% for field in form.hidden_fields %}
{{ field }}
{% endfor %}
<hr>
<h4>Spolupráce s&nbsp;dalšími řešiteli</h4>
<p>Pokud řešíte ve více lidech, je <strong>potřeba</strong> přidat tyto lidi jako „Další autory“. V&nbsp;tomto poli se vyhledává podle přezdívek, které si lze nastavit v&nbsp;„Osobních údajích“. Sebe vyplňovat nemusíte a za skupinu odevzdávejte pouze <strong>jednou</strong> (ne každý sám).</p>
zelvuska marked this conversation as resolved Outdated

„Další auto__ry__“

„Další auto__ry__“

„Osobní__ch__ údaj__ích__“

„Osobní__ch__ údaj__ích__“
<table class='form'>
{% with field=form.resitele %}
<tr>
<td>
<label class="field-label{% if field.field.required %} field-required{% endif %}" for="{{ field.id_for_label }}">
{{ field.label }}:
</label>
</td>
<td>
{{ field }}
</td>
</tr>
{% if field.errors %}
<tr>
<td colspan="2"><span class="field-error">{{ field.errors }}</span></td>
</tr>
{% endif %}
{% endwith %}
</table>
<hr> <hr>
{% include "odevzdavatko/prilohy.html" %} {% include "odevzdavatko/prilohy.html" %}
{{form.non_field_errors}}
<hr> <hr>
<h4>Odevzdat řešení</h4> <h4>Odevzdat řešení</h4>
<input type="submit" value="Odevzdat"> <input type="submit" value="Odevzdat">

View file

@ -0,0 +1,21 @@
{% extends "base.html" %}
{% load static %}
{% block content %}
<h1>
{% block nadpis1a %}
Nahrát řešení
{% endblock %}
</h1>
<h4>Seznam témat k odevzdání</h4>
<ul>
{% for problem in object_list %}
<li><a href="{% url 'seminar_nahraj_reseni' problem.id %}">{{ problem }}</a></li>
{% empty %}
<li>Nelze nic odevzdávat.</li>
{% endfor %}
</ul>
{% endblock %}

View file

@ -2,8 +2,9 @@
<h4>Soubory s řešením</h4> <h4>Soubory s řešením</h4>
<p style="text-align: justify">Maximální součet velikostí příloh je cca 49&nbsp;MB. Pokud je to možné a&nbsp;dává to smysl, pošli nám prosím své řešení ve formátu PDF, ostatní formáty nemusíme umět otevřít.</p> <p style="text-align: justify">Pokud je to možné a&nbsp;dává to smysl (tj.&nbsp;není to třeba kód nebo doprovodný obrázek), pošli nám prosím své řešení ve formátu <strong>PDF</strong>, ostatní formáty nemusíme umět otevřít.</p>
<p style="text-align: justify">Pokud svůj soubor rozumně pojmenuješ, urychlíš opravování a&nbsp;předejdeš tomu, že si nějakého tvého řešení nevšimneme. Například z&nbsp;<code>img_250921_101205.pdf</code> nepoznáme, kterou úlohu jsi odevzdal, zato <code>uloha_3.pdf</code> nebo <code>tema_1.pdf</code>, to už je něco jiného. Případně můžeš využít i&nbsp;poznámku řešitele.</p> <p style="text-align: justify">Pokud svůj soubor <strong>rozumně pojmenuješ</strong>, urychlíš opravování a&nbsp;předejdeš tomu, že si nějakého tvého řešení nevšimneme. Například z&nbsp;<code>img_250921_101205.pdf</code> nepoznáme, kterou úlohu jsi odevzdal, zato <code>uloha_3.pdf</code> nebo <code>tema_1.pdf</code>, to už je něco jiného. Případně můžeš využít i&nbsp;poznámku řešitele.</p>
<p style="text-align: justify">Maximální součet velikostí příloh je cca <strong>49&nbsp;MB</strong>.</p>
<div id="form_set"> <div id="form_set">
{% for form in prilohy.forms %} {% for form in prilohy.forms %}

View file

@ -19,8 +19,9 @@ from seminar.utils import org_required, resitel_required, viewMethodSwitch, \
from . import views from . import views
urlpatterns = [ urlpatterns = [
path('org/add_solution', org_required(views.PosliReseniView.as_view()), name='seminar_vloz_reseni'), path('org/add_solution', org_required(views.VlozReseniView.as_view()), name='seminar_vloz_reseni'),
path('resitel/nahraj_reseni', resitel_required(views.NahrajReseniView.as_view()), name='seminar_nahraj_reseni'), path('resitel/nahraj_reseni', resitel_required(views.NahrajReseniRozcestnikTematekView.as_view()), name='seminar_nahraj_reseni'),
path('resitel/nahraj_reseni/<int:nadproblem_id>/', resitel_required(views.NahrajReseniView.as_view()), name='seminar_nahraj_reseni'),
path('resitel/odevzdana_reseni/', resitel_or_org_required(views.PrehledOdevzdanychReseni.as_view()), name='seminar_resitel_odevzdana_reseni'), path('resitel/odevzdana_reseni/', resitel_or_org_required(views.PrehledOdevzdanychReseni.as_view()), name='seminar_resitel_odevzdana_reseni'),
path('org/reseni/', org_required(views.TabulkaOdevzdanychReseniView.as_view()), name='odevzdavatko_tabulka'), path('org/reseni/', org_required(views.TabulkaOdevzdanychReseniView.as_view()), name='odevzdavatko_tabulka'),

View file

@ -367,8 +367,8 @@ class SeznamAktualnichReseniView(SeznamReseniView):
return qs return qs
class PosliReseniView(LoginRequiredMixin, FormView): class VlozReseniView(LoginRequiredMixin, FormView):
template_name = 'odevzdavatko/posli_reseni.html' template_name = 'odevzdavatko/vloz_reseni.html'
form_class = f.PosliReseniForm form_class = f.PosliReseniForm
def form_valid(self, form): def form_valid(self, form):
@ -399,12 +399,27 @@ class PosliReseniView(LoginRequiredMixin, FormView):
return data return data
class NahrajReseniRozcestnikTematekView(LoginRequiredMixin, ListView):
zelvuska marked this conversation as resolved Outdated

Možná lepší pojmenovat ve stylu NahrajReseniRozcestnikTematekView nebo NahrajReseniVyberHlavnihoProblemuView, nebo dokonce OdevzdavatkoVyberTematkaView? Tady se žádné řešení nenahrává, a technicky to ani není nadproblém, ale hlavní problém (bo i nadproblém může mít nadproblém, teoreticky).

Možná lepší pojmenovat ve stylu `NahrajReseniRozcestnikTematekView` nebo `NahrajReseniVyberHlavnihoProblemuView`, nebo dokonce `OdevzdavatkoVyberTematkaView`? Tady se žádné řešení nenahrává, a technicky to ani není nadproblém, ale hlavní problém (bo i nadproblém může mít nadproblém, teoreticky).
model = m.Problem
template_name = 'odevzdavatko/nahraj_reseni_nadproblem.html'
def get_queryset(self):
return super().get_queryset().filter(stav=m.Problem.STAV_ZADANY, nadproblem__isnull=True)
class NahrajReseniView(LoginRequiredMixin, CreateView): class NahrajReseniView(LoginRequiredMixin, CreateView):
model = m.Reseni model = m.Reseni
template_name = 'odevzdavatko/nahraj_reseni.html' template_name = 'odevzdavatko/nahraj_reseni.html'
form_class = f.NahrajReseniForm form_class = f.NahrajReseniForm
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
# Zaříznutí nezadaných problémů
nadproblem_id = self.kwargs["nadproblem_id"]
self.nadproblem = get_object_or_404(m.Problem, id=nadproblem_id)
if self.nadproblem.stav != "zadany":
raise PermissionDenied()
# Zaříznutí starých řešitelů: # Zaříznutí starých řešitelů:
# FIXME: Je to tady dost naprasené, mělo by to asi být jinde… # FIXME: Je to tady dost naprasené, mělo by to asi být jinde…
zelvuska marked this conversation as resolved
Review

Tohle FIXME se vztahuje IMHO i na zaříznutí nezadaných problémů… Teď bych to asi neřešil, ale možná ho dává smysl přesunout výš…

(.get() není funkce, která by měla za úkol řešit jakákoliv práva, takže by to sémanticky mělo být někam vyčleněno. Ale pořád nemám rozmyšleno / nastudováno kam…)

Tohle FIXME se vztahuje IMHO i na zaříznutí nezadaných problémů… Teď bych to asi neřešil, ale možná ho dává smysl přesunout výš… (`.get()` není funkce, která by měla za úkol řešit jakákoliv práva, takže by to sémanticky mělo být někam vyčleněno. Ale pořád nemám rozmyšleno / nastudováno kam…)
osoba = m.Osoba.objects.get(user=self.request.user) osoba = m.Osoba.objects.get(user=self.request.user)
@ -417,12 +432,23 @@ class NahrajReseniView(LoginRequiredMixin, CreateView):
}) })
return super().get(request, *args, **kwargs) return super().get(request, *args, **kwargs)
def get_initial(self):
nadproblem_id = self.nadproblem.id
zelvuska marked this conversation as resolved Outdated

Nechceme ten nadproblém rovnou uložit jako Problém do self? Na první čtení jsem tady měl pocit, že když mi někdo předhodí blbost do URL, že někde něco musí spadnout…

Nechceme ten nadproblém rovnou uložit jako Problém do `self`? Na první čtení jsem tady měl pocit, že když mi někdo předhodí blbost do URL, že někde něco musí spadnout…

Užitečnost uložení přímo Problemu je i v tom, že pak podproblémy můžeš určit jako nadproblem.podproblemy.count() > 0 o kus níž (v rychlosti nevidím, jestli related managers umí i .exists())

Disclaimer: kód haluzím, nechtělo se mi to hledat přesně :-)

Užitečnost uložení přímo Problemu je i v tom, že pak podproblémy můžeš určit jako `nadproblem.podproblemy.count() > 0` o kus níž (v rychlosti nevidím, jestli related managers umí i `.exists()`) Disclaimer: kód haluzím, nechtělo se mi to hledat přesně :-)
return {
"nadproblem_id": nadproblem_id,
"problem": [] if self.nadproblem.podproblem.filter(stav=m.Problem.STAV_ZADANY).exists() else nadproblem_id
zelvuska marked this conversation as resolved Outdated

Uh, tohle dělá co? Ať už to dělá cokoliv, tak to z toho není zřejmé. Zejména není samozřejmé, kdy je QuerySet falsey.

Po chvíli čtení: když to má podproblémy, tak nic nenabízíme, jinak nabídneme tu věc přímo.

Buď bych za ten QuerySet aspoň přidal .exists() (Dokumentace), nebo úplně vyčlenil:

...
ma_podproblemy = QuerySet.exists()
...
return {
	...
    "problem": [] if ma_podproblemy else nadproblem_id,
    ...
    }
Uh, tohle dělá co? Ať už to dělá cokoliv, tak to z toho není zřejmé. Zejména není samozřejmé, kdy je `QuerySet` falsey. Po chvíli čtení: když to má podproblémy, tak nic nenabízíme, jinak nabídneme tu věc přímo. Buď bych za ten QuerySet aspoň přidal `.exists()` ([Dokumentace](https://docs.djangoproject.com/en/3.2/ref/models/querysets/#exists)), nebo úplně vyčlenil: ```python3 ... ma_podproblemy = QuerySet….exists() ... return { ... "problem": [] if ma_podproblemy else nadproblem_id, ... } ```
}
def get_context_data(self,**kwargs): def get_context_data(self,**kwargs):
data = super().get_context_data(**kwargs) data = super().get_context_data(**kwargs)
if self.request.POST: if self.request.POST:
data['prilohy'] = f.ReseniSPrilohamiFormSet(self.request.POST,self.request.FILES) data['prilohy'] = f.ReseniSPrilohamiFormSet(self.request.POST,self.request.FILES)
else: else:
data['prilohy'] = f.ReseniSPrilohamiFormSet() data['prilohy'] = f.ReseniSPrilohamiFormSet()
data["nadproblem_id"] = self.nadproblem.id
data["nadproblem"] = get_object_or_404(m.Problem, id=self.nadproblem.id)
return data return data
# FIXME prepsat tak, aby form_valid se volalo jen tehdy, kdyz je form i formset validni # FIXME prepsat tak, aby form_valid se volalo jen tehdy, kdyz je form i formset validni
@ -474,4 +500,8 @@ class NahrajReseniView(LoginRequiredMixin, CreateView):
to=list(prijemci), to=list(prijemci),
).send() ).send()
return formularOKView(self.request, text='Řešení úspěšně odevzdáno') return formularOKView(
self.request,
text='Řešení úspěšně odevzdáno',
dalsi_odkazy=[("Odevzdat další řešení", reverse("seminar_nahraj_reseni"))],
)

View file

@ -11,7 +11,7 @@
<a href="{% url 'logout' %}">Odhlásit se</a><br> <a href="{% url 'logout' %}">Odhlásit se</a><br>
<a href="{% url 'seminar_resitel_edit' %}">Upravit údaje</a><br> <a href="{% url 'seminar_resitel_edit' %}">Upravit údaje</a><br>
<a href="{% url 'seminar_nahraj_reseni' %}">Poslat řešení</a><br> <a href="{% url 'seminar_nahraj_reseni' %}">Nahrát řešení</a><br>
zelvuska marked this conversation as resolved Outdated

„Nahrát “ bez í asi

„Nahrát “ bez í asi
<a href="{% url 'seminar_resitel_odevzdana_reseni' %}">Již odevzdaná řešení</a><br> <a href="{% url 'seminar_resitel_odevzdana_reseni' %}">Již odevzdaná řešení</a><br>

View file

@ -173,7 +173,11 @@ def resitelEditView(request):
msg = "Unknown school {}, {}".format(fcd['skola_nazev'],fcd['skola_adresa']) msg = "Unknown school {}, {}".format(fcd['skola_nazev'],fcd['skola_adresa'])
resitel_edit.save() resitel_edit.save()
osoba_edit.save() osoba_edit.save()
return formularOKView(request, text=f'Údaje byly úspěšně uloženy. <a href="{reverse("profil")}">Vrátit se zpět na profil.</a>') return formularOKView(
request,
text='Údaje byly úspěšně uloženy.',
dalsi_odkazy=[("Vrátit se zpět na profil", reverse("profil"))],
)
return render(request, 'personalni/udaje/edit.html', {'form': form}) return render(request, 'personalni/udaje/edit.html', {'form': form})

View file

@ -35,6 +35,7 @@ from django.conf import settings
import unicodedata import unicodedata
import logging import logging
import time import time
from collections.abc import Sequence
from seminar.utils import aktivniResitele from seminar.utils import aktivniResitele
@ -677,9 +678,9 @@ def StavDatabazeView(request):
# Interní, nemá se nikdy objevit v urls (jinak to účastníci vytrolí) # Interní, nemá se nikdy objevit v urls (jinak to účastníci vytrolí)
def formularOKView(request, text=''): def formularOKView(request, text='', dalsi_odkazy: Sequence[tuple[str, str]] = ()):
zelvuska marked this conversation as resolved Outdated

V zájmu bezpečnosti bych možná spíš než Iterable použil Sequence, tj. věc, kde mají věci dané pořadí (množina a slovník jsou Iterable, ale ne Sequence; asi chceme, aby to pořadí, co tady napíšeme, se dodrželo.)

Ne že by někdo aktuálně moc kontroloval typy, ale třeba jednou bude :-D

V zájmu bezpečnosti bych možná spíš než `Iterable` použil `Sequence`, tj. věc, kde mají věci dané pořadí (množina a slovník jsou Iterable, ale ne Sequence; asi chceme, aby to pořadí, co tady napíšeme, se dodrželo.) Ne že by někdo aktuálně moc kontroloval typy, ale třeba jednou bude :-D
template_name = 'seminar/formular_ok.html' template_name = 'seminar/formular_ok.html'
odkazy = [ odkazy = list(dalsi_odkazy) + [
# (Text, odkaz) # (Text, odkaz)
('Vrátit se na titulní stránku', reverse('titulni_strana')), ('Vrátit se na titulní stránku', reverse('titulni_strana')),
('Zobrazit aktuální zadání', reverse('seminar_aktualni_zadani')), ('Zobrazit aktuální zadání', reverse('seminar_aktualni_zadani')),