diff --git a/api/views/autocomplete.py b/api/views/autocomplete.py index 84a915bf..edc81ff7 100644 --- a/api/views/autocomplete.py +++ b/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): diff --git a/data/sitetree.json b/data/sitetree.json index 7cfa87b0..5332c20f 100644 --- a/data/sitetree.json +++ b/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 } -] \ No newline at end of file +] diff --git a/docs/dalsi_soubory.rst b/docs/dalsi_soubory.rst index 1a59ee15..627a59d7 100644 --- a/docs/dalsi_soubory.rst +++ b/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 diff --git a/mamweb/static/css/mamweb.css b/mamweb/static/css/mamweb.css index 3833ff92..a43bf310 100644 --- a/mamweb/static/css/mamweb.css +++ b/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; +} diff --git a/odevzdavatko/__init__.py b/odevzdavatko/__init__.py index a4ee2679..ee78a49b 100644 --- a/odevzdavatko/__init__.py +++ b/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 mé ř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 mé ř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? -""" \ No newline at end of file +""" diff --git a/odevzdavatko/forms.py b/odevzdavatko/forms.py index 0b93d555..074bc6da 100644 --- a/odevzdavatko/forms.py +++ b/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, diff --git a/odevzdavatko/templates/odevzdavatko/nahraj_reseni.html b/odevzdavatko/templates/odevzdavatko/nahraj_reseni.html index 739340c3..ca326d67 100644 --- a/odevzdavatko/templates/odevzdavatko/nahraj_reseni.html +++ b/odevzdavatko/templates/odevzdavatko/nahraj_reseni.html @@ -7,19 +7,20 @@ {% block content %}

{% block nadpis1a %} - Vložit řešení + Nahrát řešení {% endblock %}

-

Když řešení různých témátek vložíš každé zvlášť, lépe se v nich vyznáme a třeba ti je i rychleji opravíme.

- -

Pokud řešíte ve více lidech, je nutné 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 jednou (ne každý sám).

- -
+ {% csrf_token %} - +
+ + + + + + {% with field=form.problem %} - {% for field in form %} - {% endfor %} + + {% if field.errors %} + + + + {% endif %} + + {% endwith %}
{{ field }}
{{ field.errors }}
+ {% for field in form.hidden_fields %} + {{ field }} + {% endfor %} + +
+

Spolupráce s dalšími řešiteli

+ +

Pokud řešíte ve více lidech, je potřeba přidat tyto lidi jako „Další autory“. V tomto poli se vyhledává podle přezdívek, které si lze nastavit v „Osobních údajích“. Sebe vyplňovat nemusíte a za skupinu odevzdávejte pouze jednou (ne každý sám).

+ + + {% with field=form.resitele %} + + + + + + {% if field.errors %} + + + + {% endif %} + + {% endwith %} +
+ + + {{ field }} +
{{ field.errors }}

{% include "odevzdavatko/prilohy.html" %} +{{form.non_field_errors}} +

Odevzdat řešení

diff --git a/odevzdavatko/templates/odevzdavatko/nahraj_reseni_nadproblem.html b/odevzdavatko/templates/odevzdavatko/nahraj_reseni_nadproblem.html new file mode 100644 index 00000000..ccf505fa --- /dev/null +++ b/odevzdavatko/templates/odevzdavatko/nahraj_reseni_nadproblem.html @@ -0,0 +1,21 @@ +{% extends "base.html" %} +{% load static %} + +{% block content %} +

+ {% block nadpis1a %} + Nahrát řešení + {% endblock %} +

+ +

Seznam témat k odevzdání

+ + + +{% endblock %} diff --git a/odevzdavatko/templates/odevzdavatko/prilohy.html b/odevzdavatko/templates/odevzdavatko/prilohy.html index 4946546b..1e33376f 100644 --- a/odevzdavatko/templates/odevzdavatko/prilohy.html +++ b/odevzdavatko/templates/odevzdavatko/prilohy.html @@ -2,8 +2,9 @@

Soubory s řešením

-

Maximální součet velikostí příloh je cca 49 MB. Pokud je to možné a 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.

-

Pokud svůj soubor rozumně pojmenuješ, urychlíš opravování a předejdeš tomu, že si nějakého tvého řešení nevšimneme. Například z img_250921_101205.pdf nepoznáme, kterou úlohu jsi odevzdal, zato uloha_3.pdf nebo tema_1.pdf, to už je něco jiného. Případně můžeš využít i poznámku řešitele.

+

Pokud je to možné a dává to smysl (tj. není to třeba kód nebo doprovodný obrázek), pošli nám prosím své řešení ve formátu PDF, ostatní formáty nemusíme umět otevřít.

+

Pokud svůj soubor rozumně pojmenuješ, urychlíš opravování a předejdeš tomu, že si nějakého tvého řešení nevšimneme. Například z img_250921_101205.pdf nepoznáme, kterou úlohu jsi odevzdal, zato uloha_3.pdf nebo tema_1.pdf, to už je něco jiného. Případně můžeš využít i poznámku řešitele.

+

Maximální součet velikostí příloh je cca 49 MB.

{% for form in prilohy.forms %} diff --git a/odevzdavatko/templates/odevzdavatko/posli_reseni.html b/odevzdavatko/templates/odevzdavatko/vloz_reseni.html similarity index 100% rename from odevzdavatko/templates/odevzdavatko/posli_reseni.html rename to odevzdavatko/templates/odevzdavatko/vloz_reseni.html diff --git a/odevzdavatko/urls.py b/odevzdavatko/urls.py index 8c53de6b..6b021f2e 100644 --- a/odevzdavatko/urls.py +++ b/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//', 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'), diff --git a/odevzdavatko/views.py b/odevzdavatko/views.py index e87e19ea..1e19921a 100644 --- a/odevzdavatko/views.py +++ b/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"))], + ) diff --git a/personalni/templates/personalni/profil/resitel.html b/personalni/templates/personalni/profil/resitel.html index 9c933f0a..0bd92d63 100644 --- a/personalni/templates/personalni/profil/resitel.html +++ b/personalni/templates/personalni/profil/resitel.html @@ -11,7 +11,7 @@ Odhlásit se
Upravit údaje
-Poslat řešení
+Nahrát řešení
Již odevzdaná řešení
diff --git a/personalni/views.py b/personalni/views.py index a45aee52..876cc7ec 100644 --- a/personalni/views.py +++ b/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. Vrátit se zpět na profil.') + 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}) diff --git a/seminar/views/views_all.py b/seminar/views/views_all.py index 4627989e..e35742c7 100644 --- a/seminar/views/views_all.py +++ b/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')),