Move odevzdavatko do aplikace odevzdavatko
This commit is contained in:
parent
fd8ef17959
commit
ef68d3fb75
26 changed files with 599 additions and 534 deletions
|
@ -138,7 +138,7 @@ INSTALLED_APPS = (
|
||||||
'various.autentizace',
|
'various.autentizace',
|
||||||
'api',
|
'api',
|
||||||
'aesop',
|
'aesop',
|
||||||
|
'odevzdavatko',
|
||||||
# Admin upravy:
|
# Admin upravy:
|
||||||
|
|
||||||
# 'material',
|
# 'material',
|
||||||
|
|
|
@ -16,6 +16,9 @@ urlpatterns = [
|
||||||
|
|
||||||
# Seminarova aplikace (ma vlastni podadresare)
|
# Seminarova aplikace (ma vlastni podadresare)
|
||||||
path('', include('seminar.urls')),
|
path('', include('seminar.urls')),
|
||||||
|
|
||||||
|
# Odevzdavatko (ma vlastni podadresare)
|
||||||
|
path('', include('odevzdavatko.urls')),
|
||||||
|
|
||||||
# Korekturovaci aplikace (ma vlastni podadresare)
|
# Korekturovaci aplikace (ma vlastni podadresare)
|
||||||
path('', include('korektury.urls')),
|
path('', include('korektury.urls')),
|
||||||
|
|
11
odevzdavatko/__init__.py
Normal file
11
odevzdavatko/__init__.py
Normal file
|
@ -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 mé řešení. (Tj. řešení se vztahem k aktuálnímu uživateli.)
|
||||||
|
Nahrát řešení = Nahrání řešení bez vztahu k aktuálnímu uživateli.
|
||||||
|
|
||||||
|
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
Normal file
28
odevzdavatko/admin.py
Normal file
|
@ -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
Normal file
5
odevzdavatko/apps.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class OdevzdavatkoConfig(AppConfig):
|
||||||
|
name = 'odevzdavatko'
|
218
odevzdavatko/forms.py
Normal file
218
odevzdavatko/forms.py
Normal file
|
@ -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
Normal file
0
odevzdavatko/migrations/__init__.py
Normal file
Before Width: | Height: | Size: 717 B After Width: | Height: | Size: 717 B |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
{% block content %}
|
{% 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'>
|
<script type='text/javascript'>
|
||||||
// Credit https://medium.com/all-about-django/adding-forms-dynamically-to-a-django-formset-375f1090c2b0
|
// Credit https://medium.com/all-about-django/adding-forms-dynamically-to-a-django-formset-375f1090c2b0
|
||||||
function updateElementIndex(el, prefix, ndx) {
|
function updateElementIndex(el, prefix, ndx) {
|
||||||
|
@ -104,14 +104,14 @@ $(document).ready(function(){
|
||||||
<td>{{ subform.problem }}</td>
|
<td>{{ subform.problem }}</td>
|
||||||
<td>{{ subform.body }}</td>
|
<td>{{ subform.body }}</td>
|
||||||
<td>{{ subform.cislo_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>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</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>
|
<input type=submit value="Uložit"></form>
|
||||||
|
|
||||||
<table id="empty_form" style="display: none;">
|
<table id="empty_form" style="display: none;">
|
||||||
|
@ -119,7 +119,7 @@ $(document).ready(function(){
|
||||||
<td>{{ form.empty_form.problem }}</td>
|
<td>{{ form.empty_form.problem }}</td>
|
||||||
<td>{{ form.empty_form.body }}</td>
|
<td>{{ form.empty_form.body }}</td>
|
||||||
<td>{{ form.empty_form.cislo_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>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% load staticfiles %}
|
{% load staticfiles %}
|
||||||
{% block script %}
|
{% block script %}
|
||||||
<script src="{% static 'seminar/dynamic_formsets.js' %}"></script>
|
<script src="{% static 'odevzdavatko/dynamic_formsets.js' %}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
|
@ -3,7 +3,6 @@
|
||||||
{% block script %}
|
{% block script %}
|
||||||
<!--script type="text/javascript" src="{% static 'admin/js/vendor/jquery/jquery.js' %}"></script!-->
|
<!--script type="text/javascript" src="{% static 'admin/js/vendor/jquery/jquery.js' %}"></script!-->
|
||||||
{{form.media}}
|
{{form.media}}
|
||||||
<script src="{% static 'seminar/prihlaska.js' %}"></script>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<form method=get action=.>
|
<form method=get action=../odevzdavatko>
|
||||||
{{ filtr.resitele }}
|
{{ filtr.resitele }}
|
||||||
{{ filtr.problemy }}
|
{{ filtr.problemy }}
|
||||||
Od: {{ filtr.reseni_od }}
|
Od: {{ filtr.reseni_od }}
|
18
odevzdavatko/urls.py
Normal file
18
odevzdavatko/urls.py
Normal file
|
@ -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())),
|
||||||
|
]
|
|
@ -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.list import MultipleObjectTemplateResponseMixin,MultipleObjectMixin
|
||||||
from django.views.generic.base import View
|
from django.views.generic.base import View
|
||||||
from django.views.generic.detail import SingleObjectMixin
|
from django.shortcuts import redirect, get_object_or_404, render
|
||||||
from django.shortcuts import redirect, get_object_or_404
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
@ -13,9 +15,10 @@ from itertools import groupby
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import seminar.models as m
|
import seminar.models as m
|
||||||
import seminar.forms as f
|
from . import forms as f
|
||||||
from seminar.forms import OdevzdavatkoTabulkaFiltrForm as FiltrForm
|
from .forms import OdevzdavatkoTabulkaFiltrForm as FiltrForm
|
||||||
from seminar.utils import aktivniResitele, resi_v_rocniku, deadline
|
from seminar.utils import resi_v_rocniku, deadline
|
||||||
|
from seminar.views import formularOKView
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -41,7 +44,7 @@ class SouhrnReseni:
|
||||||
|
|
||||||
|
|
||||||
class TabulkaOdevzdanychReseniView(ListView):
|
class TabulkaOdevzdanychReseniView(ListView):
|
||||||
template_name = 'seminar/odevzdavatko/tabulka.html'
|
template_name = 'odevzdavatko/tabulka.html'
|
||||||
model = m.Hodnoceni
|
model = m.Hodnoceni
|
||||||
|
|
||||||
def inicializuj_osy_tabulky(self):
|
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?
|
# Velmi silně inspirováno zdrojáky, FIXME: Nedá se to udělat smysluplněji?
|
||||||
class ReseniProblemuView(MultipleObjectTemplateResponseMixin, MultipleObjectMixin, View):
|
class ReseniProblemuView(MultipleObjectTemplateResponseMixin, MultipleObjectMixin, View):
|
||||||
model = m.Reseni
|
model = m.Reseni
|
||||||
template_name = 'seminar/odevzdavatko/seznam.html'
|
template_name = 'odevzdavatko/seznam.html'
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
qs = super().get_queryset()
|
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
|
## XXX: https://docs.djangoproject.com/en/3.1/topics/class-based-views/mixins/#avoid-anything-more-complex
|
||||||
class DetailReseniView(DetailView):
|
class DetailReseniView(DetailView):
|
||||||
model = m.Reseni
|
model = m.Reseni
|
||||||
template_name = 'seminar/odevzdavatko/detail.html'
|
template_name = 'odevzdavatko/detail.html'
|
||||||
|
|
||||||
def aktualni_hodnoceni(self):
|
def aktualni_hodnoceni(self):
|
||||||
self.reseni = get_object_or_404(m.Reseni, id=self.kwargs['pk'])
|
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):
|
def hodnoceniReseniView(request, pk, *args, **kwargs):
|
||||||
reseni = get_object_or_404(m.Reseni, pk=pk)
|
reseni = get_object_or_404(m.Reseni, pk=pk)
|
||||||
template_name = 'seminar/odevzdavatko/detail.html'
|
template_name = 'odevzdavatko/detail.html'
|
||||||
form_class = f.OhodnoceniReseniFormSet
|
form_class = f.OhodnoceniReseniFormSet
|
||||||
success_url = reverse('odevzdavatko_detail_reseni', kwargs={'pk': pk})
|
success_url = reverse('odevzdavatko_detail_reseni', kwargs={'pk': pk})
|
||||||
|
|
||||||
|
@ -266,7 +269,7 @@ def hodnoceniReseniView(request, pk, *args, **kwargs):
|
||||||
|
|
||||||
class PrehledOdevzdanychReseni(ListView):
|
class PrehledOdevzdanychReseni(ListView):
|
||||||
model = m.Hodnoceni
|
model = m.Hodnoceni
|
||||||
template_name = 'seminar/odevzdavatko/resitel_prehled.html'
|
template_name = 'odevzdavatko/prehled_reseni.html'
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
if not self.request.user.is_authenticated:
|
if not self.request.user.is_authenticated:
|
||||||
|
@ -292,7 +295,7 @@ class PrehledOdevzdanychReseni(ListView):
|
||||||
|
|
||||||
class SeznamReseniView(ListView):
|
class SeznamReseniView(ListView):
|
||||||
model = m.Reseni
|
model = m.Reseni
|
||||||
template_name = 'seminar/odevzdavatko/seznam.html'
|
template_name = 'odevzdavatko/seznam.html'
|
||||||
|
|
||||||
class SeznamAktualnichReseniView(SeznamReseniView):
|
class SeznamAktualnichReseniView(SeznamReseniView):
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
|
@ -301,3 +304,94 @@ class SeznamAktualnichReseniView(SeznamReseniView):
|
||||||
resitele = resi_v_rocniku(akt_rocnik)
|
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
|
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
|
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')
|
|
@ -230,27 +230,6 @@ class SoustredeniAdmin(admin.ModelAdmin):
|
||||||
inlines = [SoustredeniUcastniciInline, SoustredeniOrganizatoriInline]
|
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.Pohadka)
|
||||||
admin.site.register(m.Obrazek)
|
admin.site.register(m.Obrazek)
|
||||||
|
|
||||||
|
|
201
seminar/forms.py
201
seminar/forms.py
|
@ -3,10 +3,8 @@ from dal import autocomplete
|
||||||
from django.contrib.auth.forms import PasswordResetForm
|
from django.contrib.auth.forms import PasswordResetForm
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.contrib.auth.models import User
|
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
|
import seminar.models as m
|
||||||
|
|
||||||
from datetime import date
|
from datetime import date
|
||||||
|
@ -222,205 +220,8 @@ class PoMaturiteProfileEditForm(ProfileEditForm):
|
||||||
label='Rok maturity',
|
label='Rok maturity',
|
||||||
required=True)
|
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 NahrajObrazekKTreeNoduForm(forms.ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = m.Obrazek
|
model = m.Obrazek
|
||||||
fields = ('na_web',)
|
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
Normal file
2
seminar/models/__init__.py
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
from .models_all import *
|
||||||
|
from .odevzdavatko import *
|
|
@ -9,7 +9,6 @@ import logging
|
||||||
from django.contrib.sites.shortcuts import get_current_site
|
from django.contrib.sites.shortcuts import get_current_site
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.contrib import auth
|
from django.contrib import auth
|
||||||
from django.db.models import Sum
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils.encoding import force_text
|
from django.utils.encoding import force_text
|
||||||
|
@ -316,6 +315,7 @@ class Resitel(SeminarModelBase):
|
||||||
def vsechny_body(self):
|
def vsechny_body(self):
|
||||||
"Spočítá body odjakživa."
|
"Spočítá body odjakživa."
|
||||||
vsechna_reseni = self.reseni_set.all()
|
vsechna_reseni = self.reseni_set.all()
|
||||||
|
from .odevzdavatko import Hodnoceni
|
||||||
vsechna_hodnoceni = Hodnoceni.objects.filter(
|
vsechna_hodnoceni = Hodnoceni.objects.filter(
|
||||||
reseni__in=vsechna_reseni)
|
reseni__in=vsechna_reseni)
|
||||||
return sum(h.body for h in list(vsechna_hodnoceni) if h.body is not None)
|
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
|
# - 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ů
|
# - 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.
|
# - 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())
|
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)
|
novejsi_hodnoceni = Hodnoceni.objects.filter(reseni__in=self.reseni_set.all()).difference(hodnoceni_do_25_rocniku)
|
||||||
|
|
||||||
|
@ -399,6 +400,7 @@ class Resitel(SeminarModelBase):
|
||||||
else:
|
else:
|
||||||
return Titul.akad
|
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())
|
hodnoceni_do_26_rocniku = Hodnoceni.objects.filter(cislo_body__rocnik__rocnik__lte=26,reseni__in=self.reseni_set.all())
|
||||||
novejsi_body = body_z_hodnoceni(
|
novejsi_body = body_z_hodnoceni(
|
||||||
Hodnoceni.objects.filter(reseni__in=self.reseni_set.all())
|
Hodnoceni.objects.filter(reseni__in=self.reseni_set.all())
|
||||||
|
@ -1084,101 +1086,6 @@ class Uloha(Problem):
|
||||||
zadani_node = self.ulohazadaninode
|
zadani_node = self.ulohazadaninode
|
||||||
return treelib.get_upper_node_of_type(zadani_node, CisloNode)
|
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):
|
def aux_generate_filename(self, filename):
|
||||||
"""Pomocná funkce generující ošetřený název souboru v adresáři s datem"""
|
"""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):
|
class Pohadka(SeminarModelBase):
|
||||||
|
@ -1385,29 +1253,6 @@ class Konfera(Problem):
|
||||||
def cislo_node(self):
|
def cislo_node(self):
|
||||||
return None
|
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)
|
@reversion.register(ignore_duplicates=True)
|
||||||
class Konfery_Ucastnici(models.Model):
|
class Konfery_Ucastnici(models.Model):
|
||||||
|
@ -1705,21 +1550,6 @@ class CastNode(TreeNode):
|
||||||
def getOdkazStr(self):
|
def getOdkazStr(self):
|
||||||
return str(self.nadpis)
|
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)
|
@reversion.register(ignore_duplicates=True)
|
||||||
class Nastaveni(SingletonModel):
|
class Nastaveni(SingletonModel):
|
188
seminar/models/odevzdavatko.py
Normal file
188
seminar/models/odevzdavatko.py
Normal file
|
@ -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)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
from django.urls import path, include, re_path
|
from django.urls import path, include, re_path
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from . import views
|
from . import views
|
||||||
from .utils import org_required, resitel_required, viewMethodSwitch, resitel_or_org_required
|
from .utils import org_required
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
# path('aktualni/temata/', views.TemataRozcestnikView),
|
# path('aktualni/temata/', views.TemataRozcestnikView),
|
||||||
|
@ -116,8 +116,6 @@ urlpatterns = [
|
||||||
|
|
||||||
path('prihlaska/',views.prihlaskaView, name='seminar_prihlaska'),
|
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(
|
path(
|
||||||
'resitel/osobni-udaje/',
|
'resitel/osobni-udaje/',
|
||||||
login_required(views.resitelEditView),
|
login_required(views.resitelEditView),
|
||||||
|
@ -127,19 +125,9 @@ urlpatterns = [
|
||||||
# Obecný view na profil -- orgům dá rozcestník, řešitelům jejich stránku
|
# Obecný view na profil -- orgům dá rozcestník, řešitelům jejich stránku
|
||||||
path('profil/', views.profilView, name='profil'),
|
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'),
|
re_path(r'^temp/vue/.*$',views.VueTestView.as_view(),name='vue_test_view'),
|
||||||
path('temp/image_upload/', views.NahrajObrazekKTreeNoduView.as_view()),
|
path('temp/image_upload/', views.NahrajObrazekKTreeNoduView.as_view()),
|
||||||
|
|
||||||
path('', views.TitulniStranaView.as_view(), name='titulni_strana'),
|
path('', views.TitulniStranaView.as_view(), name='titulni_strana'),
|
||||||
path('jak-resit/', views.JakResitView.as_view(), name='jak_resit'),
|
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,6 +1,5 @@
|
||||||
from .views_all import *
|
from .views_all import *
|
||||||
from .views_rest import *
|
from .views_rest import *
|
||||||
from .odevzdavatko import *
|
|
||||||
|
|
||||||
# Dočsasné views
|
# Dočsasné views
|
||||||
from .docasne import *
|
from .docasne import *
|
||||||
|
|
|
@ -1,26 +1,22 @@
|
||||||
from django.shortcuts import get_object_or_404, render, redirect
|
from django.shortcuts import get_object_or_404, render, redirect
|
||||||
from django.http import HttpResponse, HttpResponseRedirect, HttpResponseForbidden, JsonResponse
|
from django.http import HttpResponse, JsonResponse
|
||||||
from django.urls import reverse,reverse_lazy
|
from django.urls import reverse
|
||||||
from django.core.exceptions import PermissionDenied, ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.core.mail import send_mail
|
|
||||||
from django.views import generic
|
from django.views import generic
|
||||||
from django.utils.translation import ugettext as _
|
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.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.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.views.generic.base import TemplateView, RedirectView
|
||||||
from django.contrib.auth.models import User, Permission, Group
|
from django.contrib.auth.models import User, Permission, Group
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.core import serializers
|
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.forms.models import model_to_dict
|
|
||||||
|
|
||||||
import seminar.models as s
|
import seminar.models as s
|
||||||
import seminar.models as m
|
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 .models import VysledkyZaCislo, VysledkyKCisluZaRocnik, VysledkyKCisluOdjakziva
|
||||||
from seminar import utils, treelib
|
from seminar import utils, treelib
|
||||||
from seminar.forms import PrihlaskaForm, ProfileEditForm, PoMaturiteProfileEditForm
|
from seminar.forms import PrihlaskaForm, ProfileEditForm, PoMaturiteProfileEditForm
|
||||||
|
@ -29,7 +25,7 @@ import seminar.templatetags.treenodes as tnltt
|
||||||
import seminar.views.views_rest as vr
|
import seminar.views.views_rest as vr
|
||||||
from seminar.views.vysledkovka import vysledkovka_rocniku, vysledkovka_cisla, body_resitelu
|
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 django.utils import timezone
|
||||||
from itertools import groupby
|
from itertools import groupby
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
@ -40,14 +36,11 @@ import os
|
||||||
import os.path as op
|
import os.path as op
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
import unicodedata
|
import unicodedata
|
||||||
import json
|
|
||||||
import traceback
|
|
||||||
import sys
|
|
||||||
import csv
|
import csv
|
||||||
import logging
|
import logging
|
||||||
import time
|
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.views import LoginView
|
||||||
from various.autentizace.utils import posli_reset_hesla
|
from various.autentizace.utils import posli_reset_hesla
|
||||||
|
|
||||||
|
@ -1040,97 +1033,6 @@ class ResitelView(LoginRequiredMixin,generic.DetailView):
|
||||||
# - přidat do forms
|
# - přidat do forms
|
||||||
# - includovat do html
|
# - 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):
|
def prihlaska_log_gdpr_safe(logger, gdpr_logger, msg, form_data):
|
||||||
msg = "{}, form_hash:{}".format(msg,hash(frozenset(form_data.items)))
|
msg = "{}, form_hash:{}".format(msg,hash(frozenset(form_data.items)))
|
||||||
logger.warn(msg)
|
logger.warn(msg)
|
||||||
|
|
Loading…
Reference in a new issue