Vylepšení odevzdávátka #13

Merged
zelvuska merged 17 commits from vylepseni_odevzdavatka into master 2023-01-02 20:25:01 +01:00
17 changed files with 218 additions and 170 deletions

View file

@ -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'),

View file

@ -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(
zelvuska marked this conversation as resolved Outdated

Nebylo by lepší ty řádky spojit do něčeho jako prezdivka__len__gt=0?

Nebylo by lepší ty řádky spojit do něčeho jako `prezdivka__len__gt=0`?

Mě to takhle asi přijde popisnější a jsem si jistější, že to funguje…

Mě to takhle asi přijde popisnější a jsem si jistější, že to funguje…
prezdivka_resitele__isnull=False
).exclude(
zelvuska marked this conversation as resolved Outdated

Možná to chceme mít aspoň case-insensitive.

Možná to chceme mít aspoň case-insensitive.

asi …__icontains=self.q

asi `…__icontains=self.q`
prezdivka_resitele=""
zelvuska marked this conversation as resolved
Review

Tenhle způsob zalámání nejspíš nikde nemáme – trochu mi to rozbíjí čtení kódu. Ale možná nikde nemáme takovýhle kód, takže by to pokus o fluent kód (vizte níž) udělal možná taky…

Pod fluent kódem si představuji cca toto:

qs = (m.Resitel.objects
    .filter()
    .exclude(...)
    .whatever()
    .all())

(Je to o něco kratší a neskáče tam odsazení jak na pile, což mi přijde trošičku lepší pro čtení, ale je to jen malá preference…)

Tenhle způsob zalámání nejspíš nikde nemáme – trochu mi to rozbíjí čtení kódu. Ale možná nikde nemáme takovýhle kód, takže by to pokus o fluent kód (vizte níž) udělal možná taky… Pod fluent kódem si představuji cca toto: ```python3 qs = (m.Resitel.objects .filter(…) .exclude(...) .whatever() .all()) ``` (Je to o něco kratší a neskáče tam odsazení jak na pile, což mi přijde trošičku lepší pro čtení, ale je to jen malá preference…)
Review

Tak takhle mi zase chvíli trvá, než mi dojde, k čemu ta tečka na začátku řádku patří…

Tak takhle mi zase chvíli trvá, než mi dojde, k čemu ta tečka na začátku řádku patří…
).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):

View file

@ -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'},
)
}

View file

@ -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
zelvuska marked this conversation as resolved Outdated

i

i
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;
}

View file

@ -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<formCount; i++) {
$(forms.get(i)).find(':input').each(function() {
updateElementIndex(this, prefix, i);
});
}
}
return false;
}
// Credit: https://simpleit.rocks/python/django/dynamic-add-form-with-add-button-in-django-modelformset-template/
$(document).ready(function(){
$('#pridat_hodnoceni').click(function() {
var form_idx = $('#id_form-TOTAL_FORMS').val();
var new_form = $('#empty_form').html().replace(/__prefix__/g, form_idx);
$('#form_set').append(new_form);
// Newly created form has not the binding between remove button and remove function
// We need to add it manually
$('.smazat_hodnoceni').click(function(){
deleteForm("form",this);
});
// Copy deadline
if (form_idx !== "0") {
$('#id_form-' + form_idx + '-deadline_body')[0].value = $('#id_form-' + (form_idx - 1) + '-deadline_body')[0].value
}
$('#id_form-TOTAL_FORMS').val(parseInt(form_idx) + 1);
});
$('.smazat_hodnoceni').click(function(){
deleteForm("form",this);
});
});

View file

@ -4,68 +4,21 @@
{% block content %}
{# FIXME: Necopypastovat! Tohle je zkopírované ze static/odevzdavatko/dynamic_formsets.js #}
<script type='text/javascript'>
// 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<formCount; i++) {
$(forms.get(i)).find(':input').each(function() {
updateElementIndex(this, prefix, i);
});
}
}
return false;
}
// Credit: https://simpleit.rocks/python/django/dynamic-add-form-with-add-button-in-django-modelformset-template/
$(document).ready(function(){
$('#pridat_hodnoceni').click(function() {
var form_idx = $('#id_form-TOTAL_FORMS').val();
var new_form = $('#empty_form').html().replace(/__prefix__/g, form_idx);
$('#form_set').append(new_form);
// Newly created form has not the binding between remove button and remove function
// We need to add it manually
$('.smazat_hodnoceni').click(function(){
deleteForm("form",this);
});
// Copy deadline
if (form_idx !== "0") {
$('#id_form-' + form_idx + '-deadline_body')[0].value = $('#id_form-' + (form_idx - 1) + '-deadline_body')[0].value
}
$('#id_form-TOTAL_FORMS').val(parseInt(form_idx) + 1);
});
$('.smazat_hodnoceni').click(function(){
deleteForm("form",this);
});
});
</script>
{% if edit %}
<script src="{% static 'odevzdavatko/dynamic_formsets_for_detail.js' %}"></script>
<script src="{% static 'odevzdavatko/check_for_detail.js' %}"></script>
{% endif %}
<p>Řešené problémy: {{ object.problem.all | join:", " }}</p>
<p>Řešitelé: {% for r in object.resitele.all %} {{ r }} (<a href="mailto:{{ r.osoba.email }}?subject={{ "Oprava řešení M&M " | urlencode }}{{ object.problem.all.0.hlavni_problem | urlencode }}">{{ r.osoba.email }}</a>)
{% if forloop.revcounter0 != 0 %}, {% endif %} {% endfor %}</p>
{% if edit %}

Přijde mi lepší se nesnažit všechno nacpat na jeden řádek. To HTML se vykreslí stejně a v tomhle se mi špatně orientuje…

Přijde mi lepší se nesnažit všechno nacpat na jeden řádek. To HTML se vykreslí stejně a v tomhle se mi špatně orientuje…

No až na to, že tam přibydou mezery. Nechceš to nasekat ty?

No až na to, že tam přibydou mezery. Nechceš to nasekat ty?

Sice nevím, kde přibudou mezery, kouknu na testweb, ale pak to klidně nasekám…

Sice nevím, kde přibudou mezery, kouknu na testweb, ale pak to klidně nasekám…

Na testwebu to aktuálně běží…

Na testwebu to aktuálně běží…

LGTM

LGTM

Za mě všechno, nasekáš to?

Za mě všechno, nasekáš to?

Klidně to mergni, nasekám, ale možná to o pár desítek minut odložím (a když na to zapomenu úplně, tak to bolet taky nebude…)

Klidně to mergni, nasekám, ale možná to o pár desítek minut odložím (a když na to zapomenu úplně, tak to bolet taky nebude…)

Na jak velkou míru enterprise kódu míříme? DjangoTemplates jsou trochu na palici a těch mezer kvůli odřádkování se neumí zbavit, ale skoro určitě se to dá obejít pomocí triviálního filtru, takže by se pak dalo použít něco jako {{ o | withemail }}, což by rovnou udělalo tu výslednou věc a ten kód by byl přehlednější, ač na jednom řádku.

Ale znamená to mít k tomu ještě kousek kódu vedle.

Alternativně se něco takového dá spočítat už ve view, což se ale bude motat s výkonným kódem, který už tak je nejspíš na spodní hranici přehlednosti…

(A nebo se smíříme s tím, že ten řádek nejde tak jednoduše přečíst. Ostatně, nebyl by první – výsledkovky v TeXu trpí stejným problémem…)

Možná se mi podaří tam dát aspoň pár málo odřádkování, ale těžko odhadnout…

Na jak velkou míru enterprise kódu míříme? DjangoTemplates jsou trochu na palici a těch mezer kvůli odřádkování se neumí zbavit, ale skoro určitě se to dá obejít pomocí triviálního filtru, takže by se pak dalo použít něco jako `{{ o | withemail }}`, což by rovnou udělalo tu výslednou věc a ten kód by byl přehlednější, ač na jednom řádku. Ale znamená to mít k tomu ještě kousek kódu vedle. Alternativně se něco takového dá spočítat už ve view, což se ale bude motat s výkonným kódem, který už tak je nejspíš na spodní hranici přehlednosti… (A nebo se smíříme s tím, že ten řádek nejde tak jednoduše přečíst. Ostatně, nebyl by první – výsledkovky v TeXu trpí stejným problémem…) Možná se mi podaří tam dát aspoň pár málo odřádkování, ale těžko odhadnout…

Co takhle?

Co takhle?

👎 Pořád je to na jednom řádku a teď tam navíc překážejí komentáře.

👎 Pořád je to na jednom řádku a teď tam navíc překážejí komentáře.

Tak to nechávám na tobě, protože na tohle máš očividně daleko vyšší nároky (mě to totiž ty komentáře naopak hezky oddělují…).

Tak to nechávám na tobě, protože na tohle máš očividně daleko vyšší nároky (mě to totiž ty komentáře naopak hezky oddělují…).

Nebo mě ještě napadlo tohle…

Nebo mě ještě napadlo tohle…

Případně ještě něco jako

    {% for r in object.resitele.all %}{{ r }} (<a
        href="mailto:{{ r.osoba.email }}?subject={{ "Oprava řešení M&M " | urlencode }}{{ object.problem.all.0.hlavni_problem | urlencode }}">{{ r.osoba.email }}</a
    >){% if forloop.revcounter0 != 0 %}, {% endif %}{% endfor %}

čímž celý e-mail bude na jednom řádku, ale zase je to dost nahouby z pohledu tagů

Případně ještě něco jako ``` {% for r in object.resitele.all %}{{ r }} (<a href="mailto:{{ r.osoba.email }}?subject={{ "Oprava řešení M&M " | urlencode }}{{ object.problem.all.0.hlavni_problem | urlencode }}">{{ r.osoba.email }}</a >){% if forloop.revcounter0 != 0 %}, {% endif %}{% endfor %} ``` čímž celý e-mail bude na jednom řádku, ale zase je to dost nahouby z pohledu tagů

Případně

    {% for r in object.resitele.all %}{{ r }} (<a
      href="mailto:{{ r.osoba.email }}?subject={{ "Oprava řešení M&M " | urlencode }}{{ object.problem.all.0.hlavni_problem | urlencode }}"
      >{{ r.osoba.email }}</a
    >){% if forloop.revcounter0 != 0 %}, {% endif %}{% endfor %}

nebo

    {% for r in object.resitele.all %}{{ r }} (<a
      href="mailto:{{ r.osoba.email }}?subject={{ "Oprava řešení M&M " | urlencode }}{{ object.problem.all.0.hlavni_problem | urlencode }}"
      >{{ r.osoba.email }}</a>){% if forloop.revcounter0 != 0 %}, {% endif %}{% endfor %}
Případně ``` {% for r in object.resitele.all %}{{ r }} (<a href="mailto:{{ r.osoba.email }}?subject={{ "Oprava řešení M&M " | urlencode }}{{ object.problem.all.0.hlavni_problem | urlencode }}" >{{ r.osoba.email }}</a >){% if forloop.revcounter0 != 0 %}, {% endif %}{% endfor %} ``` nebo ``` {% for r in object.resitele.all %}{{ r }} (<a href="mailto:{{ r.osoba.email }}?subject={{ "Oprava řešení M&M " | urlencode }}{{ object.problem.all.0.hlavni_problem | urlencode }}" >{{ r.osoba.email }}</a>){% if forloop.revcounter0 != 0 %}, {% endif %}{% endfor %} ```
<p>Řešitelé:
{% for r in object.resitele.all %}{{ r }} (<a href="mailto:{{ r.osoba.email }}?subject={{ "Oprava řešení M&M " | urlencode }}{{ object.problem.all.0.hlavni_problem | urlencode }}">{{ r.osoba.email }}</a>){% if forloop.revcounter0 != 0 %}, {% endif %}{% endfor %}
</p>
{% else %}
<p>Řešitelé: {{ object.resitele.all | join:", " }}</p>
{% endif %}
zelvuska marked this conversation as resolved
Review

Pardon, snažil jsem se nějak dostat sem do konverzace aktuální stav těch 5 řádků, ale asi to neumím.

Pardon, snažil jsem se nějak dostat sem do konverzace aktuální stav těch 5 řádků, ale asi to neumím.
{# https://docs.djangoproject.com/en/3.1/ref/models/instances/#django.db.models.Model.get_FOO_display #}
<p>Forma: {{ object.get_forma_display }}</p>
@ -82,13 +35,13 @@ $(document).ready(function(){
<td><a href="{{ priloha.soubor.url }}" download>{{ priloha.split | last }}</a></td>
<td>{{ priloha.res_poznamka }}</td>
<td>{{ priloha.vytvoreno }}</td></tr>
{# TODO: Orgo-poznámka, ideálně jako formulář #}
{% endfor %}
</table>
{% else %}
<p>Žádné přílohy</p>
{% endif %}
{% if edit %}
<form method=post onsubmit="return zkontroluj_hodnoceni();">
{# Poznámka #}
<h3>Neveřejná poznámka:</h3>
@ -116,7 +69,7 @@ $(document).ready(function(){
</table>
<a href="#" title="Přidat hodnocení"> <img src="{% static "odevzdavatko/plus.png" %}" id="pridat_hodnoceni" alt="Přidat hodnocení"></a> </br>
<a href="#" title="Přidat hodnocení"> <img src="{% static "odevzdavatko/plus.png" %}" id="pridat_hodnoceni" alt="Přidat hodnocení"></a> <br/>
<input type=submit value="Uložit"></form>
<table id="empty_form" style="display: none;">
@ -129,28 +82,19 @@ $(document).ready(function(){
</tr>
</table>
{% else %}
<h3>Hodnocení:</h3>
<table class="dosla_reseni">
<tr><th>Problém</th><th>Body</th><th>Zpětná vazba od opravovatele</th></tr>
{% for h in hodnoceni %}
<tr class="hodnoceni">
<td>{{ h.problem }}</td>
ledoian marked this conversation as resolved
Review

Tohle nečtu, to se uvidí na testwebu, jak to vypadá… Čitelné to nejspíš je, tak asi dobrý…

Tohle nečtu, to se uvidí na testwebu, jak to vypadá… Čitelné to nejspíš je, tak asi dobrý…
Review

A jak to vypadá?

A jak to vypadá?
Review

LGTM

LGTM
<td>{{ h.body }}</td>
<td>{{ h.feedback }}</td>
</tr>
{% endfor %}
</table>
{% endif %}
<script type="text/javascript">
function zkontroluj_hodnoceni() {
const pocet = $('.hodnoceni').length;
if (pocet === 1) { {# vydí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;
}
</script>
{% endblock %}

View file

@ -1,51 +0,0 @@
{% extends "base.html" %}
{% load static %}
{% load deadliny %}
{% block content %}
<p>Řešené problémy: {{ object.problem.all | join:", " }}</p>
<p>Řešitelé: {% for r in object.resitele.all %} {{ r }}
{% if forloop.revcounter0 != 0 %}, {% endif %} {% endfor %}</p>
{# https://docs.djangoproject.com/en/3.1/ref/models/instances/#django.db.models.Model.get_FOO_display #}
<p>Forma: {{ object.get_forma_display }}</p>
<p>Doručeno {{ object.cas_doruceni }}, deadline: {{object.deadline_reseni | deadline_html }}</p>
{# Soubory: #}
<h3>Přílohy:</h3>
{% if object.prilohy.all %}
<table class="dosla_reseni">
<tr><th>Soubor</th><th>Řešitelova poznámka</th><th>Datum</th></tr>
{% for priloha in object.prilohy.all %}
<tr>
<td><a href="{{ priloha.soubor.url }}" download>{{ priloha.split | last }}</a></td>
<td>{{ priloha.res_poznamka }}</td>
<td>{{ priloha.vytvoreno }}</td></tr>
{# TODO: Orgo-poznámka, ideálně jako formulář #}
{% endfor %}
</table>
{% else %}
<p>Žádné přílohy</p>
{% endif %}
{#<h3>Poznámka:</h3>#}
{#<p>{{ poznamka }}</p>#}
{# Hodnocení: #}
<h3>Hodnocení:</h3>
<table id="form_set" class="dosla_reseni">
<tr><th>Problém</th><th>Body</th><th>Zpětná vazba od opravovatele</th>{# <th>Deadline pro body</th> #}</tr>
{% for h in hodnoceni %}
<tr class="hodnoceni">
<td>{{ h.problem }}</td>
<td>{{ h.body }}</td>
<td>{{ h.feedback }}</td>
{# <td>{{ h.deadline_body }}</td>#}
</tr>
{% endfor %}
</table>
{% endblock %}

View file

@ -13,6 +13,8 @@
<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>
zelvuska marked this conversation as resolved Outdated

Nejsem si jistý, jestli není lepší spíš používat <emph> a <strong>. A možná bych si odpustil ty vykřičníky, mohly by působit trochu děsivě…

(To, jak moc velká zeď textu v odevzdávátku vzniká, posoudím za chvíli vizuálně…)

Nejsem si jistý, jestli není lepší spíš používat `<emph>` a `<strong>`. A možná bych si odpustil ty vykřičníky, mohly by působit trochu děsivě… (To, jak moc velká zeď textu v odevzdávátku vzniká, posoudím za chvíli vizuálně…)
<form enctype="multipart/form-data" action="{% url 'seminar_nahraj_reseni' %}" method="post" onsubmit="return zkontroluj_prilohy();">
{% csrf_token %}
<table class='form' id="reseni">

View file

@ -26,9 +26,9 @@ urlpatterns = [
path('org/reseni/', org_required(views.TabulkaOdevzdanychReseniView.as_view()), name='odevzdavatko_tabulka'),
path('org/reseni/rocnik/<int:rocnik>/', org_required(views.TabulkaOdevzdanychReseniView.as_view()), name='odevzdavatko_tabulka'),
path('org/reseni/<int:problem>/<int:resitel>/', org_required(views.ReseniProblemuView.as_view()), name='odevzdavatko_reseni_resitele_k_problemu'),
path('org/reseni/<int:pk>', org_required(viewMethodSwitch(get=views.DetailReseniView.as_view(), post=views.hodnoceniReseniView)), name='odevzdavatko_detail_reseni'),
path('org/reseni/<int:pk>', 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/<int:pk>', resitel_or_org_required(views.ResitelReseniView.as_view()), name='odevzdavatko_resitel_reseni'),
path('resitel/reseni/<int:pk>', resitel_or_org_required(views.DetailReseniView.as_view()), name='odevzdavatko_resitel_reseni'),
]

View file

@ -211,6 +211,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'
@ -227,18 +228,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()
)
ctx['poznamka_form'] = f.PoznamkaReseniForm(instance=self.reseni)
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
zelvuska marked this conversation as resolved Outdated

Tohle nemá být docstring, ale komentář.

Tohle nemá být docstring, ale komentář.
if not self.request.user.je_org:
zelvuska marked this conversation as resolved Outdated

„Better safe than sorry“? Je to ±jeden řádek tak jak tak, a aspoň stačí vidět jen jeden soubor…

„Better safe than sorry“? Je to ±jeden řádek tak jak tak, a aspoň stačí vidět jen jeden soubor…
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
ledoian marked this conversation as resolved Outdated

Tohle zmizelo proč? (Na první pohled to vypadá jako velmi podezřelá změna, ale ještě jsem nedočetl zbytek kódu…)

Tohle zmizelo proč? (Na první pohled to vypadá jako velmi podezřelá změna, ale ještě jsem nedočetl zbytek kódu…)

Dočetl jsem a tuhle změnu pořád nechápu.

Dočetl jsem a tuhle změnu pořád nechápu.

Protože se to tam vůbec nepoužívalo?

Protože se to tam vůbec nepoužívalo?

Já vůbec nechápu proč to tam bylo…

Já vůbec nechápu proč to tam bylo…

LOL aha. Já čekal, že se ten template vykresluje, ale on je tam redirect, tak je to jedno. Budiž :-)

LOL aha. Já čekal, že se ten template vykresluje, ale on je tam redirect, tak je to jedno. Budiž :-)
success_url = reverse('odevzdavatko_detail_reseni', kwargs={'pk': pk})
# FIXME: Použit initial i tady a nebastlit hodnocení tak nízkoúrovňově
@ -270,33 +297,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):
@ -408,6 +408,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()

View file

@ -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):
zelvuska marked this conversation as resolved Outdated

Tohle velmi zní, jako že ta přezdívka prostě nejde editovat vůbec – pokud už v databázi jsem, tak tohle matchne.

Tohle velmi zní, jako že ta přezdívka prostě nejde editovat vůbec – pokud už v databázi jsem, tak tohle matchne.

?

?

Pokud si Alice nastaví přezdívku „Axolotl“, tak ji má v databázi. Dotaz Restitel.objects.get(prezdivka=Axolotl) tedy tuto přezdívku najde a celý ResitelEditForm kvůli tomu nepůjde Alici odeslat, přestože je to její přezdívka.

Pokud si Alice nastaví přezdívku „Axolotl“, tak ji má v databázi. Dotaz `Restitel.objects.get(prezdivka=Axolotl)` tedy tuto přezdívku najde a celý `ResitelEditForm` kvůli tomu nepůjde Alici odeslat, přestože je to její přezdívka.

Promiň, tvůj komentář jsem pochopil jinak. Samozřejmě tohle je špatně… Dík

Promiň, tvůj komentář jsem pochopil jinak. Samozřejmě tohle je špatně… Dík

Ha, já nevím, podle čeho určit, který řešitel to edituje…

Ha, já nevím, podle čeho určit, který řešitel to edituje…

A nestačí to prostě uložit a zjistit, jestli to vyšlo? (Dost možná nestačí, nemám nastudovanou interakci formů a modelů, ale kdyby to stačilo, tak je to zdaleka nejjednodušší…)

A nestačí to prostě uložit a zjistit, jestli to vyšlo? (Dost možná nestačí, nemám nastudovanou interakci formů a modelů, ale kdyby to stačilo, tak je to zdaleka nejjednodušší…)

A poznáš, co selhalo? A přijde mi, že tohle by mělo být ještě v clean_…

A poznáš, co selhalo? A přijde mi, že tohle by mělo být ještě v clean_…

Vyřešeno. Ten objekt má při použití nastavený username…

Vyřešeno. Ten objekt má při použití nastavený username…
err_logger = logging.getLogger('seminar.prihlaska.problem')
email = self.cleaned_data.get('email')

View file

@ -44,6 +44,7 @@
</h4>
<table class="form">
{% 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 %}

View file

@ -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&amp;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.
</p>
<p class="gdpr">
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.
zelvuska marked this conversation as resolved
Review

👍

👍
</p>
<p class="gdpr">
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.

View file

@ -46,6 +46,7 @@
</h4>
<table class="form">
{% 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 %}

View file

@ -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']

View file

@ -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'),
),
]

View file

@ -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)