diff --git a/api/urls.py b/api/urls.py index 2570ded9..a3b5a4aa 100644 --- a/api/urls.py +++ b/api/urls.py @@ -22,6 +22,7 @@ urlpatterns = [ # Autocomplete 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_public/', views.PublicResitelAutocomplete.as_view(), name='autocomplete_resitel_public'), 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'), diff --git a/api/views/autocomplete.py b/api/views/autocomplete.py index 4558ad4a..601f4e35 100644 --- a/api/views/autocomplete.py +++ b/api/views/autocomplete.py @@ -44,6 +44,29 @@ class ResitelAutocomplete(LoginRequiredAjaxMixin,autocomplete.Select2QuerySetVie qs = qs.filter(query) return qs + +class PublicResitelAutocomplete(LoginRequiredAjaxMixin, autocomplete.Select2QuerySetView): + """ + View k :mod:`dal.autocomplete` pro vyhledávání řešitelů podle přezdívky + především v odevzdávátku. + """ + def get_queryset(self): + letos = m.Nastaveni.get_solo().aktualni_rocnik + qs = m.Resitel.objects.filter( + rok_maturity__gte=letos.druhy_rok() + ).filter( + prezdivka_resitele__isnull=False + ).exclude( + prezdivka_resitele="" + ).filter( + prezdivka_resitele__icontains=self.q + ).all() + return qs + + def get_result_label(self, result): + return result.prezdivka_resitele + + 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): diff --git a/odevzdavatko/forms.py b/odevzdavatko/forms.py index 65a8b7ac..a31122dd 100644 --- a/odevzdavatko/forms.py +++ b/odevzdavatko/forms.py @@ -63,7 +63,7 @@ class PosliReseniForm(forms.Form): class NahrajReseniForm(forms.ModelForm): class Meta: model = m.Reseni - fields = ('problem',) + fields = ('problem', 'resitele') help_texts = {'problem':''} # Nezobrazovat help text ve formuláři widgets = {'problem': @@ -72,6 +72,13 @@ class NahrajReseniForm(forms.ModelForm): attrs = {'data-placeholder--id': '-1', 'data-placeholder--text' : '---', 'data-allow-clear': 'true'}, + ), + 'resitele': + autocomplete.ModelSelect2Multiple( + url='autocomplete_resitel_public', + attrs = {'data-placeholder--id': '-1', + 'data-placeholder--text' : '---', + 'data-allow-clear': 'true'}, ) } diff --git a/odevzdavatko/static/odevzdavatko/check_for_detail.js b/odevzdavatko/static/odevzdavatko/check_for_detail.js new file mode 100644 index 00000000..b694dc88 --- /dev/null +++ b/odevzdavatko/static/odevzdavatko/check_for_detail.js @@ -0,0 +1,22 @@ +// Kontrola, že org neposílá nějakou blbost v detail.html + +function zkontroluj_hodnoceni() { + const pocet = $('.hodnoceni').length; + if (pocet === 1) { // vidím pouze plusko + const vysledek = confirm("Odstranil jsi všechny problémy tohoto řešení. Nepůjde tedy dohledat přes problémy, co řeší, tj. například v došlých řešeních. Přesto odeslat?"); + if (!vysledek) { + event.preventDefault(); + return false; + } + } + + function problem_is_empty(elem, index, array) {return elem.firstElementChild.children.length !== 1 && elem.firstElementChild.children[1].textContent === "";} + + if ($('.hodnoceni').toArray().some(problem_is_empty)) { + alert("Neuloženo! Nezadal jsi problém, ke kterému posíláš hodnocení. Pokud je toto hodnocení navíc, smaž ho prosím křížkem a znovu odešli.") + event.preventDefault() + return false; + } + + return true; +} diff --git a/odevzdavatko/static/odevzdavatko/dynamic_formsets_for_detail.js b/odevzdavatko/static/odevzdavatko/dynamic_formsets_for_detail.js new file mode 100644 index 00000000..a14c9f8f --- /dev/null +++ b/odevzdavatko/static/odevzdavatko/dynamic_formsets_for_detail.js @@ -0,0 +1,56 @@ +// FIXME: Necopypastovat! Tohle je zkopírované ze static/odevzdavatko/dynamic_formsets.js + + +// Credit https://medium.com/all-about-django/adding-forms-dynamically-to-a-django-formset-375f1090c2b0 +function updateElementIndex(el, prefix, ndx) { + var id_regex = new RegExp('(' + prefix + '-\\d+)'); + var replacement = prefix + '-' + ndx; + if ($(el).attr("for")) { + $(el).attr("for", $(el).attr("for").replace(id_regex, replacement)); + } + if (el.id) { + el.id = el.id.replace(id_regex, replacement); + } + if (el.name) { + el.name = el.name.replace(id_regex, replacement); + } +} + +// Credit https://medium.com/all-about-django/adding-forms-dynamically-to-a-django-formset-375f1090c2b0 +function deleteForm(prefix, btn) { + var total = parseInt($('#id_' + prefix + '-TOTAL_FORMS').val()); + if (total >= 1){ + btn.closest('tr').remove(); + var forms = $('.hodnoceni'); + var formCount = forms.length - 1; // There is one extra such form hidden as template! + $('#id_' + prefix + '-TOTAL_FORMS').val(formCount); + for (var i=0; i -// Credit https://medium.com/all-about-django/adding-forms-dynamically-to-a-django-formset-375f1090c2b0 -function updateElementIndex(el, prefix, ndx) { - var id_regex = new RegExp('(' + prefix + '-\\d+)'); - var replacement = prefix + '-' + ndx; - if ($(el).attr("for")) { - $(el).attr("for", $(el).attr("for").replace(id_regex, replacement)); - } - if (el.id) { - el.id = el.id.replace(id_regex, replacement); - } - if (el.name) { - el.name = el.name.replace(id_regex, replacement); - } -} - -// Credit https://medium.com/all-about-django/adding-forms-dynamically-to-a-django-formset-375f1090c2b0 -function deleteForm(prefix, btn) { - var total = parseInt($('#id_' + prefix + '-TOTAL_FORMS').val()); - if (total >= 1){ - btn.closest('tr').remove(); - var forms = $('.hodnoceni'); - var formCount = forms.length - 1; // There is one extra such form hidden as template! - $('#id_' + prefix + '-TOTAL_FORMS').val(formCount); - for (var i=0; i + {% if edit %} + + + {% endif %}

Řešené problémy: {{ object.problem.all | join:", " }}

-

Řešitelé: {% for r in object.resitele.all %} {{ r }} ({{ r.osoba.email }}) -{% if forloop.revcounter0 != 0 %}, {% endif %} {% endfor %}

+{% if edit %} +

Řešitelé: + {% for r in object.resitele.all %}{{ r }} ({{ r.osoba.email }}){% if forloop.revcounter0 != 0 %}, {% endif %}{% endfor %} +

+{% else %} +

Řešitelé: {{ object.resitele.all | join:", " }}

+{% endif %} {# https://docs.djangoproject.com/en/3.1/ref/models/instances/#django.db.models.Model.get_FOO_display #}

Forma: {{ object.get_forma_display }}

@@ -82,13 +35,13 @@ $(document).ready(function(){ {{ priloha.split | last }} {{ priloha.res_poznamka }} {{ priloha.vytvoreno }} - {# TODO: Orgo-poznámka, ideálně jako formulář #} {% endfor %} {% else %}

Žádné přílohy

{% endif %} + {% if edit %}
{# Poznámka #}

Neveřejná poznámka:

@@ -116,7 +69,7 @@ $(document).ready(function(){ - Přidat hodnocení
+ Přidat hodnocení
@@ -129,28 +82,19 @@ $(document).ready(function(){ + {% else %} +

Hodnocení:

+ + +{% for h in hodnoceni %} + + + + + +{% endfor %} +
ProblémBodyZpětná vazba od opravovatele
{{ h.problem }}{{ h.body }}{{ h.feedback }}
+ {% endif %} - {% endblock %} diff --git a/odevzdavatko/templates/odevzdavatko/detail_resitele.html b/odevzdavatko/templates/odevzdavatko/detail_resitele.html deleted file mode 100644 index fb0cb5fb..00000000 --- a/odevzdavatko/templates/odevzdavatko/detail_resitele.html +++ /dev/null @@ -1,51 +0,0 @@ -{% extends "base.html" %} -{% load static %} -{% load deadliny %} - -{% block content %} - -

Řešené problémy: {{ object.problem.all | join:", " }}

- -

Řešitelé: {% for r in object.resitele.all %} {{ r }} - {% if forloop.revcounter0 != 0 %}, {% endif %} {% endfor %}

- -{# https://docs.djangoproject.com/en/3.1/ref/models/instances/#django.db.models.Model.get_FOO_display #} -

Forma: {{ object.get_forma_display }}

- -

Doručeno {{ object.cas_doruceni }}, deadline: {{object.deadline_reseni | deadline_html }}

- -{# Soubory: #} -

Přílohy:

-{% if object.prilohy.all %} - - -{% for priloha in object.prilohy.all %} - - - - - {# TODO: Orgo-poznámka, ideálně jako formulář #} -{% endfor %} -
SouborŘešitelova poznámkaDatum
{{ priloha.split | last }}{{ priloha.res_poznamka }}{{ priloha.vytvoreno }}
-{% else %} -

Žádné přílohy

-{% endif %} - -{#

Poznámka:

#} -{#

{{ poznamka }}

#} - -{# Hodnocení: #} -

Hodnocení:

- -{# #} -{% for h in hodnoceni %} - - - - -{# #} - -{% endfor %} -
ProblémBodyZpětná vazba od opravovateleDeadline pro body
{{ h.problem }}{{ h.body }}{{ h.feedback }}{{ h.deadline_body }}
- -{% endblock %} diff --git a/odevzdavatko/templates/odevzdavatko/nahraj_reseni.html b/odevzdavatko/templates/odevzdavatko/nahraj_reseni.html index 64ef92c1..739340c3 100644 --- a/odevzdavatko/templates/odevzdavatko/nahraj_reseni.html +++ b/odevzdavatko/templates/odevzdavatko/nahraj_reseni.html @@ -13,6 +13,8 @@

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 %} diff --git a/odevzdavatko/urls.py b/odevzdavatko/urls.py index e15b3807..8c53de6b 100644 --- a/odevzdavatko/urls.py +++ b/odevzdavatko/urls.py @@ -26,9 +26,9 @@ urlpatterns = [ path('org/reseni/', org_required(views.TabulkaOdevzdanychReseniView.as_view()), name='odevzdavatko_tabulka'), path('org/reseni/rocnik//', org_required(views.TabulkaOdevzdanychReseniView.as_view()), name='odevzdavatko_tabulka'), path('org/reseni///', org_required(views.ReseniProblemuView.as_view()), name='odevzdavatko_reseni_resitele_k_problemu'), - path('org/reseni/', org_required(viewMethodSwitch(get=views.DetailReseniView.as_view(), post=views.hodnoceniReseniView)), name='odevzdavatko_detail_reseni'), + path('org/reseni/', org_required(viewMethodSwitch(get=views.EditReseniView.as_view(), post=views.hodnoceniReseniView)), name='odevzdavatko_detail_reseni'), path('org/reseni/all', org_required(views.SeznamReseniView.as_view())), path('org/reseni/akt', org_required(views.SeznamAktualnichReseniView.as_view())), - path('resitel/reseni/', resitel_or_org_required(views.ResitelReseniView.as_view()), name='odevzdavatko_resitel_reseni'), + path('resitel/reseni/', resitel_or_org_required(views.DetailReseniView.as_view()), name='odevzdavatko_resitel_reseni'), ] diff --git a/odevzdavatko/views.py b/odevzdavatko/views.py index 1e27d4f1..664482bd 100644 --- a/odevzdavatko/views.py +++ b/odevzdavatko/views.py @@ -216,6 +216,7 @@ class ReseniProblemuView(MultipleObjectTemplateResponseMixin, MultipleObjectMixi ## XXX: https://docs.djangoproject.com/en/3.1/topics/class-based-views/mixins/#avoid-anything-more-complex class DetailReseniView(DetailView): + """ Náhled na řešení. Editace je v :py:class:`EditReseniView`. """ model = m.Reseni template_name = 'odevzdavatko/detail.html' @@ -232,18 +233,44 @@ class DetailReseniView(DetailView): return result def get_context_data(self, **kw): + self.check_access() ctx = super().get_context_data(**kw) - ctx['form'] = f.OhodnoceniReseniFormSet( - initial = self.aktualni_hodnoceni() - ) + hodnoceni = self.aktualni_hodnoceni() + ctx["hodnoceni"] = hodnoceni + return ctx + + def get(self, request, *args, **kwargs): + """ + Oproti :py:class:`django.views.generic.detail.BaseDetailView` + kontroluje přístup pomocí :py:meth:`check_access` + """ + response = super().get(self, request, *args, **kwargs) + self.check_access() + return response + + def check_access(self): + """ Řešitel musí být součástí řešení, jinak se na něj nemá co dívat. """ + if not self.object.resitele.filter(osoba__user=self.request.user).exists(): + raise PermissionDenied() + + +class EditReseniView(DetailReseniView): + """ Editace (hlavně hodnocení) řešení. """ + def get_context_data(self, **kw): + ctx = super().get_context_data(**kw) + ctx['form'] = f.OhodnoceniReseniFormSet(initial=ctx["hodnoceni"]) ctx['poznamka_form'] = f.PoznamkaReseniForm(instance=self.reseni) + ctx['edit'] = True return ctx + def check_access(self): + # Na orga máme nároky už v urls.py ale better safe then sorry + if not self.request.user.je_org: + raise PermissionDenied() + def hodnoceniReseniView(request, pk, *args, **kwargs): reseni = get_object_or_404(m.Reseni, pk=pk) - template_name = 'odevzdavatko/detail.html' - form_class = f.OhodnoceniReseniFormSet success_url = reverse('odevzdavatko_detail_reseni', kwargs={'pk': pk}) # FIXME: Použit initial i tady a nebastlit hodnocení tak nízkoúrovňově @@ -275,33 +302,6 @@ def hodnoceniReseniView(request, pk, *args, **kwargs): return redirect(success_url) -class ResitelReseniView(DetailView): - model = m.Reseni - template_name = 'odevzdavatko/detail_resitele.html' - - def aktualni_hodnoceni(self): - self.reseni = get_object_or_404(m.Reseni, id=self.kwargs['pk']) - result = [] - for hodn in m.Hodnoceni.objects.filter(reseni=self.reseni): - result.append( - { - "problem": hodn.problem, - "body": hodn.body, - "feedback": hodn.feedback, - # "deadline_body": hodn.deadline_body, - } - ) - return result - - def get_context_data(self, **kw): - ctx = super().get_context_data(**kw) - hodnoceni = self.aktualni_hodnoceni() - if not self.reseni.resitele.filter(osoba__user=self.request.user).exists(): - raise PermissionDenied() - # ctx['poznamka'] = f.PoznamkaReseniForm(instance=self.reseni) - ctx["hodnoceni"] = hodnoceni - return ctx - class PrehledOdevzdanychReseni(ListView): @@ -413,6 +413,7 @@ class NahrajReseniView(LoginRequiredMixin, CreateView): with transaction.atomic(): self.object = form.save() self.object.resitele.add(m.Resitel.objects.get(osoba__user = self.request.user)) + self.object.resitele.add(*form.cleaned_data["resitele"]) self.object.cas_doruceni = timezone.now() self.object.forma = m.Reseni.FORMA_UPLOAD self.object.save() diff --git a/personalni/forms.py b/personalni/forms.py index ea200267..f9d90182 100644 --- a/personalni/forms.py +++ b/personalni/forms.py @@ -32,6 +32,7 @@ class PrihlaskaForm(PasswordResetForm): help_text='Tímto jménem se následně budeš přihlašovat pro odevzdání řešení a další činnosti v semináři') jmeno = forms.CharField(label='Jméno', max_length=256, required=True) + prezdivka_resitele = forms.CharField(label='Přezdívka (veřejná)', max_length=256, required=False) prijmeni = forms.CharField(label='Příjmení', max_length=256, required=True) pohlavi_muz = forms.ChoiceField(label='Pohlaví', choices = ((True,'muž'),(False,'žena')), required=True) @@ -105,6 +106,14 @@ class PrihlaskaForm(PasswordResetForm): pass return email + def clean_prezdivka_resitele(self): + prezdivka_resitele = self.cleaned_data.get('prezdivka_resitele') + if prezdivka_resitele == '': + return prezdivka_resitele + if Resitel.objects.filter(prezdivka_resitele=prezdivka_resitele).count() > 0: + raise forms.ValidationError('Přezdívka je již použita') + return prezdivka_resitele + def clean_zasilat(self): zasilat = self.cleaned_data.get('zasilat') ulice = self.cleaned_data.get('ulice') @@ -138,6 +147,7 @@ class ProfileEditForm(forms.Form): disabled=True) jmeno = forms.CharField(label='Jméno', max_length=256, required=True) + prezdivka_resitele = forms.CharField(label='Přezdívka (veřejná)', max_length=256, required=False) prijmeni = forms.CharField(label='Příjmení', max_length=256, required=True) pohlavi_muz = forms.ChoiceField(label='Pohlaví', choices = ((True,'muž'),(False,'žena')), required=True) @@ -190,6 +200,15 @@ class ProfileEditForm(forms.Form): # pass # return username # + + def clean_prezdivka_resitele(self): + prezdivka_resitele = self.cleaned_data.get('prezdivka_resitele') + if prezdivka_resitele == '': + return prezdivka_resitele + if Resitel.objects.filter(prezdivka_resitele=prezdivka_resitele).exclude(osoba__user__username=self.username).count() > 0: + raise forms.ValidationError('Přezdívka je již použita') + return prezdivka_resitele + def clean_email(self): err_logger = logging.getLogger('seminar.prihlaska.problem') email = self.cleaned_data.get('email') diff --git a/personalni/templates/personalni/udaje/edit.html b/personalni/templates/personalni/udaje/edit.html index 5ec690d9..9091925d 100644 --- a/personalni/templates/personalni/udaje/edit.html +++ b/personalni/templates/personalni/udaje/edit.html @@ -44,6 +44,7 @@
{% include "personalni/udaje/prihlaska_field.html" with field=form.jmeno %} + {% include "personalni/udaje/prihlaska_field.html" with field=form.prezdivka_resitele %} {% include "personalni/udaje/prihlaska_field.html" with field=form.prijmeni %} {% include "personalni/udaje/prihlaska_field.html" with field=form.pohlavi_muz%} {% include "personalni/udaje/prihlaska_field.html" with field=form.email %} diff --git a/personalni/templates/personalni/udaje/gdpr.html b/personalni/templates/personalni/udaje/gdpr.html index 3e85de78..5d9af535 100644 --- a/personalni/templates/personalni/udaje/gdpr.html +++ b/personalni/templates/personalni/udaje/gdpr.html @@ -11,7 +11,7 @@ Získáváme od Tebe údaje vyplněné v přihlášce do semináře (jméno, př Slibujeme Ti, že Tvá osobní data nezneužijeme k ničemu, co by nesouviselo s M&M nebo s dalšími aktivitami Matfyzu, a nikdy je nepředáme nikomu cizímu. Údaje využíváme k zajištění chodu semináře a také je sdílíme s ostatními propagačními akcemi Matfyzu, abychom mohli vyhodnocovat úspěšnost akcí. Pokud budeš mít zájem, budeme Ti také posílat zajímavé zprávy a novinky týkajíci se Matfyzu.

-Veřejně vystavujeme pouze výsledkové listiny, které také uchováváme pro archivní účely. Pokud ale z nějakého důvodu nebudeš chtít mít své jméno či školu uvedené ve výsledkové listině, není problém to zařídit, napiš nám. Z tištěných materiálů samozřejmě údaje už odstranit nemůžeme. +Veřejně vystavujeme pouze seznam přezdívek (pro výběr spoluřešitelů k řešení) a výsledkové listiny, které také uchováváme pro archivní účely. Pokud ale z nějakého důvodu nebudeš chtít mít své jméno či školu uvedené ve výsledkové listině, není problém to zařídit, napiš nám. Z tištěných materiálů samozřejmě údaje už odstranit nemůžeme.

Na soustředěních a dalších akcích semináře navíc pořizujeme fotografie a videozáznamy a používáme je ke zpravodajským a propagačním účelům. Pro propagační účely si od Tebe vyžádáme samostatný souhlas na začátku akce. diff --git a/personalni/templates/personalni/udaje/prihlaska.html b/personalni/templates/personalni/udaje/prihlaska.html index 5e6434bf..33adba03 100644 --- a/personalni/templates/personalni/udaje/prihlaska.html +++ b/personalni/templates/personalni/udaje/prihlaska.html @@ -46,6 +46,7 @@

{% include "personalni/udaje/prihlaska_field.html" with field=form.jmeno %} + {% include "personalni/udaje/prihlaska_field.html" with field=form.prezdivka_resitele %} {% include "personalni/udaje/prihlaska_field.html" with field=form.prijmeni %} {% include "personalni/udaje/prihlaska_field.html" with field=form.pohlavi_muz%} {% include "personalni/udaje/prihlaska_field.html" with field=form.email %} diff --git a/personalni/views.py b/personalni/views.py index 94b90dea..d7d52540 100644 --- a/personalni/views.py +++ b/personalni/views.py @@ -160,6 +160,7 @@ def resitelEditView(request): if resitel_edit: ## Změny v řešiteli + resitel_edit.prezdivka_resitele = fcd['prezdivka_resitele'] resitel_edit.skola = fcd['skola'] resitel_edit.rok_maturity = fcd['rok_maturity'] resitel_edit.zasilat = fcd['zasilat'] @@ -263,6 +264,7 @@ def prihlaskaView(request): err_logger.warning(f'Zaregistrovala se osoba s kolizním jménem. ID osob: {[o.id for o in kolize]}') r = s.Resitel( + prezdivka_resitele=fcd['prezdivka_resitele'], rok_maturity = fcd['rok_maturity'], zasilat = fcd['zasilat'], zasilat_cislo_emailem = fcd['zasilat_cislo_emailem'] diff --git a/seminar/migrations/0110_resitel_prezdivka.py b/seminar/migrations/0110_resitel_prezdivka.py new file mode 100644 index 00000000..51d25822 --- /dev/null +++ b/seminar/migrations/0110_resitel_prezdivka.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.28 on 2022-11-21 22:07 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('seminar', '0109_hodnoceni_feedback'), + ] + + operations = [ + migrations.AddField( + model_name='resitel', + name='prezdivka_resitele', + field=models.CharField(blank=True, max_length=256, null=True, unique=True, verbose_name='přezdívka řešitele'), + ), + ] diff --git a/seminar/models/personalni.py b/seminar/models/personalni.py index 28deec4d..3e64af45 100644 --- a/seminar/models/personalni.py +++ b/seminar/models/personalni.py @@ -211,6 +211,8 @@ class Resitel(SeminarModelBase): # Interní ID id = models.AutoField(primary_key = True) + prezdivka_resitele = models.CharField('přezdívka řešitele', blank=True, null=True, max_length=256, unique=True) + osoba = models.OneToOneField(Osoba, blank=False, null=False, verbose_name='osoba', on_delete=models.PROTECT)