Upgrade odevzdavatka #30

Merged
zelvuska merged 37 commits from upgrade_odevzdavatka into master 1 year ago
  1. 22
      api/views/autocomplete.py
  2. 38
      data/sitetree.json
  3. 2
      docs/dalsi_soubory.rst
  4. 19
      mamweb/static/css/mamweb.css
  5. 4
      odevzdavatko/__init__.py
  6. 27
      odevzdavatko/forms.py
  7. 58
      odevzdavatko/templates/odevzdavatko/nahraj_reseni.html
  8. 21
      odevzdavatko/templates/odevzdavatko/nahraj_reseni_nadproblem.html
  9. 5
      odevzdavatko/templates/odevzdavatko/prilohy.html
  10. 0
      odevzdavatko/templates/odevzdavatko/vloz_reseni.html
  11. 5
      odevzdavatko/urls.py
  12. 36
      odevzdavatko/views.py
  13. 2
      personalni/templates/personalni/profil/resitel.html
  14. 6
      personalni/views.py
  15. 5
      seminar/views/views_all.py

22
api/views/autocomplete.py

@ -70,23 +70,17 @@ class PublicResitelAutocomplete(LoginRequiredAjaxMixin, autocomplete.Select2Quer
class OdevzdatelnyProblemAutocomplete(autocomplete.Select2QuerySetView):
""" View k :mod:`dal.autocomplete` pro vyhledávání problémů především v odevzdávátku. """
def get_queryset(self):
nastaveni = get_object_or_404(m.Nastaveni)
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)
qs = m.Problem.objects.filter(stav=m.Problem.STAV_ZADANY)
if self.q:
qs = qs.filter(
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))
return qs
class ProblemAutocomplete(autocomplete.Select2QuerySetView):

38
data/sitetree.json

@ -437,7 +437,7 @@
"insitetree": true,
"parent": 21,
"sort_order": 36,
"title": "Poslat řešení",
"title": "Nahrát řešení",
"tree": 1,
"url": "seminar_nahraj_reseni",
"urlaspattern": true
@ -719,7 +719,7 @@
"insitetree": true,
"parent": 21,
"sort_order": 36,
"title": "Nahrát řešení",
"title": "Vložit řešení",
"tree": 1,
"url": "seminar_vloz_reseni",
"urlaspattern": true
@ -1026,6 +1026,36 @@
"model": "sitetree.treeitem",
"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": {
"access_guest": false,
@ -1041,13 +1071,13 @@
"inmenu": true,
"insitetree": true,
"parent": 28,
"sort_order": 52,
"sort_order": 53,
"title": "Přidat PDF",
"tree": 1,
"url": "/admin/korektury/korekturovanepdf/add/",
"urlaspattern": false
},
"model": "sitetree.treeitem",
"pk": 52
"pk": 53
}
]

2
docs/dalsi_soubory.rst

@ -28,7 +28,7 @@ Generuje se za pomocí::
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
deploy_v2

19
mamweb/static/css/mamweb.css

@ -1260,3 +1260,22 @@ label[for=id_skola] {
.bodovani>input {
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`?
}

4
odevzdavatko/__init__.py

@ -4,8 +4,8 @@ Obsahuje vše, co se týká odevzdávání (+ nahrávání) a opravování řeš
Slovníček:
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ý.
Poslat ř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.
Nahrát řešení = Odevdat řešení. (Tj. řešení se vztahem 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?
"""

27
odevzdavatko/forms.py

@ -29,6 +29,8 @@ class PosliReseniForm(forms.Form):
attrs={
'data-placeholder--id': '-1',
'data-placeholder--text': '---',
'data-close-on-select': 'false',
'data-dropdown-css-class': 's2m-se-zaskrtavatky',
'data-allow-clear': 'true'
},
),
@ -43,6 +45,8 @@ class PosliReseniForm(forms.Form):
url='autocomplete_resitel',
attrs = {'data-placeholder--id': '-1',
'data-placeholder--text' : '---',
'data-close-on-select': 'false',
'data-dropdown-css-class': 's2m-se-zaskrtavatky',
'data-allow-clear': 'true'})
)
@ -62,12 +66,6 @@ class PosliReseniForm(forms.Form):
#poznamka = models.TextField('neveřejná poznámka', blank=True,
# 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 Meta:
@ -80,23 +78,40 @@ class NahrajReseniForm(forms.ModelForm):
url='autocomplete_problem_odevzdatelny',
attrs = {'data-placeholder--id': '-1',
'data-placeholder--text' : '---',
'data-close-on-select': 'false',
'data-dropdown-css-class': 's2m-se-zaskrtavatky',
'data-allow-clear': 'true'},
forward=["nadproblem_id"],
),
'resitele':
autocomplete.ModelSelect2Multiple(
url='autocomplete_resitel_public',
attrs = {'data-placeholder--id': '-1',
'data-placeholder--text' : '---',
'data-close-on-select': 'false',
'data-dropdown-css-class': 's2m-se-zaskrtavatky',
'data-allow-clear': 'true'},
)
}
nadproblem_id = forms.IntegerField(required=False, disabled=True, widget=forms.HiddenInput())
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# FIXME Z nějakého důvodu se do této třídy dostaneme i bez resitele
if 'resitele' in self.fields:
# 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'].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,
form = NahrajReseniForm,

58
odevzdavatko/templates/odevzdavatko/nahraj_reseni.html

@ -7,19 +7,20 @@
{% block content %}
<h1>
{% block nadpis1a %}
Vložit řešení
Nahrát řešení
{% endblock %}
</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>
<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();">
<form enctype="multipart/form-data" action="{% url 'seminar_nahraj_reseni' nadproblem_id %}" method="post" onsubmit="return zkontroluj_prilohy();">
{% 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>
{% for field in form %}
<td>
<label class="field-label{% if field.field.required %} field-required{% endif %}" for="{{ field.id_for_label }}">
{{ field.label }}:
@ -28,15 +29,54 @@
<td>
{{ field }}
</td>
{% endfor %}
</tr>
{% if field.errors %}
<tr>
<td colspan="2"><span class="field-error">{{ field.errors }}</span></td>
</tr>
{% endif %}
{% endwith %}
</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>
<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>
{% include "odevzdavatko/prilohy.html" %}
{{form.non_field_errors}}
<hr>
<h4>Odevzdat řešení</h4>
<input type="submit" value="Odevzdat">

21
odevzdavatko/templates/odevzdavatko/nahraj_reseni_nadproblem.html

@ -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 %}

5
odevzdavatko/templates/odevzdavatko/prilohy.html

@ -2,8 +2,9 @@
<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 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 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 <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">
{% for form in prilohy.forms %}

0
odevzdavatko/templates/odevzdavatko/posli_reseni.html → odevzdavatko/templates/odevzdavatko/vloz_reseni.html

5
odevzdavatko/urls.py

@ -19,8 +19,9 @@ from seminar.utils import org_required, resitel_required, viewMethodSwitch, \
from . import views
urlpatterns = [
path('org/add_solution', org_required(views.PosliReseniView.as_view()), name='seminar_vloz_reseni'),
path('resitel/nahraj_reseni', resitel_required(views.NahrajReseniView.as_view()), name='seminar_nahraj_reseni'),
path('org/add_solution', org_required(views.VlozReseniView.as_view()), name='seminar_vloz_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('org/reseni/', org_required(views.TabulkaOdevzdanychReseniView.as_view()), name='odevzdavatko_tabulka'),

36
odevzdavatko/views.py

@ -367,8 +367,8 @@ class SeznamAktualnichReseniView(SeznamReseniView):
return qs
class PosliReseniView(LoginRequiredMixin, FormView):
template_name = 'odevzdavatko/posli_reseni.html'
class VlozReseniView(LoginRequiredMixin, FormView):
template_name = 'odevzdavatko/vloz_reseni.html'
form_class = f.PosliReseniForm
def form_valid(self, form):
@ -399,12 +399,27 @@ class PosliReseniView(LoginRequiredMixin, FormView):
return data
class NahrajReseniRozcestnikTematekView(LoginRequiredMixin, ListView):
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):
model = m.Reseni
template_name = 'odevzdavatko/nahraj_reseni.html'
form_class = f.NahrajReseniForm
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ů:
# 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)
@ -417,12 +432,23 @@ class NahrajReseniView(LoginRequiredMixin, CreateView):
})
return super().get(request, *args, **kwargs)
def get_initial(self):
nadproblem_id = self.nadproblem.id
return {
"nadproblem_id": nadproblem_id,
"problem": [] if self.nadproblem.podproblem.filter(stav=m.Problem.STAV_ZADANY).exists() else nadproblem_id
}
def get_context_data(self,**kwargs):
data = super().get_context_data(**kwargs)
if self.request.POST:
data['prilohy'] = f.ReseniSPrilohamiFormSet(self.request.POST,self.request.FILES)
else:
data['prilohy'] = f.ReseniSPrilohamiFormSet()
data["nadproblem_id"] = self.nadproblem.id
data["nadproblem"] = get_object_or_404(m.Problem, id=self.nadproblem.id)
return data
# 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),
).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"))],
)

2
personalni/templates/personalni/profil/resitel.html

@ -11,7 +11,7 @@
<a href="{% url 'logout' %}">Odhlásit se</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>
<a href="{% url 'seminar_resitel_odevzdana_reseni' %}">Již odevzdaná řešení</a><br>

6
personalni/views.py

@ -173,7 +173,11 @@ def resitelEditView(request):
msg = "Unknown school {}, {}".format(fcd['skola_nazev'],fcd['skola_adresa'])
resitel_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})

5
seminar/views/views_all.py

@ -35,6 +35,7 @@ from django.conf import settings
import unicodedata
import logging
import time
from collections.abc import Sequence
from seminar.utils import aktivniResitele
@ -677,9 +678,9 @@ def StavDatabazeView(request):
# 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]] = ()):
template_name = 'seminar/formular_ok.html'
odkazy = [
odkazy = list(dalsi_odkazy) + [
# (Text, odkaz)
('Vrátit se na titulní stránku', reverse('titulni_strana')),
('Zobrazit aktuální zadání', reverse('seminar_aktualni_zadani')),

Loading…
Cancel
Save