Browse Source

Move odevzdavatko do aplikace odevzdavatko

export_seznamu_prednasek
Jonas Havelka 3 years ago
parent
commit
ef68d3fb75
  1. 2
      mamweb/settings_common.py
  2. 3
      mamweb/urls.py
  3. 11
      odevzdavatko/__init__.py
  4. 28
      odevzdavatko/admin.py
  5. 5
      odevzdavatko/apps.py
  6. 218
      odevzdavatko/forms.py
  7. 0
      odevzdavatko/migrations/__init__.py
  8. 0
      odevzdavatko/static/odevzdavatko/cross.png
  9. 0
      odevzdavatko/static/odevzdavatko/dynamic_formsets.js
  10. 0
      odevzdavatko/static/odevzdavatko/plus.png
  11. 8
      odevzdavatko/templates/odevzdavatko/detail.html
  12. 2
      odevzdavatko/templates/odevzdavatko/nahraj_reseni.html
  13. 1
      odevzdavatko/templates/odevzdavatko/posli_reseni.html
  14. 0
      odevzdavatko/templates/odevzdavatko/prehled_reseni.html
  15. 0
      odevzdavatko/templates/odevzdavatko/seznam.html
  16. 2
      odevzdavatko/templates/odevzdavatko/tabulka.html
  17. 18
      odevzdavatko/urls.py
  18. 118
      odevzdavatko/views.py
  19. 21
      seminar/admin.py
  20. 201
      seminar/forms.py
  21. 2
      seminar/models/__init__.py
  22. 176
      seminar/models/models_all.py
  23. 188
      seminar/models/odevzdavatko.py
  24. 14
      seminar/urls.py
  25. 1
      seminar/views/__init__.py
  26. 114
      seminar/views/views_all.py

2
mamweb/settings_common.py

@ -138,7 +138,7 @@ INSTALLED_APPS = (
'various.autentizace',
'api',
'aesop',
'odevzdavatko',
# Admin upravy:
# 'material',

3
mamweb/urls.py

@ -16,6 +16,9 @@ urlpatterns = [
# Seminarova aplikace (ma vlastni podadresare)
path('', include('seminar.urls')),
# Odevzdavatko (ma vlastni podadresare)
path('', include('odevzdavatko.urls')),
# Korekturovaci aplikace (ma vlastni podadresare)
path('', include('korektury.urls')),

11
odevzdavatko/__init__.py

@ -0,0 +1,11 @@
"""
Obsahuje vše, co se týká odevzdávání (+ nahrávání) a opravování řešení řešitelů.
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.
TODO: Místo vložit řešení v nahrávání a posílání řešení dát něco jiného?
"""

28
odevzdavatko/admin.py

@ -0,0 +1,28 @@
from django.contrib import admin
from django_reverse_admin import ReverseModelAdmin
import seminar.models as m
class PrilohaReseniInline(admin.TabularInline):
model = m.PrilohaReseni
extra = 1
class Reseni_ResiteleInline(admin.TabularInline):
model = m.Reseni_Resitele
@admin.register(m.Reseni)
class ReseniAdmin(ReverseModelAdmin):
base_model = m.Reseni
inline_type = 'tabular'
# inline_reverse = ['text_cely','resitele'] TODO vrátit zpět a zrychlit dotaz
inline_reverse = ['resitele']
exclude = ['text_zkraceny', 'text_zkraceny_set']
inlines = [PrilohaReseniInline]
# FAIL in template
# inlines = [PrilohaReseniInline,Reseni_ResiteleInline]
admin.site.register(m.PrilohaReseni)
admin.site.register(m.Hodnoceni)

5
odevzdavatko/apps.py

@ -0,0 +1,5 @@
from django.apps import AppConfig
class OdevzdavatkoConfig(AppConfig):
name = 'odevzdavatko'

218
odevzdavatko/forms.py

@ -0,0 +1,218 @@
from django import forms
from dal import autocomplete
from django.forms import formset_factory
from django.forms.models import inlineformset_factory
from seminar.models import Resitel
import seminar.models as m
import logging
# pro přidání políčka do formuláře je potřeba
# - mít v modelu tu položku, kterou chci upravovat
# - přidat do views (prihlaskaView, resitelEditView)
# - přidat do forms
# - includovat do html
class DateInput(forms.DateInput):
# aby se datum dalo vybírat z kalendáře
input_type = 'date'
class PosliReseniForm(forms.Form):
#FIXME jen podproblémy daného problému
problem = forms.ModelChoiceField(label='Problém',queryset=m.Problem.objects.all())
# to_field_name
#problem = models.ManyToManyField(Problem, verbose_name='problém', help_text='Problém',
# through='Hodnoceni')
# FIXME pridat vice resitelu
resitel = forms.ModelChoiceField(label="Řešitel",
queryset=Resitel.objects.all(),
widget=autocomplete.ModelSelect2(
url='autocomplete_resitel',
attrs = {'data-placeholder--id': '-1',
'data-placeholder--text' : '---',
'data-allow-clear': 'true'})
)
#resitele = models.ManyToManyField(Resitel, verbose_name='autoři řešení',
# help_text='Seznam autorů řešení', through='Reseni_Resitele')
cas_doruceni = forms.DateField(widget=DateInput(),label="Čas doručení")
#cas_doruceni = models.DateTimeField('čas_doručení', default=timezone.now, blank=True)
forma = forms.ChoiceField(label="Forma řešení",choices = m.Reseni.FORMA_CHOICES)
#forma = models.CharField('forma řešení', max_length=16, choices=FORMA_CHOICES, blank=False,
# default=FORMA_EMAIL)
poznamka = forms.CharField(label='Neveřejná poznámka', required=False)
#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:
model = m.Reseni
fields = ('problem',)
help_texts = {'problem':''} # Nezobrazovat help text ve formuláři
widgets = {'problem':
autocomplete.ModelSelect2Multiple(
url='autocomplete_problem_odevzdatelny',
attrs = {'data-placeholder--id': '-1',
'data-placeholder--text' : '---',
'data-allow-clear': 'true'},
)
}
ReseniSPrilohamiFormSet = inlineformset_factory(m.Reseni,m.PrilohaReseni,
form = NahrajReseniForm,
fields = ('soubor','res_poznamka'),
widgets = {'res_poznamka':forms.TextInput()},
extra = 1,
can_delete = False,
)
class JednoHodnoceniForm(forms.ModelForm):
class Meta:
model = m.Hodnoceni
fields = ('problem', 'body', 'cislo_body')
widgets = {
'problem': autocomplete.ModelSelect2(
url='autocomplete_problem_odevzdatelny', # FIXME: Dovolit i starší?
)
}
OhodnoceniReseniFormSet = formset_factory(JednoHodnoceniForm,
extra = 0,
)
class PoznamkaReseniForm(forms.ModelForm):
class Meta:
model = m.Reseni
fields = ('poznamka',)
# FIXME: Ideálně by mělo být součástí třídy níž, ale neumím to udělat
DATE_FORMAT = '%Y-%m-%d'
class OdevzdavatkoTabulkaFiltrForm(forms.Form):
"""Form pro filtrování přehledové odevzdávátkové tabulky
Inspirováno https://kam.mff.cuni.cz/mffzoom/"""
# Věci definované níž se importují i ve views pro odevzdávátko (Inspirováno https://docs.djangoproject.com/en/3.1/ref/models/fields/#field-choices)
RESITELE_RELEVANTNI = 'relevantni'
RESITELE_NEODMATUROVAVSI = 'neodmaturovavsi'
RESITELE_CHOICES = [
(RESITELE_RELEVANTNI, 'Relevantní řešitelé'), # I.e. nezobrazovat prázdné řádky tabulky
(RESITELE_NEODMATUROVAVSI, 'Všichni bez maturity'),
# Možná: všechny vč. historických?
]
PROBLEMY_MOJE = 'moje'
PROBLEMY_LETOSNI = 'letosni'
PROBLEMY_CHOICES = [
(PROBLEMY_MOJE, 'Moje problémy'), # Letošní problémy, které mají v sobě nebo v nadproblémech přiřazeného daného orga
(PROBLEMY_LETOSNI, 'Všechny letošní'),
# TODO: *hlavní problémy, možná všechny...
# XXX: Chtělo by to i "aktuálně zadané...
]
# TODO: Typy problémů (problémy, úlohy, ostatní, všechny)? Jen některá řešení (obodovaná/neobodovaná, víc řešitelů, ...)?
@classmethod
def gen_terminy(cls, rocnik=None):
import datetime
from time import strftime
from django.db.utils import OperationalError
try:
aktualni_rocnik = m.Nastaveni.get_solo().aktualni_rocnik
aktualni_cislo = m.Nastaveni.get_solo().aktualni_cislo
except OperationalError:
# django.db.utils.OperationalError: no such table: seminar_nastaveni
# Nemáme databázi, takže to selhalo. Pro jistotu vrátíme aspoň dvě možnosti, ať to nepadá dál
logger = logging.getLogger(__name__)
logger.error("Rozbitá databáze (před počátečními migracemi?)")
return [('broken', 'Je to rozbitý'), ('fubar', 'Nefunguje to')]
# FIXME: Tohle je hnusný monkey patch, mělo by to být nějak zahrnuto výš.
if rocnik is not None:
aktualni_rocnik = rocnik
aktualni_cislo = m.Cislo.objects.filter(rocnik=rocnik).order_by('poradi').last()
result = []
for cislo in m.Cislo.objects.filter(
rocnik=aktualni_rocnik,
poradi__lte=aktualni_cislo.poradi,
).reverse(): # Standardně se řadí od nejnovějšího čísla
# Předem je mi líto kohokoliv, kdo tyhle řádky bude číst...
if cislo.datum_vydani is not None and cislo.datum_vydani <= datetime.date.today():
result.append((
strftime(DATE_FORMAT, cislo.datum_vydani.timetuple()),
f"Vydání {cislo.poradi}. čísla"))
if cislo.datum_preddeadline is not None and cislo.datum_preddeadline <= datetime.date.today():
result.append((
strftime(DATE_FORMAT, cislo.datum_preddeadline.timetuple()),
f"Předdeadline {cislo.poradi}. čísla"))
if cislo.datum_deadline_soustredeni is not None and cislo.datum_deadline_soustredeni <= datetime.date.today():
result.append((
strftime(DATE_FORMAT, cislo.datum_deadline_soustredeni.timetuple()),
f"Sous. deadline {cislo.poradi}. čísla"))
if cislo.datum_deadline is not None and cislo.datum_deadline <= datetime.date.today():
result.append((
strftime(DATE_FORMAT, cislo.datum_deadline.timetuple()),
f"Finální deadline {cislo.poradi}. čísla"))
result.append((
strftime(DATE_FORMAT, datetime.date.today().timetuple()), f"Dnes"))
return result
@classmethod
def gen_initial(cls, rocnik=None):
terminy = cls.gen_terminy(rocnik)
initial = {
'resitele': cls.RESITELE_RELEVANTNI,
'problemy': cls.PROBLEMY_MOJE,
# Pokud chceme neaktuální ročník, tak nás nejspíš zajímají všechna řešení…
'reseni_od': terminy[-2] if rocnik is None else terminy[0],
'reseni_do': terminy[-1],
'neobodovane': False,
}
return initial
def __init__(self, *args, rocnik=None, **kwargs):
if 'initial' not in kwargs:
super().__init__(initial=self.gen_initial(rocnik), *args, **kwargs)
else:
super().__init__(*args, **kwargs)
# choices jako parametr Select widgetu neumí brát callable, jen iterable, takže si pro jednoduchost můžu rovnou uložit výsledek sem...
# A "sem" znamená do libovolné metody, protože jinak se jedná o kód, který django spustí při inicializaci a protože potřebujeme databázi, tak by spadnul při vyrábění testdat...
self.terminy = self.gen_terminy(rocnik)
self.fields['reseni_od'].widget = forms.Select(choices=self.gen_terminy(rocnik))
# Pokud chceme neaktuální ročník, tak nás nejspíš zajímají všechna řešení…
self.fields['reseni_od'].initial = self.terminy[-2] if rocnik is None else self.terminy[0]
self.fields['reseni_do'].widget = forms.Select(choices=self.gen_terminy(rocnik))
self.fields['reseni_do'].initial = self.terminy[-1]
# NOTE: Initial definuji pro jednotlivé fieldy, aby to bylo tady a nebylo potřeba to řešit ve views...
resitele = forms.ChoiceField(choices=RESITELE_CHOICES)
problemy = forms.ChoiceField(choices=PROBLEMY_CHOICES)
reseni_od = forms.DateField(input_formats=[DATE_FORMAT])
reseni_do = forms.DateField(input_formats=[DATE_FORMAT])
neobodovane = forms.BooleanField(required=False)

0
odevzdavatko/migrations/__init__.py

0
seminar/static/seminar/cross.png → odevzdavatko/static/odevzdavatko/cross.png

Before

Width:  |  Height:  |  Size: 717 B

After

Width:  |  Height:  |  Size: 717 B

0
seminar/static/seminar/dynamic_formsets.js → odevzdavatko/static/odevzdavatko/dynamic_formsets.js

0
seminar/static/seminar/plus.png → odevzdavatko/static/odevzdavatko/plus.png

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

8
seminar/templates/seminar/odevzdavatko/detail.html → odevzdavatko/templates/odevzdavatko/detail.html

@ -4,7 +4,7 @@
{% block content %}
{# FIXME: Necopypastovat! Tohle je zkopírované ze static/seminar/dynamic_formsets.js #}
{# 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) {
@ -104,14 +104,14 @@ $(document).ready(function(){
<td>{{ subform.problem }}</td>
<td>{{ subform.body }}</td>
<td>{{ subform.cislo_body }}</td>
<td><a href="#" class="smazat_hodnoceni" id="id_{{subform.prefix}}-jsremove"><img src="{% static "seminar/cross.png" %}" alt="Smazat"></a></td>
<td><a href="#" class="smazat_hodnoceni" id="id_{{subform.prefix}}-jsremove"><img src="{% static "odevzdavatko/cross.png" %}" alt="Smazat"></a></td>
</tr>
</tbody>
{% endfor %}
</table>
<a href="#"> <img src="{% static "seminar/plus.png" %}" id="pridat_hodnoceni" alt="Přidat hodnocení"></a> </br>
<a href="#"> <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;">
@ -119,7 +119,7 @@ $(document).ready(function(){
<td>{{ form.empty_form.problem }}</td>
<td>{{ form.empty_form.body }}</td>
<td>{{ form.empty_form.cislo_body }}</td>
<td><a href="#" class="smazat_hodnoceni" id="id_{{subform.prefix}}-jsremove"><img src="{% static "seminar/cross.png" %}" alt="Smazat"></a></td>
<td><a href="#" class="smazat_hodnoceni" id="id_{{subform.prefix}}-jsremove"><img src="{% static "odevzdavatko/cross.png" %}" alt="Smazat"></a></td>
</tr>
</table>

2
seminar/templates/seminar/profil/nahraj_reseni.html → odevzdavatko/templates/odevzdavatko/nahraj_reseni.html

@ -1,7 +1,7 @@
{% extends "base.html" %}
{% load staticfiles %}
{% block script %}
<script src="{% static 'seminar/dynamic_formsets.js' %}"></script>
<script src="{% static 'odevzdavatko/dynamic_formsets.js' %}"></script>
{% endblock %}
{% block content %}

1
seminar/templates/seminar/org/vloz_reseni.html → odevzdavatko/templates/odevzdavatko/posli_reseni.html

@ -3,7 +3,6 @@
{% block script %}
<!--script type="text/javascript" src="{% static 'admin/js/vendor/jquery/jquery.js' %}"></script!-->
{{form.media}}
<script src="{% static 'seminar/prihlaska.js' %}"></script>
{% endblock %}
{% block content %}

0
seminar/templates/seminar/odevzdavatko/resitel_prehled.html → odevzdavatko/templates/odevzdavatko/prehled_reseni.html

0
seminar/templates/seminar/odevzdavatko/seznam.html → odevzdavatko/templates/odevzdavatko/seznam.html

2
seminar/templates/seminar/odevzdavatko/tabulka.html → odevzdavatko/templates/odevzdavatko/tabulka.html

@ -4,7 +4,7 @@
{% block content %}
<form method=get action=.>
<form method=get action=../odevzdavatko>
{{ filtr.resitele }}
{{ filtr.problemy }}
Od: {{ filtr.reseni_od }}

18
odevzdavatko/urls.py

@ -0,0 +1,18 @@
from django.urls import path
from seminar.utils import org_required, resitel_required, viewMethodSwitch, \
resitel_or_org_required
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('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'),
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/all', org_required(views.SeznamReseniView.as_view())),
path('org/reseni/akt', org_required(views.SeznamAktualnichReseniView.as_view())),
]

118
seminar/views/odevzdavatko.py → odevzdavatko/views.py

@ -1,8 +1,10 @@
from django.views.generic import ListView, DetailView, FormView
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.mail import send_mail
from django.utils import timezone
from django.views.generic import ListView, DetailView, FormView, CreateView
from django.views.generic.list import MultipleObjectTemplateResponseMixin,MultipleObjectMixin
from django.views.generic.base import View
from django.views.generic.detail import SingleObjectMixin
from django.shortcuts import redirect, get_object_or_404
from django.shortcuts import redirect, get_object_or_404, render
from django.urls import reverse
from django.db import transaction
from django.db.models import Q
@ -13,9 +15,10 @@ from itertools import groupby
import logging
import seminar.models as m
import seminar.forms as f
from seminar.forms import OdevzdavatkoTabulkaFiltrForm as FiltrForm
from seminar.utils import aktivniResitele, resi_v_rocniku, deadline
from . import forms as f
from .forms import OdevzdavatkoTabulkaFiltrForm as FiltrForm
from seminar.utils import resi_v_rocniku, deadline
from seminar.views import formularOKView
logger = logging.getLogger(__name__)
@ -41,7 +44,7 @@ class SouhrnReseni:
class TabulkaOdevzdanychReseniView(ListView):
template_name = 'seminar/odevzdavatko/tabulka.html'
template_name = 'odevzdavatko/tabulka.html'
model = m.Hodnoceni
def inicializuj_osy_tabulky(self):
@ -161,7 +164,7 @@ class TabulkaOdevzdanychReseniView(ListView):
# Velmi silně inspirováno zdrojáky, FIXME: Nedá se to udělat smysluplněji?
class ReseniProblemuView(MultipleObjectTemplateResponseMixin, MultipleObjectMixin, View):
model = m.Reseni
template_name = 'seminar/odevzdavatko/seznam.html'
template_name = 'odevzdavatko/seznam.html'
def get_queryset(self):
qs = super().get_queryset()
@ -203,7 +206,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):
model = m.Reseni
template_name = 'seminar/odevzdavatko/detail.html'
template_name = 'odevzdavatko/detail.html'
def aktualni_hodnoceni(self):
self.reseni = get_object_or_404(m.Reseni, id=self.kwargs['pk'])
@ -227,7 +230,7 @@ class DetailReseniView(DetailView):
def hodnoceniReseniView(request, pk, *args, **kwargs):
reseni = get_object_or_404(m.Reseni, pk=pk)
template_name = 'seminar/odevzdavatko/detail.html'
template_name = 'odevzdavatko/detail.html'
form_class = f.OhodnoceniReseniFormSet
success_url = reverse('odevzdavatko_detail_reseni', kwargs={'pk': pk})
@ -266,7 +269,7 @@ def hodnoceniReseniView(request, pk, *args, **kwargs):
class PrehledOdevzdanychReseni(ListView):
model = m.Hodnoceni
template_name = 'seminar/odevzdavatko/resitel_prehled.html'
template_name = 'odevzdavatko/prehled_reseni.html'
def get_queryset(self):
if not self.request.user.is_authenticated:
@ -292,7 +295,7 @@ class PrehledOdevzdanychReseni(ListView):
class SeznamReseniView(ListView):
model = m.Reseni
template_name = 'seminar/odevzdavatko/seznam.html'
template_name = 'odevzdavatko/seznam.html'
class SeznamAktualnichReseniView(SeznamReseniView):
def get_queryset(self):
@ -301,3 +304,94 @@ class SeznamAktualnichReseniView(SeznamReseniView):
resitele = resi_v_rocniku(akt_rocnik)
qs = qs.filter(resitele__in=resitele) # FIXME: Najde řešení i ze starých ročníků, která odevzdal alespoň jeden aktuální řešitel
return qs
class PosliReseniView(LoginRequiredMixin, FormView):
template_name = 'odevzdavatko/posli_reseni.html'
form_class = f.PosliReseniForm
def form_valid(self, form):
data = form.cleaned_data
nove_reseni = m.Reseni.objects.create(
cas_doruceni=data['cas_doruceni'],
forma=data['forma'],
poznamka=data['poznamka'],
)
nove_reseni.resitele.add(data['resitel'])
nove_reseni.problem.add(data['problem'])
nove_reseni.save()
# Chtěl jsem, aby bylo vidět, že se to uložilo, tak přesměrovávám na profil.
return redirect(reverse('profil'))
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í 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)
resitel = osoba.resitel
if resitel.rok_maturity <= m.Nastaveni.get_solo().aktualni_rocnik.prvni_rok:
return render(request, 'universal.html', {
'title': 'Nelze odevzdat',
'error': 'Zdá se, že jsi již odmaturoval/a, a tedy nemůžeš odevzdat do našeho semináře řešení.',
'text': 'Pokud se ti zdá, že to je chyba, napiš nám prosím e-mail. Díky.',
})
return super().get(request, *args, **kwargs)
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()
return data
# FIXME prepsat tak, aby form_valid se volalo jen tehdy, kdyz je form i formset validni
# Inspirace: https://stackoverflow.com/questions/41599809/using-a-django-filefield-in-an-inline-formset
def form_valid(self,form):
context = self.get_context_data()
prilohy = context['prilohy']
if not prilohy.is_valid():
return super().form_invalid(form)
with transaction.atomic():
self.object = form.save()
self.object.resitele.add(m.Resitel.objects.get(osoba__user = self.request.user))
self.object.cas_doruceni = timezone.now()
self.object.forma = m.Reseni.FORMA_UPLOAD
self.object.save()
prilohy.instance = self.object
prilohy.save()
# Pošleme mail opravovatelům a garantovi
# FIXME: Nechat spočítat databázi? Je to pár dotazů (pravděpodobně), takže to za to možná nestojí
prijemci = set()
problemy = []
for prob in form.cleaned_data['problem']:
prijemci.update(prob.opravovatele.all())
if prob.garant is not None:
prijemci.add(prob.garant)
problemy.append(prob)
# FIXME: Možná poslat mail i relevantním orgům nadproblémů?
if len(prijemci) < 1:
logger.warning(f"Pozor, neposílám e-mail nikomu. Problémy: {problemy}")
# FIXME: Víc informativní obsah mailů, možná vč. příloh?
prijemci = map(lambda it: it.osoba.email, prijemci)
resitel = m.Osoba.objects.get(user = self.request.user)
seznam = "problému " + str(problemy[0]) if len(problemy) == 1 else 'následujícím problémům:\n' + ', \n'.join(map(str, problemy))
seznam_do_subjectu = "problému " + str(problemy[0]) + ("" if len(problemy) == 1 else f" (a dalším { len(problemy) - 1 })")
send_mail(
subject="Nové řešení k " + seznam_do_subjectu,
message=f"Řešitel{ '' if resitel.pohlavi_muz else 'ka' } { resitel } právě nahrál{'' if resitel.pohlavi_muz else 'a' } nové řešení k { seznam }.\n\nHurá do opravování: { self.object.absolute_url() }",
from_email="submitovatko@mam.mff.cuni.cz", # FIXME: Chceme to mít radši tady, nebo v nastavení?
recipient_list=list(prijemci),
)
return formularOKView(self.request, text='Řešení úspěšně odevzdáno')

21
seminar/admin.py

@ -230,27 +230,6 @@ class SoustredeniAdmin(admin.ModelAdmin):
inlines = [SoustredeniUcastniciInline, SoustredeniOrganizatoriInline]
class PrilohaReseniInline(admin.TabularInline):
model = m.PrilohaReseni
extra = 1
admin.site.register(m.PrilohaReseni)
class Reseni_ResiteleInline(admin.TabularInline):
model = m.Reseni_Resitele
@admin.register(m.Reseni)
class ReseniAdmin(ReverseModelAdmin):
base_model = m.Reseni
inline_type = 'tabular'
# inline_reverse = ['text_cely','resitele'] TODO vrátit zpět a zrychlit dotaz
inline_reverse = ['resitele']
exclude = ['text_zkraceny', 'text_zkraceny_set']
inlines = [PrilohaReseniInline]
# FAIL in template
# inlines = [PrilohaReseniInline,Reseni_ResiteleInline]
admin.site.register(m.Hodnoceni)
admin.site.register(m.Pohadka)
admin.site.register(m.Obrazek)

201
seminar/forms.py

@ -3,10 +3,8 @@ from dal import autocomplete
from django.contrib.auth.forms import PasswordResetForm
from django.core.exceptions import ObjectDoesNotExist
from django.contrib.auth.models import User
from django.forms import formset_factory
from django.forms.models import inlineformset_factory
from .models import Skola, Resitel, Osoba, Problem
from .models import Skola, Resitel, Osoba
import seminar.models as m
from datetime import date
@ -222,205 +220,8 @@ class PoMaturiteProfileEditForm(ProfileEditForm):
label='Rok maturity',
required=True)
class VlozReseniForm(forms.Form):
#FIXME jen podproblémy daného problému
problem = forms.ModelChoiceField(label='Problém',queryset=m.Problem.objects.all())
# to_field_name
#problem = models.ManyToManyField(Problem, verbose_name='problém', help_text='Problém',
# through='Hodnoceni')
# FIXME pridat vice resitelu
resitel = forms.ModelChoiceField(label="Řešitel",
queryset=Resitel.objects.all(),
widget=autocomplete.ModelSelect2(
url='autocomplete_resitel',
attrs = {'data-placeholder--id': '-1',
'data-placeholder--text' : '---',
'data-allow-clear': 'true'})
)
#resitele = models.ManyToManyField(Resitel, verbose_name='autoři řešení',
# help_text='Seznam autorů řešení', through='Reseni_Resitele')
cas_doruceni = forms.DateField(widget=DateInput(),label="Čas doručení")
#cas_doruceni = models.DateTimeField('čas_doručení', default=timezone.now, blank=True)
forma = forms.ChoiceField(label="Forma řešení",choices = m.Reseni.FORMA_CHOICES)
#forma = models.CharField('forma řešení', max_length=16, choices=FORMA_CHOICES, blank=False,
# default=FORMA_EMAIL)
poznamka = forms.CharField(label='Neveřejná poznámka', required=False)
#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:
model = m.Reseni
fields = ('problem',)
help_texts = {'problem':''} # Nezobrazovat help text ve formuláři
widgets = {'problem':
autocomplete.ModelSelect2Multiple(
url='autocomplete_problem_odevzdatelny',
attrs = {'data-placeholder--id': '-1',
'data-placeholder--text' : '---',
'data-allow-clear': 'true'},
)
}
ReseniSPrilohamiFormSet = inlineformset_factory(m.Reseni,m.PrilohaReseni,
form = NahrajReseniForm,
fields = ('soubor','res_poznamka'),
widgets = {'res_poznamka':forms.TextInput()},
extra = 1,
can_delete = False,
)
class NahrajObrazekKTreeNoduForm(forms.ModelForm):
class Meta:
model = m.Obrazek
fields = ('na_web',)
class JednoHodnoceniForm(forms.ModelForm):
class Meta:
model = m.Hodnoceni
fields = ('problem', 'body', 'cislo_body')
widgets = {
'problem': autocomplete.ModelSelect2(
url='autocomplete_problem_odevzdatelny', # FIXME: Dovolit i starší?
)
}
OhodnoceniReseniFormSet = formset_factory(JednoHodnoceniForm,
extra = 0,
)
class PoznamkaReseniForm(forms.ModelForm):
class Meta:
model = m.Reseni
fields = ('poznamka',)
# FIXME: Ideálně by mělo být součástí třídy níž, ale neumím to udělat
DATE_FORMAT = '%Y-%m-%d'
class OdevzdavatkoTabulkaFiltrForm(forms.Form):
"""Form pro filtrování přehledové odevzdávátkové tabulky
Inspirováno https://kam.mff.cuni.cz/mffzoom/"""
# Věci definované níž se importují i ve views pro odevzdávátko (Inspirováno https://docs.djangoproject.com/en/3.1/ref/models/fields/#field-choices)
RESITELE_RELEVANTNI = 'relevantni'
RESITELE_NEODMATUROVAVSI = 'neodmaturovavsi'
RESITELE_CHOICES = [
(RESITELE_RELEVANTNI, 'Relevantní řešitelé'), # I.e. nezobrazovat prázdné řádky tabulky
(RESITELE_NEODMATUROVAVSI, 'Všichni bez maturity'),
# Možná: všechny vč. historických?
]
PROBLEMY_MOJE = 'moje'
PROBLEMY_LETOSNI = 'letosni'
PROBLEMY_CHOICES = [
(PROBLEMY_MOJE, 'Moje problémy'), # Letošní problémy, které mají v sobě nebo v nadproblémech přiřazeného daného orga
(PROBLEMY_LETOSNI, 'Všechny letošní'),
# TODO: *hlavní problémy, možná všechny...
# XXX: Chtělo by to i "aktuálně zadané...
]
# TODO: Typy problémů (problémy, úlohy, ostatní, všechny)? Jen některá řešení (obodovaná/neobodovaná, víc řešitelů, ...)?
@classmethod
def gen_terminy(cls, rocnik=None):
import datetime
from time import strftime
from django.db.utils import OperationalError
try:
aktualni_rocnik = m.Nastaveni.get_solo().aktualni_rocnik
aktualni_cislo = m.Nastaveni.get_solo().aktualni_cislo
except OperationalError:
# django.db.utils.OperationalError: no such table: seminar_nastaveni
# Nemáme databázi, takže to selhalo. Pro jistotu vrátíme aspoň dvě možnosti, ať to nepadá dál
logger = logging.getLogger(__name__)
logger.error("Rozbitá databáze (před počátečními migracemi?)")
return [('broken', 'Je to rozbitý'), ('fubar', 'Nefunguje to')]
# FIXME: Tohle je hnusný monkey patch, mělo by to být nějak zahrnuto výš.
if rocnik is not None:
aktualni_rocnik = rocnik
aktualni_cislo = m.Cislo.objects.filter(rocnik=rocnik).order_by('poradi').last()
result = []
for cislo in m.Cislo.objects.filter(
rocnik=aktualni_rocnik,
poradi__lte=aktualni_cislo.poradi,
).reverse(): # Standardně se řadí od nejnovějšího čísla
# Předem je mi líto kohokoliv, kdo tyhle řádky bude číst...
if cislo.datum_vydani is not None and cislo.datum_vydani <= datetime.date.today():
result.append((
strftime(DATE_FORMAT, cislo.datum_vydani.timetuple()),
f"Vydání {cislo.poradi}. čísla"))
if cislo.datum_preddeadline is not None and cislo.datum_preddeadline <= datetime.date.today():
result.append((
strftime(DATE_FORMAT, cislo.datum_preddeadline.timetuple()),
f"Předdeadline {cislo.poradi}. čísla"))
if cislo.datum_deadline_soustredeni is not None and cislo.datum_deadline_soustredeni <= datetime.date.today():
result.append((
strftime(DATE_FORMAT, cislo.datum_deadline_soustredeni.timetuple()),
f"Sous. deadline {cislo.poradi}. čísla"))
if cislo.datum_deadline is not None and cislo.datum_deadline <= datetime.date.today():
result.append((
strftime(DATE_FORMAT, cislo.datum_deadline.timetuple()),
f"Finální deadline {cislo.poradi}. čísla"))
result.append((
strftime(DATE_FORMAT, datetime.date.today().timetuple()), f"Dnes"))
return result
@classmethod
def gen_initial(cls, rocnik=None):
terminy = cls.gen_terminy(rocnik)
initial = {
'resitele': cls.RESITELE_RELEVANTNI,
'problemy': cls.PROBLEMY_MOJE,
# Pokud chceme neaktuální ročník, tak nás nejspíš zajímají všechna řešení…
'reseni_od': terminy[-2] if rocnik is None else terminy[0],
'reseni_do': terminy[-1],
'neobodovane': False,
}
return initial
def __init__(self, *args, rocnik=None, **kwargs):
if 'initial' not in kwargs:
super().__init__(initial=self.gen_initial(rocnik), *args, **kwargs)
else:
super().__init__(*args, **kwargs)
# choices jako parametr Select widgetu neumí brát callable, jen iterable, takže si pro jednoduchost můžu rovnou uložit výsledek sem...
# A "sem" znamená do libovolné metody, protože jinak se jedná o kód, který django spustí při inicializaci a protože potřebujeme databázi, tak by spadnul při vyrábění testdat...
self.terminy = self.gen_terminy(rocnik)
self.fields['reseni_od'].widget = forms.Select(choices=self.gen_terminy(rocnik))
# Pokud chceme neaktuální ročník, tak nás nejspíš zajímají všechna řešení…
self.fields['reseni_od'].initial = self.terminy[-2] if rocnik is None else self.terminy[0]
self.fields['reseni_do'].widget = forms.Select(choices=self.gen_terminy(rocnik))
self.fields['reseni_do'].initial = self.terminy[-1]
# NOTE: Initial definuji pro jednotlivé fieldy, aby to bylo tady a nebylo potřeba to řešit ve views...
resitele = forms.ChoiceField(choices=RESITELE_CHOICES)
problemy = forms.ChoiceField(choices=PROBLEMY_CHOICES)
reseni_od = forms.DateField(input_formats=[DATE_FORMAT])
reseni_do = forms.DateField(input_formats=[DATE_FORMAT])
neobodovane = forms.BooleanField(required=False)

2
seminar/models/__init__.py

@ -0,0 +1,2 @@
from .models_all import *
from .odevzdavatko import *

176
seminar/models.py → seminar/models/models_all.py

@ -9,7 +9,6 @@ import logging
from django.contrib.sites.shortcuts import get_current_site
from django.db import models
from django.contrib import auth
from django.db.models import Sum
from django.utils import timezone
from django.conf import settings
from django.utils.encoding import force_text
@ -316,6 +315,7 @@ class Resitel(SeminarModelBase):
def vsechny_body(self):
"Spočítá body odjakživa."
vsechna_reseni = self.reseni_set.all()
from .odevzdavatko import Hodnoceni
vsechna_hodnoceni = Hodnoceni.objects.filter(
reseni__in=vsechna_reseni)
return sum(h.body for h in list(vsechna_hodnoceni) if h.body is not None)
@ -362,6 +362,7 @@ class Resitel(SeminarModelBase):
# - body z 25. ročníku a dříve byly shledány dvakrát hodnotnějšími
# - proto se započítávají dvojnásobně a byly posunuté hranice titulů
# - staré tituly se ale nemají odebrat, pokud řešitel v t.č. minulém (26.) ročníku měl titul, má ho mít pořád.
from .odevzdavatko import Hodnoceni
hodnoceni_do_25_rocniku = Hodnoceni.objects.filter(cislo_body__rocnik__rocnik__lte=25,reseni__in=self.reseni_set.all())
novejsi_hodnoceni = Hodnoceni.objects.filter(reseni__in=self.reseni_set.all()).difference(hodnoceni_do_25_rocniku)
@ -399,6 +400,7 @@ class Resitel(SeminarModelBase):
else:
return Titul.akad
from .odevzdavatko import Hodnoceni
hodnoceni_do_26_rocniku = Hodnoceni.objects.filter(cislo_body__rocnik__rocnik__lte=26,reseni__in=self.reseni_set.all())
novejsi_body = body_z_hodnoceni(
Hodnoceni.objects.filter(reseni__in=self.reseni_set.all())
@ -1084,101 +1086,6 @@ class Uloha(Problem):
zadani_node = self.ulohazadaninode
return treelib.get_upper_node_of_type(zadani_node, CisloNode)
@reversion.register(ignore_duplicates=True)
class Reseni(SeminarModelBase):
class Meta:
db_table = 'seminar_reseni'
verbose_name = 'Řešení'
verbose_name_plural = 'Řešení'
#ordering = ['-problem', 'resitele'] # FIXME: Takhle to chceme, ale nefunguje to.
ordering = ['-cas_doruceni']
# Interní ID
id = models.AutoField(primary_key = True)
# Ke každé dvojici řešní a problém existuje nanejvýš jedno hodnocení, doplnění vazby.
problem = models.ManyToManyField(Problem, verbose_name='problém', help_text='Problém',
through='Hodnoceni')
resitele = models.ManyToManyField(Resitel, verbose_name='autoři řešení',
help_text='Seznam autorů řešení', through='Reseni_Resitele')
cas_doruceni = models.DateTimeField('čas_doručení', default=timezone.now, blank=True)
FORMA_PAPIR = 'papir'
FORMA_EMAIL = 'email'
FORMA_UPLOAD = 'upload'
FORMA_CHOICES = [
(FORMA_PAPIR, 'Papírové řešení'),
(FORMA_EMAIL, 'Emailem'),
(FORMA_UPLOAD, 'Upload přes web'),
]
forma = models.CharField('forma řešení', max_length=16, choices=FORMA_CHOICES, blank=False,
default=FORMA_EMAIL)
text_cely = models.OneToOneField('ReseniNode', verbose_name='Plná verze textu řešení',
blank=True, null=True, related_name="reseni_cely_set",
on_delete=models.PROTECT)
poznamka = models.TextField('neveřejná poznámka', blank=True,
help_text='Neveřejná poznámka k řešení (plain text)')
zverejneno = models.BooleanField('řešení zveřejněno', default=False,
help_text='Udává, zda je řešení zveřejněno')
def verejne_url(self):
return str(reverse_lazy('odevzdavatko_detail_reseni', args=[self.id]))
def absolute_url(self):
return "https://" + str(get_current_site(None)) + self.verejne_url()
# má OneToOneField s:
# Konfera
# má ForeignKey s:
# Hodnoceni
def sum_body(self):
return self.hodnoceni_set.all().aggregate(Sum('body'))["body__sum"]
def __str__(self):
return "{}({}): {}({})".format(self.resitele.first(),len(self.resitele.all()), self.problem.first() ,len(self.problem.all()))
# NOTE: Potenciální DB HOG (bez select_related)
## Pravdepodobne uz nebude potreba:
# def save(self, *args, **kwargs):
# if ((self.cislo_body is None) and (self.problem.cislo_reseni) and
# (self.problem.typ == Problem.TYP_ULOHA)):
# self.cislo_body = self.problem.cislo_reseni
# super(Reseni, self).save(*args, **kwargs)
class Hodnoceni(SeminarModelBase):
class Meta:
db_table = 'seminar_hodnoceni'
verbose_name = 'Hodnocení'
verbose_name_plural = 'Hodnocení'
# Interní ID
id = models.AutoField(primary_key = True)
body = models.DecimalField(max_digits=8, decimal_places=1, verbose_name='body',
blank=True, null=True)
cislo_body = models.ForeignKey(Cislo, verbose_name='číslo pro body',
related_name='hodnoceni', blank=True, null=True, on_delete=models.PROTECT)
reseni = models.ForeignKey(Reseni, verbose_name='řešení', on_delete=models.CASCADE)
problem = models.ForeignKey(Problem, verbose_name='problém',
related_name='hodnoceni', on_delete=models.PROTECT)
def __str__(self):
return "{}, {}, {}".format(self.problem, self.reseni, self.body)
def aux_generate_filename(self, filename):
"""Pomocná funkce generující ošetřený název souboru v adresáři s datem"""
@ -1204,45 +1111,6 @@ def generate_filename_konfera(self, filename):
)
##
def generate_filename(self, filename):
return os.path.join(
settings.SEMINAR_RESENI_DIR,
aux_generate_filename(self, filename)
)
@reversion.register(ignore_duplicates=True)
class PrilohaReseni(SeminarModelBase):
class Meta:
db_table = 'seminar_priloha_reseni'
verbose_name = 'Příloha řešení'
verbose_name_plural = 'Přílohy řešení'
ordering = ['reseni', 'vytvoreno']
# Interní ID
id = models.AutoField(primary_key = True)
reseni = models.ForeignKey(Reseni, verbose_name='řešení', related_name='prilohy',
on_delete=models.CASCADE)
vytvoreno = models.DateTimeField('vytvořeno', default=timezone.now, blank=True, editable=False)
soubor = models.FileField('soubor', upload_to = generate_filename)
poznamka = models.TextField('neveřejná poznámka', blank=True,
help_text='Neveřejná poznámka k příloze řešení (plain text), např. o původu')
res_poznamka = models.TextField('poznámka řešitele', blank=True,
help_text='Poznámka k příloze řešení, např. co daný soubor obsahuje')
def __str__(self):
return str(self.soubor)
def split(self):
"Vrátí cestu rozsekanou po složkách. To se hodí v templatech"
# Věřím, že tohle funguje, případně použít os.path nebo pathlib.
return self.soubor.url.split('/')
class Pohadka(SeminarModelBase):
@ -1385,29 +1253,6 @@ class Konfera(Problem):
def cislo_node(self):
return None
# Vazebna tabulka. Mozna se generuje automaticky.
@reversion.register(ignore_duplicates=True)
class Reseni_Resitele(models.Model):
class Meta:
db_table = 'seminar_reseni_resitele'
verbose_name = 'Řešení řešitelů'
verbose_name_plural = 'Řešení řešitelů'
ordering = ['reseni', 'resitele']
# Interní ID
id = models.AutoField(primary_key = True)
resitele = models.ForeignKey(Resitel, verbose_name='řešitel', on_delete=models.PROTECT)
reseni = models.ForeignKey(Reseni, verbose_name='řešení', on_delete=models.CASCADE)
# podil - jakou merou se ktery resitel podilel na danem reseni
# - pouziti v budoucnu, pokud by resitele nemeli dostat vsichni stejne bodu za spolecne reseni
def __str__(self):
return '{} od {}'.format(self.reseni, self.resitel)
# NOTE: Poteciální DB HOG bez select_related
@reversion.register(ignore_duplicates=True)
class Konfery_Ucastnici(models.Model):
@ -1705,21 +1550,6 @@ class CastNode(TreeNode):
def getOdkazStr(self):
return str(self.nadpis)
class ReseniNode(TreeNode):
class Meta:
db_table = 'seminar_nodes_otistene_reseni'
verbose_name = 'Otištěné řešení (Node)'
verbose_name_plural = 'Otištěná řešení (Node)'
reseni = models.ForeignKey(Reseni,
on_delete=models.PROTECT,
verbose_name = 'reseni')
def aktualizuj_nazev(self):
self.nazev = "ReseniNode: "+str(self.reseni)
def getOdkazStr(self):
return str(self.reseni)
@reversion.register(ignore_duplicates=True)
class Nastaveni(SingletonModel):

188
seminar/models/odevzdavatko.py

@ -0,0 +1,188 @@
import os
import reversion
from django.contrib.sites.shortcuts import get_current_site
from django.db import models
from django.db.models import Sum
from django.urls import reverse_lazy
from django.utils import timezone
from django.conf import settings
from seminar.models import models_all as am
@reversion.register(ignore_duplicates=True)
class Reseni(am.SeminarModelBase):
class Meta:
db_table = 'seminar_reseni'
verbose_name = 'Řešení'
verbose_name_plural = 'Řešení'
#ordering = ['-problem', 'resitele'] # FIXME: Takhle to chceme, ale nefunguje to.
ordering = ['-cas_doruceni']
# Interní ID
id = models.AutoField(primary_key = True)
# Ke každé dvojici řešní a problém existuje nanejvýš jedno hodnocení, doplnění vazby.
problem = models.ManyToManyField(am.Problem, verbose_name='problém', help_text='Problém',
through='Hodnoceni')
resitele = models.ManyToManyField(am.Resitel, verbose_name='autoři řešení',
help_text='Seznam autorů řešení', through='Reseni_Resitele')
cas_doruceni = models.DateTimeField('čas_doručení', default=timezone.now, blank=True)
FORMA_PAPIR = 'papir'
FORMA_EMAIL = 'email'
FORMA_UPLOAD = 'upload'
FORMA_CHOICES = [
(FORMA_PAPIR, 'Papírové řešení'),
(FORMA_EMAIL, 'Emailem'),
(FORMA_UPLOAD, 'Upload přes web'),
]
forma = models.CharField('forma řešení', max_length=16, choices=FORMA_CHOICES, blank=False,
default=FORMA_EMAIL)
text_cely = models.OneToOneField('ReseniNode', verbose_name='Plná verze textu řešení',
blank=True, null=True, related_name="reseni_cely_set",
on_delete=models.PROTECT)
poznamka = models.TextField('neveřejná poznámka', blank=True,
help_text='Neveřejná poznámka k řešení (plain text)')
zverejneno = models.BooleanField('řešení zveřejněno', default=False,
help_text='Udává, zda je řešení zveřejněno')
def verejne_url(self):
return str(reverse_lazy('odevzdavatko_detail_reseni', args=[self.id]))
def absolute_url(self):
return "https://" + str(get_current_site(None)) + self.verejne_url()
# má OneToOneField s:
# Konfera
# má ForeignKey s:
# Hodnoceni
def sum_body(self):
return self.hodnoceni_set.all().aggregate(Sum('body'))["body__sum"]
def __str__(self):
return "{}({}): {}({})".format(self.resitele.first(),len(self.resitele.all()), self.problem.first() ,len(self.problem.all()))
# NOTE: Potenciální DB HOG (bez select_related)
## Pravdepodobne uz nebude potreba:
# def save(self, *args, **kwargs):
# if ((self.cislo_body is None) and (self.problem.cislo_reseni) and
# (self.problem.typ == Problem.TYP_ULOHA)):
# self.cislo_body = self.problem.cislo_reseni
# super(Reseni, self).save(*args, **kwargs)
class Hodnoceni(am.SeminarModelBase):
class Meta:
db_table = 'seminar_hodnoceni'
verbose_name = 'Hodnocení'
verbose_name_plural = 'Hodnocení'
# Interní ID
id = models.AutoField(primary_key = True)
body = models.DecimalField(max_digits=8, decimal_places=1, verbose_name='body',
blank=True, null=True)
cislo_body = models.ForeignKey(am.Cislo, verbose_name='číslo pro body',
related_name='hodnoceni', blank=True, null=True, on_delete=models.PROTECT)
reseni = models.ForeignKey(Reseni, verbose_name='řešení', on_delete=models.CASCADE)
problem = models.ForeignKey(am.Problem, verbose_name='problém',
related_name='hodnoceni', on_delete=models.PROTECT)
def __str__(self):
return "{}, {}, {}".format(self.problem, self.reseni, self.body)
def generate_filename(self, filename):
return os.path.join(
settings.SEMINAR_RESENI_DIR,
am.aux_generate_filename(self, filename)
)
@reversion.register(ignore_duplicates=True)
class PrilohaReseni(am.SeminarModelBase):
class Meta:
db_table = 'seminar_priloha_reseni'
verbose_name = 'Příloha řešení'
verbose_name_plural = 'Přílohy řešení'
ordering = ['reseni', 'vytvoreno']
# Interní ID
id = models.AutoField(primary_key = True)
reseni = models.ForeignKey(Reseni, verbose_name='řešení', related_name='prilohy',
on_delete=models.CASCADE)
vytvoreno = models.DateTimeField('vytvořeno', default=timezone.now, blank=True, editable=False)
soubor = models.FileField('soubor', upload_to = generate_filename)
poznamka = models.TextField('neveřejná poznámka', blank=True,
help_text='Neveřejná poznámka k příloze řešení (plain text), např. o původu')
res_poznamka = models.TextField('poznámka řešitele', blank=True,
help_text='Poznámka k příloze řešení, např. co daný soubor obsahuje')
def __str__(self):
return str(self.soubor)
def split(self):
"Vrátí cestu rozsekanou po složkách. To se hodí v templatech"
# Věřím, že tohle funguje, případně použít os.path nebo pathlib.
return self.soubor.url.split('/')
# Vazebna tabulka. Mozna se generuje automaticky.
@reversion.register(ignore_duplicates=True)
class Reseni_Resitele(models.Model):
class Meta:
db_table = 'seminar_reseni_resitele'
verbose_name = 'Řešení řešitelů'
verbose_name_plural = 'Řešení řešitelů'
ordering = ['reseni', 'resitele']
# Interní ID
id = models.AutoField(primary_key = True)
resitele = models.ForeignKey(am.Resitel, verbose_name='řešitel', on_delete=models.PROTECT)
reseni = models.ForeignKey(Reseni, verbose_name='řešení', on_delete=models.CASCADE)
# podil - jakou merou se ktery resitel podilel na danem reseni
# - pouziti v budoucnu, pokud by resitele nemeli dostat vsichni stejne bodu za spolecne reseni
def __str__(self):
return '{} od {}'.format(self.reseni, self.resitel)
# NOTE: Poteciální DB HOG bez select_related
class ReseniNode(am.TreeNode):
class Meta:
db_table = 'seminar_nodes_otistene_reseni'
verbose_name = 'Otištěné řešení (Node)'
verbose_name_plural = 'Otištěná řešení (Node)'
reseni = models.ForeignKey(Reseni,
on_delete=models.PROTECT,
verbose_name = 'reseni')
def aktualizuj_nazev(self):
self.nazev = "ReseniNode: "+str(self.reseni)
def getOdkazStr(self):
return str(self.reseni)

14
seminar/urls.py

@ -1,7 +1,7 @@
from django.urls import path, include, re_path
from django.contrib.auth.decorators import login_required
from . import views
from .utils import org_required, resitel_required, viewMethodSwitch, resitel_or_org_required
from .utils import org_required
urlpatterns = [
# path('aktualni/temata/', views.TemataRozcestnikView),
@ -116,8 +116,6 @@ urlpatterns = [
path('prihlaska/',views.prihlaskaView, name='seminar_prihlaska'),
path('resitel/odevzdana_reseni/', resitel_or_org_required(views.PrehledOdevzdanychReseni.as_view()), name='seminar_resitel_odevzdana_reseni'),
path(
'resitel/osobni-udaje/',
login_required(views.resitelEditView),
@ -127,19 +125,9 @@ urlpatterns = [
# Obecný view na profil -- orgům dá rozcestník, řešitelům jejich stránku
path('profil/', views.profilView, name='profil'),
path('org/add_solution', org_required(views.AddSolutionView.as_view()), name='seminar_vloz_reseni'),
path('resitel/nahraj_reseni', resitel_required(views.NahrajReseniView.as_view()), name='seminar_nahraj_reseni'),
re_path(r'^temp/vue/.*$',views.VueTestView.as_view(),name='vue_test_view'),
path('temp/image_upload/', views.NahrajObrazekKTreeNoduView.as_view()),
path('', views.TitulniStranaView.as_view(), name='titulni_strana'),
path('jak-resit/', views.JakResitView.as_view(), name='jak_resit'),
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/all', org_required(views.SeznamReseniView.as_view())),
path('org/reseni/akt', org_required(views.SeznamAktualnichReseniView.as_view())),
]

1
seminar/views/__init__.py

@ -1,6 +1,5 @@
from .views_all import *
from .views_rest import *
from .odevzdavatko import *
# Dočsasné views
from .docasne import *

114
seminar/views/views_all.py

@ -1,26 +1,22 @@
from django.shortcuts import get_object_or_404, render, redirect
from django.http import HttpResponse, HttpResponseRedirect, HttpResponseForbidden, JsonResponse
from django.urls import reverse,reverse_lazy
from django.core.exceptions import PermissionDenied, ObjectDoesNotExist
from django.core.mail import send_mail
from django.http import HttpResponse, JsonResponse
from django.urls import reverse
from django.core.exceptions import ObjectDoesNotExist
from django.views import generic
from django.utils.translation import ugettext as _
from django.http import Http404,HttpResponseBadRequest,HttpResponseRedirect
from django.http import Http404
from django.db.models import Q, Sum, Count
from django.views.decorators.csrf import ensure_csrf_cookie
from django.views.decorators.debug import sensitive_post_parameters
from django.views.generic.edit import FormView, CreateView
from django.views.generic.edit import CreateView
from django.views.generic.base import TemplateView, RedirectView
from django.contrib.auth.models import User, Permission, Group
from django.contrib.auth.mixins import LoginRequiredMixin
from django.db import transaction
from django.core import serializers
from django.core.exceptions import PermissionDenied
from django.forms.models import model_to_dict
import seminar.models as s
import seminar.models as m
from seminar.models import Problem, Cislo, Reseni, Nastaveni, Rocnik, Soustredeni, Organizator, Resitel, Novinky, Soustredeni_Ucastnici, Pohadka, Tema, Clanek, Osoba, Skola # Tohle je stare a chceme se toho zbavit. Pouzivejte s.ToCoChci
from seminar.models import Problem, Cislo, Reseni, Nastaveni, Rocnik, Soustredeni, Organizator, Resitel, Novinky, Soustredeni_Ucastnici, Tema, Clanek, Osoba # Tohle je stare a chceme se toho zbavit. Pouzivejte s.ToCoChci
#from .models import VysledkyZaCislo, VysledkyKCisluZaRocnik, VysledkyKCisluOdjakziva
from seminar import utils, treelib
from seminar.forms import PrihlaskaForm, ProfileEditForm, PoMaturiteProfileEditForm
@ -29,7 +25,7 @@ import seminar.templatetags.treenodes as tnltt
import seminar.views.views_rest as vr
from seminar.views.vysledkovka import vysledkovka_rocniku, vysledkovka_cisla, body_resitelu
from datetime import timedelta, date, datetime, MAXYEAR
from datetime import date, datetime
from django.utils import timezone
from itertools import groupby
from collections import OrderedDict
@ -40,14 +36,11 @@ import os
import os.path as op
from django.conf import settings
import unicodedata
import json
import traceback
import sys
import csv
import logging
import time
from seminar.utils import aktivniResitele, resi_v_rocniku, problemy_rocniku, cisla_rocniku, hlavni_problemy_f
from seminar.utils import aktivniResitele, problemy_rocniku, cisla_rocniku, hlavni_problemy_f
from various.autentizace.views import LoginView
from various.autentizace.utils import posli_reset_hesla
@ -1040,97 +1033,6 @@ class ResitelView(LoginRequiredMixin,generic.DetailView):
# - přidat do forms
# - includovat do html
class AddSolutionView(LoginRequiredMixin, FormView):
template_name = 'seminar/org/vloz_reseni.html'
form_class = f.VlozReseniForm
def form_valid(self, form):
data = form.cleaned_data
nove_reseni = m.Reseni.objects.create(
cas_doruceni=data['cas_doruceni'],
forma=data['forma'],
poznamka=data['poznamka'],
)
nove_reseni.resitele.add(data['resitel'])
nove_reseni.problem.add(data['problem'])
nove_reseni.save()
# Chtěl jsem, aby bylo vidět, že se to uložilo, tak přesměrovávám na profil.
return redirect(reverse('profil'))
class NahrajReseniView(LoginRequiredMixin, CreateView):
model = s.Reseni
template_name = 'seminar/profil/nahraj_reseni.html'
form_class = f.NahrajReseniForm
def get(self, request, *args, **kwargs):
# 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)
resitel = osoba.resitel
if resitel.rok_maturity <= m.Nastaveni.get_solo().aktualni_rocnik.prvni_rok:
return render(request, 'universal.html', {
'title': 'Nelze odevzdat',
'error': 'Zdá se, že jsi již odmaturoval/a, a tedy nemůžeš odevzdat do našeho semináře řešení.',
'text': 'Pokud se ti zdá, že to je chyba, napiš nám prosím e-mail. Díky.',
})
return super().get(request, *args, **kwargs)
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()
return data
# FIXME prepsat tak, aby form_valid se volalo jen tehdy, kdyz je form i formset validni
# Inspirace: https://stackoverflow.com/questions/41599809/using-a-django-filefield-in-an-inline-formset
def form_valid(self,form):
context = self.get_context_data()
prilohy = context['prilohy']
if not prilohy.is_valid():
return super().form_invalid(form)
with transaction.atomic():
self.object = form.save()
self.object.resitele.add(Resitel.objects.get(osoba__user = self.request.user))
self.object.cas_doruceni = timezone.now()
self.object.forma = s.Reseni.FORMA_UPLOAD
self.object.save()
prilohy.instance = self.object
prilohy.save()
# Pošleme mail opravovatelům a garantovi
# FIXME: Nechat spočítat databázi? Je to pár dotazů (pravděpodobně), takže to za to možná nestojí
prijemci = set()
problemy = []
for prob in form.cleaned_data['problem']:
prijemci.update(prob.opravovatele.all())
if prob.garant is not None:
prijemci.add(prob.garant)
problemy.append(prob)
# FIXME: Možná poslat mail i relevantním orgům nadproblémů?
if len(prijemci) < 1:
logger.warning(f"Pozor, neposílám e-mail nikomu. Problémy: {problemy}")
# FIXME: Víc informativní obsah mailů, možná vč. příloh?
prijemci = map(lambda it: it.osoba.email, prijemci)
resitel = Osoba.objects.get(user = self.request.user)
seznam = "problému " + str(problemy[0]) if len(problemy) == 1 else 'následujícím problémům:\n' + ', \n'.join(map(str, problemy))
seznam_do_subjectu = "problému " + str(problemy[0]) + ("" if len(problemy) == 1 else f" (a dalším { len(problemy) - 1 })")
send_mail(
subject="Nové řešení k " + seznam_do_subjectu,
message=f"Řešitel{ '' if resitel.pohlavi_muz else 'ka' } { resitel } právě nahrál{'' if resitel.pohlavi_muz else 'a' } nové řešení k { seznam }.\n\nHurá do opravování: { self.object.absolute_url() }",
from_email="submitovatko@mam.mff.cuni.cz", # FIXME: Chceme to mít radši tady, nebo v nastavení?
recipient_list=list(prijemci),
)
return formularOKView(self.request, text='Řešení úspěšně odevzdáno')
def prihlaska_log_gdpr_safe(logger, gdpr_logger, msg, form_data):
msg = "{}, form_hash:{}".format(msg,hash(frozenset(form_data.items)))
logger.warn(msg)

Loading…
Cancel
Save