move: Přesunutí models.py do správných aplikací
This commit is contained in:
parent
03f0a6fd7a
commit
6c044b3632
73 changed files with 2399 additions and 1910 deletions
|
|
@ -11,7 +11,8 @@ from django.views import generic
|
|||
from django.utils.encoding import force_text
|
||||
|
||||
from .utils import default_ovvpfile
|
||||
from seminar.models import Rocnik, Soustredeni
|
||||
from tvorba.models.rocnik import Rocnik
|
||||
from soustredeni.models.soustredeni import Soustredeni
|
||||
from vysledkovky import utils
|
||||
from seminar.utils import aktivniResitele
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
#coding: utf-8
|
||||
|
||||
from django import forms
|
||||
from seminar.models import Soustredeni
|
||||
from soustredeni.models.soustredeni import Soustredeni
|
||||
|
||||
class KomentarForm(forms.Form):
|
||||
komentar = forms.CharField(label = "Komentář:", max_length = 300, required=False)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ from imagekit.processors import ResizeToFit, Transpose
|
|||
|
||||
import os
|
||||
|
||||
from seminar.models import Soustredeni
|
||||
from soustredeni.models.soustredeni import Soustredeni
|
||||
|
||||
VZDY=0
|
||||
ORG=1
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ from django.template import RequestContext
|
|||
from datetime import datetime
|
||||
|
||||
from galerie.models import Obrazek, Galerie
|
||||
from seminar.models import Soustredeni
|
||||
from soustredeni.models.soustredeni import Soustredeni
|
||||
from galerie.forms import KomentarForm, NewGalerieForm
|
||||
|
||||
def zobrazit(galerie, request):
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ from django.core.exceptions import ObjectDoesNotExist
|
|||
from django.utils.functional import cached_property
|
||||
from django.utils.text import get_valid_filename
|
||||
|
||||
from seminar.models import Organizator
|
||||
from personalni.models.organizator import Organizator
|
||||
|
||||
import subprocess
|
||||
from reversion import revisions as reversion
|
||||
|
|
|
|||
1
mamweb/models/__init__.py
Normal file
1
mamweb/models/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from .base import *
|
||||
|
|
@ -150,6 +150,7 @@ INSTALLED_APPS = (
|
|||
'personalni',
|
||||
'soustredeni',
|
||||
'treenode',
|
||||
'tvorba',
|
||||
|
||||
# Admin upravy:
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ s dekorátorem :func:`django.contrib.admin.register`.
|
|||
"""
|
||||
from django.contrib import admin
|
||||
from django_reverse_admin import ReverseModelAdmin
|
||||
import seminar.models as m
|
||||
import odevzdavatko.models as m
|
||||
|
||||
|
||||
class PrilohaReseniInline(admin.TabularInline):
|
||||
|
|
|
|||
|
|
@ -4,8 +4,11 @@ from django.forms import formset_factory
|
|||
from django.forms.models import inlineformset_factory
|
||||
from django.utils import timezone
|
||||
|
||||
from seminar.models import Resitel
|
||||
import seminar.models as m
|
||||
from personalni.models.resitel import Resitel
|
||||
from tvorba.models.nastaveni import Nastaveni
|
||||
from tvorba.models.problem import Problem
|
||||
from tvorba.models.deadline import Deadline
|
||||
import odevzdavatko.models as m
|
||||
|
||||
import logging
|
||||
|
||||
|
|
@ -22,7 +25,7 @@ class DateInput(forms.DateInput):
|
|||
|
||||
class PosliReseniForm(forms.Form):
|
||||
problem = forms.ModelMultipleChoiceField(
|
||||
queryset=m.Problem.objects.all(),
|
||||
queryset=Problem.objects.all(),
|
||||
label="Problémy",
|
||||
widget=autocomplete.ModelSelect2Multiple(
|
||||
url='autocomplete_problem',
|
||||
|
|
@ -165,7 +168,7 @@ class OdevzdavatkoTabulkaFiltrForm(forms.Form):
|
|||
|
||||
from django.db.utils import OperationalError
|
||||
try:
|
||||
aktualni_rocnik = m.Nastaveni.get_solo().aktualni_rocnik
|
||||
aktualni_rocnik = Nastaveni.get_solo().aktualni_rocnik
|
||||
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
|
||||
|
|
@ -181,7 +184,7 @@ class OdevzdavatkoTabulkaFiltrForm(forms.Form):
|
|||
|
||||
result.append(("0001-01-01", f"Odjakživa"))
|
||||
|
||||
for deadline in m.Deadline.objects.filter(
|
||||
for deadline in Deadline.objects.filter(
|
||||
deadline__lte=timezone.now(),
|
||||
cislo__rocnik=aktualni_rocnik
|
||||
).order_by("deadline"):
|
||||
|
|
|
|||
4
odevzdavatko/models/__init__.py
Normal file
4
odevzdavatko/models/__init__.py
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
from .hodnoceni import Hodnoceni
|
||||
from .priloha_reseni import PrilohaReseni
|
||||
from .reseni import Reseni
|
||||
from .reseni_resitele import Reseni_Resitele
|
||||
52
odevzdavatko/models/hodnoceni.py
Normal file
52
odevzdavatko/models/hodnoceni.py
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
from django.db import models
|
||||
|
||||
from mamweb.models.base import SeminarModelBase
|
||||
from .reseni import Reseni
|
||||
from tvorba.models.cislo import Cislo
|
||||
from tvorba.models.deadline import Deadline
|
||||
from tvorba.models.problem import Problem
|
||||
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
# V ročníku < 26 nastaveno na deadline vygenerovaný pro původní cislo_body
|
||||
deadline_body = models.ForeignKey(
|
||||
Deadline, verbose_name='deadline 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,
|
||||
)
|
||||
|
||||
feedback = models.TextField(
|
||||
'zpětná vazba', blank=True, default='',
|
||||
help_text='Zpětná vazba řešiteli (plain text)',
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return "{}, {}, {}".format(self.problem, self.reseni, self.body)
|
||||
59
odevzdavatko/models/priloha_reseni.py
Normal file
59
odevzdavatko/models/priloha_reseni.py
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import os
|
||||
import reversion
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
|
||||
from various.utils import aux_generate_filename
|
||||
from mamweb.models.base import SeminarModelBase
|
||||
from .reseni import Reseni
|
||||
|
||||
|
||||
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('/')
|
||||
104
odevzdavatko/models/reseni.py
Normal file
104
odevzdavatko/models/reseni.py
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
import reversion
|
||||
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from django.contrib.sites.shortcuts import get_current_site
|
||||
from django.urls import reverse_lazy
|
||||
from django.db.models import Sum
|
||||
|
||||
from mamweb.models.base import SeminarModelBase
|
||||
from personalni.models.resitel import Resitel
|
||||
from tvorba.models.problem import Problem
|
||||
from tvorba.models.deadline import Deadline
|
||||
|
||||
|
||||
@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)
|
||||
|
||||
def deadline_reseni(self):
|
||||
return Deadline.objects.filter(deadline__gte=self.cas_doruceni).order_by("deadline").first()
|
||||
|
||||
## 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)
|
||||
34
odevzdavatko/models/reseni_resitele.py
Normal file
34
odevzdavatko/models/reseni_resitele.py
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import reversion
|
||||
|
||||
from django.db import models
|
||||
|
||||
from .reseni import Reseni
|
||||
from personalni.models.resitel import Resitel
|
||||
|
||||
|
||||
# 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
|
||||
|
|
@ -2,8 +2,8 @@ from django import template
|
|||
register = template.Library()
|
||||
|
||||
from personalni.utils import normalizuj_jmeno
|
||||
import seminar.models as m # jen kvůli typové anotaci…
|
||||
from personalni.models.osoba import Osoba
|
||||
|
||||
@register.filter
|
||||
def jmeno_jako_prefix(o: m.Osoba):
|
||||
def jmeno_jako_prefix(o: Osoba):
|
||||
return normalizuj_jmeno(o).replace(' ', '_')
|
||||
|
|
|
|||
|
|
@ -16,7 +16,15 @@ import datetime
|
|||
from itertools import groupby
|
||||
import logging
|
||||
|
||||
import seminar.models as m
|
||||
from personalni.models.resitel import Resitel
|
||||
from personalni.models.organizator import Organizator
|
||||
from personalni.models.osoba import Osoba
|
||||
from tvorba.models.problem import Problem
|
||||
from tvorba.models.nastaveni import Nastaveni
|
||||
from tvorba.models.rocnik import Rocnik
|
||||
from tvorba.models.deadline import Deadline
|
||||
import odevzdavatko.models as m
|
||||
|
||||
from . import forms as f
|
||||
from .forms import OdevzdavatkoTabulkaFiltrForm as FiltrForm
|
||||
from seminar.utils import resi_v_rocniku
|
||||
|
|
@ -54,13 +62,13 @@ class TabulkaOdevzdanychReseniView(ListView):
|
|||
# FIXME: jméno metody není vypovídající...
|
||||
# NOTE: Tenhle blok nemůže být přímo ve třídě, protože před vyrobením databáze neexistují ty objekty (?). TODO: Otestovat
|
||||
# TODO: Prefetches, Select related, ...
|
||||
self.resitele = m.Resitel.objects.all()
|
||||
self.problemy = m.Problem.objects.all()
|
||||
self.resitele = Resitel.objects.all()
|
||||
self.problemy = Problem.objects.all()
|
||||
self.reseni = m.Reseni.objects.all()
|
||||
|
||||
self.aktualni_rocnik = m.Nastaveni.get_solo().aktualni_rocnik # .get_solo() vrátí tu jedinou instanci
|
||||
self.aktualni_rocnik = Nastaveni.get_solo().aktualni_rocnik # .get_solo() vrátí tu jedinou instanci
|
||||
if 'rocnik' in self.kwargs:
|
||||
self.aktualni_rocnik = get_object_or_404(m.Rocnik, rocnik=self.kwargs['rocnik'])
|
||||
self.aktualni_rocnik = get_object_or_404(Rocnik, rocnik=self.kwargs['rocnik'])
|
||||
|
||||
form = FiltrForm(self.request.GET, rocnik=self.aktualni_rocnik)
|
||||
if form.is_valid():
|
||||
|
|
@ -91,14 +99,14 @@ class TabulkaOdevzdanychReseniView(ListView):
|
|||
self.resitele = self.resitele.filter(rok_maturity__gt=self.aktualni_rocnik.prvni_rok)
|
||||
|
||||
if problemy == FiltrForm.PROBLEMY_MOJE:
|
||||
org = m.Organizator.objects.get(osoba__user=self.request.user)
|
||||
org = Organizator.objects.get(osoba__user=self.request.user)
|
||||
self.problemy = self.problemy.filter(
|
||||
Q(autor=org)|Q(garant=org)|Q(opravovatele=org),
|
||||
Q(stav=m.Problem.STAV_ZADANY)|Q(stav=m.Problem.STAV_VYRESENY),
|
||||
Q(stav=Problem.STAV_ZADANY)|Q(stav=Problem.STAV_VYRESENY),
|
||||
)
|
||||
elif problemy == FiltrForm.PROBLEMY_LETOSNI:
|
||||
self.problemy = self.problemy.filter(
|
||||
Q(stav=m.Problem.STAV_ZADANY)|Q(stav=m.Problem.STAV_VYRESENY),
|
||||
Q(stav=Problem.STAV_ZADANY)|Q(stav=Problem.STAV_VYRESENY),
|
||||
)
|
||||
#self.problemy = list(filter(lambda problem: problem.rocnik() == self.aktualni_rocnik, self.problemy)) # DB HOG? # FIXME: některé problémy nemají ročník....
|
||||
# NOTE: Protože řešení odkazuje přímo na Problém a QuerySet na Hodnocení je nepolymorfní, musíme porovnávat taky s nepolymorfními Problémy.
|
||||
|
|
@ -164,7 +172,7 @@ class TabulkaOdevzdanychReseniView(ListView):
|
|||
# Pro použití hacku na automatické {{form.media}} v template:
|
||||
ctx['form'] = ctx['filtr']
|
||||
# Pro maximum v přesměrovátku ročníků
|
||||
ctx['aktualni_rocnik'] = m.Nastaveni.get_solo().aktualni_rocnik
|
||||
ctx['aktualni_rocnik'] = Nastaveni.get_solo().aktualni_rocnik
|
||||
if 'rocnik' in self.kwargs:
|
||||
ctx['rocnik'] = self.kwargs['rocnik']
|
||||
else:
|
||||
|
|
@ -186,8 +194,8 @@ class ReseniProblemuView(MultipleObjectTemplateResponseMixin, MultipleObjectMixi
|
|||
if problem_id is None:
|
||||
raise ValueError("Nemám problém! (To je problém!)")
|
||||
|
||||
resitel = m.Resitel.objects.get(id=resitel_id)
|
||||
problem = m.Problem.objects.get(id=problem_id)
|
||||
resitel = Resitel.objects.get(id=resitel_id)
|
||||
problem = Problem.objects.get(id=problem_id)
|
||||
qs = qs.filter(
|
||||
problem__in=[problem],
|
||||
resitele__in=[resitel],
|
||||
|
|
@ -316,7 +324,7 @@ class PrehledOdevzdanychReseni(ListView):
|
|||
if not self.request.user.is_authenticated:
|
||||
raise RuntimeError("Uživatel měl být přihlášený!")
|
||||
# get_or_none, aby neexistence řešitele (např. u orgů) neházela chybu
|
||||
resitel = m.Resitel.objects.filter(osoba__user=self.request.user).first()
|
||||
resitel = Resitel.objects.filter(osoba__user=self.request.user).first()
|
||||
qs = super().get_queryset()
|
||||
qs = qs.filter(reseni__resitele__in=[resitel])
|
||||
# Setřídíme podle času doručení řešení, aby se netřídily podle okamžiku vyrobení Hodnocení
|
||||
|
|
@ -343,7 +351,7 @@ class SeznamReseniView(ListView):
|
|||
class SeznamAktualnichReseniView(SeznamReseniView):
|
||||
def get_queryset(self):
|
||||
qs = super().get_queryset()
|
||||
akt_rocnik = m.Nastaveni.get_solo().aktualni_rocnik # .get_solo() vrátí tu jedinou instanci, asi...
|
||||
akt_rocnik = Nastaveni.get_solo().aktualni_rocnik # .get_solo() vrátí tu jedinou instanci, asi...
|
||||
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
|
||||
|
|
@ -389,9 +397,9 @@ class NahrajReseniView(LoginRequiredMixin, CreateView):
|
|||
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)
|
||||
osoba = Osoba.objects.get(user=self.request.user)
|
||||
resitel = osoba.resitel
|
||||
if resitel.rok_maturity <= m.Nastaveni.get_solo().aktualni_rocnik.prvni_rok:
|
||||
if resitel.rok_maturity <= 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í.',
|
||||
|
|
@ -416,7 +424,7 @@ class NahrajReseniView(LoginRequiredMixin, CreateView):
|
|||
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.resitele.add(Resitel.objects.get(osoba__user = self.request.user))
|
||||
self.object.resitele.add(*form.cleaned_data["resitele"])
|
||||
self.object.cas_doruceni = timezone.now()
|
||||
self.object.forma = m.Reseni.FORMA_UPLOAD
|
||||
|
|
@ -426,7 +434,7 @@ class NahrajReseniView(LoginRequiredMixin, CreateView):
|
|||
prilohy.save()
|
||||
|
||||
for hodnoceni in self.object.hodnoceni_set.all():
|
||||
hodnoceni.deadline_body = m.Deadline.objects.filter(deadline__gte=self.object.cas_doruceni).first()
|
||||
hodnoceni.deadline_body = Deadline.objects.filter(deadline__gte=self.object.cas_doruceni).first()
|
||||
hodnoceni.save()
|
||||
|
||||
# Pošleme mail opravovatelům a garantovi
|
||||
|
|
@ -444,7 +452,7 @@ class NahrajReseniView(LoginRequiredMixin, CreateView):
|
|||
# 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)
|
||||
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 })")
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
from django.contrib import admin
|
||||
from django.contrib.auth.models import Group
|
||||
from django_reverse_admin import ReverseModelAdmin
|
||||
import seminar.models as m
|
||||
import personalni.models as m
|
||||
|
||||
|
||||
@admin.register(m.Osoba)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ from django.contrib.auth.forms import PasswordResetForm
|
|||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from seminar.models import Skola, Resitel, Osoba
|
||||
from personalni.models import Skola, Resitel, Osoba
|
||||
|
||||
from datetime import date
|
||||
import logging
|
||||
|
|
|
|||
5
personalni/models/__init__.py
Normal file
5
personalni/models/__init__.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
from .osoba import Osoba
|
||||
from .skola import Skola
|
||||
from .prijemce import Prijemce
|
||||
from .resitel import Resitel
|
||||
from .organizator import Organizator
|
||||
76
personalni/models/organizator.py
Normal file
76
personalni/models/organizator.py
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
import reversion
|
||||
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
from mamweb.models.base import SeminarModelBase
|
||||
from .osoba import Osoba
|
||||
|
||||
|
||||
@reversion.register(ignore_duplicates=True)
|
||||
class Organizator(SeminarModelBase):
|
||||
osoba = models.OneToOneField(
|
||||
Osoba, verbose_name='osoba', related_name='org',
|
||||
help_text='osobní údaje organizátora', null=False, blank=False,
|
||||
on_delete=models.PROTECT,
|
||||
)
|
||||
|
||||
vytvoreno = models.DateTimeField(
|
||||
'Vytvořeno',
|
||||
default=timezone.now,
|
||||
blank=True,
|
||||
editable=False,
|
||||
)
|
||||
|
||||
# Ne, date to nebude:
|
||||
# SQLite: invalid literal for int() with base 10: b'17 23:00:00'
|
||||
organizuje_od = models.DateTimeField('Organizuje od', blank=True, null=True)
|
||||
|
||||
organizuje_do = models.DateTimeField('Organizuje do', blank=True, null=True)
|
||||
|
||||
studuje = models.CharField(
|
||||
'Studium aj.', max_length=256,
|
||||
null=True, blank=True,
|
||||
help_text="Např. 'Studuje Obecnou fyziku (Bc.), 3. ročník', 'Vystudovala Diskrétní modely a algoritmy (Mgr.)' nebo 'Přednáší na MFF'",
|
||||
)
|
||||
|
||||
strucny_popis_organizatora = models.TextField(
|
||||
'Stručný popis organizátora',
|
||||
null=True, blank=True,
|
||||
)
|
||||
|
||||
skola = models.CharField(
|
||||
'Škola, kterou studuje', max_length=256, null=True, blank=True,
|
||||
help_text="Škola, např. MFF, VŠCHT, VUT, ... prostě aby se nemuselo psát do studuje školu, ale jen obor, možnost zobrazit zvlášť",
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
if (
|
||||
self.organizuje_od and
|
||||
self.organizuje_do and
|
||||
(self.organizuje_od > self.organizuje_do)
|
||||
):
|
||||
raise ValidationError(
|
||||
"Organizátor nemůže skončit s organizováním dříve než začal!"
|
||||
)
|
||||
super().clean()
|
||||
|
||||
def __str__(self):
|
||||
if self.osoba.prezdivka:
|
||||
return "{} '{}' {}".format(
|
||||
self.osoba.jmeno,
|
||||
self.osoba.prezdivka,
|
||||
self.osoba.prijmeni,
|
||||
)
|
||||
else:
|
||||
return "{} {}".format(self.osoba.jmeno, self.osoba.prijmeni)
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'Organizátor'
|
||||
verbose_name_plural = 'Organizátoři'
|
||||
# Řadí aktivní orgy na začátek, pod tím v pořadí od nejstarších neaktivní orgy.
|
||||
# TODO: Chtěl bych spíš mít nejstarší orgy dole.
|
||||
# TODO: Zohledňovat přezdívky?
|
||||
# TODO: Sjednotit s tím, jak se řadí organizátoři v seznau orgů na webu
|
||||
ordering = ['-organizuje_do', 'osoba__jmeno', 'osoba__prijmeni']
|
||||
126
personalni/models/osoba.py
Normal file
126
personalni/models/osoba.py
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
import reversion
|
||||
from imagekit.models import ImageSpecField, ProcessedImageField
|
||||
from imagekit.processors import ResizeToFit, Transpose
|
||||
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from django.conf import settings
|
||||
from django_countries.fields import CountryField
|
||||
|
||||
from mamweb.models.base import SeminarModelBase
|
||||
|
||||
|
||||
@reversion.register(ignore_duplicates=True)
|
||||
class Osoba(SeminarModelBase):
|
||||
|
||||
class Meta:
|
||||
db_table = 'seminar_osoby'
|
||||
verbose_name = 'Osoba'
|
||||
verbose_name_plural = 'Osoby'
|
||||
ordering = ['prijmeni', 'jmeno']
|
||||
|
||||
id = models.AutoField(primary_key=True)
|
||||
|
||||
jmeno = models.CharField('jméno', max_length=256)
|
||||
|
||||
prijmeni = models.CharField('příjmení', max_length=256)
|
||||
|
||||
prezdivka = models.CharField(
|
||||
'přezdívka', blank=True, null=True, max_length=256
|
||||
)
|
||||
|
||||
# User, pokud má na webu účet
|
||||
user = models.OneToOneField(
|
||||
settings.AUTH_USER_MODEL, blank=True, null=True,
|
||||
verbose_name='uživatel', on_delete=models.DO_NOTHING,
|
||||
)
|
||||
|
||||
# Pohlaví. Že ho neznáme se snad nestane (a ušetří to práci při programování)
|
||||
pohlavi_muz = models.BooleanField('pohlaví (muž)', default=False)
|
||||
|
||||
email = models.EmailField('e-mail', max_length=256, blank=True, default='')
|
||||
|
||||
telefon = models.CharField('telefon', max_length=256, blank=True, default='')
|
||||
|
||||
datum_narozeni = models.DateField('datum narození', blank=True, null=True)
|
||||
|
||||
# NULL dokud nedali souhlas
|
||||
datum_souhlasu_udaje = models.DateField(
|
||||
'datum souhlasu (údaje)', blank=True, null=True,
|
||||
help_text='Datum souhlasu se zpracováním osobních údajů',
|
||||
)
|
||||
|
||||
# NULL dokud nedali souhlas
|
||||
datum_souhlasu_zasilani = models.DateField(
|
||||
'datum souhlasu (spam)', blank=True, null=True,
|
||||
help_text='Datum souhlasu se zasíláním MFF materiálů',
|
||||
)
|
||||
|
||||
# Alespoň odhad (rok či i měsíc)
|
||||
datum_registrace = models.DateField(
|
||||
'datum registrace do semináře', default=timezone.now,
|
||||
)
|
||||
|
||||
# Ulice může být i jen číslo
|
||||
ulice = models.CharField('ulice', max_length=256, blank=True, default='')
|
||||
|
||||
mesto = models.CharField('město', max_length=256, blank=True, default='')
|
||||
|
||||
psc = models.CharField('PSČ', max_length=32, blank=True, default='')
|
||||
|
||||
# ISO 3166-1 dvojznakovy kod zeme velkym pismem (CZ, SK)
|
||||
# Ekvivalentní s CharField(max_length=2, default='CZ', ...)
|
||||
stat = CountryField(
|
||||
'stát', default='CZ',
|
||||
help_text='ISO 3166-1 kód země velkými písmeny (CZ, SK, ...)',
|
||||
)
|
||||
|
||||
jak_se_dozvedeli = models.TextField('Jak se dozvěděli', blank=True)
|
||||
|
||||
poznamka = models.TextField(
|
||||
'neveřejná poznámka', blank=True,
|
||||
help_text='Neveřejná poznámka k osobě (plain text)',
|
||||
)
|
||||
|
||||
foto = ProcessedImageField(
|
||||
verbose_name='Fotografie osoby',
|
||||
upload_to='image_osoby/velke/%Y/', null=True, blank=True,
|
||||
help_text='Vlož fotografii osoby o libovolné velikosti',
|
||||
processors=[
|
||||
Transpose(Transpose.AUTO),
|
||||
ResizeToFit(500, 500, upscale=False),
|
||||
],
|
||||
options={'quality': 95}
|
||||
)
|
||||
|
||||
foto_male = ImageSpecField(
|
||||
source='foto',
|
||||
processors=[ResizeToFit(200, 200, upscale=False)],
|
||||
options={'quality': 95},
|
||||
)
|
||||
|
||||
# má OneToOneField nejvýše s:
|
||||
# Resitel
|
||||
# Prijemce
|
||||
# Organizator
|
||||
|
||||
def plne_jmeno(self):
|
||||
return '{} {}'.format(self.jmeno, self.prijmeni)
|
||||
|
||||
def inicial_krestni(self):
|
||||
jmena = self.jmeno.split()
|
||||
return " ".join(['{}.'.format(jmeno[0]) for jmeno in jmena])
|
||||
|
||||
def __str__(self):
|
||||
return self.plne_jmeno()
|
||||
|
||||
# Overridujeme save Osoby, aby když si změní e-mail, aby se projevil i v
|
||||
# Userovi (a tak se dal poslat mail s resetem hesla)
|
||||
def save(self, *args, **kwargs):
|
||||
if self.user is not None:
|
||||
u = self.user
|
||||
# U svatého tučňáka, prosím ať tohle funguje.
|
||||
# (Takhle se kódit asi nemá...)
|
||||
u.email = self.email
|
||||
u.save()
|
||||
super().save()
|
||||
37
personalni/models/prijemce.py
Normal file
37
personalni/models/prijemce.py
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
from django.db import models
|
||||
|
||||
from mamweb.models.base import SeminarModelBase
|
||||
from .osoba import Osoba
|
||||
|
||||
|
||||
class Prijemce(SeminarModelBase):
|
||||
class Meta:
|
||||
db_table = 'seminar_prijemce'
|
||||
verbose_name = 'příjemce'
|
||||
verbose_name_plural = 'příjemce'
|
||||
|
||||
# Interní ID
|
||||
id = models.AutoField(primary_key=True)
|
||||
|
||||
poznamka = models.TextField(
|
||||
'neveřejná poznámka', blank=True,
|
||||
help_text='Neveřejná poznámka k příemci čísel (plain text)',
|
||||
)
|
||||
|
||||
osoba = models.OneToOneField(
|
||||
Osoba, verbose_name='komu', blank=False, null=False,
|
||||
help_text='Které osobě či na jakou adresu se mají zasílat čísla',
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
|
||||
zasilat_cislo_emailem = models.BooleanField(
|
||||
'zasílat číslo emailem',
|
||||
help_text='True pokud chce příjemce dostávat číslo emailem',
|
||||
default=False,
|
||||
)
|
||||
|
||||
# FIXME: možná chceme něco jako vazbu na osobu XOR školu a počet kusů k zaslání
|
||||
# FIXME: a možná taky posílání na mail a možná taky přes něj chceme posílat i řešitelům
|
||||
|
||||
def __str__(self):
|
||||
return self.osoba.plne_jmeno()
|
||||
221
personalni/models/resitel.py
Normal file
221
personalni/models/resitel.py
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
import reversion
|
||||
|
||||
from django.db import models
|
||||
|
||||
from mamweb.models.base import SeminarModelBase
|
||||
from .osoba import Osoba
|
||||
from .skola import Skola
|
||||
|
||||
|
||||
@reversion.register(ignore_duplicates=True)
|
||||
class Resitel(SeminarModelBase):
|
||||
|
||||
class Meta:
|
||||
db_table = 'seminar_resitele'
|
||||
verbose_name = 'Řešitel'
|
||||
verbose_name_plural = 'Řešitelé'
|
||||
ordering = ['osoba']
|
||||
|
||||
# Interní ID
|
||||
id = models.AutoField(primary_key=True)
|
||||
|
||||
prezdivka_resitele = models.CharField(
|
||||
'přezdívka řešitele', blank=True, null=True, max_length=256, unique=True,
|
||||
)
|
||||
|
||||
osoba = models.OneToOneField(
|
||||
Osoba, blank=False, null=False, verbose_name='osoba',
|
||||
on_delete=models.PROTECT,
|
||||
)
|
||||
|
||||
skola = models.ForeignKey(
|
||||
Skola, blank=True, null=True, verbose_name='škola',
|
||||
on_delete=models.SET_NULL,
|
||||
)
|
||||
|
||||
# Očekávaný rok maturity a vyřazení z aktivních řešitelů
|
||||
rok_maturity = models.IntegerField('rok maturity', blank=True, null=True)
|
||||
|
||||
ZASILAT_DOMU = 'domu'
|
||||
ZASILAT_DO_SKOLY = 'do_skoly'
|
||||
ZASILAT_NIKAM = 'nikam'
|
||||
ZASILAT_CHOICES = [
|
||||
(ZASILAT_DOMU, 'Domů'),
|
||||
(ZASILAT_DO_SKOLY, 'Do školy'),
|
||||
(ZASILAT_NIKAM, 'Nezasílat papírově'),
|
||||
]
|
||||
|
||||
zasilat = models.CharField(
|
||||
'kam zasílat', max_length=32, choices=ZASILAT_CHOICES, blank=False,
|
||||
default=ZASILAT_DOMU,
|
||||
)
|
||||
|
||||
zasilat_cislo_emailem = models.BooleanField(
|
||||
'zasílat číslo emailem',
|
||||
help_text='True pokud chce řešitel dostávat číslo emailem',
|
||||
default=False,
|
||||
)
|
||||
|
||||
zasilat_cislo_papirove = models.BooleanField(
|
||||
'zasílat číslo papírově',
|
||||
help_text='True pokud chce řešitel dostávat číslo papírově',
|
||||
default=True,
|
||||
)
|
||||
|
||||
poznamka = models.TextField(
|
||||
'neveřejná poznámka', blank=True,
|
||||
help_text='Neveřejná poznámka k řešiteli (plain text)',
|
||||
)
|
||||
|
||||
def export_row(self):
|
||||
"""Slovnik pro pouziti v AESOP exportu"""
|
||||
return {
|
||||
'id': self.id,
|
||||
'name': self.osoba.jmeno,
|
||||
'surname': self.osoba.prijmeni,
|
||||
'gender': 'M' if self.osoba.pohlavi_muz else 'F',
|
||||
'born': self.osoba.datum_narozeni.isoformat() if self.osoba.datum_narozeni else '',
|
||||
'email': self.osoba.email,
|
||||
'end-year': self.rok_maturity or '',
|
||||
|
||||
'street': self.osoba.ulice,
|
||||
'town': self.osoba.mesto,
|
||||
'postcode': self.osoba.psc,
|
||||
'country': self.osoba.stat,
|
||||
|
||||
'spam-flag': 'Y' if self.osoba.datum_souhlasu_zasilani else '',
|
||||
'spam-date': self.osoba.datum_souhlasu_zasilani.isoformat() if self.osoba.datum_souhlasu_zasilani else '',
|
||||
|
||||
'school': self.skola.aesop_id if self.skola else '',
|
||||
'school-name': str(self.skola) if self.skola else 'Skola neni znama',
|
||||
}
|
||||
|
||||
def rocnik(self, rocnik):
|
||||
"""Vrati skolni rocnik resitele pro zadany Rocnik.
|
||||
Vraci '' pro neznamy rok maturity resitele, Z* pro ekvivalent ZŠ."""
|
||||
if self.rok_maturity is None:
|
||||
return ''
|
||||
rozdil = 5 - (self.rok_maturity - rocnik.prvni_rok)
|
||||
if rozdil >= 1:
|
||||
return str(rozdil)
|
||||
else:
|
||||
return 'Z' + str(rozdil + 9)
|
||||
|
||||
def vsechny_body(self):
|
||||
"""Spočítá body odjakživa."""
|
||||
vsechna_reseni = self.reseni_set.all()
|
||||
from odevzdavatko.models 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)
|
||||
|
||||
def get_titul(self, body=None):
|
||||
"""Vrati titul jako řetězec."""
|
||||
|
||||
# Nejprve si zadefinujeme titul
|
||||
from enum import Enum
|
||||
from functools import total_ordering
|
||||
|
||||
@total_ordering
|
||||
class Titul(Enum):
|
||||
"""Třída reprezentující možné tituly. Hodnoty jsou dvojice (dolní hranice, stringifikace)."""
|
||||
nic = (0, '')
|
||||
bc = (20, 'Bc.')
|
||||
mgr = (50, 'Mgr.')
|
||||
dr = (100, 'Dr.')
|
||||
doc = (200, 'Doc.')
|
||||
prof = (500, 'Prof.')
|
||||
akad = (1000, 'Akad.')
|
||||
|
||||
def __lt__(self, other):
|
||||
return True if self.value[0] < other.value[0] else False
|
||||
|
||||
def __eq__(self, other): # Měla by být implicitní, ale klidně explicitně.
|
||||
return True if self.value[0] == other.value[0] else False
|
||||
|
||||
def __str__(self):
|
||||
return self.value[1]
|
||||
|
||||
@classmethod
|
||||
def z_bodu(cls, body):
|
||||
aktualni = cls.nic
|
||||
# TODO: ověřit, že to funguje
|
||||
for titul in cls: # Kdyžtak použít __members__.items()
|
||||
if titul.value[0] <= body:
|
||||
aktualni = titul
|
||||
else:
|
||||
break
|
||||
return aktualni
|
||||
|
||||
# Hledáme body v databázi
|
||||
# V listopadu 2020 jsme se na filosofické schůzce shodli o změně hranic titulů:
|
||||
# - 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.models import Hodnoceni
|
||||
hodnoceni_do_25_rocniku = Hodnoceni.objects.filter(
|
||||
deadline_body__cislo__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)
|
||||
|
||||
def body_z_hodnoceni(hh: list):
|
||||
return sum(h.body for h in hh if h.body is not None)
|
||||
|
||||
stare_body = body_z_hodnoceni(hodnoceni_do_25_rocniku)
|
||||
if body is None:
|
||||
nove_body = body_z_hodnoceni(novejsi_hodnoceni)
|
||||
else:
|
||||
# Zjistíme, kolik bodů jsou staré, tedy hodnotnější
|
||||
nove_body = max(0, body - stare_body) # Všechny body nad počet původních hodnotnějších
|
||||
stare_body = min(stare_body, body) # Skutečný počet hodnotnějších bodů
|
||||
logicke_body = 2*stare_body + nove_body
|
||||
|
||||
# Titul se určí následovně:
|
||||
# - Pokud se řeší body, které jsou starší, než do 26 ročníku (včetně), dáváme tituly postaru.
|
||||
# - Jinak dáváme tituly po novu...
|
||||
# - ... ale titul se nesmí odebrat, pokud se zmenšil.
|
||||
def titul_do_26_rocniku(body):
|
||||
"""Původní hranice bodů za tituly"""
|
||||
if body < 10:
|
||||
return Titul.nic
|
||||
elif body < 20:
|
||||
return Titul.bc
|
||||
elif body < 50:
|
||||
return Titul.mgr
|
||||
elif body < 100:
|
||||
return Titul.dr
|
||||
elif body < 200:
|
||||
return Titul.doc
|
||||
elif body < 500:
|
||||
return Titul.prof
|
||||
else:
|
||||
return Titul.akad
|
||||
|
||||
from odevzdavatko.models import Hodnoceni
|
||||
hodnoceni_do_26_rocniku = Hodnoceni.objects.filter(
|
||||
deadline_body__cislo__rocnik__rocnik__lte=26,
|
||||
reseni__in=self.reseni_set.all()
|
||||
)
|
||||
novejsi_body = body_z_hodnoceni(
|
||||
Hodnoceni.objects.filter(reseni__in=self.reseni_set.all())
|
||||
.difference(hodnoceni_do_26_rocniku)
|
||||
)
|
||||
starsi_body = body_z_hodnoceni(hodnoceni_do_26_rocniku)
|
||||
if body is not None:
|
||||
# Ještě z toho vybereme ty správně staré body
|
||||
novejsi_body = max(0, body - starsi_body)
|
||||
starsi_body = min(starsi_body, body)
|
||||
|
||||
# Titul pro 26. ročník
|
||||
stary_titul = titul_do_26_rocniku(starsi_body)
|
||||
# Titul podle aktuálních pravidel
|
||||
novy_titul = Titul.z_bodu(logicke_body)
|
||||
|
||||
if novejsi_body == 0:
|
||||
# Žádné nové body -- titul podle starých pravidel
|
||||
return str(stary_titul)
|
||||
return str(max(novy_titul, stary_titul))
|
||||
|
||||
def __str__(self):
|
||||
return self.osoba.plne_jmeno()
|
||||
82
personalni/models/skola.py
Normal file
82
personalni/models/skola.py
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
from reversion import revisions as reversion
|
||||
|
||||
from django.db import models
|
||||
from django_countries.fields import CountryField
|
||||
|
||||
from mamweb.models.base import SeminarModelBase
|
||||
from .osoba import Osoba
|
||||
|
||||
|
||||
#
|
||||
# Mělo by být částečně vytaženo z Aesopa
|
||||
# viz https://ovvp.mff.cuni.cz/wiki/aesop/export-skol.
|
||||
#
|
||||
|
||||
@reversion.register(ignore_duplicates=True)
|
||||
class Skola(SeminarModelBase):
|
||||
|
||||
class Meta:
|
||||
db_table = 'seminar_skoly'
|
||||
verbose_name = 'Škola'
|
||||
verbose_name_plural = 'Školy'
|
||||
ordering = ['mesto', 'nazev']
|
||||
|
||||
# Interní ID
|
||||
id = models.AutoField(primary_key=True)
|
||||
|
||||
# Aesopi ID "izo:..." nebo "aesop:..."
|
||||
# NULL znamená v exportu do aesopa "ufo"
|
||||
aesop_id = models.CharField(
|
||||
'Aesop ID', max_length=32, blank=True, default='',
|
||||
help_text='Aesopi ID typu "izo:..." nebo "aesop:..."',
|
||||
)
|
||||
|
||||
# IZO školy (jen české školy)
|
||||
izo = models.CharField(
|
||||
'IZO', max_length=32, blank=True,
|
||||
help_text='IZO školy (jen české školy)',
|
||||
)
|
||||
|
||||
# Celý název školy
|
||||
nazev = models.CharField(
|
||||
'název', max_length=256,
|
||||
help_text='Celý název školy',
|
||||
)
|
||||
|
||||
# Zkraceny nazev pro zobrazení ve výsledkovce, volitelné.
|
||||
# Není v Aesopovi, musíme vytvářet sami.
|
||||
kratky_nazev = models.CharField(
|
||||
'zkrácený název', max_length=256, blank=True,
|
||||
help_text="Zkrácený název pro zobrazení ve výsledkovce",
|
||||
)
|
||||
|
||||
# Ulice může být jen číslo
|
||||
ulice = models.CharField('ulice', max_length=256)
|
||||
|
||||
mesto = models.CharField('město', max_length=256)
|
||||
|
||||
psc = models.CharField('PSČ', max_length=32)
|
||||
|
||||
# ISO 3166-1 dvojznakovy kod zeme velkym pismem (CZ, SK)
|
||||
# Ekvivalentní s CharField(max_length=2, default='CZ', ...)
|
||||
stat = CountryField(
|
||||
'stát', default='CZ',
|
||||
help_text='ISO 3166-1 kód země velkými písmeny (CZ, SK, ...)',
|
||||
)
|
||||
|
||||
# Jaké vzdělání škpla poskytuje?
|
||||
je_zs = models.BooleanField('základní stupeň', default=True)
|
||||
je_ss = models.BooleanField('střední stupeň', default=True)
|
||||
|
||||
poznamka = models.TextField(
|
||||
'neveřejná poznámka', blank=True,
|
||||
help_text='Neveřejná poznámka ke škole (plain text)',
|
||||
)
|
||||
|
||||
kontaktni_osoba = models.ForeignKey(
|
||||
Osoba, verbose_name='Kontaktní osoba',
|
||||
blank=True, null=True, on_delete=models.SET_NULL,
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return '{}, {}, {}'.format(self.nazev, self.ulice, self.mesto)
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import seminar.models as m
|
||||
import personalni.models as m
|
||||
from various.utils import bez_diakritiky_translate
|
||||
import re
|
||||
|
||||
|
|
|
|||
|
|
@ -9,8 +9,14 @@ from django.contrib.auth.mixins import LoginRequiredMixin
|
|||
from django.db import transaction
|
||||
from django.http import HttpResponse
|
||||
|
||||
import seminar.models as s
|
||||
import seminar.models as m
|
||||
from tvorba.models.tema import Tema
|
||||
from tvorba.models.clanek import Clanek
|
||||
from tvorba.models.uloha import Uloha
|
||||
from tvorba.models.nastaveni import Nastaveni
|
||||
from soustredeni.models.soustredeni import Soustredeni
|
||||
from odevzdavatko.models.hodnoceni import Hodnoceni
|
||||
import personalni.models as s
|
||||
import personalni.models as m
|
||||
from .forms import PrihlaskaForm, ProfileEditForm, PoMaturiteProfileEditForm
|
||||
|
||||
from datetime import date
|
||||
|
|
@ -31,16 +37,16 @@ class OrgoRozcestnikView(TemplateView):
|
|||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['posledni_soustredeni'] = s.Soustredeni.objects.order_by('-datum_konce').first()
|
||||
nastaveni = s.Nastaveni.objects.first()
|
||||
context['posledni_soustredeni'] = Soustredeni.objects.order_by('-datum_konce').first()
|
||||
nastaveni = Nastaveni.objects.first()
|
||||
aktualni_rocnik = nastaveni.aktualni_rocnik
|
||||
context['posledni_cislo_url'] = nastaveni.aktualni_cislo.verejne_url()
|
||||
# TODO možná chceme odkazovat na právě rozpracované číslo, a ne to poslední vydané
|
||||
# pokud nechceme haluzit kód (= poradi) dalšího čísla, bude asi potřeba jít
|
||||
# přes treenody (a dát si přitom pozor na MezicisloNode)
|
||||
|
||||
neobodovana_reseni = s.Hodnoceni.objects.filter(body__isnull=True)
|
||||
reseni_mimo_cislo = s.Hodnoceni.objects.filter(deadline_body__isnull=True)
|
||||
neobodovana_reseni = Hodnoceni.objects.filter(body__isnull=True)
|
||||
reseni_mimo_cislo = Hodnoceni.objects.filter(deadline_body__isnull=True)
|
||||
context['pocet_neobodovanych_reseni'] = neobodovana_reseni.count()
|
||||
context['pocet_reseni_mimo_cislo'] = reseni_mimo_cislo.count()
|
||||
|
||||
|
|
@ -56,11 +62,11 @@ class OrgoRozcestnikView(TemplateView):
|
|||
context["pocty_neopravenych_reseni"] = [(it['problem__nazev'], it['cas'].date) for it in pocty_neopravenych_reseni.all()]
|
||||
|
||||
#FIXME: přidat stav='STAV_ZADANY'
|
||||
temata = s.Tema.objects.filter(Q(garant=organizator) | Q(autor=organizator) | Q(opravovatele__in=[organizator]),
|
||||
temata = Tema.objects.filter(Q(garant=organizator) | Q(autor=organizator) | Q(opravovatele__in=[organizator]),
|
||||
rocnik=aktualni_rocnik).distinct()
|
||||
ulohy = s.Uloha.objects.filter(Q(garant=organizator) | Q(autor=organizator) | Q(opravovatele__in=[organizator]),
|
||||
ulohy = Uloha.objects.filter(Q(garant=organizator) | Q(autor=organizator) | Q(opravovatele__in=[organizator]),
|
||||
cislo_zadani__rocnik=aktualni_rocnik).distinct()
|
||||
clanky = s.Clanek.objects.filter(Q(garant=organizator) | Q(autor=organizator) | Q(opravovatele__in=[organizator]),
|
||||
clanky = Clanek.objects.filter(Q(garant=organizator) | Q(autor=organizator) | Q(opravovatele__in=[organizator]),
|
||||
cislo__rocnik=aktualni_rocnik).distinct()
|
||||
|
||||
context['temata'] = temata
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ from django.utils.safestring import mark_safe
|
|||
from django.utils.html import escape
|
||||
|
||||
from .models import Prednaska, Seznam, STAV_NAVRH
|
||||
from seminar.models import Soustredeni
|
||||
from soustredeni.models.soustredeni import Soustredeni
|
||||
|
||||
|
||||
class Seznam_PrednaskaInline(admin.TabularInline):
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@
|
|||
from django.db import models
|
||||
from django.utils.encoding import force_text
|
||||
|
||||
from seminar.models import Organizator, Soustredeni
|
||||
from personalni.models.organizator import Organizator
|
||||
from soustredeni.models.soustredeni import Soustredeni
|
||||
|
||||
STAV_NAVRH = 1
|
||||
STAV_BUDE = 2
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@ from django.db.models import Sum
|
|||
from django.forms import Form
|
||||
|
||||
from prednasky.models import Prednaska, Hlasovani, Seznam, STAV_NAVRH
|
||||
from seminar.models import Soustredeni, Osoba
|
||||
from soustredeni.models.soustredeni import Soustredeni
|
||||
from personalni.models.osoba import Osoba
|
||||
|
||||
def newPrednaska(request):
|
||||
# hlasovani se vztahuje k nejnovejsimu soustredeni
|
||||
|
|
|
|||
155
seminar/admin.py
155
seminar/admin.py
|
|
@ -1,168 +1,19 @@
|
|||
from django.contrib import admin
|
||||
from django.db import models
|
||||
from django.forms import widgets, ModelForm
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
from polymorphic.admin import PolymorphicParentModelAdmin, PolymorphicChildModelAdmin, PolymorphicChildModelFilter
|
||||
from solo.admin import SingletonModelAdmin
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
# Todo: reversion
|
||||
from django.forms import widgets
|
||||
|
||||
import seminar.models as m
|
||||
|
||||
admin.site.register(m.Rocnik)
|
||||
|
||||
admin.site.register(m.Deadline)
|
||||
admin.site.register(m.ZmrazenaVysledkovka)
|
||||
|
||||
|
||||
class DeadlineAdminInline(admin.TabularInline):
|
||||
model = m.Deadline
|
||||
extra = 0
|
||||
|
||||
|
||||
class CisloForm(ModelForm):
|
||||
class Meta:
|
||||
model = m.Cislo
|
||||
fields = '__all__'
|
||||
|
||||
def clean(self):
|
||||
if self.cleaned_data.get('verejne_db') == False:
|
||||
return self.cleaned_data
|
||||
# cn = m.CisloNode.objects.get(cislo=self.instance)
|
||||
# errors = []
|
||||
# for ch in tl.all_children(cn):
|
||||
# if isinstance(ch, m.TemaVCisleNode):
|
||||
# if ch.tema.stav not in \
|
||||
# (m.Problem.STAV_ZADANY, m.Problem.STAV_VYRESENY):
|
||||
# errors.append(ValidationError('Téma %(tema)s není zadané ani vyřešené', params={'tema':ch.tema}))
|
||||
#
|
||||
# if isinstance(ch, m.UlohaZadaniNode) or isinstance(ch, m.UlohaVzorakNode):
|
||||
# if ch.uloha.stav not in \
|
||||
# (m.Problem.STAV_ZADANY, m.Problem.STAV_VYRESENY):
|
||||
# errors.append(ValidationError('Úloha %(uloha)s není zadaná ani vyřešená', params={'uloha':ch.uloha}))
|
||||
# if isinstance(ch, m.ReseniNode):
|
||||
# for problem in ch.reseni.problem_set:
|
||||
# if problem not in \
|
||||
# (m.Problem.STAV_ZADANY, m.Problem.STAV_VYRESENY):
|
||||
# errors.append(ValidationError('Problém %s není zadaný ani vyřešený', code=problem))
|
||||
# if errors:
|
||||
# errors.append(ValidationError(mark_safe('<b>Pokud chceš učinit všechny problémy, co nejsou zadané ani vyřešené, zadanými a číslo zveřejnit, můžeš to udělat pomocí akce v <a href="/admin/seminar/cislo">seznamu čísel</a></b>')))
|
||||
# raise ValidationError(errors)
|
||||
|
||||
errors = []
|
||||
for ch in m.Uloha.objects.filter(cislo_zadani=self.instance):
|
||||
if ch.stav not in (m.Problem.STAV_ZADANY, m.Problem.STAV_VYRESENY):
|
||||
errors.append(
|
||||
ValidationError('Úloha %(uloha)s není zadaná ani vyřešená', params={'uloha': ch}))
|
||||
if errors:
|
||||
errors.append(ValidationError(mark_safe(
|
||||
'<b>Pokud chceš učinit všechny problémy, co nejsou zadané ani vyřešené, zadanými a číslo zveřejnit, můžeš to udělat pomocí akce v <a href="/admin/seminar/cislo">seznamu čísel</a></b>')))
|
||||
if self.cleaned_data.get('datum_vydani') == None:
|
||||
self.add_error('datum_vydani','Číslo určené ke zveřejnění nemá nastavené datum vydání')
|
||||
|
||||
if errors:
|
||||
raise ValidationError(errors)
|
||||
|
||||
return self.cleaned_data
|
||||
|
||||
|
||||
@admin.register(m.Cislo)
|
||||
class CisloAdmin(admin.ModelAdmin):
|
||||
form = CisloForm
|
||||
actions = ['force_publish']
|
||||
inlines = (DeadlineAdminInline,)
|
||||
|
||||
def force_publish(self,request,queryset):
|
||||
for cislo in queryset:
|
||||
# cn = m.CisloNode.objects.get(cislo=cislo)
|
||||
# for ch in tl.all_children(cn):
|
||||
# if isinstance(ch, m.TemaVCisleNode):
|
||||
# if ch.tema.stav not in (m.Problem.STAV_ZADANY, m.Problem.STAV_VYRESENY):
|
||||
# ch.tema.stav = m.Problem.STAV_ZADANY
|
||||
# ch.tema.save()
|
||||
#
|
||||
# if isinstance(ch, m.UlohaZadaniNode) or isinstance(ch, m.UlohaVzorakNode):
|
||||
# if ch.uloha.stav not in (m.Problem.STAV_ZADANY, m.Problem.STAV_VYRESENY):
|
||||
# ch.uloha.stav = m.Problem.STAV_ZADANY
|
||||
# ch.uloha.save()
|
||||
# if isinstance(ch, m.ReseniNode):
|
||||
# for problem in ch.reseni.problem_set:
|
||||
# if problem not in (m.Problem.STAV_ZADANY, m.Problem.STAV_VYRESENY):
|
||||
# problem.stav = m.Problem.STAV_ZADANY
|
||||
# problem.save()
|
||||
|
||||
for ch in m.Uloha.objects.filter(cislo_zadani=cislo):
|
||||
if ch.stav not in (m.Problem.STAV_ZADANY, m.Problem.STAV_VYRESENY):
|
||||
ch.stav = m.Problem.STAV_ZADANY
|
||||
ch.save()
|
||||
|
||||
hp = ch.hlavni_problem
|
||||
if hp.stav not in (m.Problem.STAV_ZADANY, m.Problem.STAV_VYRESENY):
|
||||
hp.stav = m.Problem.STAV_ZADANY
|
||||
hp.save()
|
||||
|
||||
# TODO Řešení, vzoráky?
|
||||
# TODO Konfera/Článek?
|
||||
|
||||
cislo.verejne_db = True
|
||||
cislo.save()
|
||||
|
||||
force_publish.short_description = 'Zveřejnit vybraná čísla a všechny návrhy úloh v nich učinit zadanými'
|
||||
|
||||
|
||||
@admin.register(m.Problem)
|
||||
class ProblemAdmin(PolymorphicParentModelAdmin):
|
||||
base_model = m.Problem
|
||||
child_models = [
|
||||
m.Tema,
|
||||
m.Clanek,
|
||||
m.Uloha,
|
||||
m.Konfera,
|
||||
]
|
||||
# Pokud chceme orezavat na aktualni rocnik, musime do modelu pridat odkaz na rocnik. Zatim bere vse.
|
||||
search_fields = ['nazev']
|
||||
|
||||
# V ProblemAdmin to nejde, protoze se to nepropise do deti
|
||||
class ProblemAdminMixin(object):
|
||||
show_in_index = True
|
||||
autocomplete_fields = ['nadproblem','autor','garant']
|
||||
filter_horizontal = ['opravovatele']
|
||||
|
||||
|
||||
@admin.register(m.Tema)
|
||||
class TemaAdmin(ProblemAdminMixin,PolymorphicChildModelAdmin):
|
||||
base_model = m.Tema
|
||||
|
||||
@admin.register(m.Clanek)
|
||||
class ClanekAdmin(ProblemAdminMixin,PolymorphicChildModelAdmin):
|
||||
base_model = m.Clanek
|
||||
|
||||
@admin.register(m.Uloha)
|
||||
class UlohaAdmin(ProblemAdminMixin,PolymorphicChildModelAdmin):
|
||||
base_model = m.Uloha
|
||||
|
||||
@admin.register(m.Konfera)
|
||||
class KonferaAdmin(ProblemAdminMixin,PolymorphicChildModelAdmin):
|
||||
base_model = m.Konfera
|
||||
|
||||
|
||||
class TextAdminInline(admin.TabularInline):
|
||||
model = m.Text
|
||||
formfield_overrides = {
|
||||
models.TextField: {'widget': widgets.TextInput}
|
||||
}
|
||||
exclude = ['text_zkraceny_set','text_zkraceny']
|
||||
exclude = ['text_zkraceny_set', 'text_zkraceny']
|
||||
|
||||
|
||||
admin.site.register(m.Text)
|
||||
|
||||
class ResitelInline(admin.TabularInline):
|
||||
model = m.Resitel
|
||||
extra = 1
|
||||
|
||||
|
||||
# admin.site.register(m.Pohadka)
|
||||
admin.site.register(m.Obrazek)
|
||||
admin.site.register(m.Nastaveni, SingletonModelAdmin)
|
||||
admin.site.register(m.Novinky)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# Generated by Django 2.2.24 on 2021-11-29 22:54
|
||||
|
||||
from django.db import migrations, models
|
||||
import seminar.models.tvorba
|
||||
import tvorba.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
|
@ -14,6 +14,6 @@ class Migration(migrations.Migration):
|
|||
migrations.AlterField(
|
||||
model_name='cislo',
|
||||
name='pdf',
|
||||
field=models.FileField(blank=True, help_text='PDF čísla, které si mohou řešitelé stáhnout', null=True, storage=seminar.models.tvorba.OverwriteStorage(), upload_to=seminar.models.tvorba.cislo_pdf_filename, verbose_name='pdf'),
|
||||
field=models.FileField(blank=True, help_text='PDF čísla, které si mohou řešitelé stáhnout', null=True, storage=tvorba.tvorba.OverwriteStorage(), upload_to=tvorba.tvorba.cislo_pdf_filename, verbose_name='pdf'),
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,8 +1,2 @@
|
|||
from .tvorba import *
|
||||
from .odevzdavatko import *
|
||||
from .base import *
|
||||
from .personalni import *
|
||||
from .soustredeni import *
|
||||
from .pomocne import *
|
||||
from .treenode import *
|
||||
from .novinky import *
|
||||
from .pomocne import Text, Obrazek
|
||||
from .novinky import Novinky
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ from imagekit.processors import ResizeToFit
|
|||
|
||||
from reversion import revisions as reversion
|
||||
|
||||
from . import personalni as pm
|
||||
from personalni.models.organizator import Organizator
|
||||
|
||||
@reversion.register(ignore_duplicates=True)
|
||||
class Novinky(models.Model):
|
||||
|
|
@ -26,7 +26,7 @@ class Novinky(models.Model):
|
|||
],
|
||||
options={'quality': 95})
|
||||
|
||||
autor = models.ForeignKey(pm.Organizator, verbose_name='Autor novinky', null=True,
|
||||
autor = models.ForeignKey(Organizator, verbose_name='Autor novinky', null=True,
|
||||
on_delete=models.SET_NULL)
|
||||
|
||||
zverejneno = models.BooleanField('Zveřejněno', default=False)
|
||||
|
|
|
|||
|
|
@ -1,200 +0,0 @@
|
|||
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 tvorba as am
|
||||
from seminar.models import personalni as pm
|
||||
from seminar.models import treenode as tm
|
||||
from seminar.models import base as bm
|
||||
|
||||
|
||||
@reversion.register(ignore_duplicates=True)
|
||||
class Reseni(bm.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(pm.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)
|
||||
|
||||
def deadline_reseni(self):
|
||||
return am.Deadline.objects.filter(deadline__gte=self.cas_doruceni).order_by("deadline").first()
|
||||
|
||||
## 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(bm.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)
|
||||
|
||||
# V ročníku < 26 nastaveno na deadline vygenerovaný pro původní cislo_body
|
||||
deadline_body = models.ForeignKey(am.Deadline, verbose_name='deadline 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)
|
||||
|
||||
feedback = models.TextField('zpětná vazba', blank=True, default='', help_text='Zpětná vazba řešiteli (plain text)')
|
||||
|
||||
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(bm.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(pm.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(tm.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,447 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import logging
|
||||
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from imagekit.models import ImageSpecField, ProcessedImageField
|
||||
from imagekit.processors import ResizeToFit, Transpose
|
||||
|
||||
from django_countries.fields import CountryField
|
||||
|
||||
from reversion import revisions as reversion
|
||||
|
||||
from .base import SeminarModelBase
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@reversion.register(ignore_duplicates=True)
|
||||
class Osoba(SeminarModelBase):
|
||||
|
||||
class Meta:
|
||||
db_table = 'seminar_osoby'
|
||||
verbose_name = 'Osoba'
|
||||
verbose_name_plural = 'Osoby'
|
||||
ordering = ['prijmeni','jmeno']
|
||||
|
||||
id = models.AutoField(primary_key = True)
|
||||
|
||||
jmeno = models.CharField('jméno', max_length=256)
|
||||
|
||||
prijmeni = models.CharField('příjmení', max_length=256)
|
||||
|
||||
prezdivka = models.CharField('přezdívka', blank=True, null=True, max_length=256)
|
||||
|
||||
# User, pokud má na webu účet
|
||||
user = models.OneToOneField(settings.AUTH_USER_MODEL, blank=True, null=True,
|
||||
verbose_name='uživatel', on_delete=models.DO_NOTHING)
|
||||
|
||||
# Pohlaví. Že ho neznáme se snad nestane (a ušetří to práci při programování)
|
||||
pohlavi_muz = models.BooleanField('pohlaví (muž)', default=False)
|
||||
|
||||
email = models.EmailField('e-mail', max_length=256, blank=True, default='')
|
||||
|
||||
telefon = models.CharField('telefon', max_length=256, blank=True, default='')
|
||||
|
||||
datum_narozeni = models.DateField('datum narození', blank=True, null=True)
|
||||
|
||||
# NULL dokud nedali souhlas
|
||||
datum_souhlasu_udaje = models.DateField('datum souhlasu (údaje)', blank=True, null=True,
|
||||
help_text='Datum souhlasu se zpracováním osobních údajů')
|
||||
|
||||
# NULL dokud nedali souhlas
|
||||
datum_souhlasu_zasilani = models.DateField('datum souhlasu (spam)', blank=True, null=True,
|
||||
help_text='Datum souhlasu se zasíláním MFF materiálů')
|
||||
|
||||
# Alespoň odhad (rok či i měsíc)
|
||||
datum_registrace = models.DateField('datum registrace do semináře', default=timezone.now)
|
||||
|
||||
# Ulice může být i jen číslo
|
||||
ulice = models.CharField('ulice', max_length=256, blank=True, default='')
|
||||
|
||||
mesto = models.CharField('město', max_length=256, blank=True, default='')
|
||||
|
||||
psc = models.CharField('PSČ', max_length=32, blank=True, default='')
|
||||
|
||||
# ISO 3166-1 dvojznakovy kod zeme velkym pismem (CZ, SK)
|
||||
# Ekvivalentní s CharField(max_length=2, default='CZ', ...)
|
||||
stat = CountryField('stát', default='CZ',
|
||||
help_text='ISO 3166-1 kód země velkými písmeny (CZ, SK, ...)')
|
||||
|
||||
jak_se_dozvedeli = models.TextField('Jak se dozvěděli', blank=True)
|
||||
|
||||
poznamka = models.TextField('neveřejná poznámka', blank=True,
|
||||
help_text='Neveřejná poznámka k osobě (plain text)')
|
||||
|
||||
foto = ProcessedImageField(verbose_name='Fotografie osoby',
|
||||
upload_to='image_osoby/velke/%Y/', null = True, blank = True,
|
||||
help_text = 'Vlož fotografii osoby o libovolné velikosti',
|
||||
processors=[
|
||||
Transpose(Transpose.AUTO),
|
||||
ResizeToFit(500, 500, upscale=False)
|
||||
],
|
||||
options={'quality': 95})
|
||||
foto_male = ImageSpecField(source='foto',
|
||||
processors=[
|
||||
ResizeToFit(200, 200, upscale=False)
|
||||
],
|
||||
options={'quality': 95})
|
||||
|
||||
# má OneToOneField nejvýše s:
|
||||
# Resitel
|
||||
# Prijemce
|
||||
# Organizator
|
||||
|
||||
def plne_jmeno(self):
|
||||
return '{} {}'.format(self.jmeno, self.prijmeni)
|
||||
|
||||
def inicial_krestni(self):
|
||||
jmena = self.jmeno.split()
|
||||
return " ".join(['{}.'.format(jmeno[0]) for jmeno in jmena])
|
||||
|
||||
def __str__(self):
|
||||
return self.plne_jmeno()
|
||||
|
||||
# Overridujeme save Osoby, aby když si změní e-mail, aby se projevil i v
|
||||
# Userovi (a tak se dal poslat mail s resetem hesla)
|
||||
def save(self, *args, **kwargs):
|
||||
if self.user is not None:
|
||||
u = self.user
|
||||
# U svatého tučňáka, prosím ať tohle funguje.
|
||||
# (Takhle se kódit asi nemá...)
|
||||
u.email = self.email
|
||||
u.save()
|
||||
super().save()
|
||||
|
||||
#
|
||||
# Mělo by být částečně vytaženo z Aesopa
|
||||
# viz https://ovvp.mff.cuni.cz/wiki/aesop/export-skol.
|
||||
#
|
||||
|
||||
@reversion.register(ignore_duplicates=True)
|
||||
class Skola(SeminarModelBase):
|
||||
|
||||
class Meta:
|
||||
db_table = 'seminar_skoly'
|
||||
verbose_name = 'Škola'
|
||||
verbose_name_plural = 'Školy'
|
||||
ordering = ['mesto', 'nazev']
|
||||
|
||||
# Interní ID
|
||||
id = models.AutoField(primary_key = True)
|
||||
|
||||
# Aesopi ID "izo:..." nebo "aesop:..."
|
||||
# NULL znamená v exportu do aesopa "ufo"
|
||||
aesop_id = models.CharField('Aesop ID', max_length=32, blank=True, default='',
|
||||
help_text='Aesopi ID typu "izo:..." nebo "aesop:..."')
|
||||
|
||||
# IZO školy (jen české školy)
|
||||
izo = models.CharField('IZO', max_length=32, blank=True,
|
||||
help_text='IZO školy (jen české školy)')
|
||||
|
||||
# Celý název školy
|
||||
nazev = models.CharField('název', max_length=256,
|
||||
help_text='Celý název školy')
|
||||
|
||||
# Zkraceny nazev pro zobrazení ve výsledkovce, volitelné.
|
||||
# Není v Aesopovi, musíme vytvářet sami.
|
||||
kratky_nazev = models.CharField('zkrácený název', max_length=256, blank=True,
|
||||
help_text="Zkrácený název pro zobrazení ve výsledkovce")
|
||||
|
||||
# Ulice může být jen číslo
|
||||
ulice = models.CharField('ulice', max_length=256)
|
||||
|
||||
mesto = models.CharField('město', max_length=256)
|
||||
|
||||
psc = models.CharField('PSČ', max_length=32)
|
||||
|
||||
# ISO 3166-1 dvojznakovy kod zeme velkym pismem (CZ, SK)
|
||||
# Ekvivalentní s CharField(max_length=2, default='CZ', ...)
|
||||
stat = CountryField('stát', default='CZ',
|
||||
help_text='ISO 3166-1 kód země velkými písmeny (CZ, SK, ...)')
|
||||
|
||||
# Jaké vzdělání škpla poskytuje?
|
||||
je_zs = models.BooleanField('základní stupeň', default=True)
|
||||
je_ss = models.BooleanField('střední stupeň', default=True)
|
||||
|
||||
poznamka = models.TextField('neveřejná poznámka', blank=True,
|
||||
help_text='Neveřejná poznámka ke škole (plain text)')
|
||||
|
||||
kontaktni_osoba = models.ForeignKey(Osoba, verbose_name='Kontaktní osoba',
|
||||
blank=True, null=True, on_delete=models.SET_NULL)
|
||||
|
||||
def __str__(self):
|
||||
return '{}, {}, {}'.format(self.nazev, self.ulice, self.mesto)
|
||||
|
||||
class Prijemce(SeminarModelBase):
|
||||
class Meta:
|
||||
db_table = 'seminar_prijemce'
|
||||
verbose_name = 'příjemce'
|
||||
verbose_name_plural = 'příjemce'
|
||||
|
||||
|
||||
# Interní ID
|
||||
id = models.AutoField(primary_key = True)
|
||||
|
||||
poznamka = models.TextField('neveřejná poznámka', blank=True,
|
||||
help_text='Neveřejná poznámka k příemci čísel (plain text)')
|
||||
|
||||
osoba = models.OneToOneField(Osoba, verbose_name='komu', blank=False, null=False,
|
||||
help_text='Které osobě či na jakou adresu se mají zasílat čísla',
|
||||
on_delete=models.CASCADE)
|
||||
|
||||
zasilat_cislo_emailem = models.BooleanField('zasílat číslo emailem', help_text='True pokud chce příjemce dostávat číslo emailem', default=False)
|
||||
|
||||
# FIXME: možná chceme něco jako vazbu na osobu XOR školu a počet kusů k zaslání
|
||||
# FIXME: a možná taky posílání na mail a možná taky přes něj chceme posílat i řešitelům
|
||||
|
||||
def __str__(self):
|
||||
return self.osoba.plne_jmeno()
|
||||
|
||||
|
||||
@reversion.register(ignore_duplicates=True)
|
||||
class Resitel(SeminarModelBase):
|
||||
|
||||
class Meta:
|
||||
db_table = 'seminar_resitele'
|
||||
verbose_name = 'Řešitel'
|
||||
verbose_name_plural = 'Řešitelé'
|
||||
ordering = ['osoba']
|
||||
|
||||
# Interní ID
|
||||
id = models.AutoField(primary_key = True)
|
||||
|
||||
prezdivka_resitele = models.CharField('přezdívka řešitele', blank=True, null=True, max_length=256, unique=True)
|
||||
|
||||
osoba = models.OneToOneField(Osoba, blank=False, null=False, verbose_name='osoba',
|
||||
on_delete=models.PROTECT)
|
||||
|
||||
|
||||
skola = models.ForeignKey(Skola, blank=True, null=True, verbose_name='škola',
|
||||
on_delete=models.SET_NULL)
|
||||
|
||||
# Očekávaný rok maturity a vyřazení z aktivních řešitelů
|
||||
rok_maturity = models.IntegerField('rok maturity', blank=True, null=True)
|
||||
|
||||
ZASILAT_DOMU = 'domu'
|
||||
ZASILAT_DO_SKOLY = 'do_skoly'
|
||||
ZASILAT_NIKAM = 'nikam'
|
||||
ZASILAT_CHOICES = [
|
||||
(ZASILAT_DOMU, 'Domů'),
|
||||
(ZASILAT_DO_SKOLY, 'Do školy'),
|
||||
(ZASILAT_NIKAM, 'Nezasílat papírově'),
|
||||
]
|
||||
|
||||
zasilat = models.CharField('kam zasílat', max_length=32, choices=ZASILAT_CHOICES, blank=False, default=ZASILAT_DOMU)
|
||||
|
||||
zasilat_cislo_emailem = models.BooleanField('zasílat číslo emailem', help_text='True pokud chce řešitel dostávat číslo emailem', default=False)
|
||||
|
||||
zasilat_cislo_papirove = models.BooleanField('zasílat číslo papírově', help_text='True pokud chce řešitel dostávat číslo papírově', default=True)
|
||||
|
||||
poznamka = models.TextField('neveřejná poznámka', blank=True,
|
||||
help_text='Neveřejná poznámka k řešiteli (plain text)')
|
||||
|
||||
|
||||
def export_row(self):
|
||||
"Slovnik pro pouziti v AESOP exportu"
|
||||
return {
|
||||
'id': self.id,
|
||||
'name': self.osoba.jmeno,
|
||||
'surname': self.osoba.prijmeni,
|
||||
'gender': 'M' if self.osoba.pohlavi_muz else 'F',
|
||||
'born': self.osoba.datum_narozeni.isoformat() if self.osoba.datum_narozeni else '',
|
||||
'email': self.osoba.email,
|
||||
'end-year': self.rok_maturity or '',
|
||||
|
||||
'street': self.osoba.ulice,
|
||||
'town': self.osoba.mesto,
|
||||
'postcode': self.osoba.psc,
|
||||
'country': self.osoba.stat,
|
||||
|
||||
'spam-flag': 'Y' if self.osoba.datum_souhlasu_zasilani else '',
|
||||
'spam-date': self.osoba.datum_souhlasu_zasilani.isoformat() if self.osoba.datum_souhlasu_zasilani else '',
|
||||
|
||||
'school': self.skola.aesop_id if self.skola else '',
|
||||
'school-name': str(self.skola) if self.skola else 'Skola neni znama',
|
||||
}
|
||||
|
||||
def rocnik(self, rocnik):
|
||||
"""Vrati skolni rocnik resitele pro zadany Rocnik.
|
||||
Vraci '' pro neznamy rok maturity resitele, Z* pro ekvivalent ZŠ."""
|
||||
if self.rok_maturity is None:
|
||||
return ''
|
||||
rozdil = 5 - (self.rok_maturity - rocnik.prvni_rok)
|
||||
if rozdil >= 1:
|
||||
return str(rozdil)
|
||||
else:
|
||||
return 'Z' + str(rozdil + 9)
|
||||
|
||||
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)
|
||||
|
||||
|
||||
def get_titul(self, body=None):
|
||||
"Vrati titul jako řetězec."
|
||||
|
||||
# Nejprve si zadefinujeme titul
|
||||
from enum import Enum
|
||||
from functools import total_ordering
|
||||
@total_ordering
|
||||
class Titul(Enum):
|
||||
""" Třída reprezentující možné tituly. Hodnoty jsou dvojice (dolní hranice, stringifikace). """
|
||||
nic = (0, '')
|
||||
bc = (20, 'Bc.')
|
||||
mgr = (50, 'Mgr.')
|
||||
dr = (100, 'Dr.')
|
||||
doc = (200, 'Doc.')
|
||||
prof = (500, 'Prof.')
|
||||
akad = (1000, 'Akad.')
|
||||
|
||||
def __lt__(self, other):
|
||||
return True if self.value[0] < other.value[0] else False
|
||||
def __eq__(self, other): # Měla by být implicitní, ale klidně explicitně.
|
||||
return True if self.value[0] == other.value[0] else False
|
||||
|
||||
def __str__(self):
|
||||
return self.value[1]
|
||||
|
||||
@classmethod
|
||||
def z_bodu(cls, body):
|
||||
aktualni = cls.nic
|
||||
# TODO: ověřit, že to funguje
|
||||
for titul in cls: # Kdyžtak použít __members__.items()
|
||||
if titul.value[0] <= body:
|
||||
aktualni = titul
|
||||
else:
|
||||
break
|
||||
return aktualni
|
||||
|
||||
# Hledáme body v databázi
|
||||
# V listopadu 2020 jsme se na filosofické schůzce shodli o změně hranic titulů:
|
||||
# - 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(deadline_body__cislo__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)
|
||||
|
||||
def body_z_hodnoceni(hh : list):
|
||||
return sum(h.body for h in hh if h.body is not None)
|
||||
|
||||
stare_body = body_z_hodnoceni(hodnoceni_do_25_rocniku)
|
||||
if body is None:
|
||||
nove_body = body_z_hodnoceni(novejsi_hodnoceni)
|
||||
else:
|
||||
# Zjistíme, kolik bodů jsou staré, tedy hodnotnější
|
||||
nove_body = max(0, body - stare_body) # Všechny body nad počet původních hodnotnějších
|
||||
stare_body = min(stare_body, body) # Skutečný počet hodnotnějších bodů
|
||||
logicke_body = 2*stare_body + nove_body
|
||||
|
||||
|
||||
# Titul se určí následovně:
|
||||
# - Pokud se řeší body, které jsou starší, než do 26 ročníku (včetně), dáváme tituly postaru.
|
||||
# - Jinak dáváme tituly po novu...
|
||||
# - ... ale titul se nesmí odebrat, pokud se zmenšil.
|
||||
def titul_do_26_rocniku(body):
|
||||
""" Původní hranice bodů za tituly """
|
||||
if body < 10:
|
||||
return Titul.nic
|
||||
elif body < 20:
|
||||
return Titul.bc
|
||||
elif body < 50:
|
||||
return Titul.mgr
|
||||
elif body < 100:
|
||||
return Titul.dr
|
||||
elif body < 200:
|
||||
return Titul.doc
|
||||
elif body < 500:
|
||||
return Titul.prof
|
||||
else:
|
||||
return Titul.akad
|
||||
|
||||
from .odevzdavatko import Hodnoceni
|
||||
hodnoceni_do_26_rocniku = Hodnoceni.objects.filter(deadline_body__cislo__rocnik__rocnik__lte=26,reseni__in=self.reseni_set.all())
|
||||
novejsi_body = body_z_hodnoceni(
|
||||
Hodnoceni.objects.filter(reseni__in=self.reseni_set.all())
|
||||
.difference(hodnoceni_do_26_rocniku)
|
||||
)
|
||||
starsi_body = body_z_hodnoceni(hodnoceni_do_26_rocniku)
|
||||
if body is not None:
|
||||
# Ještě z toho vybereme ty správně staré body
|
||||
novejsi_body = max(0, body - starsi_body)
|
||||
starsi_body = min(starsi_body, body)
|
||||
|
||||
# Titul pro 26. ročník
|
||||
stary_titul = titul_do_26_rocniku(starsi_body)
|
||||
# Titul podle aktuálních pravidel
|
||||
novy_titul = Titul.z_bodu(logicke_body)
|
||||
|
||||
if novejsi_body == 0:
|
||||
# Žádné nové body -- titul podle starých pravidel
|
||||
return str(stary_titul)
|
||||
return str(max(novy_titul, stary_titul))
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return self.osoba.plne_jmeno()
|
||||
|
||||
|
||||
@reversion.register(ignore_duplicates=True)
|
||||
class Organizator(SeminarModelBase):
|
||||
osoba = models.OneToOneField(Osoba, verbose_name='osoba', related_name='org',
|
||||
help_text='osobní údaje organizátora', null=False, blank=False,
|
||||
on_delete=models.PROTECT)
|
||||
|
||||
vytvoreno = models.DateTimeField(
|
||||
'Vytvořeno',
|
||||
default=timezone.now,
|
||||
blank=True,
|
||||
editable=False
|
||||
)
|
||||
|
||||
# Ne, date to nebude. SQLite: invalid literal for int() with base 10: b'17 23:00:00'
|
||||
organizuje_od = models.DateTimeField('Organizuje od', blank=True, null=True)
|
||||
|
||||
organizuje_do = models.DateTimeField('Organizuje do', blank=True, null=True)
|
||||
|
||||
studuje = models.CharField('Studium aj.', max_length = 256,
|
||||
null = True, blank = True,
|
||||
help_text="Např. 'Studuje Obecnou fyziku (Bc.), 3. ročník', "
|
||||
"'Vystudovala Diskrétní modely a algoritmy (Mgr.)' nebo "
|
||||
"'Přednáší na MFF'")
|
||||
|
||||
strucny_popis_organizatora = models.TextField('Stručný popis organizátora',
|
||||
null = True, blank = True)
|
||||
|
||||
skola = models.CharField('Škola, kterou studuje', max_length = 256, null=True, blank=True,
|
||||
help_text="Škola, např. MFF, VŠCHT, VUT, ... prostě aby se nemuselo psát do studuje"
|
||||
"školu, ale jen obor, možnost zobrazit zvlášť")
|
||||
|
||||
def clean(self):
|
||||
if self.organizuje_od and self.organizuje_do and (self.organizuje_od > self.organizuje_do):
|
||||
raise ValidationError("Organizátor nemůže skončit s organizováním dříve než začal!")
|
||||
super().clean()
|
||||
|
||||
def __str__(self):
|
||||
if self.osoba.prezdivka:
|
||||
return "{} '{}' {}".format(self.osoba.jmeno,
|
||||
self.osoba.prezdivka,
|
||||
self.osoba.prijmeni)
|
||||
else:
|
||||
return "{} {}".format(self.osoba.jmeno, self.osoba.prijmeni)
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'Organizátor'
|
||||
verbose_name_plural = 'Organizátoři'
|
||||
# Řadí aktivní orgy na začátek, pod tím v pořadí od nejstarších neaktivní orgy.
|
||||
# TODO: Chtěl bych spíš mít nejstarší orgy dole.
|
||||
# TODO: Zohledňovat přezdívky?
|
||||
# TODO: Sjednotit s tím, jak se řadí organizátoři v seznau orgů na webu
|
||||
ordering = ['-organizuje_do', 'osoba__jmeno', 'osoba__prijmeni']
|
||||
|
|
@ -3,7 +3,7 @@ import logging
|
|||
import os
|
||||
from django.db import models
|
||||
|
||||
from .base import SeminarModelBase
|
||||
from mamweb.models.base import SeminarModelBase
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,216 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import logging
|
||||
import os
|
||||
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from reversion import revisions as reversion
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from . import personalni as pm
|
||||
|
||||
from .base import SeminarModelBase
|
||||
from seminar.models import tvorba as am
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@reversion.register(ignore_duplicates=True)
|
||||
class Soustredeni(SeminarModelBase):
|
||||
|
||||
class Meta:
|
||||
db_table = 'seminar_soustredeni'
|
||||
verbose_name = 'Soustředění'
|
||||
verbose_name_plural = 'Soustředění'
|
||||
ordering = ['-rocnik__rocnik', '-datum_zacatku']
|
||||
|
||||
# Interní ID
|
||||
id = models.AutoField(primary_key = True)
|
||||
|
||||
rocnik = models.ForeignKey(am.Rocnik, verbose_name='ročník', related_name='soustredeni',
|
||||
on_delete=models.PROTECT)
|
||||
|
||||
datum_zacatku = models.DateField('datum začátku', blank=True, null=True,
|
||||
help_text='První den soustředění')
|
||||
|
||||
datum_konce = models.DateField('datum konce', blank=True, null=True,
|
||||
help_text='Poslední den soustředění')
|
||||
|
||||
verejne_db = models.BooleanField('soustředění zveřejněno', db_column='verejne', default=False)
|
||||
|
||||
misto = models.CharField('místo soustředění', max_length=256, blank=True, default='',
|
||||
help_text='Místo (název obce, volitelně též objektu')
|
||||
|
||||
ucastnici = models.ManyToManyField(pm.Resitel, verbose_name='účastníci soustředění',
|
||||
help_text='Seznam účastníků soustředění', through='Soustredeni_Ucastnici')
|
||||
|
||||
organizatori = models.ManyToManyField(pm.Organizator,
|
||||
verbose_name='Organizátoři soustředění',
|
||||
help_text='Seznam organizátorů soustředění',
|
||||
through='Soustredeni_Organizatori')
|
||||
|
||||
text = models.TextField('text k soustředění (HTML)', blank=True, default='')
|
||||
|
||||
TYP_JARNI = 'jarni'
|
||||
TYP_PODZIMNI = 'podzimni'
|
||||
TYP_VIKEND = 'vikend'
|
||||
TYP_VYLET = 'vylet'
|
||||
TYP_CHOICES = [
|
||||
(TYP_JARNI, 'Jarní soustředění'),
|
||||
(TYP_PODZIMNI, 'Podzimní soustředění'),
|
||||
(TYP_VIKEND, 'Víkendový sraz'),
|
||||
(TYP_VYLET, 'Výlet'),
|
||||
]
|
||||
typ = models.CharField('typ akce', max_length=16, choices=TYP_CHOICES, blank=False, default=TYP_PODZIMNI)
|
||||
|
||||
exportovat = models.BooleanField('export do AESOPa', db_column='exportovat', default=False,
|
||||
help_text='Exportuje se jen podle tohoto flagu (ne veřejnosti)')
|
||||
|
||||
def __str__(self):
|
||||
return '{} ({})'.format(self.misto, self.datum_zacatku)
|
||||
|
||||
def verejne(self):
|
||||
return self.verejne_db
|
||||
verejne.boolean = True
|
||||
|
||||
def verejne_url(self):
|
||||
#return reverse('seminar_soustredeni', kwargs={'pk': self.id})
|
||||
return reverse('seminar_seznam_soustredeni')
|
||||
|
||||
|
||||
@reversion.register(ignore_duplicates=True)
|
||||
class Soustredeni_Ucastnici(SeminarModelBase):
|
||||
# zmena dedicnosti z models.Model na SeminarModelBase, potencialni vznik bugu
|
||||
|
||||
class Meta:
|
||||
db_table = 'seminar_soustredeni_ucastnici'
|
||||
verbose_name = 'Účast na soustředění'
|
||||
verbose_name_plural = 'Účasti na soustředění'
|
||||
ordering = ['soustredeni', 'resitel']
|
||||
|
||||
# Interní ID
|
||||
id = models.AutoField(primary_key = True)
|
||||
|
||||
resitel = models.ForeignKey(pm.Resitel, verbose_name='řešitel', on_delete=models.PROTECT)
|
||||
|
||||
soustredeni = models.ForeignKey(Soustredeni, verbose_name='soustředění',
|
||||
on_delete=models.PROTECT)
|
||||
|
||||
poznamka = models.TextField('neveřejná poznámka', blank=True,
|
||||
help_text='Neveřejná poznámka k účasti (plain text)')
|
||||
|
||||
def __str__(self):
|
||||
return '{} na {}'.format(self.resitel, self.soustredeni)
|
||||
# NOTE: Poteciální DB HOG bez select_related
|
||||
|
||||
|
||||
@reversion.register(ignore_duplicates=True)
|
||||
class Soustredeni_Organizatori(SeminarModelBase):
|
||||
# zmena dedicnosti z models.Model na SeminarModelBase, potencialni vznik bugu
|
||||
|
||||
class Meta:
|
||||
db_table = 'seminar_soustredeni_organizatori'
|
||||
verbose_name = 'Účast organizátorů na soustředění'
|
||||
verbose_name_plural = 'Účasti organizátorů na soustředění'
|
||||
ordering = ['soustredeni', 'organizator']
|
||||
|
||||
# Interní ID
|
||||
id = models.AutoField(primary_key = True)
|
||||
|
||||
organizator = models.ForeignKey(pm.Organizator, verbose_name='organizátor',
|
||||
on_delete=models.PROTECT)
|
||||
|
||||
soustredeni = models.ForeignKey(Soustredeni, verbose_name='soustředění',
|
||||
on_delete=models.PROTECT)
|
||||
|
||||
poznamka = models.TextField('neveřejná poznámka', blank=True,
|
||||
help_text='Neveřejná poznámka k účasti organizátora (plain text)')
|
||||
|
||||
def __str__(self):
|
||||
return '{} na {}'.format(self.organizator, self.soustredeni)
|
||||
# NOTE: Poteciální DB HOG bez select_related
|
||||
|
||||
|
||||
# FIXME cycle import
|
||||
|
||||
|
||||
# Django neumí jednoduše serializovat partial nebo třídu s __call__
|
||||
# (https://docs.djangoproject.com/en/1.8/topics/migrations/),
|
||||
# neprojdou pak migrace. Takže rozlišení funkcí generujících názvy souboru
|
||||
# podle adresáře řešíme takto.
|
||||
|
||||
##
|
||||
def generate_filename_konfera(self, filename):
|
||||
return os.path.join(
|
||||
settings.SEMINAR_KONFERY_DIR,
|
||||
am.aux_generate_filename(self, filename)
|
||||
)
|
||||
|
||||
##
|
||||
|
||||
@reversion.register(ignore_duplicates=True)
|
||||
class Konfera(am.Problem):
|
||||
class Meta:
|
||||
db_table = 'seminar_konfera'
|
||||
verbose_name = 'Konfera'
|
||||
verbose_name_plural = 'Konfery'
|
||||
|
||||
anotace = models.TextField('anotace', blank=True,
|
||||
help_text='Popis, o čem bude konfera.')
|
||||
|
||||
abstrakt = models.TextField('abstrakt', blank=True,
|
||||
help_text='Abstrakt konfery tak, jak byl uveden ve sborníku')
|
||||
|
||||
# FIXME: Umíme omezit jen na účastníky daného soustřeďka?
|
||||
ucastnici = models.ManyToManyField(pm.Resitel, verbose_name='účastníci konfery',
|
||||
help_text='Seznam účastníků konfery', through='Konfery_Ucastnici')
|
||||
|
||||
soustredeni = models.ForeignKey(Soustredeni, verbose_name='soustředění',
|
||||
related_name='konfery', on_delete = models.SET_NULL, null=True)
|
||||
|
||||
TYP_VELETRH = 'veletrh'
|
||||
TYP_PREZENTACE = 'prezentace'
|
||||
TYP_CHOICES = [
|
||||
(TYP_VELETRH, 'Veletrh (postery)'),
|
||||
(TYP_PREZENTACE, 'Prezentace (přednáška)'),
|
||||
]
|
||||
typ_prezentace = models.CharField('typ prezentace', max_length=16, choices=TYP_CHOICES,
|
||||
blank=False, default=TYP_VELETRH)
|
||||
|
||||
prezentace = models.FileField('prezentace',help_text = 'Prezentace nebo fotka posteru',
|
||||
upload_to = generate_filename_konfera, blank=True)
|
||||
|
||||
materialy = models.FileField('materialy',
|
||||
help_text = 'Další materiály ke konfeře zabalené do jednoho souboru',
|
||||
upload_to = generate_filename_konfera, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return "{}: ({})".format(self.nazev, self.soustredeni)
|
||||
|
||||
def cislo_node(self):
|
||||
return None
|
||||
|
||||
|
||||
@reversion.register(ignore_duplicates=True)
|
||||
class Konfery_Ucastnici(models.Model):
|
||||
|
||||
class Meta:
|
||||
db_table = 'seminar_konfery_ucastnici'
|
||||
verbose_name = 'Účast na konfeře'
|
||||
verbose_name_plural = 'Účasti na konfeře'
|
||||
ordering = ['konfera', 'resitel']
|
||||
|
||||
# Interní ID
|
||||
id = models.AutoField(primary_key = True)
|
||||
|
||||
resitel = models.ForeignKey(pm.Resitel, verbose_name='řešitel', on_delete=models.PROTECT)
|
||||
|
||||
konfera = models.ForeignKey(Konfera, verbose_name='konfera', on_delete=models.CASCADE)
|
||||
|
||||
poznamka = models.TextField('neveřejná poznámka', blank=True,
|
||||
help_text='Neveřejná poznámka k účasti (plain text)')
|
||||
|
||||
def __str__(self):
|
||||
return '{} na {}'.format(self.resitel, self.konfera)
|
||||
# NOTE: Poteciální DB HOG bez select_related
|
||||
|
|
@ -1,758 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
import os
|
||||
import subprocess
|
||||
import pathlib
|
||||
import tempfile
|
||||
import logging
|
||||
|
||||
from django.contrib.sites.shortcuts import get_current_site
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils import timezone
|
||||
from django.conf import settings
|
||||
from django.urls import reverse
|
||||
from django.core.cache import cache
|
||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||
from django.core.files.storage import FileSystemStorage
|
||||
from django.utils.text import get_valid_filename
|
||||
from django.utils.functional import cached_property
|
||||
|
||||
from solo.models import SingletonModel
|
||||
from taggit.managers import TaggableManager
|
||||
|
||||
from reversion import revisions as reversion
|
||||
|
||||
from seminar.utils import roman
|
||||
from treenode import treelib
|
||||
|
||||
from unidecode import unidecode # Používám pro získání ID odkazu (ještě je to někde po někom zakomentované)
|
||||
|
||||
from polymorphic.models import PolymorphicModel
|
||||
|
||||
from django.core.mail import EmailMessage
|
||||
from seminar.utils import aktivniResitele
|
||||
|
||||
from . import personalni as pm
|
||||
|
||||
from .base import SeminarModelBase
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class OverwriteStorage(FileSystemStorage):
|
||||
""" Varianta FileSystemStorage, která v případě, že soubor cílového
|
||||
jména již existuje, ho smaže a místo něj uloží soubor nový"""
|
||||
def get_available_name(self,name, max_length=None):
|
||||
if self.exists(name):
|
||||
os.remove(os.path.join(self.location,name))
|
||||
return super().get_available_name(name,max_length)
|
||||
|
||||
@reversion.register(ignore_duplicates=True)
|
||||
class Rocnik(SeminarModelBase):
|
||||
|
||||
class Meta:
|
||||
db_table = 'seminar_rocniky'
|
||||
verbose_name = 'Ročník'
|
||||
verbose_name_plural = 'Ročníky'
|
||||
ordering = ['-rocnik']
|
||||
|
||||
# Interní ID
|
||||
id = models.AutoField(primary_key = True)
|
||||
|
||||
prvni_rok = models.IntegerField('první rok', db_index=True, unique=True)
|
||||
|
||||
rocnik = models.IntegerField('číslo ročníku', db_index=True, unique=True)
|
||||
|
||||
exportovat = models.BooleanField('export do AESOPa', db_column='exportovat', default=False,
|
||||
help_text='Exportuje se jen podle tohoto flagu (ne veřejnosti),'
|
||||
' a to jen čísla s veřejnou výsledkovkou')
|
||||
|
||||
# má OneToOneField s:
|
||||
# RocnikNode
|
||||
|
||||
def __str__(self):
|
||||
return '{} ({}/{})'.format(self.rocnik, self.prvni_rok, self.prvni_rok+1)
|
||||
|
||||
# Ročník v římských číslech
|
||||
def roman(self):
|
||||
return roman(int(self.rocnik))
|
||||
|
||||
def verejne(self):
|
||||
return len(self.verejna_cisla()) > 0
|
||||
verejne.boolean = True
|
||||
verejne.short_description = 'Veřejný (jen dle čísel)'
|
||||
|
||||
def neverejna_cisla(self):
|
||||
vc = [c for c in self.cisla.all() if not c.verejne()]
|
||||
vc.sort(key=lambda c: c.poradi)
|
||||
return vc
|
||||
|
||||
def verejna_cisla(self):
|
||||
vc = [c for c in self.cisla.all() if c.verejne()]
|
||||
vc.sort(key=lambda c: c.poradi)
|
||||
return vc
|
||||
|
||||
def posledni_verejne_cislo(self):
|
||||
vc = self.verejna_cisla()
|
||||
return vc[-1] if vc else None
|
||||
|
||||
def verejne_vysledkovky_cisla(self):
|
||||
vc = list(self.cisla.filter(deadline_v_cisle__verejna_vysledkovka=True).distinct())
|
||||
vc.sort(key=lambda c: c.poradi)
|
||||
return vc
|
||||
|
||||
def posledni_zverejnena_vysledkovka_cislo(self):
|
||||
vc = self.verejne_vysledkovky_cisla()
|
||||
return vc[-1] if vc else None
|
||||
|
||||
def druhy_rok(self):
|
||||
return self.prvni_rok + 1
|
||||
|
||||
def verejne_url(self):
|
||||
return reverse('seminar_rocnik', kwargs={'rocnik': self.rocnik})
|
||||
|
||||
@classmethod
|
||||
def cached_rocnik(cls, r_id):
|
||||
name = 'rocnik_%s' % (r_id, )
|
||||
c = cache.get(name)
|
||||
if c is None:
|
||||
c = cls.objects.get(id=r_id)
|
||||
cache.set(name, c, 300)
|
||||
return c
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
super().save(*args, **kwargs)
|
||||
# *Node.save() aktualizuje název *Nodu.
|
||||
try:
|
||||
self.rocniknode.save()
|
||||
except ObjectDoesNotExist:
|
||||
# Neexistující *Node nemá smysl aktualizovat.
|
||||
pass
|
||||
|
||||
def cislo_pdf_filename(self, filename):
|
||||
rocnik = str(self.rocnik.rocnik)
|
||||
return pathlib.Path('cislo', 'pdf', rocnik, '{}-{}.pdf'.format(rocnik, self.poradi))
|
||||
|
||||
def cislo_png_filename(self, filename):
|
||||
rocnik = str(self.rocnik.rocnik)
|
||||
return pathlib.Path('cislo', 'png', rocnik, '{}-{}.png'.format(rocnik, self.poradi))
|
||||
|
||||
@reversion.register(ignore_duplicates=True)
|
||||
class Cislo(SeminarModelBase):
|
||||
|
||||
class Meta:
|
||||
db_table = 'seminar_cisla'
|
||||
verbose_name = 'Číslo'
|
||||
verbose_name_plural = 'Čísla'
|
||||
ordering = ['-rocnik__rocnik', '-poradi']
|
||||
|
||||
# Interní ID
|
||||
id = models.AutoField(primary_key = True)
|
||||
|
||||
rocnik = models.ForeignKey(Rocnik, verbose_name='ročník', related_name='cisla',
|
||||
db_index=True,on_delete=models.PROTECT)
|
||||
|
||||
poradi = models.CharField('název čísla', max_length=32, db_index=True,
|
||||
help_text='Většinou jen "1", vyjímečně "7-8", lexikograficky určuje pořadí v ročníku!')
|
||||
|
||||
datum_vydani = models.DateField('datum vydání', blank=True, null=True,
|
||||
help_text='Datum vydání finální verze')
|
||||
|
||||
verejne_db = models.BooleanField('číslo zveřejněno',
|
||||
db_column='verejne', default=False)
|
||||
|
||||
poznamka = models.TextField('neveřejná poznámka', blank=True,
|
||||
help_text='Neveřejná poznámka k číslu (plain text)')
|
||||
|
||||
pdf = models.FileField('pdf', upload_to=cislo_pdf_filename, null=True, blank=True,
|
||||
help_text='PDF čísla, které si mohou řešitelé stáhnout', storage=OverwriteStorage())
|
||||
|
||||
titulka_nahled = models.ImageField('Obrázek titulní strany', upload_to=cislo_png_filename, null=True, blank=True,
|
||||
help_text='Obrázek titulní strany, generuje se automaticky')
|
||||
|
||||
# má OneToOneField s:
|
||||
# CisloNode
|
||||
|
||||
def kod(self):
|
||||
return '%s.%s' % (self.rocnik.rocnik, self.poradi)
|
||||
kod.short_description = 'Kód čísla'
|
||||
|
||||
def __str__(self):
|
||||
# Potenciální DB HOG, pokud by se ročník necachoval
|
||||
r = Rocnik.cached_rocnik(self.rocnik_id)
|
||||
return '{}.{}'.format(r.rocnik, self.poradi)
|
||||
|
||||
def verejne(self):
|
||||
return self.verejne_db
|
||||
verejne.boolean = True
|
||||
|
||||
def verejne_url(self):
|
||||
return reverse('seminar_cislo', kwargs={'rocnik': self.rocnik.rocnik, 'cislo': self.poradi})
|
||||
|
||||
def absolute_url(self):
|
||||
return "https://" + str(get_current_site(None)) + self.verejne_url()
|
||||
|
||||
def nasledujici(self):
|
||||
"Vrací None, pokud je toto poslední"
|
||||
return self.relativni_v_rocniku(1)
|
||||
|
||||
def predchozi(self):
|
||||
"Vrací None, pokud je toto první"
|
||||
return self.relativni_v_rocniku(-1)
|
||||
|
||||
def relativni_v_rocniku(self, rel_index):
|
||||
"Číslo o `index` dále v ročníku. None pokud neexistuje."
|
||||
cs = self.rocnik.cisla.order_by('poradi').all()
|
||||
i = list(cs).index(self) + rel_index
|
||||
if (i < 0) or (i >= len(cs)):
|
||||
return None
|
||||
return cs[i]
|
||||
|
||||
def vygeneruj_nahled(self):
|
||||
VYSKA = 594
|
||||
sirka = int(VYSKA*210/297)
|
||||
if not self.pdf:
|
||||
return
|
||||
|
||||
|
||||
# Pokud obrázek neexistuje nebo není aktuální, vytvoř jej
|
||||
if not self.titulka_nahled or os.path.getmtime(self.titulka_nahled.path) < os.path.getmtime(self.pdf.path):
|
||||
png_filename = pathlib.Path(tempfile.mkdtemp(), 'nahled.png')
|
||||
|
||||
subprocess.run([
|
||||
"gs",
|
||||
"-sstdout=%stderr",
|
||||
"-dSAFER",
|
||||
"-dNOPAUSE",
|
||||
"-dBATCH",
|
||||
"-dNOPROMPT",
|
||||
"-sDEVICE=png16m",
|
||||
"-r300x300",
|
||||
"-dFirstPage=1d",
|
||||
"-dLastPage=1d",
|
||||
"-sOutputFile=" + str(png_filename),
|
||||
"-f%s" % self.pdf.path
|
||||
],
|
||||
check=True,
|
||||
capture_output=True
|
||||
)
|
||||
|
||||
with open(png_filename,'rb') as f:
|
||||
self.titulka_nahled.save('',f,True)
|
||||
|
||||
png_filename.unlink()
|
||||
png_filename.parent.rmdir()
|
||||
|
||||
|
||||
|
||||
@classmethod
|
||||
def get(cls, rocnik, cislo):
|
||||
try:
|
||||
r = Rocnik.objects.get(rocnik=rocnik)
|
||||
c = r.cisla.get(poradi=cislo)
|
||||
except ObjectDoesNotExist:
|
||||
return None
|
||||
return c
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.__original_verejne = self.verejne_db
|
||||
|
||||
def posli_cislo_mailem(self):
|
||||
# parametry e-mailu
|
||||
odkaz = self.absolute_url()
|
||||
|
||||
poslat_z_mailu = 'zadani@mam.mff.cuni.cz'
|
||||
predmet = 'Vyšlo číslo {}'.format(self.kod())
|
||||
# TODO Možná nechceme všem psát „Ahoj“, např. příjemcům…
|
||||
text_mailu = 'Ahoj,\n' \
|
||||
'na adrese {} najdete nejnovější číslo.\n' \
|
||||
'Vaše M&M\n'.format(odkaz)
|
||||
|
||||
predmet_prvni = 'Právě vyšlo 1. číslo M&M, pomoz nám ho poslat dál!'
|
||||
text_mailu_prvni = 'Milý řešiteli,\n'\
|
||||
'právě jsme na našem webu zveřejnili první číslo {}. ročníku, najdeš ho na tomto odkazu: {}.\n\n'\
|
||||
'Doufáme, že tě M&M baví, a byli bychom rádi, kdyby mohlo dělat radost i dalším středoškolákům. Máme na tebe proto jednu prosbu. Sdílej prosím odkaz alespoň s jedním svým kamarádem, který by mohl mít o řešení M&M zájem. Je to pro nás moc důležité a velmi nám tím pomůžeš. Díky!\n\n'\
|
||||
'Organizátoři M&M\n'.format(self.rocnik.rocnik, odkaz)
|
||||
|
||||
predmet_resitel = predmet_prvni if self.poradi == "1" else predmet
|
||||
text_mailu_resitel = text_mailu_prvni if self.poradi == "1" else text_mailu
|
||||
|
||||
|
||||
# Prijemci e-mailu
|
||||
resitele_vsichni = aktivniResitele(self).filter(zasilat_cislo_emailem=True)
|
||||
|
||||
def posli(subject, text, resitele):
|
||||
emaily = map(lambda resitel: resitel.osoba.email, resitele)
|
||||
if not settings.POSLI_MAILOVOU_NOTIFIKACI:
|
||||
print("Poslal bych upozornění na tyto adresy: ", " ".join(emaily))
|
||||
return
|
||||
|
||||
email = EmailMessage(
|
||||
subject=subject,
|
||||
body=text,
|
||||
from_email=poslat_z_mailu,
|
||||
bcc=list(emaily)
|
||||
#bcc = příjemci skryté kopie
|
||||
)
|
||||
|
||||
email.send()
|
||||
|
||||
paticka = "---\nK odběru těchto e-mailů jste se přihlásili na stránkách https://mam.matfyz.cz. Z odběru se lze odhlásit na https://mam.matfyz.cz/resitel/osobni-udaje/"
|
||||
|
||||
posli(predmet_resitel, text_mailu_resitel + paticka, resitele_vsichni.filter(zasilat_cislo_papirove=False))
|
||||
posli(predmet_resitel, text_mailu_resitel + 'P. S. Brzy budeme též rozesílat papírovou verzi čísla. Připomínáme, že pokud papírovou verzi čísla nevyužijete, můžete v https://mam.mff.cuni.cz/resitel/osobni-udaje/ zaškrtnout, abychom vám ji neposílali. Čísla vždy můžete nalézt v našem archivu a dál vám budou chodit e-mailem. Děkujeme.\n' + paticka,
|
||||
resitele_vsichni.filter(zasilat_cislo_papirove=True))
|
||||
|
||||
paticka_prijemce = "---\nPokud tyto e-maily nechcete nadále dostávat, prosíme, ozvěte se nám na mam@matfyz.cz."
|
||||
posli(predmet, text_mailu + paticka_prijemce, pm.Prijemce.objects.filter(zasilat_cislo_emailem=True))
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
super().save(*args, **kwargs)
|
||||
self.vygeneruj_nahled()
|
||||
# Při zveřejnění pošle mail
|
||||
if self.verejne_db and not self.__original_verejne:
|
||||
self.posli_cislo_mailem()
|
||||
# *Node.save() aktualizuje název *Nodu.
|
||||
try:
|
||||
self.cislonode.save()
|
||||
except ObjectDoesNotExist:
|
||||
# Neexistující *Node nemá smysl aktualizovat, ale je potřeba ho naopak vyrobit
|
||||
logger.warning(f'Číslo {self} nemělo ČísloNode, vyrábím…')
|
||||
from seminar.models.treenode import CisloNode
|
||||
CisloNode.objects.create(cislo=self)
|
||||
|
||||
def zlomovy_deadline_pro_papirove_cislo(self):
|
||||
prvni_deadline = Deadline.objects.filter(Q(typ=Deadline.TYP_PRVNI) | Q(typ=Deadline.TYP_PRVNI_A_SOUS), cislo=self).first()
|
||||
if prvni_deadline is None:
|
||||
posledni_deadline = self.posledni_deadline
|
||||
if posledni_deadline is None:
|
||||
# TODO promyslet, co se má stát tady
|
||||
return Deadline.objects.filter(Q(cislo__poradi__lt=self.poradi, cislo__rocnik=self.rocnik) | Q(cislo__rocnik__rocnik__lt=self.rocnik.rocnik)).order_by("deadline").last()
|
||||
return posledni_deadline
|
||||
return prvni_deadline
|
||||
|
||||
@property
|
||||
def posledni_deadline(self):
|
||||
return self.deadline_v_cisle.all().order_by("deadline").last()
|
||||
|
||||
class Deadline(SeminarModelBase):
|
||||
class Meta:
|
||||
db_table = 'seminar_deadliny'
|
||||
verbose_name = 'Deadline'
|
||||
verbose_name_plural = 'Deadliny'
|
||||
ordering = ['deadline']
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.__original_verejna_vysledkovka = self.verejna_vysledkovka
|
||||
|
||||
id = models.AutoField(primary_key=True)
|
||||
|
||||
# V ročníku < 26 nastaveno na datetime.datetime.combine(datetime.date(1994 + cislo.rocnik.rocnik, 6, int(cislo.poradi[0])), datetime.time.min)
|
||||
deadline = models.DateTimeField(blank=False, default=timezone.make_aware(datetime.datetime.combine(timezone.now(), datetime.time.max)))
|
||||
|
||||
cislo = models.ForeignKey(Cislo, verbose_name='deadline v čísle',
|
||||
related_name='deadline_v_cisle', blank=False,
|
||||
on_delete=models.CASCADE)
|
||||
|
||||
TYP_CISLA = 'cisla'
|
||||
TYP_PRVNI_A_SOUS = 'prvniasous'
|
||||
TYP_PRVNI = 'prvni'
|
||||
TYP_SOUS = 'sous'
|
||||
TYP_CHOICES = [
|
||||
(TYP_CISLA, 'Deadline celého čísla'),
|
||||
(TYP_PRVNI, 'První deadline'),
|
||||
(TYP_PRVNI_A_SOUS, 'Sousový a první deadline'),
|
||||
(TYP_SOUS, 'Sousový deadline'),
|
||||
]
|
||||
CHOICES_MAP = dict(TYP_CHOICES)
|
||||
typ = models.CharField('typ deadlinu', max_length=32,
|
||||
choices=TYP_CHOICES, blank=False)
|
||||
|
||||
verejna_vysledkovka = models.BooleanField('veřejná výsledkovka',
|
||||
db_column='verejna_vysledkovka',
|
||||
default=False)
|
||||
|
||||
def __str__(self):
|
||||
return self.CHOICES_MAP[self.typ] + " " + str(self.cislo)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
super().save(*args, **kwargs)
|
||||
if self.verejna_vysledkovka and not self.__original_verejna_vysledkovka:
|
||||
self.vygeneruj_vysledkovku()
|
||||
if not self.verejna_vysledkovka and hasattr(self, "vysledkovka_v_deadlinu"):
|
||||
self.vysledkovka_v_deadlinu.delete()
|
||||
|
||||
def vygeneruj_vysledkovku(self):
|
||||
from vysledkovky.utils import VysledkovkaCisla
|
||||
if hasattr(self, "vysledkovka_v_deadlinu"):
|
||||
self.vysledkovka_v_deadlinu.delete()
|
||||
vysledkovka = VysledkovkaCisla(self.cislo, jen_verejne=True, do_deadlinu=self)
|
||||
if len(vysledkovka.radky_vysledkovky) != 0:
|
||||
ZmrazenaVysledkovka.objects.create(
|
||||
deadline=self,
|
||||
html=render_to_string(
|
||||
"vysledkovky/vysledkovka_cisla.html",
|
||||
context={"vysledkovka": vysledkovka, "oznaceni_vysledkovky": self.id}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class ZmrazenaVysledkovka(SeminarModelBase):
|
||||
class Meta:
|
||||
db_table = 'seminar_vysledkovky'
|
||||
verbose_name = 'Zmražená výsledkovka'
|
||||
verbose_name_plural = 'Zmražené výsledkovky'
|
||||
|
||||
deadline = models.OneToOneField(
|
||||
Deadline,
|
||||
on_delete=models.CASCADE,
|
||||
primary_key=True,
|
||||
related_name="vysledkovka_v_deadlinu"
|
||||
)
|
||||
|
||||
html = models.TextField(null=False, blank=False)
|
||||
|
||||
|
||||
@reversion.register(ignore_duplicates=True)
|
||||
# Pozor na následující řádek. *Nekrmit, asi kouše!*
|
||||
class Problem(SeminarModelBase,PolymorphicModel):
|
||||
|
||||
class Meta:
|
||||
# Není abstraktní, protože se na něj jinak nedají dělat ForeignKeys.
|
||||
# TODO: Udělat to polymorfní (pomocí django-polymorphic), abychom dostali
|
||||
# po těch vazbách přímo tu úlohu/témátko vč. fieldů, které nejsou součástí
|
||||
# modelu Problem?
|
||||
|
||||
#abstract = True
|
||||
db_table = 'seminar_problemy'
|
||||
verbose_name = 'Problém'
|
||||
verbose_name_plural = 'Problémy'
|
||||
ordering = ['nazev']
|
||||
|
||||
# Interní ID
|
||||
id = models.AutoField(primary_key = True)
|
||||
|
||||
# Název
|
||||
nazev = models.CharField('název', max_length=256) # Zveřejnitelný na stránky
|
||||
|
||||
# Problém má podproblémy
|
||||
nadproblem = models.ForeignKey('self', verbose_name='nadřazený problém',
|
||||
related_name='podproblem', null=True, blank=True,
|
||||
on_delete=models.SET_NULL)
|
||||
|
||||
STAV_NAVRH = 'navrh'
|
||||
STAV_ZADANY = 'zadany'
|
||||
STAV_VYRESENY = 'vyreseny'
|
||||
STAV_SMAZANY = 'smazany'
|
||||
STAV_CHOICES = [
|
||||
(STAV_NAVRH, 'Návrh'),
|
||||
(STAV_ZADANY, 'Zadaný'),
|
||||
(STAV_VYRESENY, 'Vyřešený'),
|
||||
(STAV_SMAZANY, 'Smazaný'),
|
||||
]
|
||||
stav = models.CharField('stav problému', max_length=32, choices=STAV_CHOICES, blank=False, default=STAV_NAVRH)
|
||||
# Téma je taky Problém, takže má stavy, "zadané" témátko je aktuálně otevřené a dá se k němu něco poslat (řešení nebo článek)
|
||||
|
||||
zamereni = TaggableManager(verbose_name='zaměření',
|
||||
help_text='Zaměření M/F/I/O problému, příp. další tagy', blank=True)
|
||||
|
||||
poznamka = models.TextField('org poznámky (HTML)', blank=True,
|
||||
help_text='Neveřejný návrh úlohy, návrh řešení, text zadání, poznámky ...')
|
||||
|
||||
autor = models.ForeignKey(pm.Organizator, verbose_name='autor problému',
|
||||
related_name='autor_problemu_%(class)s', null=True, blank=True,
|
||||
on_delete=models.SET_NULL)
|
||||
|
||||
garant = models.ForeignKey(pm.Organizator, verbose_name='garant zadaného problému',
|
||||
related_name='garant_problemu_%(class)s', null=True, blank=True,
|
||||
on_delete=models.SET_NULL)
|
||||
|
||||
opravovatele = models.ManyToManyField(pm.Organizator, verbose_name='opravovatelé',
|
||||
blank=True, related_name='opravovatele_%(class)s')
|
||||
|
||||
kod = models.CharField('lokální kód', max_length=32, blank=True, default='',
|
||||
help_text='Číslo/kód úlohy v čísle nebo kód tématu/článku/seriálu v ročníku')
|
||||
|
||||
|
||||
vytvoreno = models.DateTimeField('vytvořeno', default=timezone.now, blank=True, editable=False)
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return self.nazev
|
||||
|
||||
# Implicitini implementace, jednotlivé dědící třídy si přepíšou
|
||||
@cached_property
|
||||
def kod_v_rocniku(self):
|
||||
if self.stav == Problem.STAV_ZADANY or self.stav == Problem.STAV_VYRESENY:
|
||||
if self.nadproblem:
|
||||
return self.nadproblem.kod_v_rocniku+".{}".format(self.kod)
|
||||
return str(self.kod)
|
||||
logger.warning(f"K problému {self} byl vyžadován kód v ročníku, i když není zadaný ani vyřešený.")
|
||||
return '<Není zadaný>'
|
||||
|
||||
# def verejne(self):
|
||||
# # aktuálně podle stavu problému
|
||||
# # FIXME pro některé problémy možná chceme override
|
||||
# # FIXME vrací veřejnost čistě problému, nezávisle na čísle, ve kterém je.
|
||||
# # Je to tak správně? Podle aktuální představy ano.
|
||||
# stav_verejny = False
|
||||
# if self.stav == 'zadany' or self.stav == 'vyreseny':
|
||||
# stav_verejny = True
|
||||
# print("stav_verejny: {}".format(stav_verejny))
|
||||
#
|
||||
# cislo_verejne = False
|
||||
# cislonode = self.cislo_node()
|
||||
# if cislonode is None:
|
||||
# # problém nemá vlastní node, veřejnost posuzujeme jen podle stavu
|
||||
# print("empty node")
|
||||
# return stav_verejny
|
||||
# else:
|
||||
# cislo_zadani = cislonode.cislo
|
||||
# if (cislo_zadani and cislo_zadani.verejne()):
|
||||
# print("cislo: {}".format(cislo_zadani))
|
||||
# cislo_verejne = True
|
||||
# print("stav_verejny: {}".format(stav_verejny))
|
||||
# print("cislo_verejne: {}".format(cislo_verejne))
|
||||
# return (stav_verejny and cislo_verejne)
|
||||
# verejne.boolean = True
|
||||
|
||||
def verejne_url(self):
|
||||
return reverse('seminar_problem', kwargs={'pk': self.id})
|
||||
|
||||
def admin_url(self):
|
||||
return reverse('admin:seminar_problem_change', args=(self.id, ))
|
||||
|
||||
@cached_property
|
||||
def hlavni_problem(self):
|
||||
""" Pro daný problém vrátí jeho nejvyšší nadproblém."""
|
||||
problem = self
|
||||
while not (problem.nadproblem is None):
|
||||
problem = problem.nadproblem
|
||||
return problem
|
||||
|
||||
# FIXME - k úloze
|
||||
def body_v_zavorce(self):
|
||||
"""Vrať string s body v závorce jsou-li u problému vyplněné, jinak ''
|
||||
|
||||
Je-li desetinná část nulová, nezobrazuj ji.
|
||||
"""
|
||||
pocet_bodu = None
|
||||
if self.body:
|
||||
b = self.body
|
||||
pocet_bodu = int(b) if int(b) == b else b
|
||||
return "({}\u2009b)".format(pocet_bodu) if self.body else ""
|
||||
|
||||
class Tema(Problem):
|
||||
class Meta:
|
||||
db_table = 'seminar_temata'
|
||||
verbose_name = 'Téma'
|
||||
verbose_name_plural = 'Témata'
|
||||
|
||||
TEMA_TEMA = 'tema'
|
||||
TEMA_SERIAL = 'serial'
|
||||
TEMA_CHOICES = [
|
||||
(TEMA_TEMA, 'Téma'),
|
||||
(TEMA_SERIAL, 'Seriál'),
|
||||
]
|
||||
tema_typ = models.CharField('Typ tématu', max_length=16, choices=TEMA_CHOICES,
|
||||
blank=False, default=TEMA_TEMA)
|
||||
|
||||
rocnik = models.ForeignKey(Rocnik, verbose_name='ročník',related_name='temata',blank=True, null=True,
|
||||
on_delete=models.PROTECT)
|
||||
|
||||
abstrakt = models.TextField('Abstrakt na rozcestník', blank=True)
|
||||
obrazek = models.ImageField('Obrázek na rozcestník', null=True, blank=True)
|
||||
|
||||
@cached_property
|
||||
def kod_v_rocniku(self):
|
||||
if self.stav == Problem.STAV_ZADANY or self.stav == Problem.STAV_VYRESENY:
|
||||
if self.nadproblem:
|
||||
return self.nadproblem.kod_v_rocniku+".t{}".format(self.kod)
|
||||
return "t{}".format(self.kod)
|
||||
logger.warning(f"K problému {self} byl vyžadován kód v ročníku, i když není zadaný ani vyřešený.")
|
||||
return '<Není zadaný>'
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
super().save(*args, **kwargs)
|
||||
# *Node.save() aktualizuje název *Nodu.
|
||||
for tvcn in self.temavcislenode_set.all():
|
||||
tvcn.save()
|
||||
|
||||
def cislo_node(self):
|
||||
tema_node_set = self.temavcislenode_set.all()
|
||||
tema_cisla_vyskyt = []
|
||||
from seminar.models.treenode import CisloNode
|
||||
for tn in tema_node_set:
|
||||
tema_cisla_vyskyt.append(
|
||||
treelib.get_upper_node_of_type(tn, CisloNode).cislo)
|
||||
tema_cisla_vyskyt.sort(key=lambda x:x.datum_vydani)
|
||||
prvni_zadani = tema_cisla_vyskyt[0]
|
||||
return prvni_zadani.cislonode
|
||||
|
||||
class Clanek(Problem):
|
||||
class Meta:
|
||||
db_table = 'seminar_clanky'
|
||||
verbose_name = 'Článek'
|
||||
verbose_name_plural = 'Články'
|
||||
|
||||
cislo = models.ForeignKey(Cislo, blank=True, null=True, on_delete=models.PROTECT,
|
||||
verbose_name='číslo vydání', related_name='vydane_clanky')
|
||||
|
||||
@cached_property
|
||||
def kod_v_rocniku(self):
|
||||
if self.stav == Problem.STAV_ZADANY or self.stav == Problem.STAV_VYRESENY:
|
||||
# Nemělo by být potřeba
|
||||
# if self.nadproblem:
|
||||
# return self.nadproblem.kod_v_rocniku+".c{}".format(self.kod)
|
||||
return "c{}".format(self.kod)
|
||||
logger.warning(f"K problému {self} byl vyžadován kód v ročníku, i když není zadaný ani vyřešený.")
|
||||
return '<Není zadaný>'
|
||||
|
||||
def node(self):
|
||||
return None
|
||||
|
||||
|
||||
class Uloha(Problem):
|
||||
class Meta:
|
||||
db_table = 'seminar_ulohy'
|
||||
verbose_name = 'Úloha'
|
||||
verbose_name_plural = 'Úlohy'
|
||||
|
||||
cislo_zadani = models.ForeignKey(Cislo, verbose_name='číslo zadání', blank=True,
|
||||
null=True, related_name='zadane_ulohy', on_delete=models.PROTECT)
|
||||
|
||||
cislo_deadline = models.ForeignKey(Cislo, verbose_name='číslo deadlinu', blank=True,
|
||||
null=True, related_name='deadlinove_ulohy', on_delete=models.PROTECT)
|
||||
|
||||
cislo_reseni = models.ForeignKey(Cislo, verbose_name='číslo řešení', blank=True,
|
||||
null=True, related_name='resene_ulohy',
|
||||
help_text='Číslo s řešením úlohy, jen pro úlohy',
|
||||
on_delete=models.PROTECT)
|
||||
|
||||
max_body = models.DecimalField(max_digits=8, decimal_places=1, verbose_name='maximum bodů',
|
||||
blank=True, null=True)
|
||||
|
||||
# má OneToOneField s:
|
||||
# UlohaZadaniNode
|
||||
# UlohaVzorakNode
|
||||
|
||||
@cached_property
|
||||
def kod_v_rocniku(self):
|
||||
if self.stav == Problem.STAV_ZADANY or self.stav == Problem.STAV_VYRESENY:
|
||||
name="{}.u{}".format(self.cislo_zadani.poradi,self.kod)
|
||||
if self.nadproblem:
|
||||
return self.nadproblem.kod_v_rocniku+name
|
||||
return name
|
||||
logger.warning(f"K problému {self} byl vyžadován kód v ročníku, i když není zadaný ani vyřešený.")
|
||||
return '<Není zadaný>'
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
super().save(*args, **kwargs)
|
||||
# *Node.save() aktualizuje název *Nodu.
|
||||
try:
|
||||
self.ulohazadaninode.save()
|
||||
except ObjectDoesNotExist:
|
||||
# Neexistující *Node nemá smysl aktualizovat.
|
||||
pass
|
||||
try:
|
||||
self.ulohavzoraknode.save()
|
||||
except ObjectDoesNotExist:
|
||||
# Neexistující *Node nemá smysl aktualizovat.
|
||||
pass
|
||||
|
||||
def cislo_node(self):
|
||||
zadani_node = self.ulohazadaninode
|
||||
from seminar.models.treenode import CisloNode
|
||||
return treelib.get_upper_node_of_type(zadani_node, CisloNode)
|
||||
|
||||
|
||||
def aux_generate_filename(self, filename):
|
||||
"""Pomocná funkce generující ošetřený název souboru v adresáři s datem"""
|
||||
clean = get_valid_filename(
|
||||
unidecode(filename.replace('/', '-').replace('\0', ''))
|
||||
)
|
||||
datedir = timezone.now().strftime('%Y-%m')
|
||||
fname = "{}/{}".format(
|
||||
timezone.now().strftime('%Y-%m-%d-%H:%M'),
|
||||
clean)
|
||||
return os.path.join(datedir, fname)
|
||||
|
||||
|
||||
class Pohadka(SeminarModelBase):
|
||||
"""Kus pohádky před/za úlohou v čísle"""
|
||||
|
||||
class Meta:
|
||||
db_table = 'seminar_pohadky'
|
||||
verbose_name = 'Pohádka'
|
||||
verbose_name_plural = 'Pohádky'
|
||||
ordering = ['vytvoreno']
|
||||
|
||||
# Interní ID
|
||||
id = models.AutoField(primary_key=True)
|
||||
|
||||
autor = models.ForeignKey(
|
||||
pm.Organizator,
|
||||
verbose_name="Autor pohádky",
|
||||
|
||||
# Při nahrávání z TeXu není vyplnění vyžadováno, v adminu je
|
||||
null=True,
|
||||
blank=False,
|
||||
on_delete=models.SET_NULL
|
||||
)
|
||||
|
||||
vytvoreno = models.DateTimeField(
|
||||
'Vytvořeno',
|
||||
default=timezone.now,
|
||||
blank=True,
|
||||
editable=False
|
||||
)
|
||||
|
||||
# má OneToOneField s:
|
||||
# PohadkaNode
|
||||
|
||||
def __str__(self):
|
||||
uryvek = self.text if len(self.text) < 50 else self.text[:(50-3)]+"..."
|
||||
return uryvek
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
super().save(*args, **kwargs)
|
||||
# *Node.save() aktualizuje název *Nodu.
|
||||
try:
|
||||
self.pohadkanode.save()
|
||||
except ObjectDoesNotExist:
|
||||
# Neexistující *Node nemá smysl aktualizovat.
|
||||
pass
|
||||
|
||||
|
||||
@reversion.register(ignore_duplicates=True)
|
||||
class Nastaveni(SingletonModel):
|
||||
|
||||
class Meta:
|
||||
db_table = 'seminar_nastaveni'
|
||||
verbose_name = 'Nastavení semináře'
|
||||
|
||||
# aktualni_rocnik = models.ForeignKey(Rocnik, verbose_name='aktuální ročník',
|
||||
# null=False, on_delete=models.PROTECT)
|
||||
|
||||
aktualni_cislo = models.ForeignKey(Cislo, verbose_name='Aktuální číslo',
|
||||
null=False, on_delete=models.PROTECT)
|
||||
|
||||
cena_sous = models.IntegerField(null=False,
|
||||
verbose_name="Účastnický poplatek za soustředění",
|
||||
default=1000)
|
||||
|
||||
@property
|
||||
def aktualni_rocnik(self):
|
||||
return self.aktualni_cislo.rocnik
|
||||
|
||||
def __str__(self):
|
||||
return 'Nastavení semináře'
|
||||
|
||||
def admin_url(self):
|
||||
return reverse('admin:seminar_nastaveni_change', args=(self.id, ))
|
||||
|
||||
def verejne(self):
|
||||
return False
|
||||
|
|
@ -1,30 +1,30 @@
|
|||
from django import template
|
||||
from django.utils.safestring import mark_safe
|
||||
register = template.Library()
|
||||
import seminar.models as m
|
||||
from tvorba.models.deadline import Deadline
|
||||
|
||||
@register.filter(name='deadline_kratseji')
|
||||
def deadline_kratsi_text(deadline: m.Deadline):
|
||||
def deadline_kratsi_text(deadline: Deadline):
|
||||
if deadline is None:
|
||||
return 'NONE'
|
||||
strings = {
|
||||
m.Deadline.TYP_PRVNI: f"{deadline.cislo} ⭯",
|
||||
m.Deadline.TYP_SOUS: f"{deadline.cislo} Ⓢ",
|
||||
m.Deadline.TYP_PRVNI_A_SOUS: f"{deadline.cislo} ⭯Ⓢ",
|
||||
m.Deadline.TYP_CISLA: f"{deadline.cislo} ✓",
|
||||
Deadline.TYP_PRVNI: f"{deadline.cislo} ⭯",
|
||||
Deadline.TYP_SOUS: f"{deadline.cislo} Ⓢ",
|
||||
Deadline.TYP_PRVNI_A_SOUS: f"{deadline.cislo} ⭯Ⓢ",
|
||||
Deadline.TYP_CISLA: f"{deadline.cislo} ✓",
|
||||
}
|
||||
return strings[deadline.typ]
|
||||
|
||||
@register.filter(name='deadline_html')
|
||||
def deadline_html(deadline: m.Deadline):
|
||||
def deadline_html(deadline: Deadline):
|
||||
if deadline is None:
|
||||
return 'Neznámý deadline'
|
||||
text = deadline_kratsi_text(deadline)
|
||||
classes = {
|
||||
m.Deadline.TYP_PRVNI: 'preddeadline',
|
||||
m.Deadline.TYP_SOUS: 'sous_deadline',
|
||||
m.Deadline.TYP_PRVNI_A_SOUS: 'sous_deadline',
|
||||
m.Deadline.TYP_CISLA: 'final_deadline',
|
||||
Deadline.TYP_PRVNI: 'preddeadline',
|
||||
Deadline.TYP_SOUS: 'sous_deadline',
|
||||
Deadline.TYP_PRVNI_A_SOUS: 'sous_deadline',
|
||||
Deadline.TYP_CISLA: 'final_deadline',
|
||||
}
|
||||
return mark_safe(f'<span class="{classes[deadline.typ]}" title="{deadline}">{text}</span>')
|
||||
|
||||
|
|
|
|||
|
|
@ -9,11 +9,17 @@ from django.db.models import Q, Sum, Count
|
|||
from django.views.generic.base import RedirectView
|
||||
from django.core.exceptions import PermissionDenied
|
||||
|
||||
import seminar.models as s
|
||||
import seminar.models as m
|
||||
from seminar.models import Problem, Cislo, Reseni, Nastaveni, Rocnik, \
|
||||
Organizator, Resitel, Novinky, Tema, Clanek, \
|
||||
Deadline # Tohle je stare a chceme se toho zbavit. Pouzivejte s.ToCoChci
|
||||
from tvorba.models.problem import Problem
|
||||
from tvorba.models.tema import Tema
|
||||
from tvorba.models.clanek import Clanek
|
||||
from tvorba.models.nastaveni import Nastaveni
|
||||
from tvorba.models.rocnik import Rocnik
|
||||
from tvorba.models.cislo import Cislo
|
||||
from tvorba.models.deadline import Deadline
|
||||
from personalni.models.organizator import Organizator
|
||||
from personalni.models.resitel import Resitel
|
||||
from seminar.models.novinky import Novinky
|
||||
|
||||
#from .models import VysledkyZaCislo, VysledkyKCisluZaRocnik, VysledkyKCisluOdjakziva
|
||||
from seminar import utils
|
||||
from treenode import treelib
|
||||
|
|
@ -113,7 +119,7 @@ def ZadaniTemataView(request):
|
|||
nastaveni = get_object_or_404(Nastaveni)
|
||||
verejne = nastaveni.aktualni_cislo.verejne()
|
||||
akt_rocnik = nastaveni.aktualni_cislo.rocnik
|
||||
temata = s.Tema.objects.filter(rocnik=akt_rocnik, stav='zadany')
|
||||
temata = Tema.objects.filter(rocnik=akt_rocnik, stav='zadany')
|
||||
return render(request, 'seminar/tematka/rozcestnik.html',
|
||||
{
|
||||
'tematka': temata,
|
||||
|
|
@ -244,7 +250,7 @@ class TitulniStranaView(generic.ListView):
|
|||
context = super(TitulniStranaView, self).get_context_data(**kwargs)
|
||||
nastaveni = get_object_or_404(Nastaveni)
|
||||
|
||||
deadline = m.Deadline.objects.filter(deadline__gte=timezone.now()).order_by("deadline").first()
|
||||
deadline = Deadline.objects.filter(deadline__gte=timezone.now()).order_by("deadline").first()
|
||||
context['nejblizsi_deadline'] = deadline
|
||||
|
||||
# Aktuální témata
|
||||
|
|
@ -349,7 +355,7 @@ def resiteleRocnikuCsvExportView(request, rocnik):
|
|||
assert request.method in ('GET', 'HEAD')
|
||||
return dataResiteluCsvResponse(
|
||||
utils.resi_v_rocniku(
|
||||
get_object_or_404(m.Rocnik, rocnik=rocnik)
|
||||
get_object_or_404(Rocnik, rocnik=rocnik)
|
||||
)
|
||||
)
|
||||
|
||||
|
|
@ -411,10 +417,10 @@ class CisloView(generic.DetailView):
|
|||
deadliny_s_vysledkovkami = []
|
||||
|
||||
nadpisy = {
|
||||
m.Deadline.TYP_CISLA: "Výsledkovka",
|
||||
m.Deadline.TYP_PRVNI: "Výsledkovka do prvního deadlinu",
|
||||
m.Deadline.TYP_PRVNI_A_SOUS: "Výsledkovka do prvního deadlinu a deadlinu pro účast na soustředění",
|
||||
m.Deadline.TYP_SOUS: "Výsledkovka do deadlinu pro účast na soustředění",
|
||||
Deadline.TYP_CISLA: "Výsledkovka",
|
||||
Deadline.TYP_PRVNI: "Výsledkovka do prvního deadlinu",
|
||||
Deadline.TYP_PRVNI_A_SOUS: "Výsledkovka do prvního deadlinu a deadlinu pro účast na soustředění",
|
||||
Deadline.TYP_SOUS: "Výsledkovka do deadlinu pro účast na soustředění",
|
||||
}
|
||||
|
||||
for deadline in deadliny:
|
||||
|
|
@ -703,5 +709,5 @@ class AktualniRocnikRedirectView(RedirectView):
|
|||
pattern_name = 'seminar_rocnik'
|
||||
|
||||
def get_redirect_url(self, *args, **kwargs):
|
||||
aktualni_rocnik = m.Nastaveni.get_solo().aktualni_rocnik.rocnik
|
||||
aktualni_rocnik = Nastaveni.get_solo().aktualni_rocnik.rocnik
|
||||
return super().get_redirect_url(rocnik=aktualni_rocnik, *args, **kwargs)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
from django.contrib import admin
|
||||
from django.forms import widgets
|
||||
from django.db import models
|
||||
from polymorphic.admin import PolymorphicChildModelAdmin
|
||||
|
||||
from seminar.models import soustredeni as m
|
||||
from soustredeni import models as m
|
||||
|
||||
from tvorba.admin import ProblemAdminMixin
|
||||
|
||||
|
||||
class SoustredeniUcastniciInline(admin.TabularInline):
|
||||
|
|
@ -41,3 +44,8 @@ class SoustredeniAdmin(admin.ModelAdmin):
|
|||
inline_type = 'tabular'
|
||||
inlines = [SoustredeniUcastniciInline, SoustredeniOrganizatoriInline]
|
||||
|
||||
|
||||
@admin.register(m.Konfera)
|
||||
class KonferaAdmin(ProblemAdminMixin, PolymorphicChildModelAdmin):
|
||||
base_model = m.Konfera
|
||||
|
||||
|
|
|
|||
5
soustredeni/models/__init__.py
Normal file
5
soustredeni/models/__init__.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
from .soustredeni import Soustredeni
|
||||
from .soustredeni_ucastnici import Soustredeni_Ucastnici
|
||||
from .soustredeni_organizatori import Soustredeni_Organizatori
|
||||
from .konfera import Konfera
|
||||
from .konfery_ucastnici import Konfery_Ucastnici
|
||||
83
soustredeni/models/konfera.py
Normal file
83
soustredeni/models/konfera.py
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import os
|
||||
from reversion import revisions as reversion
|
||||
|
||||
from django.db import models
|
||||
from django.conf import settings
|
||||
|
||||
from various.utils import aux_generate_filename
|
||||
|
||||
from personalni.models.resitel import Resitel
|
||||
from tvorba.models.problem import Problem
|
||||
from .soustredeni import Soustredeni
|
||||
|
||||
|
||||
# Django neumí jednoduše serializovat partial nebo třídu s __call__
|
||||
# (https://docs.djangoproject.com/en/1.8/topics/migrations/),
|
||||
# neprojdou pak migrace. Takže rozlišení funkcí generujících názvy souboru
|
||||
# podle adresáře řešíme takto.
|
||||
|
||||
##
|
||||
def generate_filename_konfera(self, filename):
|
||||
return os.path.join(
|
||||
settings.SEMINAR_KONFERY_DIR,
|
||||
aux_generate_filename(self, filename)
|
||||
)
|
||||
|
||||
##
|
||||
|
||||
|
||||
@reversion.register(ignore_duplicates=True)
|
||||
class Konfera(Problem):
|
||||
class Meta:
|
||||
db_table = 'seminar_konfera'
|
||||
verbose_name = 'Konfera'
|
||||
verbose_name_plural = 'Konfery'
|
||||
|
||||
anotace = models.TextField(
|
||||
'anotace', blank=True,
|
||||
help_text='Popis, o čem bude konfera.',
|
||||
)
|
||||
|
||||
abstrakt = models.TextField(
|
||||
'abstrakt', blank=True,
|
||||
help_text='Abstrakt konfery tak, jak byl uveden ve sborníku',
|
||||
)
|
||||
|
||||
# FIXME: Umíme omezit jen na účastníky daného soustřeďka?
|
||||
ucastnici = models.ManyToManyField(
|
||||
Resitel, verbose_name='účastníci konfery',
|
||||
help_text='Seznam účastníků konfery', through='Konfery_Ucastnici',
|
||||
)
|
||||
|
||||
soustredeni = models.ForeignKey(
|
||||
Soustredeni, verbose_name='soustředění',
|
||||
related_name='konfery', on_delete=models.SET_NULL, null=True,
|
||||
)
|
||||
|
||||
TYP_VELETRH = 'veletrh'
|
||||
TYP_PREZENTACE = 'prezentace'
|
||||
TYP_CHOICES = [
|
||||
(TYP_VELETRH, 'Veletrh (postery)'),
|
||||
(TYP_PREZENTACE, 'Prezentace (přednáška)'),
|
||||
]
|
||||
typ_prezentace = models.CharField(
|
||||
'typ prezentace', max_length=16, choices=TYP_CHOICES,
|
||||
blank=False, default=TYP_VELETRH,
|
||||
)
|
||||
|
||||
prezentace = models.FileField(
|
||||
'prezentace', help_text='Prezentace nebo fotka posteru',
|
||||
upload_to=generate_filename_konfera, blank=True,
|
||||
)
|
||||
|
||||
materialy = models.FileField(
|
||||
'materialy',
|
||||
help_text='Další materiály ke konfeře zabalené do jednoho souboru',
|
||||
upload_to=generate_filename_konfera, blank=True,
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return "{}: ({})".format(self.nazev, self.soustredeni)
|
||||
|
||||
def cislo_node(self):
|
||||
return None
|
||||
35
soustredeni/models/konfery_ucastnici.py
Normal file
35
soustredeni/models/konfery_ucastnici.py
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
from reversion import revisions as reversion
|
||||
|
||||
from django.db import models
|
||||
|
||||
from personalni.models.resitel import Resitel
|
||||
from .konfera import Konfera
|
||||
|
||||
|
||||
@reversion.register(ignore_duplicates=True)
|
||||
class Konfery_Ucastnici(models.Model):
|
||||
class Meta:
|
||||
db_table = 'seminar_konfery_ucastnici'
|
||||
verbose_name = 'Účast na konfeře'
|
||||
verbose_name_plural = 'Účasti na konfeře'
|
||||
ordering = ['konfera', 'resitel']
|
||||
|
||||
# Interní ID
|
||||
id = models.AutoField(primary_key=True)
|
||||
|
||||
resitel = models.ForeignKey(
|
||||
Resitel, verbose_name='řešitel', on_delete=models.PROTECT,
|
||||
)
|
||||
|
||||
konfera = models.ForeignKey(
|
||||
Konfera, verbose_name='konfera', on_delete=models.CASCADE,
|
||||
)
|
||||
|
||||
poznamka = models.TextField(
|
||||
'neveřejná poznámka', blank=True,
|
||||
help_text='Neveřejná poznámka k účasti (plain text)',
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return '{} na {}'.format(self.resitel, self.konfera)
|
||||
# NOTE: Poteciální DB HOG bez select_related
|
||||
92
soustredeni/models/soustredeni.py
Normal file
92
soustredeni/models/soustredeni.py
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
from reversion import revisions as reversion
|
||||
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
|
||||
from mamweb.models.base import SeminarModelBase
|
||||
|
||||
from personalni.models.resitel import Resitel
|
||||
from personalni.models.organizator import Organizator
|
||||
from tvorba.models.rocnik import Rocnik
|
||||
|
||||
|
||||
@reversion.register(ignore_duplicates=True)
|
||||
class Soustredeni(SeminarModelBase):
|
||||
|
||||
class Meta:
|
||||
db_table = 'seminar_soustredeni'
|
||||
verbose_name = 'Soustředění'
|
||||
verbose_name_plural = 'Soustředění'
|
||||
ordering = ['-rocnik__rocnik', '-datum_zacatku']
|
||||
|
||||
# Interní ID
|
||||
id = models.AutoField(primary_key=True)
|
||||
|
||||
rocnik = models.ForeignKey(
|
||||
Rocnik, verbose_name='ročník', related_name='soustredeni',
|
||||
on_delete=models.PROTECT,
|
||||
)
|
||||
|
||||
datum_zacatku = models.DateField(
|
||||
'datum začátku', blank=True, null=True,
|
||||
help_text='První den soustředění',
|
||||
)
|
||||
|
||||
datum_konce = models.DateField(
|
||||
'datum konce', blank=True, null=True,
|
||||
help_text='Poslední den soustředění',
|
||||
)
|
||||
|
||||
verejne_db = models.BooleanField(
|
||||
'soustředění zveřejněno', db_column='verejne', default=False,
|
||||
)
|
||||
|
||||
misto = models.CharField(
|
||||
'místo soustředění', max_length=256, blank=True, default='',
|
||||
help_text='Místo (název obce, volitelně též objektu',
|
||||
)
|
||||
|
||||
ucastnici = models.ManyToManyField(
|
||||
Resitel, verbose_name='účastníci soustředění',
|
||||
help_text='Seznam účastníků soustředění', through='Soustredeni_Ucastnici',
|
||||
)
|
||||
|
||||
organizatori = models.ManyToManyField(
|
||||
Organizator,
|
||||
verbose_name='Organizátoři soustředění',
|
||||
help_text='Seznam organizátorů soustředění',
|
||||
through='Soustredeni_Organizatori',
|
||||
)
|
||||
|
||||
text = models.TextField('text k soustředění (HTML)', blank=True, default='')
|
||||
|
||||
TYP_JARNI = 'jarni'
|
||||
TYP_PODZIMNI = 'podzimni'
|
||||
TYP_VIKEND = 'vikend'
|
||||
TYP_VYLET = 'vylet'
|
||||
TYP_CHOICES = [
|
||||
(TYP_JARNI, 'Jarní soustředění'),
|
||||
(TYP_PODZIMNI, 'Podzimní soustředění'),
|
||||
(TYP_VIKEND, 'Víkendový sraz'),
|
||||
(TYP_VYLET, 'Výlet'),
|
||||
]
|
||||
typ = models.CharField(
|
||||
'typ akce', max_length=16, choices=TYP_CHOICES, blank=False,
|
||||
default=TYP_PODZIMNI,
|
||||
)
|
||||
|
||||
exportovat = models.BooleanField(
|
||||
'export do AESOPa', db_column='exportovat', default=False,
|
||||
help_text='Exportuje se jen podle tohoto flagu (ne veřejnosti)',
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return '{} ({})'.format(self.misto, self.datum_zacatku)
|
||||
|
||||
def verejne(self):
|
||||
return self.verejne_db
|
||||
verejne.boolean = True
|
||||
|
||||
def verejne_url(self):
|
||||
# return reverse('seminar_soustredeni', kwargs={'pk': self.id})
|
||||
return reverse('seminar_seznam_soustredeni')
|
||||
40
soustredeni/models/soustredeni_organizatori.py
Normal file
40
soustredeni/models/soustredeni_organizatori.py
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
from reversion import revisions as reversion
|
||||
|
||||
from django.db import models
|
||||
|
||||
from mamweb.models.base import SeminarModelBase
|
||||
from personalni.models.organizator import Organizator
|
||||
from .soustredeni import Soustredeni
|
||||
|
||||
|
||||
@reversion.register(ignore_duplicates=True)
|
||||
class Soustredeni_Organizatori(SeminarModelBase):
|
||||
# zmena dedicnosti z models.Model na SeminarModelBase, potencialni vznik bugu
|
||||
|
||||
class Meta:
|
||||
db_table = 'seminar_soustredeni_organizatori'
|
||||
verbose_name = 'Účast organizátorů na soustředění'
|
||||
verbose_name_plural = 'Účasti organizátorů na soustředění'
|
||||
ordering = ['soustredeni', 'organizator']
|
||||
|
||||
# Interní ID
|
||||
id = models.AutoField(primary_key=True)
|
||||
|
||||
organizator = models.ForeignKey(
|
||||
Organizator, verbose_name='organizátor',
|
||||
on_delete=models.PROTECT,
|
||||
)
|
||||
|
||||
soustredeni = models.ForeignKey(
|
||||
Soustredeni, verbose_name='soustředění',
|
||||
on_delete=models.PROTECT,
|
||||
)
|
||||
|
||||
poznamka = models.TextField(
|
||||
'neveřejná poznámka', blank=True,
|
||||
help_text='Neveřejná poznámka k účasti organizátora (plain text)',
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return '{} na {}'.format(self.organizator, self.soustredeni)
|
||||
# NOTE: Poteciální DB HOG bez select_related
|
||||
39
soustredeni/models/soustredeni_ucastnici.py
Normal file
39
soustredeni/models/soustredeni_ucastnici.py
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
from reversion import revisions as reversion
|
||||
|
||||
from django.db import models
|
||||
|
||||
from mamweb.models.base import SeminarModelBase
|
||||
from personalni.models.resitel import Resitel
|
||||
from .soustredeni import Soustredeni
|
||||
|
||||
|
||||
@reversion.register(ignore_duplicates=True)
|
||||
class Soustredeni_Ucastnici(SeminarModelBase):
|
||||
# zmena dedicnosti z models.Model na SeminarModelBase, potencialni vznik bugu
|
||||
|
||||
class Meta:
|
||||
db_table = 'seminar_soustredeni_ucastnici'
|
||||
verbose_name = 'Účast na soustředění'
|
||||
verbose_name_plural = 'Účasti na soustředění'
|
||||
ordering = ['soustredeni', 'resitel']
|
||||
|
||||
# Interní ID
|
||||
id = models.AutoField(primary_key=True)
|
||||
|
||||
resitel = models.ForeignKey(
|
||||
Resitel, verbose_name='řešitel', on_delete=models.PROTECT,
|
||||
)
|
||||
|
||||
soustredeni = models.ForeignKey(
|
||||
Soustredeni, verbose_name='soustředění',
|
||||
on_delete=models.PROTECT,
|
||||
)
|
||||
|
||||
poznamka = models.TextField(
|
||||
'neveřejná poznámka', blank=True,
|
||||
help_text='Neveřejná poznámka k účasti (plain text)',
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return '{} na {}'.format(self.resitel, self.soustredeni)
|
||||
# NOTE: Poteciální DB HOG bez select_related
|
||||
|
|
@ -3,7 +3,9 @@ from django.http import HttpResponse
|
|||
from django.views import generic
|
||||
from django.conf import settings
|
||||
from django.contrib.staticfiles.finders import find
|
||||
from seminar.models import Soustredeni, Resitel, Soustredeni_Ucastnici, Nastaveni # Tohle je stare a chceme se toho zbavit. Pouzivejte s.ToCoChci
|
||||
from tvorba.models.nastaveni import Nastaveni
|
||||
from personalni.models.resitel import Resitel
|
||||
from soustredeni.models import Soustredeni, Soustredeni_Ucastnici
|
||||
import csv
|
||||
import tempfile
|
||||
import shutil
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ from django.contrib import admin
|
|||
|
||||
from polymorphic.admin import PolymorphicParentModelAdmin, PolymorphicChildModelAdmin, PolymorphicChildModelFilter
|
||||
|
||||
import seminar.models as m
|
||||
import treenode.models as m
|
||||
|
||||
# Polymorfismus pro stromy
|
||||
# TODO: Inlines podle https://django-polymorphic.readthedocs.io/en/stable/admin.html
|
||||
|
|
|
|||
|
|
@ -9,13 +9,15 @@ from unidecode import unidecode # Používám pro získání ID odkazu (ještě
|
|||
|
||||
from polymorphic.models import PolymorphicModel
|
||||
|
||||
from . import personalni as pm
|
||||
from personalni import models as pm
|
||||
|
||||
from .pomocne import Text
|
||||
from seminar.models.pomocne import Text
|
||||
from odevzdavatko.models.reseni import Reseni
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
from seminar.models import tvorba as am
|
||||
from tvorba import models as am
|
||||
|
||||
|
||||
class TreeNode(PolymorphicModel):
|
||||
class Meta:
|
||||
|
|
@ -264,3 +266,21 @@ 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)
|
||||
|
|
@ -1,9 +1,13 @@
|
|||
from rest_framework import serializers
|
||||
from rest_polymorphic.serializers import PolymorphicSerializer
|
||||
|
||||
import seminar.models as m
|
||||
from treenode import treelib
|
||||
|
||||
from tvorba.models.problem import Problem
|
||||
from tvorba.models.uloha import Uloha
|
||||
from odevzdavatko.models.reseni import Reseni
|
||||
import treenode.models as m
|
||||
|
||||
DEFAULT_NODE_DEPTH = 2
|
||||
|
||||
|
||||
|
|
@ -14,17 +18,17 @@ class TextSerializer(serializers.ModelSerializer):
|
|||
|
||||
class ProblemSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = m.Problem
|
||||
model = Problem
|
||||
fields = '__all__'
|
||||
|
||||
class UlohaSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = m.Uloha
|
||||
model = Uloha
|
||||
fields = '__all__'
|
||||
|
||||
class ReseniSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = m.Reseni
|
||||
model = Reseni
|
||||
fields = '__all__'
|
||||
|
||||
class RocnikNodeSerializer(serializers.ModelSerializer):
|
||||
|
|
@ -184,7 +188,7 @@ class UlohaZadaniNodeCreateSerializer(serializers.ModelSerializer):
|
|||
cislo = travelnode.cislo
|
||||
travelnode = treelib.get_parent(travelnode)
|
||||
# Vyrobime ulohu
|
||||
uloha = m.Uloha.objects.create(cislo_zadani=cislo, nadproblem = tema, **temp_uloha)
|
||||
uloha = Uloha.objects.create(cislo_zadani=cislo, nadproblem = tema, **temp_uloha)
|
||||
|
||||
# A vyrobime UlohaZadaniNode
|
||||
if where == 'syn':
|
||||
|
|
@ -211,7 +215,7 @@ class UlohaVzorakNodeSerializer(serializers.ModelSerializer):
|
|||
depth = DEFAULT_NODE_DEPTH
|
||||
|
||||
class UlohaVzorakNodeWriteSerializer(serializers.ModelSerializer):
|
||||
uloha = serializers.PrimaryKeyRelatedField(queryset=m.Uloha.objects.all(), many=False, read_only=False)
|
||||
uloha = serializers.PrimaryKeyRelatedField(queryset=Uloha.objects.all(), many=False, read_only=False)
|
||||
|
||||
class Meta:
|
||||
model = m.UlohaVzorakNode
|
||||
|
|
@ -226,7 +230,7 @@ class UlohaVzorakNodeCreateSerializer(serializers.ModelSerializer):
|
|||
|
||||
def create(self, validated_data):
|
||||
uloha_id = validated_data.pop('uloha_id')
|
||||
uloha = m.Uloha.objects.get(pk=uloha_id)
|
||||
uloha = Uloha.objects.get(pk=uloha_id)
|
||||
where = validated_data.pop('where')
|
||||
refnode_id = validated_data.pop('refnode')
|
||||
refnode = m.TreeNode.objects.get(pk=refnode_id)
|
||||
|
|
|
|||
|
|
@ -6,8 +6,9 @@ from django.views.generic.edit import CreateView
|
|||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.core.exceptions import PermissionDenied
|
||||
|
||||
import seminar.models as s
|
||||
import seminar.models as m
|
||||
from seminar.models.pomocne import Obrazek
|
||||
import treenode.models as s
|
||||
import treenode.models as m
|
||||
from treenode import treelib
|
||||
import treenode.forms as f
|
||||
import treenode.templatetags as tnltt
|
||||
|
|
@ -300,7 +301,7 @@ class VueTestView(generic.TemplateView):
|
|||
|
||||
|
||||
class NahrajObrazekKTreeNoduView(LoginRequiredMixin, CreateView):
|
||||
model = s.Obrazek
|
||||
model = Obrazek
|
||||
form_class = f.NahrajObrazekKTreeNoduForm
|
||||
|
||||
def get_initial(self):
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from rest_framework import status
|
|||
from rest_framework.response import Response
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from rest_framework.permissions import BasePermission, AllowAny
|
||||
from seminar import models as m
|
||||
from treenode import models as m
|
||||
import treenode.serializers as views
|
||||
|
||||
from treenode.permissions import AllowWrite
|
||||
|
|
|
|||
0
tvorba/__init__.py
Normal file
0
tvorba/__init__.py
Normal file
156
tvorba/admin.py
Normal file
156
tvorba/admin.py
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
from django.contrib import admin
|
||||
from django.forms import ModelForm
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.safestring import mark_safe
|
||||
from polymorphic.admin import PolymorphicParentModelAdmin, PolymorphicChildModelAdmin
|
||||
from solo.admin import SingletonModelAdmin
|
||||
|
||||
from soustredeni.models.konfera import Konfera
|
||||
import tvorba.models as m
|
||||
|
||||
|
||||
admin.site.register(m.Rocnik)
|
||||
admin.site.register(m.Deadline)
|
||||
|
||||
# Todo: reversion
|
||||
|
||||
|
||||
class DeadlineAdminInline(admin.TabularInline):
|
||||
model = m.Deadline
|
||||
extra = 0
|
||||
|
||||
|
||||
class CisloForm(ModelForm):
|
||||
class Meta:
|
||||
model = m.Cislo
|
||||
fields = '__all__'
|
||||
|
||||
def clean(self):
|
||||
if not self.cleaned_data.get('verejne_db'):
|
||||
return self.cleaned_data
|
||||
# cn = m.CisloNode.objects.get(cislo=self.instance)
|
||||
# errors = []
|
||||
# for ch in tl.all_children(cn):
|
||||
# if isinstance(ch, m.TemaVCisleNode):
|
||||
# if ch.tema.stav not in \
|
||||
# (m.Problem.STAV_ZADANY, m.Problem.STAV_VYRESENY):
|
||||
# errors.append(ValidationError('Téma %(tema)s není zadané ani vyřešené', params={'tema':ch.tema}))
|
||||
#
|
||||
# if isinstance(ch, m.UlohaZadaniNode) or isinstance(ch, m.UlohaVzorakNode):
|
||||
# if ch.uloha.stav not in \
|
||||
# (m.Problem.STAV_ZADANY, m.Problem.STAV_VYRESENY):
|
||||
# errors.append(ValidationError('Úloha %(uloha)s není zadaná ani vyřešená', params={'uloha':ch.uloha}))
|
||||
# if isinstance(ch, m.ReseniNode):
|
||||
# for problem in ch.reseni.problem_set:
|
||||
# if problem not in \
|
||||
# (m.Problem.STAV_ZADANY, m.Problem.STAV_VYRESENY):
|
||||
# errors.append(ValidationError('Problém %s není zadaný ani vyřešený', code=problem))
|
||||
# if errors:
|
||||
# errors.append(ValidationError(mark_safe('<b>Pokud chceš učinit všechny problémy, co nejsou zadané ani vyřešené, zadanými a číslo zveřejnit, můžeš to udělat pomocí akce v <a href="/admin/seminar/cislo">seznamu čísel</a></b>')))
|
||||
# raise ValidationError(errors)
|
||||
|
||||
errors = []
|
||||
for ch in m.Uloha.objects.filter(cislo_zadani=self.instance):
|
||||
if ch.stav not in (m.Problem.STAV_ZADANY, m.Problem.STAV_VYRESENY):
|
||||
errors.append(
|
||||
ValidationError(
|
||||
'Úloha %(uloha)s není zadaná ani vyřešená',
|
||||
params={'uloha': ch}
|
||||
)
|
||||
)
|
||||
if errors:
|
||||
errors.append(ValidationError(mark_safe(
|
||||
'<b>Pokud chceš učinit všechny problémy, co nejsou zadané ani vyřešené, zadanými a číslo zveřejnit, můžeš to udělat pomocí akce v <a href="/admin/seminar/cislo">seznamu čísel</a></b>'
|
||||
)))
|
||||
if self.cleaned_data.get('datum_vydani') is None:
|
||||
self.add_error(
|
||||
'datum_vydani', 'Číslo určené ke zveřejnění nemá nastavené datum vydání'
|
||||
)
|
||||
|
||||
if errors:
|
||||
raise ValidationError(errors)
|
||||
|
||||
return self.cleaned_data
|
||||
|
||||
|
||||
@admin.register(m.Cislo)
|
||||
class CisloAdmin(admin.ModelAdmin):
|
||||
form = CisloForm
|
||||
actions = ['force_publish']
|
||||
inlines = (DeadlineAdminInline,)
|
||||
|
||||
def force_publish(self, _request, queryset):
|
||||
for cislo in queryset:
|
||||
# cn = m.CisloNode.objects.get(cislo=cislo)
|
||||
# for ch in tl.all_children(cn):
|
||||
# if isinstance(ch, m.TemaVCisleNode):
|
||||
# if ch.tema.stav not in (m.Problem.STAV_ZADANY, m.Problem.STAV_VYRESENY):
|
||||
# ch.tema.stav = m.Problem.STAV_ZADANY
|
||||
# ch.tema.save()
|
||||
#
|
||||
# if isinstance(ch, m.UlohaZadaniNode) or isinstance(ch, m.UlohaVzorakNode):
|
||||
# if ch.uloha.stav not in (m.Problem.STAV_ZADANY, m.Problem.STAV_VYRESENY):
|
||||
# ch.uloha.stav = m.Problem.STAV_ZADANY
|
||||
# ch.uloha.save()
|
||||
# if isinstance(ch, m.ReseniNode):
|
||||
# for problem in ch.reseni.problem_set:
|
||||
# if problem not in (m.Problem.STAV_ZADANY, m.Problem.STAV_VYRESENY):
|
||||
# problem.stav = m.Problem.STAV_ZADANY
|
||||
# problem.save()
|
||||
|
||||
for ch in m.Uloha.objects.filter(cislo_zadani=cislo):
|
||||
if ch.stav not in (m.Problem.STAV_ZADANY, m.Problem.STAV_VYRESENY):
|
||||
ch.stav = m.Problem.STAV_ZADANY
|
||||
ch.save()
|
||||
|
||||
hp = ch.hlavni_problem
|
||||
if hp.stav not in (m.Problem.STAV_ZADANY, m.Problem.STAV_VYRESENY):
|
||||
hp.stav = m.Problem.STAV_ZADANY
|
||||
hp.save()
|
||||
|
||||
# TODO Řešení, vzoráky?
|
||||
# TODO Konfera/Článek?
|
||||
|
||||
cislo.verejne_db = True
|
||||
cislo.save()
|
||||
|
||||
force_publish.short_description = 'Zveřejnit vybraná čísla a všechny návrhy úloh v nich učinit zadanými'
|
||||
|
||||
|
||||
@admin.register(m.Problem)
|
||||
class ProblemAdmin(PolymorphicParentModelAdmin):
|
||||
base_model = m.Problem
|
||||
child_models = [
|
||||
m.Tema,
|
||||
m.Clanek,
|
||||
m.Uloha,
|
||||
Konfera,
|
||||
]
|
||||
# Pokud chceme orezavat na aktualni rocnik, musime do modelu pridat odkaz na rocnik. Zatim bere vse.
|
||||
search_fields = ['nazev']
|
||||
|
||||
|
||||
# V ProblemAdmin to nejde, protoze se to nepropise do deti
|
||||
class ProblemAdminMixin(object):
|
||||
show_in_index = True
|
||||
autocomplete_fields = ['nadproblem', 'autor', 'garant']
|
||||
filter_horizontal = ['opravovatele']
|
||||
|
||||
|
||||
@admin.register(m.Tema)
|
||||
class TemaAdmin(ProblemAdminMixin, PolymorphicChildModelAdmin):
|
||||
base_model = m.Tema
|
||||
|
||||
|
||||
@admin.register(m.Clanek)
|
||||
class ClanekAdmin(ProblemAdminMixin, PolymorphicChildModelAdmin):
|
||||
base_model = m.Clanek
|
||||
|
||||
|
||||
@admin.register(m.Uloha)
|
||||
class UlohaAdmin(ProblemAdminMixin, PolymorphicChildModelAdmin):
|
||||
base_model = m.Uloha
|
||||
|
||||
|
||||
# admin.site.register(m.Pohadka)
|
||||
admin.site.register(m.Nastaveni, SingletonModelAdmin)
|
||||
5
tvorba/apps.py
Normal file
5
tvorba/apps.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class TvorbaConfig(AppConfig):
|
||||
name = 'tvorba'
|
||||
0
tvorba/migrations/__init__.py
Normal file
0
tvorba/migrations/__init__.py
Normal file
9
tvorba/models/__init__.py
Normal file
9
tvorba/models/__init__.py
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
from .cislo import Cislo
|
||||
from .clanek import Clanek
|
||||
from .deadline import Deadline
|
||||
from .nastaveni import Nastaveni
|
||||
from .pohadka import Pohadka
|
||||
from .problem import Problem
|
||||
from .rocnik import Rocnik
|
||||
from .tema import Tema
|
||||
from .uloha import Uloha
|
||||
281
tvorba/models/cislo.py
Normal file
281
tvorba/models/cislo.py
Normal file
|
|
@ -0,0 +1,281 @@
|
|||
import os
|
||||
import subprocess
|
||||
import pathlib
|
||||
import tempfile
|
||||
import logging
|
||||
from reversion import revisions as reversion
|
||||
|
||||
from django.contrib.sites.shortcuts import get_current_site
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.conf import settings
|
||||
from django.urls import reverse
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.core.files.storage import FileSystemStorage
|
||||
from django.core.mail import EmailMessage
|
||||
|
||||
from seminar.utils import aktivniResitele
|
||||
|
||||
from mamweb.models.base import SeminarModelBase
|
||||
from personalni.models.prijemce import Prijemce
|
||||
from .rocnik import Rocnik
|
||||
|
||||
|
||||
class OverwriteStorage(FileSystemStorage):
|
||||
"""Varianta FileSystemStorage, která v případě, že soubor cílového
|
||||
jména již existuje, ho smaže a místo něj uloží soubor nový"""
|
||||
def get_available_name(self, name, max_length=None):
|
||||
if self.exists(name):
|
||||
os.remove(os.path.join(self.location, name))
|
||||
return super().get_available_name(name, max_length)
|
||||
|
||||
|
||||
def cislo_pdf_filename(self, _filename):
|
||||
rocnik = str(self.rocnik.rocnik)
|
||||
return pathlib.Path(
|
||||
'cislo', 'pdf', rocnik, '{}-{}.pdf'.format(rocnik, self.poradi),
|
||||
)
|
||||
|
||||
|
||||
def cislo_png_filename(self, _filename):
|
||||
rocnik = str(self.rocnik.rocnik)
|
||||
return pathlib.Path(
|
||||
'cislo', 'png', rocnik, '{}-{}.png'.format(rocnik, self.poradi),
|
||||
)
|
||||
|
||||
|
||||
@reversion.register(ignore_duplicates=True)
|
||||
class Cislo(SeminarModelBase):
|
||||
|
||||
class Meta:
|
||||
db_table = 'seminar_cisla'
|
||||
verbose_name = 'Číslo'
|
||||
verbose_name_plural = 'Čísla'
|
||||
ordering = ['-rocnik__rocnik', '-poradi']
|
||||
|
||||
# Interní ID
|
||||
id = models.AutoField(primary_key=True)
|
||||
|
||||
rocnik = models.ForeignKey(
|
||||
Rocnik, verbose_name='ročník', related_name='cisla',
|
||||
db_index=True, on_delete=models.PROTECT,
|
||||
)
|
||||
|
||||
poradi = models.CharField(
|
||||
'název čísla', max_length=32, db_index=True,
|
||||
help_text='Většinou jen "1", vyjímečně "7-8", lexikograficky určuje pořadí v ročníku!',
|
||||
)
|
||||
|
||||
datum_vydani = models.DateField(
|
||||
'datum vydání', blank=True, null=True,
|
||||
help_text='Datum vydání finální verze',
|
||||
)
|
||||
|
||||
verejne_db = models.BooleanField(
|
||||
'číslo zveřejněno', db_column='verejne', default=False,
|
||||
)
|
||||
|
||||
poznamka = models.TextField(
|
||||
'neveřejná poznámka', blank=True,
|
||||
help_text='Neveřejná poznámka k číslu (plain text)',
|
||||
)
|
||||
|
||||
pdf = models.FileField(
|
||||
'pdf', upload_to=cislo_pdf_filename, null=True, blank=True,
|
||||
help_text='PDF čísla, které si mohou řešitelé stáhnout',
|
||||
storage=OverwriteStorage(),
|
||||
)
|
||||
|
||||
titulka_nahled = models.ImageField(
|
||||
'Obrázek titulní strany', upload_to=cislo_png_filename,
|
||||
null=True, blank=True,
|
||||
help_text='Obrázek titulní strany, generuje se automaticky',
|
||||
)
|
||||
|
||||
# má OneToOneField s:
|
||||
# CisloNode
|
||||
|
||||
def kod(self):
|
||||
return '%s.%s' % (self.rocnik.rocnik, self.poradi)
|
||||
kod.short_description = 'Kód čísla'
|
||||
|
||||
def __str__(self):
|
||||
# Potenciální DB HOG, pokud by se ročník necachoval
|
||||
r = Rocnik.cached_rocnik(self.rocnik_id)
|
||||
return '{}.{}'.format(r.rocnik, self.poradi)
|
||||
|
||||
def verejne(self):
|
||||
return self.verejne_db
|
||||
verejne.boolean = True
|
||||
|
||||
def verejne_url(self):
|
||||
return reverse(
|
||||
'seminar_cislo',
|
||||
kwargs={'rocnik': self.rocnik.rocnik, 'cislo': self.poradi},
|
||||
)
|
||||
|
||||
def absolute_url(self):
|
||||
return "https://" + str(get_current_site(None)) + self.verejne_url()
|
||||
|
||||
def nasledujici(self):
|
||||
"""Vrací None, pokud je toto poslední"""
|
||||
return self.relativni_v_rocniku(1)
|
||||
|
||||
def predchozi(self):
|
||||
"""Vrací None, pokud je toto první"""
|
||||
return self.relativni_v_rocniku(-1)
|
||||
|
||||
def relativni_v_rocniku(self, rel_index):
|
||||
"""Číslo o `index` dále v ročníku. None pokud neexistuje."""
|
||||
cs = self.rocnik.cisla.order_by('poradi').all()
|
||||
i = list(cs).index(self) + rel_index
|
||||
if (i < 0) or (i >= len(cs)):
|
||||
return None
|
||||
return cs[i]
|
||||
|
||||
def vygeneruj_nahled(self):
|
||||
VYSKA = 594
|
||||
sirka = int(VYSKA*210/297)
|
||||
if not self.pdf:
|
||||
return
|
||||
|
||||
# Pokud obrázek neexistuje nebo není aktuální, vytvoř jej
|
||||
if not self.titulka_nahled or os.path.getmtime(self.titulka_nahled.path) < os.path.getmtime(self.pdf.path):
|
||||
png_filename = pathlib.Path(tempfile.mkdtemp(), 'nahled.png')
|
||||
|
||||
subprocess.run([
|
||||
"gs",
|
||||
"-sstdout=%stderr",
|
||||
"-dSAFER",
|
||||
"-dNOPAUSE",
|
||||
"-dBATCH",
|
||||
"-dNOPROMPT",
|
||||
"-sDEVICE=png16m",
|
||||
"-r300x300",
|
||||
"-dFirstPage=1d",
|
||||
"-dLastPage=1d",
|
||||
"-sOutputFile=" + str(png_filename),
|
||||
"-f%s" % self.pdf.path
|
||||
],
|
||||
check=True,
|
||||
capture_output=True
|
||||
)
|
||||
|
||||
with open(png_filename, 'rb') as f:
|
||||
self.titulka_nahled.save('', f, True)
|
||||
|
||||
png_filename.unlink()
|
||||
png_filename.parent.rmdir()
|
||||
|
||||
@classmethod
|
||||
def get(cls, rocnik, cislo):
|
||||
try:
|
||||
r = Rocnik.objects.get(rocnik=rocnik)
|
||||
c = r.cisla.get(poradi=cislo)
|
||||
except ObjectDoesNotExist:
|
||||
return None
|
||||
return c
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.__original_verejne = self.verejne_db
|
||||
|
||||
def posli_cislo_mailem(self):
|
||||
# parametry e-mailu
|
||||
odkaz = self.absolute_url()
|
||||
|
||||
poslat_z_mailu = 'zadani@mam.mff.cuni.cz'
|
||||
predmet = 'Vyšlo číslo {}'.format(self.kod())
|
||||
# TODO Možná nechceme všem psát „Ahoj“, např. příjemcům…
|
||||
text_mailu = (
|
||||
'Ahoj,\n'
|
||||
'na adrese {} najdete nejnovější číslo.\n'
|
||||
'Vaše M&M\n'
|
||||
).format(odkaz)
|
||||
|
||||
predmet_prvni = 'Právě vyšlo 1. číslo M&M, pomoz nám ho poslat dál!'
|
||||
text_mailu_prvni = (
|
||||
'Milý řešiteli,\n'
|
||||
'právě jsme na našem webu zveřejnili první číslo {}. ročníku, najdeš ho na tomto odkazu: {}.\n\n'
|
||||
'Doufáme, že tě M&M baví, a byli bychom rádi, kdyby mohlo dělat radost i dalším středoškolákům. Máme na tebe proto jednu prosbu. Sdílej prosím odkaz alespoň s jedním svým kamarádem, který by mohl mít o řešení M&M zájem. Je to pro nás moc důležité a velmi nám tím pomůžeš. Díky!\n\n'
|
||||
'Organizátoři M&M\n'
|
||||
).format(self.rocnik.rocnik, odkaz)
|
||||
|
||||
predmet_resitel = predmet_prvni if self.poradi == "1" else predmet
|
||||
text_mailu_resitel = text_mailu_prvni if self.poradi == "1" else text_mailu
|
||||
|
||||
# Prijemci e-mailu
|
||||
resitele_vsichni = aktivniResitele(self).filter(zasilat_cislo_emailem=True)
|
||||
|
||||
def posli(subject, text, resitele):
|
||||
emaily = map(lambda resitel: resitel.osoba.email, resitele)
|
||||
if not settings.POSLI_MAILOVOU_NOTIFIKACI:
|
||||
print("Poslal bych upozornění na tyto adresy: ", " ".join(emaily))
|
||||
return
|
||||
|
||||
email = EmailMessage(
|
||||
subject=subject,
|
||||
body=text,
|
||||
from_email=poslat_z_mailu,
|
||||
# bcc = příjemci skryté kopie
|
||||
bcc=list(emaily)
|
||||
)
|
||||
|
||||
email.send()
|
||||
|
||||
paticka = "---\nK odběru těchto e-mailů jste se přihlásili na stránkách https://mam.matfyz.cz. Z odběru se lze odhlásit na https://mam.matfyz.cz/resitel/osobni-udaje/"
|
||||
|
||||
posli(
|
||||
predmet_resitel,
|
||||
text_mailu_resitel + paticka,
|
||||
resitele_vsichni.filter(zasilat_cislo_papirove=False),
|
||||
)
|
||||
posli(
|
||||
predmet_resitel,
|
||||
text_mailu_resitel + 'P. S. Brzy budeme též rozesílat papírovou verzi čísla. Připomínáme, že pokud papírovou verzi čísla nevyužijete, můžete v https://mam.mff.cuni.cz/resitel/osobni-udaje/ zaškrtnout, abychom vám ji neposílali. Čísla vždy můžete nalézt v našem archivu a dál vám budou chodit e-mailem. Děkujeme.\n' + paticka,
|
||||
resitele_vsichni.filter(zasilat_cislo_papirove=True),
|
||||
)
|
||||
|
||||
paticka_prijemce = "---\nPokud tyto e-maily nechcete nadále dostávat, prosíme, ozvěte se nám na mam@matfyz.cz."
|
||||
posli(
|
||||
predmet,
|
||||
text_mailu + paticka_prijemce,
|
||||
Prijemce.objects.filter(zasilat_cislo_emailem=True)
|
||||
)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
super().save(*args, **kwargs)
|
||||
self.vygeneruj_nahled()
|
||||
# Při zveřejnění pošle mail
|
||||
if self.verejne_db and not self.__original_verejne:
|
||||
self.posli_cislo_mailem()
|
||||
# *Node.save() aktualizuje název *Nodu.
|
||||
try:
|
||||
self.cislonode.save()
|
||||
except ObjectDoesNotExist:
|
||||
# Neexistující *Node nemá smysl aktualizovat, ale je potřeba ho naopak vyrobit
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.warning(f'Číslo {self} nemělo ČísloNode, vyrábím…')
|
||||
from treenode.models import CisloNode
|
||||
CisloNode.objects.create(cislo=self)
|
||||
|
||||
def zlomovy_deadline_pro_papirove_cislo(self):
|
||||
from .deadline import Deadline
|
||||
prvni_deadline = Deadline.objects.filter(
|
||||
Q(typ=Deadline.TYP_PRVNI) | Q(typ=Deadline.TYP_PRVNI_A_SOUS),
|
||||
cislo=self
|
||||
).first()
|
||||
if prvni_deadline is None:
|
||||
posledni_deadline = self.posledni_deadline
|
||||
if posledni_deadline is None:
|
||||
# TODO promyslet, co se má stát tady
|
||||
return Deadline.objects.filter(
|
||||
Q(cislo__poradi__lt=self.poradi, cislo__rocnik=self.rocnik) |
|
||||
Q(cislo__rocnik__rocnik__lt=self.rocnik.rocnik)
|
||||
).order_by("deadline").last()
|
||||
return posledni_deadline
|
||||
return prvni_deadline
|
||||
|
||||
@property
|
||||
def posledni_deadline(self):
|
||||
return self.deadline_v_cisle.all().order_by("deadline").last()
|
||||
33
tvorba/models/clanek.py
Normal file
33
tvorba/models/clanek.py
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import logging
|
||||
|
||||
from django.db import models
|
||||
from django.utils.functional import cached_property
|
||||
|
||||
from .problem import Problem
|
||||
from .cislo import Cislo
|
||||
|
||||
|
||||
class Clanek(Problem):
|
||||
class Meta:
|
||||
db_table = 'seminar_clanky'
|
||||
verbose_name = 'Článek'
|
||||
verbose_name_plural = 'Články'
|
||||
|
||||
cislo = models.ForeignKey(
|
||||
Cislo, blank=True, null=True, on_delete=models.PROTECT,
|
||||
verbose_name='číslo vydání', related_name='vydane_clanky',
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def kod_v_rocniku(self):
|
||||
if self.stav == Problem.STAV_ZADANY or self.stav == Problem.STAV_VYRESENY:
|
||||
# Nemělo by být potřeba
|
||||
# if self.nadproblem:
|
||||
# return self.nadproblem.kod_v_rocniku+".c{}".format(self.kod)
|
||||
return "c{}".format(self.kod)
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.warning(f"K problému {self} byl vyžadován kód v ročníku, i když není zadaný ani vyřešený.")
|
||||
return '<Není zadaný>'
|
||||
|
||||
def node(self):
|
||||
return None
|
||||
83
tvorba/models/deadline.py
Normal file
83
tvorba/models/deadline.py
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import datetime
|
||||
|
||||
from django.utils import timezone
|
||||
from django.db import models
|
||||
from django.template.loader import render_to_string
|
||||
|
||||
from mamweb.models.base import SeminarModelBase
|
||||
from .cislo import Cislo
|
||||
|
||||
|
||||
class Deadline(SeminarModelBase):
|
||||
class Meta:
|
||||
db_table = 'seminar_deadliny'
|
||||
verbose_name = 'Deadline'
|
||||
verbose_name_plural = 'Deadliny'
|
||||
ordering = ['deadline']
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.__original_verejna_vysledkovka = self.verejna_vysledkovka
|
||||
|
||||
id = models.AutoField(primary_key=True)
|
||||
|
||||
# V ročníku < 26 nastaveno na datetime.datetime.combine(datetime.date(1994 + cislo.rocnik.rocnik, 6, int(cislo.poradi[0])), datetime.time.min)
|
||||
deadline = models.DateTimeField(
|
||||
blank=False,
|
||||
default=timezone.make_aware(
|
||||
datetime.datetime.combine(timezone.now(), datetime.time.max),
|
||||
),
|
||||
)
|
||||
|
||||
cislo = models.ForeignKey(
|
||||
Cislo, verbose_name='deadline v čísle',
|
||||
related_name='deadline_v_cisle', blank=False,
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
|
||||
TYP_CISLA = 'cisla'
|
||||
TYP_PRVNI_A_SOUS = 'prvniasous'
|
||||
TYP_PRVNI = 'prvni'
|
||||
TYP_SOUS = 'sous'
|
||||
TYP_CHOICES = [
|
||||
(TYP_CISLA, 'Deadline celého čísla'),
|
||||
(TYP_PRVNI, 'První deadline'),
|
||||
(TYP_PRVNI_A_SOUS, 'Sousový a první deadline'),
|
||||
(TYP_SOUS, 'Sousový deadline'),
|
||||
]
|
||||
CHOICES_MAP = dict(TYP_CHOICES)
|
||||
typ = models.CharField(
|
||||
'typ deadlinu', max_length=32,
|
||||
choices=TYP_CHOICES, blank=False,
|
||||
)
|
||||
|
||||
verejna_vysledkovka = models.BooleanField(
|
||||
'veřejná výsledkovka', db_column='verejna_vysledkovka', default=False,
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.CHOICES_MAP[self.typ] + " " + str(self.cislo)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
super().save(*args, **kwargs)
|
||||
if self.verejna_vysledkovka and not self.__original_verejna_vysledkovka:
|
||||
self.vygeneruj_vysledkovku()
|
||||
if not self.verejna_vysledkovka and hasattr(self, "vysledkovka_v_deadlinu"):
|
||||
self.vysledkovka_v_deadlinu.delete()
|
||||
|
||||
def vygeneruj_vysledkovku(self):
|
||||
from vysledkovky.utils import VysledkovkaCisla
|
||||
if hasattr(self, "vysledkovka_v_deadlinu"):
|
||||
self.vysledkovka_v_deadlinu.delete()
|
||||
vysledkovka = VysledkovkaCisla(
|
||||
self.cislo, jen_verejne=True, do_deadlinu=self,
|
||||
)
|
||||
if len(vysledkovka.radky_vysledkovky) != 0:
|
||||
from vysledkovky.models.zmrazena_vysledkovka import ZmrazenaVysledkovka
|
||||
ZmrazenaVysledkovka.objects.create(
|
||||
deadline=self,
|
||||
html=render_to_string(
|
||||
"vysledkovky/vysledkovka_cisla.html",
|
||||
context={"vysledkovka": vysledkovka, "oznaceni_vysledkovky": self.id}
|
||||
)
|
||||
)
|
||||
42
tvorba/models/nastaveni.py
Normal file
42
tvorba/models/nastaveni.py
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
from reversion import revisions as reversion
|
||||
|
||||
from solo.models import SingletonModel
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
|
||||
from .cislo import Cislo
|
||||
|
||||
|
||||
@reversion.register(ignore_duplicates=True)
|
||||
class Nastaveni(SingletonModel):
|
||||
|
||||
class Meta:
|
||||
db_table = 'seminar_nastaveni'
|
||||
verbose_name = 'Nastavení semináře'
|
||||
|
||||
# aktualni_rocnik = models.ForeignKey(Rocnik, verbose_name='aktuální ročník',
|
||||
# null=False, on_delete=models.PROTECT)
|
||||
|
||||
aktualni_cislo = models.ForeignKey(
|
||||
Cislo, verbose_name='Aktuální číslo',
|
||||
null=False, on_delete=models.PROTECT,
|
||||
)
|
||||
|
||||
cena_sous = models.IntegerField(
|
||||
null=False,
|
||||
verbose_name="Účastnický poplatek za soustředění",
|
||||
default=1000,
|
||||
)
|
||||
|
||||
@property
|
||||
def aktualni_rocnik(self):
|
||||
return self.aktualni_cislo.rocnik
|
||||
|
||||
def __str__(self):
|
||||
return 'Nastavení semináře'
|
||||
|
||||
def admin_url(self):
|
||||
return reverse('admin:seminar_nastaveni_change', args=(self.id, ))
|
||||
|
||||
def verejne(self):
|
||||
return False
|
||||
53
tvorba/models/pohadka.py
Normal file
53
tvorba/models/pohadka.py
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
|
||||
from mamweb.models.base import SeminarModelBase
|
||||
from personalni.models.organizator import Organizator
|
||||
|
||||
|
||||
class Pohadka(SeminarModelBase):
|
||||
"""Kus pohádky před/za úlohou v čísle"""
|
||||
|
||||
class Meta:
|
||||
db_table = 'seminar_pohadky'
|
||||
verbose_name = 'Pohádka'
|
||||
verbose_name_plural = 'Pohádky'
|
||||
ordering = ['vytvoreno']
|
||||
|
||||
# Interní ID
|
||||
id = models.AutoField(primary_key=True)
|
||||
|
||||
autor = models.ForeignKey(
|
||||
Organizator,
|
||||
verbose_name="Autor pohádky",
|
||||
|
||||
# Při nahrávání z TeXu není vyplnění vyžadováno, v adminu je
|
||||
null=True,
|
||||
blank=False,
|
||||
on_delete=models.SET_NULL
|
||||
)
|
||||
|
||||
vytvoreno = models.DateTimeField(
|
||||
'Vytvořeno',
|
||||
default=timezone.now,
|
||||
blank=True,
|
||||
editable=False
|
||||
)
|
||||
|
||||
# má OneToOneField s:
|
||||
# PohadkaNode
|
||||
|
||||
def __str__(self):
|
||||
# FIXME pohádka text nemá!
|
||||
uryvek = self.text if len(self.text) < 50 else self.text[:(50-3)]+"..."
|
||||
return uryvek
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
super().save(*args, **kwargs)
|
||||
# *Node.save() aktualizuje název *Nodu.
|
||||
try:
|
||||
self.pohadkanode.save()
|
||||
except ObjectDoesNotExist:
|
||||
# Neexistující *Node nemá smysl aktualizovat.
|
||||
pass
|
||||
161
tvorba/models/problem.py
Normal file
161
tvorba/models/problem.py
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
from reversion import revisions as reversion
|
||||
import logging
|
||||
|
||||
from taggit.managers import TaggableManager
|
||||
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from django.utils.functional import cached_property
|
||||
from django.urls import reverse
|
||||
from polymorphic.models import PolymorphicModel
|
||||
|
||||
from mamweb.models.base import SeminarModelBase
|
||||
from personalni.models.organizator import Organizator
|
||||
|
||||
|
||||
@reversion.register(ignore_duplicates=True)
|
||||
# Pozor na následující řádek. *Nekrmit, asi kouše!*
|
||||
class Problem(SeminarModelBase, PolymorphicModel):
|
||||
|
||||
class Meta:
|
||||
# Není abstraktní, protože se na něj jinak nedají dělat ForeignKeys.
|
||||
# TODO: Udělat to polymorfní (pomocí django-polymorphic), abychom dostali
|
||||
# po těch vazbách přímo tu úlohu/témátko vč. fieldů, které nejsou součástí
|
||||
# modelu Problem?
|
||||
|
||||
# abstract = True
|
||||
db_table = 'seminar_problemy'
|
||||
verbose_name = 'Problém'
|
||||
verbose_name_plural = 'Problémy'
|
||||
ordering = ['nazev']
|
||||
|
||||
# Interní ID
|
||||
id = models.AutoField(primary_key=True)
|
||||
|
||||
# Název
|
||||
nazev = models.CharField('název', max_length=256) # Zveřejnitelný na stránky
|
||||
|
||||
# Problém má podproblémy
|
||||
nadproblem = models.ForeignKey(
|
||||
'self', verbose_name='nadřazený problém',
|
||||
related_name='podproblem', null=True, blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
)
|
||||
|
||||
STAV_NAVRH = 'navrh'
|
||||
STAV_ZADANY = 'zadany'
|
||||
STAV_VYRESENY = 'vyreseny'
|
||||
STAV_SMAZANY = 'smazany'
|
||||
STAV_CHOICES = [
|
||||
(STAV_NAVRH, 'Návrh'),
|
||||
(STAV_ZADANY, 'Zadaný'),
|
||||
(STAV_VYRESENY, 'Vyřešený'),
|
||||
(STAV_SMAZANY, 'Smazaný'),
|
||||
]
|
||||
stav = models.CharField(
|
||||
'stav problému', max_length=32, choices=STAV_CHOICES, blank=False,
|
||||
default=STAV_NAVRH,
|
||||
)
|
||||
# Téma je taky Problém, takže má stavy, "zadané" témátko je aktuálně otevřené a dá se k němu něco poslat (řešení nebo článek)
|
||||
|
||||
zamereni = TaggableManager(
|
||||
verbose_name='zaměření',
|
||||
help_text='Zaměření M/F/I/O problému, příp. další tagy', blank=True,
|
||||
)
|
||||
|
||||
poznamka = models.TextField(
|
||||
'org poznámky (HTML)', blank=True,
|
||||
help_text='Neveřejný návrh úlohy, návrh řešení, text zadání, poznámky ...',
|
||||
)
|
||||
|
||||
autor = models.ForeignKey(
|
||||
Organizator, verbose_name='autor problému',
|
||||
related_name='autor_problemu_%(class)s', null=True, blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
)
|
||||
|
||||
garant = models.ForeignKey(
|
||||
Organizator, verbose_name='garant zadaného problému',
|
||||
related_name='garant_problemu_%(class)s', null=True, blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
)
|
||||
|
||||
opravovatele = models.ManyToManyField(
|
||||
Organizator, verbose_name='opravovatelé',
|
||||
blank=True, related_name='opravovatele_%(class)s',
|
||||
)
|
||||
|
||||
kod = models.CharField(
|
||||
'lokální kód', max_length=32, blank=True, default='',
|
||||
help_text='Číslo/kód úlohy v čísle nebo kód tématu/článku/seriálu v ročníku',
|
||||
)
|
||||
|
||||
vytvoreno = models.DateTimeField(
|
||||
'vytvořeno', default=timezone.now, blank=True, editable=False,
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.nazev
|
||||
|
||||
# Implicitini implementace, jednotlivé dědící třídy si přepíšou
|
||||
@cached_property
|
||||
def kod_v_rocniku(self):
|
||||
if self.stav == Problem.STAV_ZADANY or self.stav == Problem.STAV_VYRESENY:
|
||||
if self.nadproblem:
|
||||
return self.nadproblem.kod_v_rocniku+".{}".format(self.kod)
|
||||
return str(self.kod)
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.warning(f"K problému {self} byl vyžadován kód v ročníku, i když není zadaný ani vyřešený.")
|
||||
return '<Není zadaný>'
|
||||
|
||||
# def verejne(self):
|
||||
# # aktuálně podle stavu problému
|
||||
# # FIXME pro některé problémy možná chceme override
|
||||
# # FIXME vrací veřejnost čistě problému, nezávisle na čísle, ve kterém je.
|
||||
# # Je to tak správně? Podle aktuální představy ano.
|
||||
# stav_verejny = False
|
||||
# if self.stav == 'zadany' or self.stav == 'vyreseny':
|
||||
# stav_verejny = True
|
||||
# print("stav_verejny: {}".format(stav_verejny))
|
||||
#
|
||||
# cislo_verejne = False
|
||||
# cislonode = self.cislo_node()
|
||||
# if cislonode is None:
|
||||
# # problém nemá vlastní node, veřejnost posuzujeme jen podle stavu
|
||||
# print("empty node")
|
||||
# return stav_verejny
|
||||
# else:
|
||||
# cislo_zadani = cislonode.cislo
|
||||
# if (cislo_zadani and cislo_zadani.verejne()):
|
||||
# print("cislo: {}".format(cislo_zadani))
|
||||
# cislo_verejne = True
|
||||
# print("stav_verejny: {}".format(stav_verejny))
|
||||
# print("cislo_verejne: {}".format(cislo_verejne))
|
||||
# return (stav_verejny and cislo_verejne)
|
||||
# verejne.boolean = True
|
||||
|
||||
def verejne_url(self):
|
||||
return reverse('seminar_problem', kwargs={'pk': self.id})
|
||||
|
||||
def admin_url(self):
|
||||
return reverse('admin:seminar_problem_change', args=(self.id, ))
|
||||
|
||||
@cached_property
|
||||
def hlavni_problem(self):
|
||||
"""Pro daný problém vrátí jeho nejvyšší nadproblém."""
|
||||
problem = self
|
||||
while not (problem.nadproblem is None):
|
||||
problem = problem.nadproblem
|
||||
return problem
|
||||
|
||||
# FIXME - k úloze
|
||||
def body_v_zavorce(self):
|
||||
"""Vrať string s body v závorce jsou-li u problému vyplněné, jinak ''
|
||||
|
||||
Je-li desetinná část nulová, nezobrazuj ji.
|
||||
"""
|
||||
pocet_bodu = None
|
||||
if self.body:
|
||||
b = self.body
|
||||
pocet_bodu = int(b) if int(b) == b else b
|
||||
return "({}\u2009b)".format(pocet_bodu) if self.body else ""
|
||||
95
tvorba/models/rocnik.py
Normal file
95
tvorba/models/rocnik.py
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
from reversion import revisions as reversion
|
||||
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.core.cache import cache
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
|
||||
from seminar.utils import roman
|
||||
from mamweb.models.base import SeminarModelBase
|
||||
|
||||
|
||||
@reversion.register(ignore_duplicates=True)
|
||||
class Rocnik(SeminarModelBase):
|
||||
|
||||
class Meta:
|
||||
db_table = 'seminar_rocniky'
|
||||
verbose_name = 'Ročník'
|
||||
verbose_name_plural = 'Ročníky'
|
||||
ordering = ['-rocnik']
|
||||
|
||||
# Interní ID
|
||||
id = models.AutoField(primary_key=True)
|
||||
|
||||
prvni_rok = models.IntegerField('první rok', db_index=True, unique=True)
|
||||
|
||||
rocnik = models.IntegerField('číslo ročníku', db_index=True, unique=True)
|
||||
|
||||
exportovat = models.BooleanField(
|
||||
'export do AESOPa', db_column='exportovat', default=False,
|
||||
help_text='Exportuje se jen podle tohoto flagu (ne veřejnosti), a to jen čísla s veřejnou výsledkovkou',
|
||||
)
|
||||
|
||||
# má OneToOneField s:
|
||||
# RocnikNode
|
||||
|
||||
def __str__(self):
|
||||
return '{} ({}/{})'.format(self.rocnik, self.prvni_rok, self.prvni_rok+1)
|
||||
|
||||
# Ročník v římských číslech
|
||||
def roman(self):
|
||||
return roman(int(self.rocnik))
|
||||
|
||||
def verejne(self):
|
||||
return len(self.verejna_cisla()) > 0
|
||||
verejne.boolean = True
|
||||
verejne.short_description = 'Veřejný (jen dle čísel)'
|
||||
|
||||
def neverejna_cisla(self):
|
||||
vc = [c for c in self.cisla.all() if not c.verejne()]
|
||||
vc.sort(key=lambda c: c.poradi)
|
||||
return vc
|
||||
|
||||
def verejna_cisla(self):
|
||||
vc = [c for c in self.cisla.all() if c.verejne()]
|
||||
vc.sort(key=lambda c: c.poradi)
|
||||
return vc
|
||||
|
||||
def posledni_verejne_cislo(self):
|
||||
vc = self.verejna_cisla()
|
||||
return vc[-1] if vc else None
|
||||
|
||||
def verejne_vysledkovky_cisla(self):
|
||||
vc = list(
|
||||
self.cisla.filter(deadline_v_cisle__verejna_vysledkovka=True).distinct()
|
||||
)
|
||||
vc.sort(key=lambda c: c.poradi)
|
||||
return vc
|
||||
|
||||
def posledni_zverejnena_vysledkovka_cislo(self):
|
||||
vc = self.verejne_vysledkovky_cisla()
|
||||
return vc[-1] if vc else None
|
||||
|
||||
def druhy_rok(self):
|
||||
return self.prvni_rok + 1
|
||||
|
||||
def verejne_url(self):
|
||||
return reverse('seminar_rocnik', kwargs={'rocnik': self.rocnik})
|
||||
|
||||
@classmethod
|
||||
def cached_rocnik(cls, r_id):
|
||||
name = 'rocnik_%s' % (r_id, )
|
||||
c = cache.get(name)
|
||||
if c is None:
|
||||
c = cls.objects.get(id=r_id)
|
||||
cache.set(name, c, 300)
|
||||
return c
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
super().save(*args, **kwargs)
|
||||
# *Node.save() aktualizuje název *Nodu.
|
||||
try:
|
||||
self.rocniknode.save()
|
||||
except ObjectDoesNotExist:
|
||||
# Neexistující *Node nemá smysl aktualizovat.
|
||||
pass
|
||||
62
tvorba/models/tema.py
Normal file
62
tvorba/models/tema.py
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import logging
|
||||
|
||||
from django.db import models
|
||||
from django.utils.functional import cached_property
|
||||
|
||||
from treenode import treelib
|
||||
|
||||
from .problem import Problem
|
||||
from .rocnik import Rocnik
|
||||
|
||||
|
||||
class Tema(Problem):
|
||||
class Meta:
|
||||
db_table = 'seminar_temata'
|
||||
verbose_name = 'Téma'
|
||||
verbose_name_plural = 'Témata'
|
||||
|
||||
TEMA_TEMA = 'tema'
|
||||
TEMA_SERIAL = 'serial'
|
||||
TEMA_CHOICES = [
|
||||
(TEMA_TEMA, 'Téma'),
|
||||
(TEMA_SERIAL, 'Seriál'),
|
||||
]
|
||||
tema_typ = models.CharField(
|
||||
'Typ tématu', max_length=16, choices=TEMA_CHOICES,
|
||||
blank=False, default=TEMA_TEMA,
|
||||
)
|
||||
|
||||
rocnik = models.ForeignKey(
|
||||
Rocnik, verbose_name='ročník', related_name='temata',
|
||||
blank=True, null=True, on_delete=models.PROTECT,
|
||||
)
|
||||
|
||||
abstrakt = models.TextField('Abstrakt na rozcestník', blank=True)
|
||||
obrazek = models.ImageField('Obrázek na rozcestník', null=True, blank=True)
|
||||
|
||||
@cached_property
|
||||
def kod_v_rocniku(self):
|
||||
if self.stav == Problem.STAV_ZADANY or self.stav == Problem.STAV_VYRESENY:
|
||||
if self.nadproblem:
|
||||
return self.nadproblem.kod_v_rocniku+".t{}".format(self.kod)
|
||||
return "t{}".format(self.kod)
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.warning(f"K problému {self} byl vyžadován kód v ročníku, i když není zadaný ani vyřešený.")
|
||||
return '<Není zadaný>'
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
super().save(*args, **kwargs)
|
||||
# *Node.save() aktualizuje název *Nodu.
|
||||
for tvcn in self.temavcislenode_set.all():
|
||||
tvcn.save()
|
||||
|
||||
def cislo_node(self):
|
||||
tema_node_set = self.temavcislenode_set.all()
|
||||
tema_cisla_vyskyt = []
|
||||
from treenode.models import CisloNode
|
||||
for tn in tema_node_set:
|
||||
tema_cisla_vyskyt.append(
|
||||
treelib.get_upper_node_of_type(tn, CisloNode).cislo)
|
||||
tema_cisla_vyskyt.sort(key=lambda x: x.datum_vydani)
|
||||
prvni_zadani = tema_cisla_vyskyt[0]
|
||||
return prvni_zadani.cislonode
|
||||
73
tvorba/models/uloha.py
Normal file
73
tvorba/models/uloha.py
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import logging
|
||||
|
||||
from django.db import models
|
||||
from django.utils.functional import cached_property
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
|
||||
from treenode import treelib
|
||||
|
||||
from .problem import Problem
|
||||
from .cislo import Cislo
|
||||
|
||||
|
||||
class Uloha(Problem):
|
||||
class Meta:
|
||||
db_table = 'seminar_ulohy'
|
||||
verbose_name = 'Úloha'
|
||||
verbose_name_plural = 'Úlohy'
|
||||
|
||||
cislo_zadani = models.ForeignKey(
|
||||
Cislo, verbose_name='číslo zadání', blank=True,
|
||||
null=True, related_name='zadane_ulohy', on_delete=models.PROTECT,
|
||||
)
|
||||
|
||||
cislo_deadline = models.ForeignKey(
|
||||
Cislo, verbose_name='číslo deadlinu', blank=True,
|
||||
null=True, related_name='deadlinove_ulohy', on_delete=models.PROTECT,
|
||||
)
|
||||
|
||||
cislo_reseni = models.ForeignKey(
|
||||
Cislo, verbose_name='číslo řešení', blank=True,
|
||||
null=True, related_name='resene_ulohy',
|
||||
help_text='Číslo s řešením úlohy, jen pro úlohy',
|
||||
on_delete=models.PROTECT,
|
||||
)
|
||||
|
||||
max_body = models.DecimalField(
|
||||
max_digits=8, decimal_places=1, verbose_name='maximum bodů',
|
||||
blank=True, null=True,
|
||||
)
|
||||
|
||||
# má OneToOneField s:
|
||||
# UlohaZadaniNode
|
||||
# UlohaVzorakNode
|
||||
|
||||
@cached_property
|
||||
def kod_v_rocniku(self):
|
||||
if self.stav == Problem.STAV_ZADANY or self.stav == Problem.STAV_VYRESENY:
|
||||
name = "{}.u{}".format(self.cislo_zadani.poradi, self.kod)
|
||||
if self.nadproblem:
|
||||
return self.nadproblem.kod_v_rocniku+name
|
||||
return name
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.warning(f"K problému {self} byl vyžadován kód v ročníku, i když není zadaný ani vyřešený.")
|
||||
return '<Není zadaný>'
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
super().save(*args, **kwargs)
|
||||
# *Node.save() aktualizuje název *Nodu.
|
||||
try:
|
||||
self.ulohazadaninode.save()
|
||||
except ObjectDoesNotExist:
|
||||
# Neexistující *Node nemá smysl aktualizovat.
|
||||
pass
|
||||
try:
|
||||
self.ulohavzoraknode.save()
|
||||
except ObjectDoesNotExist:
|
||||
# Neexistující *Node nemá smysl aktualizovat.
|
||||
pass
|
||||
|
||||
def cislo_node(self):
|
||||
zadani_node = self.ulohazadaninode
|
||||
from treenode.models import CisloNode
|
||||
return treelib.get_upper_node_of_type(zadani_node, CisloNode)
|
||||
|
|
@ -1,3 +1,23 @@
|
|||
import os
|
||||
from unidecode import unidecode # Používám pro získání ID odkazu
|
||||
|
||||
from django.utils.text import get_valid_filename
|
||||
from django.utils import timezone
|
||||
|
||||
|
||||
def aux_generate_filename(_, filename):
|
||||
"""Pomocná funkce generující ošetřený název souboru v adresáři s datem"""
|
||||
clean = get_valid_filename(
|
||||
unidecode(filename.replace('/', '-').replace('\0', ''))
|
||||
)
|
||||
datedir = timezone.now().strftime('%Y-%m')
|
||||
fname = "{}/{}".format(
|
||||
timezone.now().strftime('%Y-%m-%d-%H:%M'),
|
||||
clean)
|
||||
return os.path.join(datedir, fname)
|
||||
|
||||
|
||||
|
||||
bez_diakritiky = ({}
|
||||
# FIXME: funguje jen pro český a slovenský text, jinak jsou špatně
|
||||
# transliterace. Potenciální řešení:
|
||||
|
|
|
|||
5
vysledkovky/admin.py
Normal file
5
vysledkovky/admin.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
from django.contrib import admin
|
||||
|
||||
import vysledkovky.models as m
|
||||
|
||||
admin.site.register(m.ZmrazenaVysledkovka)
|
||||
1
vysledkovky/models/__init__.py
Normal file
1
vysledkovky/models/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from .zmrazena_vysledkovka import ZmrazenaVysledkovka
|
||||
21
vysledkovky/models/zmrazena_vysledkovka.py
Normal file
21
vysledkovky/models/zmrazena_vysledkovka.py
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
from django.db import models
|
||||
|
||||
from mamweb.models.base import SeminarModelBase
|
||||
|
||||
from tvorba.models.deadline import Deadline
|
||||
|
||||
|
||||
class ZmrazenaVysledkovka(SeminarModelBase):
|
||||
class Meta:
|
||||
db_table = 'seminar_vysledkovky'
|
||||
verbose_name = 'Zmražená výsledkovka'
|
||||
verbose_name_plural = 'Zmražené výsledkovky'
|
||||
|
||||
deadline = models.OneToOneField(
|
||||
Deadline,
|
||||
on_delete=models.CASCADE,
|
||||
primary_key=True,
|
||||
related_name="vysledkovka_v_deadlinu"
|
||||
)
|
||||
|
||||
html = models.TextField(null=False, blank=False)
|
||||
|
|
@ -2,10 +2,18 @@ import abc
|
|||
from functools import cached_property
|
||||
from typing import Union, Iterable # TODO: s pythonem 3.10 přepsat na '|'
|
||||
|
||||
import seminar.models as m
|
||||
from django.db.models import Q, Sum
|
||||
from seminar.utils import resi_v_rocniku
|
||||
|
||||
from tvorba.models.problem import Problem
|
||||
from tvorba.models.clanek import Clanek
|
||||
from tvorba.models.rocnik import Rocnik
|
||||
from tvorba.models.cislo import Cislo
|
||||
from tvorba.models.deadline import Deadline
|
||||
from personalni.models.resitel import Resitel
|
||||
from odevzdavatko.models.hodnoceni import Hodnoceni
|
||||
from soustredeni.models.konfera import Konfera
|
||||
|
||||
ROCNIK_ZRUSENI_TEMAT = 25
|
||||
|
||||
|
||||
|
|
@ -18,11 +26,11 @@ class FixedIterator:
|
|||
|
||||
|
||||
def body_resitelu(
|
||||
za: Union[m.Cislo, m.Rocnik, None] = None,
|
||||
do: m.Deadline = None,
|
||||
od: m.Deadline = None,
|
||||
za: Union[Cislo, Rocnik, None] = None,
|
||||
do: Deadline = None,
|
||||
od: Deadline = None,
|
||||
jen_verejne: bool = True,
|
||||
resitele: Iterable[m.Resitel] = None,
|
||||
resitele: Iterable[Resitel] = None,
|
||||
null=0 # Výchozí hodnota, pokud pro daného řešitele nejsou body
|
||||
) -> dict[int, int]:
|
||||
filtr = Q()
|
||||
|
|
@ -31,9 +39,9 @@ def body_resitelu(
|
|||
filtr &= Q(reseni__hodnoceni__deadline_body__verejna_vysledkovka=True)
|
||||
|
||||
# Zjistíme, typ objektu v parametru "za"
|
||||
if isinstance(za, m.Rocnik):
|
||||
if isinstance(za, Rocnik):
|
||||
filtr &= Q(reseni__hodnoceni__deadline_body__cislo__rocnik=za)
|
||||
elif isinstance(za, m.Cislo):
|
||||
elif isinstance(za, Cislo):
|
||||
filtr &= Q(reseni__hodnoceni__deadline_body__cislo=za)
|
||||
|
||||
if do:
|
||||
|
|
@ -42,7 +50,7 @@ def body_resitelu(
|
|||
if od:
|
||||
filtr &= Q(reseni__hodnoceni__deadline_body__deadline__gte=od.deadline)
|
||||
|
||||
resiteleQuery = m.Resitel.objects.all()
|
||||
resiteleQuery = Resitel.objects.all()
|
||||
|
||||
if resitele is not None:
|
||||
resitele_id = [r.id for r in resitele]
|
||||
|
|
@ -63,12 +71,12 @@ def body_resitelu(
|
|||
|
||||
class Vysledkovka(abc.ABC):
|
||||
jen_verejne: bool
|
||||
rocnik: m.Rocnik
|
||||
do_deadlinu: m.Deadline
|
||||
rocnik: Rocnik
|
||||
do_deadlinu: Deadline
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def aktivni_resitele(self) -> list[m.Resitel]:
|
||||
def aktivni_resitele(self) -> list[Resitel]:
|
||||
...
|
||||
|
||||
@cached_property
|
||||
|
|
@ -143,20 +151,20 @@ class Vysledkovka(abc.ABC):
|
|||
|
||||
class VysledkovkaRocniku(Vysledkovka):
|
||||
|
||||
def __init__(self, rocnik: m.Rocnik, jen_verejne: bool = True):
|
||||
def __init__(self, rocnik: Rocnik, jen_verejne: bool = True):
|
||||
self.rocnik = rocnik
|
||||
self.jen_verejne = jen_verejne
|
||||
deadliny = m.Deadline.objects.filter(cislo__rocnik=rocnik)
|
||||
deadliny = Deadline.objects.filter(cislo__rocnik=rocnik)
|
||||
if jen_verejne:
|
||||
deadliny = deadliny.filter(verejna_vysledkovka=True)
|
||||
self.do_deadlinu = deadliny.order_by("deadline").last()
|
||||
|
||||
@cached_property
|
||||
def aktivni_resitele(self) -> list[m.Resitel]:
|
||||
def aktivni_resitele(self) -> list[Resitel]:
|
||||
return list(resi_v_rocniku(self.rocnik))
|
||||
|
||||
@cached_property
|
||||
def cisla_rocniku(self) -> list[m.Cislo]:
|
||||
def cisla_rocniku(self) -> list[Cislo]:
|
||||
""" Vrátí všechna čísla daného ročníku. """
|
||||
if self.jen_verejne:
|
||||
return self.rocnik.verejne_vysledkovky_cisla()
|
||||
|
|
@ -164,7 +172,7 @@ class VysledkovkaRocniku(Vysledkovka):
|
|||
return self.rocnik.cisla.all().order_by('poradi')
|
||||
|
||||
@cached_property
|
||||
def body_za_cisla_slovnik(self) -> dict[int, dict[int, int]]: # Výstup: m.Cislo.id → ( m.Resitel.id → body )
|
||||
def body_za_cisla_slovnik(self) -> dict[int, dict[int, int]]: # Výstup: Cislo.id → ( Resitel.id → body )
|
||||
# TODO: Body jsou decimal!
|
||||
body_cisla_slovnik = dict()
|
||||
for cislo in self.cisla_rocniku:
|
||||
|
|
@ -197,7 +205,7 @@ class VysledkovkaRocniku(Vysledkovka):
|
|||
radky_vysledkovky = []
|
||||
|
||||
setrizeni_resitele_dict = dict()
|
||||
for r in m.Resitel.objects.filter(
|
||||
for r in Resitel.objects.filter(
|
||||
id__in=self.setrizeni_resitele_id
|
||||
).select_related('osoba'):
|
||||
setrizeni_resitele_dict[r.id] = r
|
||||
|
|
@ -227,31 +235,31 @@ class VysledkovkaRocniku(Vysledkovka):
|
|||
class VysledkovkaCisla(Vysledkovka):
|
||||
def __init__(
|
||||
self,
|
||||
cislo: m.Cislo,
|
||||
cislo: Cislo,
|
||||
jen_verejne: bool = True,
|
||||
do_deadlinu: m.Deadline = None
|
||||
do_deadlinu: Deadline = None
|
||||
):
|
||||
self.cislo = cislo
|
||||
self.rocnik = cislo.rocnik
|
||||
self.jen_verejne = jen_verejne
|
||||
if do_deadlinu is None:
|
||||
do_deadlinu = m.Deadline.objects.filter(cislo=cislo).last()
|
||||
do_deadlinu = Deadline.objects.filter(cislo=cislo).last()
|
||||
self.do_deadlinu = do_deadlinu
|
||||
|
||||
@cached_property
|
||||
def aktivni_resitele(self) -> list[m.Resitel]:
|
||||
def aktivni_resitele(self) -> list[Resitel]:
|
||||
# TODO možná chytřeji vybírat aktivní řešitele
|
||||
return list(resi_v_rocniku(self.rocnik))
|
||||
|
||||
@cached_property
|
||||
def problemy(self) -> list[m.Problem]:
|
||||
def problemy(self) -> list[Problem]:
|
||||
""" Vrátí seznam všech problémů s body v daném čísle. """
|
||||
return m.Problem.objects.filter(
|
||||
hodnoceni__in=m.Hodnoceni.objects.filter(deadline_body__cislo=self.cislo)
|
||||
return Problem.objects.filter(
|
||||
hodnoceni__in=Hodnoceni.objects.filter(deadline_body__cislo=self.cislo)
|
||||
).distinct().non_polymorphic().select_related('nadproblem').select_related('nadproblem__nadproblem')
|
||||
|
||||
@cached_property
|
||||
def hlavni_problemy(self) -> list[m.Problem]:
|
||||
def hlavni_problemy(self) -> list[Problem]:
|
||||
""" Vrátí seznam všech problémů, které již nemají nadproblém. """
|
||||
# hlavní problémy čísla
|
||||
# (mají vlastní sloupeček ve výsledkovce, nemají nadproblém)
|
||||
|
|
@ -269,7 +277,7 @@ class VysledkovkaCisla(Vysledkovka):
|
|||
# Není cached, protože si myslím, že queryset lze použít ve for jen jednou.
|
||||
@property
|
||||
def hodnoceni_do_cisla(self):
|
||||
hodnoceni = m.Hodnoceni.objects.prefetch_related('reseni__resitele').select_related('problem', 'reseni')
|
||||
hodnoceni = Hodnoceni.objects.prefetch_related('reseni__resitele').select_related('problem', 'reseni')
|
||||
if self.jen_verejne:
|
||||
hodnoceni = hodnoceni.filter(deadline_body__verejna_vysledkovka=True)
|
||||
return hodnoceni.filter(
|
||||
|
|
@ -347,7 +355,7 @@ class VysledkovkaCisla(Vysledkovka):
|
|||
return self.sectene_body[2]
|
||||
|
||||
@cached_property
|
||||
def temata_a_spol(self) -> list[m.Problem]:
|
||||
def temata_a_spol(self) -> list[Problem]:
|
||||
if self.rocnik.rocnik < ROCNIK_ZRUSENI_TEMAT:
|
||||
return self.hlavni_problemy
|
||||
else:
|
||||
|
|
@ -358,7 +366,7 @@ class VysledkovkaCisla(Vysledkovka):
|
|||
return len(self.hlavni_problemy) - len(self.temata_a_spol) > 0
|
||||
|
||||
@cached_property
|
||||
def podproblemy(self) -> dict[int, list[m.Problem]]:
|
||||
def podproblemy(self) -> dict[int, list[Problem]]:
|
||||
podproblemy = {hp.id: [] for hp in self.temata_a_spol}
|
||||
temata_a_spol = set(self.temata_a_spol)
|
||||
podproblemy[-1] = []
|
||||
|
|
@ -381,7 +389,7 @@ class VysledkovkaCisla(Vysledkovka):
|
|||
return podproblemy
|
||||
|
||||
@cached_property
|
||||
def podproblemy_seznam(self) -> list[list[m.Problem]]:
|
||||
def podproblemy_seznam(self) -> list[list[Problem]]:
|
||||
return [self.podproblemy[it.id] for it in self.temata_a_spol] + [self.podproblemy[-1]]
|
||||
|
||||
@cached_property
|
||||
|
|
@ -411,7 +419,7 @@ class VysledkovkaCisla(Vysledkovka):
|
|||
radky_vysledkovky = []
|
||||
|
||||
setrizeni_resitele_slovnik = {}
|
||||
setrizeni_resitele = m.Resitel.objects.filter(id__in=self.setrizeni_resitele_id).select_related('osoba')
|
||||
setrizeni_resitele = Resitel.objects.filter(id__in=self.setrizeni_resitele_id).select_related('osoba')
|
||||
|
||||
for r in setrizeni_resitele:
|
||||
setrizeni_resitele_slovnik[r.id] = r
|
||||
|
|
@ -462,29 +470,29 @@ class VysledkovkaCisla(Vysledkovka):
|
|||
@staticmethod
|
||||
def ne_clanek_ne_konfera(problem):
|
||||
inst = problem.get_real_instance()
|
||||
return not (isinstance(inst, m.Clanek) or isinstance(inst, m.Konfera))
|
||||
return not (isinstance(inst, Clanek) or isinstance(inst, Konfera))
|
||||
|
||||
|
||||
class VysledkovkaDoTeXu(VysledkovkaCisla):
|
||||
def __init__(
|
||||
self,
|
||||
nejake_cislo: m.Cislo,
|
||||
od_vyjma: m.Deadline,
|
||||
do_vcetne: m.Deadline
|
||||
nejake_cislo: Cislo,
|
||||
od_vyjma: Deadline,
|
||||
do_vcetne: Deadline
|
||||
):
|
||||
super().__init__(nejake_cislo, False, do_vcetne)
|
||||
self.od_deadlinu = od_vyjma
|
||||
|
||||
@cached_property
|
||||
def problemy(self) -> list[m.Problem]:
|
||||
return m.Problem.objects.filter(hodnoceni__in=m.Hodnoceni.objects.filter(
|
||||
def problemy(self) -> list[Problem]:
|
||||
return Problem.objects.filter(hodnoceni__in=Hodnoceni.objects.filter(
|
||||
deadline_body__deadline__gt=self.od_deadlinu.deadline,
|
||||
deadline_body__deadline__lte=self.do_deadlinu.deadline,
|
||||
)).distinct().non_polymorphic().select_related('nadproblem').select_related('nadproblem__nadproblem')
|
||||
|
||||
@property
|
||||
def hodnoceni_do_cisla(self):
|
||||
hodnoceni = m.Hodnoceni.objects.prefetch_related(
|
||||
hodnoceni = Hodnoceni.objects.prefetch_related(
|
||||
'problem', 'reseni', 'reseni__resitele')
|
||||
if self.jen_verejne:
|
||||
hodnoceni = hodnoceni.filter(deadline_body__verejna_vysledkovka=True)
|
||||
|
|
|
|||
Loading…
Reference in a new issue