Browse Source

Merge pull request 'Upgrade odevzdavatka' (!30) from upgrade_odevzdavatka into master

Reviewed-on: #30
upgrade_dokumentace
Jonas Havelka 1 year ago
parent
commit
0528dbbb9c
  1. 22
      api/views/autocomplete.py
  2. 40
      data/sitetree.json
  3. 2
      docs/dalsi_soubory.rst
  4. 19
      mamweb/static/css/mamweb.css
  5. 6
      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):

40
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}}",
"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;
}

6
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…
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