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 django.utils.encoding import force_text
|
||||||
|
|
||||||
from .utils import default_ovvpfile
|
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 vysledkovky import utils
|
||||||
from seminar.utils import aktivniResitele
|
from seminar.utils import aktivniResitele
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
#coding: utf-8
|
#coding: utf-8
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from seminar.models import Soustredeni
|
from soustredeni.models.soustredeni import Soustredeni
|
||||||
|
|
||||||
class KomentarForm(forms.Form):
|
class KomentarForm(forms.Form):
|
||||||
komentar = forms.CharField(label = "Komentář:", max_length = 300, required=False)
|
komentar = forms.CharField(label = "Komentář:", max_length = 300, required=False)
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ from imagekit.processors import ResizeToFit, Transpose
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from seminar.models import Soustredeni
|
from soustredeni.models.soustredeni import Soustredeni
|
||||||
|
|
||||||
VZDY=0
|
VZDY=0
|
||||||
ORG=1
|
ORG=1
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ from django.template import RequestContext
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from galerie.models import Obrazek, Galerie
|
from galerie.models import Obrazek, Galerie
|
||||||
from seminar.models import Soustredeni
|
from soustredeni.models.soustredeni import Soustredeni
|
||||||
from galerie.forms import KomentarForm, NewGalerieForm
|
from galerie.forms import KomentarForm, NewGalerieForm
|
||||||
|
|
||||||
def zobrazit(galerie, request):
|
def zobrazit(galerie, request):
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.text import get_valid_filename
|
from django.utils.text import get_valid_filename
|
||||||
|
|
||||||
from seminar.models import Organizator
|
from personalni.models.organizator import Organizator
|
||||||
|
|
||||||
import subprocess
|
import subprocess
|
||||||
from reversion import revisions as reversion
|
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',
|
'personalni',
|
||||||
'soustredeni',
|
'soustredeni',
|
||||||
'treenode',
|
'treenode',
|
||||||
|
'tvorba',
|
||||||
|
|
||||||
# Admin upravy:
|
# Admin upravy:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ s dekorátorem :func:`django.contrib.admin.register`.
|
||||||
"""
|
"""
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django_reverse_admin import ReverseModelAdmin
|
from django_reverse_admin import ReverseModelAdmin
|
||||||
import seminar.models as m
|
import odevzdavatko.models as m
|
||||||
|
|
||||||
|
|
||||||
class PrilohaReseniInline(admin.TabularInline):
|
class PrilohaReseniInline(admin.TabularInline):
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,11 @@ from django.forms import formset_factory
|
||||||
from django.forms.models import inlineformset_factory
|
from django.forms.models import inlineformset_factory
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from seminar.models import Resitel
|
from personalni.models.resitel import Resitel
|
||||||
import seminar.models as m
|
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
|
import logging
|
||||||
|
|
||||||
|
|
@ -22,7 +25,7 @@ class DateInput(forms.DateInput):
|
||||||
|
|
||||||
class PosliReseniForm(forms.Form):
|
class PosliReseniForm(forms.Form):
|
||||||
problem = forms.ModelMultipleChoiceField(
|
problem = forms.ModelMultipleChoiceField(
|
||||||
queryset=m.Problem.objects.all(),
|
queryset=Problem.objects.all(),
|
||||||
label="Problémy",
|
label="Problémy",
|
||||||
widget=autocomplete.ModelSelect2Multiple(
|
widget=autocomplete.ModelSelect2Multiple(
|
||||||
url='autocomplete_problem',
|
url='autocomplete_problem',
|
||||||
|
|
@ -165,7 +168,7 @@ class OdevzdavatkoTabulkaFiltrForm(forms.Form):
|
||||||
|
|
||||||
from django.db.utils import OperationalError
|
from django.db.utils import OperationalError
|
||||||
try:
|
try:
|
||||||
aktualni_rocnik = m.Nastaveni.get_solo().aktualni_rocnik
|
aktualni_rocnik = Nastaveni.get_solo().aktualni_rocnik
|
||||||
except OperationalError:
|
except OperationalError:
|
||||||
# django.db.utils.OperationalError: no such table: seminar_nastaveni
|
# 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
|
# 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"))
|
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(),
|
deadline__lte=timezone.now(),
|
||||||
cislo__rocnik=aktualni_rocnik
|
cislo__rocnik=aktualni_rocnik
|
||||||
).order_by("deadline"):
|
).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()
|
register = template.Library()
|
||||||
|
|
||||||
from personalni.utils import normalizuj_jmeno
|
from personalni.utils import normalizuj_jmeno
|
||||||
import seminar.models as m # jen kvůli typové anotaci…
|
from personalni.models.osoba import Osoba
|
||||||
|
|
||||||
@register.filter
|
@register.filter
|
||||||
def jmeno_jako_prefix(o: m.Osoba):
|
def jmeno_jako_prefix(o: Osoba):
|
||||||
return normalizuj_jmeno(o).replace(' ', '_')
|
return normalizuj_jmeno(o).replace(' ', '_')
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,15 @@ import datetime
|
||||||
from itertools import groupby
|
from itertools import groupby
|
||||||
import logging
|
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 . import forms as f
|
||||||
from .forms import OdevzdavatkoTabulkaFiltrForm as FiltrForm
|
from .forms import OdevzdavatkoTabulkaFiltrForm as FiltrForm
|
||||||
from seminar.utils import resi_v_rocniku
|
from seminar.utils import resi_v_rocniku
|
||||||
|
|
@ -54,13 +62,13 @@ class TabulkaOdevzdanychReseniView(ListView):
|
||||||
# FIXME: jméno metody není vypovídající...
|
# 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
|
# 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, ...
|
# TODO: Prefetches, Select related, ...
|
||||||
self.resitele = m.Resitel.objects.all()
|
self.resitele = Resitel.objects.all()
|
||||||
self.problemy = m.Problem.objects.all()
|
self.problemy = Problem.objects.all()
|
||||||
self.reseni = m.Reseni.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:
|
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)
|
form = FiltrForm(self.request.GET, rocnik=self.aktualni_rocnik)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
|
|
@ -91,14 +99,14 @@ class TabulkaOdevzdanychReseniView(ListView):
|
||||||
self.resitele = self.resitele.filter(rok_maturity__gt=self.aktualni_rocnik.prvni_rok)
|
self.resitele = self.resitele.filter(rok_maturity__gt=self.aktualni_rocnik.prvni_rok)
|
||||||
|
|
||||||
if problemy == FiltrForm.PROBLEMY_MOJE:
|
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(
|
self.problemy = self.problemy.filter(
|
||||||
Q(autor=org)|Q(garant=org)|Q(opravovatele=org),
|
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:
|
elif problemy == FiltrForm.PROBLEMY_LETOSNI:
|
||||||
self.problemy = self.problemy.filter(
|
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....
|
#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.
|
# 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:
|
# Pro použití hacku na automatické {{form.media}} v template:
|
||||||
ctx['form'] = ctx['filtr']
|
ctx['form'] = ctx['filtr']
|
||||||
# Pro maximum v přesměrovátku ročníků
|
# 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:
|
if 'rocnik' in self.kwargs:
|
||||||
ctx['rocnik'] = self.kwargs['rocnik']
|
ctx['rocnik'] = self.kwargs['rocnik']
|
||||||
else:
|
else:
|
||||||
|
|
@ -186,8 +194,8 @@ class ReseniProblemuView(MultipleObjectTemplateResponseMixin, MultipleObjectMixi
|
||||||
if problem_id is None:
|
if problem_id is None:
|
||||||
raise ValueError("Nemám problém! (To je problém!)")
|
raise ValueError("Nemám problém! (To je problém!)")
|
||||||
|
|
||||||
resitel = m.Resitel.objects.get(id=resitel_id)
|
resitel = Resitel.objects.get(id=resitel_id)
|
||||||
problem = m.Problem.objects.get(id=problem_id)
|
problem = Problem.objects.get(id=problem_id)
|
||||||
qs = qs.filter(
|
qs = qs.filter(
|
||||||
problem__in=[problem],
|
problem__in=[problem],
|
||||||
resitele__in=[resitel],
|
resitele__in=[resitel],
|
||||||
|
|
@ -316,7 +324,7 @@ class PrehledOdevzdanychReseni(ListView):
|
||||||
if not self.request.user.is_authenticated:
|
if not self.request.user.is_authenticated:
|
||||||
raise RuntimeError("Uživatel měl být přihlášený!")
|
raise RuntimeError("Uživatel měl být přihlášený!")
|
||||||
# get_or_none, aby neexistence řešitele (např. u orgů) neházela chybu
|
# 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 = super().get_queryset()
|
||||||
qs = qs.filter(reseni__resitele__in=[resitel])
|
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í
|
# 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):
|
class SeznamAktualnichReseniView(SeznamReseniView):
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
qs = super().get_queryset()
|
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)
|
resitele = resi_v_rocniku(akt_rocnik)
|
||||||
qs = qs.filter(resitele__in=resitele) # FIXME: Najde řešení i ze starých ročníků, která odevzdal alespoň jeden aktuální řešitel
|
qs = qs.filter(resitele__in=resitele) # FIXME: Najde řešení i ze starých ročníků, která odevzdal alespoň jeden aktuální řešitel
|
||||||
return qs
|
return qs
|
||||||
|
|
@ -389,9 +397,9 @@ class NahrajReseniView(LoginRequiredMixin, CreateView):
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
# Zaříznutí starých řešitelů:
|
# Zaříznutí starých řešitelů:
|
||||||
# FIXME: Je to tady dost naprasené, mělo by to asi být jinde…
|
# 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
|
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', {
|
return render(request, 'universal.html', {
|
||||||
'title': 'Nelze odevzdat',
|
'title': 'Nelze odevzdat',
|
||||||
'error': 'Zdá se, že jsi již odmaturoval/a, a tedy nemůžeš odevzdat do našeho semináře řešení.',
|
'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)
|
return super().form_invalid(form)
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
self.object = form.save()
|
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.resitele.add(*form.cleaned_data["resitele"])
|
||||||
self.object.cas_doruceni = timezone.now()
|
self.object.cas_doruceni = timezone.now()
|
||||||
self.object.forma = m.Reseni.FORMA_UPLOAD
|
self.object.forma = m.Reseni.FORMA_UPLOAD
|
||||||
|
|
@ -426,7 +434,7 @@ class NahrajReseniView(LoginRequiredMixin, CreateView):
|
||||||
prilohy.save()
|
prilohy.save()
|
||||||
|
|
||||||
for hodnoceni in self.object.hodnoceni_set.all():
|
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()
|
hodnoceni.save()
|
||||||
|
|
||||||
# Pošleme mail opravovatelům a garantovi
|
# 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?
|
# FIXME: Víc informativní obsah mailů, možná vč. příloh?
|
||||||
prijemci = map(lambda it: it.osoba.email, prijemci)
|
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 = "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 })")
|
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 import admin
|
||||||
from django.contrib.auth.models import Group
|
from django.contrib.auth.models import Group
|
||||||
from django_reverse_admin import ReverseModelAdmin
|
from django_reverse_admin import ReverseModelAdmin
|
||||||
import seminar.models as m
|
import personalni.models as m
|
||||||
|
|
||||||
|
|
||||||
@admin.register(m.Osoba)
|
@admin.register(m.Osoba)
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ from django.contrib.auth.forms import PasswordResetForm
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
|
||||||
from seminar.models import Skola, Resitel, Osoba
|
from personalni.models import Skola, Resitel, Osoba
|
||||||
|
|
||||||
from datetime import date
|
from datetime import date
|
||||||
import logging
|
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
|
from various.utils import bez_diakritiky_translate
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,14 @@ from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
|
|
||||||
import seminar.models as s
|
from tvorba.models.tema import Tema
|
||||||
import seminar.models as m
|
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 .forms import PrihlaskaForm, ProfileEditForm, PoMaturiteProfileEditForm
|
||||||
|
|
||||||
from datetime import date
|
from datetime import date
|
||||||
|
|
@ -31,16 +37,16 @@ class OrgoRozcestnikView(TemplateView):
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
context['posledni_soustredeni'] = s.Soustredeni.objects.order_by('-datum_konce').first()
|
context['posledni_soustredeni'] = Soustredeni.objects.order_by('-datum_konce').first()
|
||||||
nastaveni = s.Nastaveni.objects.first()
|
nastaveni = Nastaveni.objects.first()
|
||||||
aktualni_rocnik = nastaveni.aktualni_rocnik
|
aktualni_rocnik = nastaveni.aktualni_rocnik
|
||||||
context['posledni_cislo_url'] = nastaveni.aktualni_cislo.verejne_url()
|
context['posledni_cislo_url'] = nastaveni.aktualni_cislo.verejne_url()
|
||||||
# TODO možná chceme odkazovat na právě rozpracované číslo, a ne to poslední vydané
|
# 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
|
# 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)
|
# přes treenody (a dát si přitom pozor na MezicisloNode)
|
||||||
|
|
||||||
neobodovana_reseni = s.Hodnoceni.objects.filter(body__isnull=True)
|
neobodovana_reseni = Hodnoceni.objects.filter(body__isnull=True)
|
||||||
reseni_mimo_cislo = s.Hodnoceni.objects.filter(deadline_body__isnull=True)
|
reseni_mimo_cislo = Hodnoceni.objects.filter(deadline_body__isnull=True)
|
||||||
context['pocet_neobodovanych_reseni'] = neobodovana_reseni.count()
|
context['pocet_neobodovanych_reseni'] = neobodovana_reseni.count()
|
||||||
context['pocet_reseni_mimo_cislo'] = reseni_mimo_cislo.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()]
|
context["pocty_neopravenych_reseni"] = [(it['problem__nazev'], it['cas'].date) for it in pocty_neopravenych_reseni.all()]
|
||||||
|
|
||||||
#FIXME: přidat stav='STAV_ZADANY'
|
#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()
|
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()
|
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()
|
cislo__rocnik=aktualni_rocnik).distinct()
|
||||||
|
|
||||||
context['temata'] = temata
|
context['temata'] = temata
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ from django.utils.safestring import mark_safe
|
||||||
from django.utils.html import escape
|
from django.utils.html import escape
|
||||||
|
|
||||||
from .models import Prednaska, Seznam, STAV_NAVRH
|
from .models import Prednaska, Seznam, STAV_NAVRH
|
||||||
from seminar.models import Soustredeni
|
from soustredeni.models.soustredeni import Soustredeni
|
||||||
|
|
||||||
|
|
||||||
class Seznam_PrednaskaInline(admin.TabularInline):
|
class Seznam_PrednaskaInline(admin.TabularInline):
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,8 @@
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.encoding import force_text
|
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_NAVRH = 1
|
||||||
STAV_BUDE = 2
|
STAV_BUDE = 2
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,8 @@ from django.db.models import Sum
|
||||||
from django.forms import Form
|
from django.forms import Form
|
||||||
|
|
||||||
from prednasky.models import Prednaska, Hlasovani, Seznam, STAV_NAVRH
|
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):
|
def newPrednaska(request):
|
||||||
# hlasovani se vztahuje k nejnovejsimu soustredeni
|
# 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.contrib import admin
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.forms import widgets, ModelForm
|
from django.forms import widgets
|
||||||
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
|
|
||||||
|
|
||||||
import seminar.models as m
|
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):
|
class TextAdminInline(admin.TabularInline):
|
||||||
model = m.Text
|
model = m.Text
|
||||||
formfield_overrides = {
|
formfield_overrides = {
|
||||||
models.TextField: {'widget': widgets.TextInput}
|
models.TextField: {'widget': widgets.TextInput}
|
||||||
}
|
}
|
||||||
exclude = ['text_zkraceny_set','text_zkraceny']
|
exclude = ['text_zkraceny_set', 'text_zkraceny']
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(m.Text)
|
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.Obrazek)
|
||||||
admin.site.register(m.Nastaveni, SingletonModelAdmin)
|
|
||||||
admin.site.register(m.Novinky)
|
admin.site.register(m.Novinky)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
# Generated by Django 2.2.24 on 2021-11-29 22:54
|
# Generated by Django 2.2.24 on 2021-11-29 22:54
|
||||||
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
import seminar.models.tvorba
|
import tvorba.models
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
@ -14,6 +14,6 @@ class Migration(migrations.Migration):
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='cislo',
|
model_name='cislo',
|
||||||
name='pdf',
|
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 .pomocne import Text, Obrazek
|
||||||
from .odevzdavatko import *
|
from .novinky import Novinky
|
||||||
from .base import *
|
|
||||||
from .personalni import *
|
|
||||||
from .soustredeni import *
|
|
||||||
from .pomocne import *
|
|
||||||
from .treenode import *
|
|
||||||
from .novinky import *
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ from imagekit.processors import ResizeToFit
|
||||||
|
|
||||||
from reversion import revisions as reversion
|
from reversion import revisions as reversion
|
||||||
|
|
||||||
from . import personalni as pm
|
from personalni.models.organizator import Organizator
|
||||||
|
|
||||||
@reversion.register(ignore_duplicates=True)
|
@reversion.register(ignore_duplicates=True)
|
||||||
class Novinky(models.Model):
|
class Novinky(models.Model):
|
||||||
|
|
@ -26,7 +26,7 @@ class Novinky(models.Model):
|
||||||
],
|
],
|
||||||
options={'quality': 95})
|
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)
|
on_delete=models.SET_NULL)
|
||||||
|
|
||||||
zverejneno = models.BooleanField('Zveřejněno', default=False)
|
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
|
import os
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
from .base import SeminarModelBase
|
from mamweb.models.base import SeminarModelBase
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
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 import template
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
import seminar.models as m
|
from tvorba.models.deadline import Deadline
|
||||||
|
|
||||||
@register.filter(name='deadline_kratseji')
|
@register.filter(name='deadline_kratseji')
|
||||||
def deadline_kratsi_text(deadline: m.Deadline):
|
def deadline_kratsi_text(deadline: Deadline):
|
||||||
if deadline is None:
|
if deadline is None:
|
||||||
return 'NONE'
|
return 'NONE'
|
||||||
strings = {
|
strings = {
|
||||||
m.Deadline.TYP_PRVNI: f"{deadline.cislo} ⭯",
|
Deadline.TYP_PRVNI: f"{deadline.cislo} ⭯",
|
||||||
m.Deadline.TYP_SOUS: f"{deadline.cislo} Ⓢ",
|
Deadline.TYP_SOUS: f"{deadline.cislo} Ⓢ",
|
||||||
m.Deadline.TYP_PRVNI_A_SOUS: f"{deadline.cislo} ⭯Ⓢ",
|
Deadline.TYP_PRVNI_A_SOUS: f"{deadline.cislo} ⭯Ⓢ",
|
||||||
m.Deadline.TYP_CISLA: f"{deadline.cislo} ✓",
|
Deadline.TYP_CISLA: f"{deadline.cislo} ✓",
|
||||||
}
|
}
|
||||||
return strings[deadline.typ]
|
return strings[deadline.typ]
|
||||||
|
|
||||||
@register.filter(name='deadline_html')
|
@register.filter(name='deadline_html')
|
||||||
def deadline_html(deadline: m.Deadline):
|
def deadline_html(deadline: Deadline):
|
||||||
if deadline is None:
|
if deadline is None:
|
||||||
return 'Neznámý deadline'
|
return 'Neznámý deadline'
|
||||||
text = deadline_kratsi_text(deadline)
|
text = deadline_kratsi_text(deadline)
|
||||||
classes = {
|
classes = {
|
||||||
m.Deadline.TYP_PRVNI: 'preddeadline',
|
Deadline.TYP_PRVNI: 'preddeadline',
|
||||||
m.Deadline.TYP_SOUS: 'sous_deadline',
|
Deadline.TYP_SOUS: 'sous_deadline',
|
||||||
m.Deadline.TYP_PRVNI_A_SOUS: 'sous_deadline',
|
Deadline.TYP_PRVNI_A_SOUS: 'sous_deadline',
|
||||||
m.Deadline.TYP_CISLA: 'final_deadline',
|
Deadline.TYP_CISLA: 'final_deadline',
|
||||||
}
|
}
|
||||||
return mark_safe(f'<span class="{classes[deadline.typ]}" title="{deadline}">{text}</span>')
|
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.views.generic.base import RedirectView
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
|
|
||||||
import seminar.models as s
|
from tvorba.models.problem import Problem
|
||||||
import seminar.models as m
|
from tvorba.models.tema import Tema
|
||||||
from seminar.models import Problem, Cislo, Reseni, Nastaveni, Rocnik, \
|
from tvorba.models.clanek import Clanek
|
||||||
Organizator, Resitel, Novinky, Tema, Clanek, \
|
from tvorba.models.nastaveni import Nastaveni
|
||||||
Deadline # Tohle je stare a chceme se toho zbavit. Pouzivejte s.ToCoChci
|
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 .models import VysledkyZaCislo, VysledkyKCisluZaRocnik, VysledkyKCisluOdjakziva
|
||||||
from seminar import utils
|
from seminar import utils
|
||||||
from treenode import treelib
|
from treenode import treelib
|
||||||
|
|
@ -113,7 +119,7 @@ def ZadaniTemataView(request):
|
||||||
nastaveni = get_object_or_404(Nastaveni)
|
nastaveni = get_object_or_404(Nastaveni)
|
||||||
verejne = nastaveni.aktualni_cislo.verejne()
|
verejne = nastaveni.aktualni_cislo.verejne()
|
||||||
akt_rocnik = nastaveni.aktualni_cislo.rocnik
|
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',
|
return render(request, 'seminar/tematka/rozcestnik.html',
|
||||||
{
|
{
|
||||||
'tematka': temata,
|
'tematka': temata,
|
||||||
|
|
@ -244,7 +250,7 @@ class TitulniStranaView(generic.ListView):
|
||||||
context = super(TitulniStranaView, self).get_context_data(**kwargs)
|
context = super(TitulniStranaView, self).get_context_data(**kwargs)
|
||||||
nastaveni = get_object_or_404(Nastaveni)
|
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
|
context['nejblizsi_deadline'] = deadline
|
||||||
|
|
||||||
# Aktuální témata
|
# Aktuální témata
|
||||||
|
|
@ -349,7 +355,7 @@ def resiteleRocnikuCsvExportView(request, rocnik):
|
||||||
assert request.method in ('GET', 'HEAD')
|
assert request.method in ('GET', 'HEAD')
|
||||||
return dataResiteluCsvResponse(
|
return dataResiteluCsvResponse(
|
||||||
utils.resi_v_rocniku(
|
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 = []
|
deadliny_s_vysledkovkami = []
|
||||||
|
|
||||||
nadpisy = {
|
nadpisy = {
|
||||||
m.Deadline.TYP_CISLA: "Výsledkovka",
|
Deadline.TYP_CISLA: "Výsledkovka",
|
||||||
m.Deadline.TYP_PRVNI: "Výsledkovka do prvního deadlinu",
|
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í",
|
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_SOUS: "Výsledkovka do deadlinu pro účast na soustředění",
|
||||||
}
|
}
|
||||||
|
|
||||||
for deadline in deadliny:
|
for deadline in deadliny:
|
||||||
|
|
@ -703,5 +709,5 @@ class AktualniRocnikRedirectView(RedirectView):
|
||||||
pattern_name = 'seminar_rocnik'
|
pattern_name = 'seminar_rocnik'
|
||||||
|
|
||||||
def get_redirect_url(self, *args, **kwargs):
|
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)
|
return super().get_redirect_url(rocnik=aktualni_rocnik, *args, **kwargs)
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.forms import widgets
|
from django.forms import widgets
|
||||||
from django.db import models
|
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):
|
class SoustredeniUcastniciInline(admin.TabularInline):
|
||||||
|
|
@ -41,3 +44,8 @@ class SoustredeniAdmin(admin.ModelAdmin):
|
||||||
inline_type = 'tabular'
|
inline_type = 'tabular'
|
||||||
inlines = [SoustredeniUcastniciInline, SoustredeniOrganizatoriInline]
|
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.views import generic
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.staticfiles.finders import find
|
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 csv
|
||||||
import tempfile
|
import tempfile
|
||||||
import shutil
|
import shutil
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ from django.contrib import admin
|
||||||
|
|
||||||
from polymorphic.admin import PolymorphicParentModelAdmin, PolymorphicChildModelAdmin, PolymorphicChildModelFilter
|
from polymorphic.admin import PolymorphicParentModelAdmin, PolymorphicChildModelAdmin, PolymorphicChildModelFilter
|
||||||
|
|
||||||
import seminar.models as m
|
import treenode.models as m
|
||||||
|
|
||||||
# Polymorfismus pro stromy
|
# Polymorfismus pro stromy
|
||||||
# TODO: Inlines podle https://django-polymorphic.readthedocs.io/en/stable/admin.html
|
# 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 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__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
from seminar.models import tvorba as am
|
from tvorba import models as am
|
||||||
|
|
||||||
|
|
||||||
class TreeNode(PolymorphicModel):
|
class TreeNode(PolymorphicModel):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
@ -264,3 +266,21 @@ class CastNode(TreeNode):
|
||||||
|
|
||||||
def getOdkazStr(self):
|
def getOdkazStr(self):
|
||||||
return str(self.nadpis)
|
return str(self.nadpis)
|
||||||
|
|
||||||
|
|
||||||
|
class ReseniNode(TreeNode):
|
||||||
|
class Meta:
|
||||||
|
db_table = 'seminar_nodes_otistene_reseni'
|
||||||
|
verbose_name = 'Otištěné řešení (Node)'
|
||||||
|
verbose_name_plural = 'Otištěná řešení (Node)'
|
||||||
|
reseni = models.ForeignKey(
|
||||||
|
Reseni,
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
verbose_name='reseni'
|
||||||
|
)
|
||||||
|
|
||||||
|
def aktualizuj_nazev(self):
|
||||||
|
self.nazev = "ReseniNode: "+str(self.reseni)
|
||||||
|
|
||||||
|
def getOdkazStr(self):
|
||||||
|
return str(self.reseni)
|
||||||
|
|
@ -1,9 +1,13 @@
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_polymorphic.serializers import PolymorphicSerializer
|
from rest_polymorphic.serializers import PolymorphicSerializer
|
||||||
|
|
||||||
import seminar.models as m
|
|
||||||
from treenode import treelib
|
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
|
DEFAULT_NODE_DEPTH = 2
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -14,17 +18,17 @@ class TextSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
class ProblemSerializer(serializers.ModelSerializer):
|
class ProblemSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = m.Problem
|
model = Problem
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
|
|
||||||
class UlohaSerializer(serializers.ModelSerializer):
|
class UlohaSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = m.Uloha
|
model = Uloha
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
|
|
||||||
class ReseniSerializer(serializers.ModelSerializer):
|
class ReseniSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = m.Reseni
|
model = Reseni
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
|
|
||||||
class RocnikNodeSerializer(serializers.ModelSerializer):
|
class RocnikNodeSerializer(serializers.ModelSerializer):
|
||||||
|
|
@ -184,7 +188,7 @@ class UlohaZadaniNodeCreateSerializer(serializers.ModelSerializer):
|
||||||
cislo = travelnode.cislo
|
cislo = travelnode.cislo
|
||||||
travelnode = treelib.get_parent(travelnode)
|
travelnode = treelib.get_parent(travelnode)
|
||||||
# Vyrobime ulohu
|
# 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
|
# A vyrobime UlohaZadaniNode
|
||||||
if where == 'syn':
|
if where == 'syn':
|
||||||
|
|
@ -211,7 +215,7 @@ class UlohaVzorakNodeSerializer(serializers.ModelSerializer):
|
||||||
depth = DEFAULT_NODE_DEPTH
|
depth = DEFAULT_NODE_DEPTH
|
||||||
|
|
||||||
class UlohaVzorakNodeWriteSerializer(serializers.ModelSerializer):
|
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:
|
class Meta:
|
||||||
model = m.UlohaVzorakNode
|
model = m.UlohaVzorakNode
|
||||||
|
|
@ -226,7 +230,7 @@ class UlohaVzorakNodeCreateSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
uloha_id = validated_data.pop('uloha_id')
|
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')
|
where = validated_data.pop('where')
|
||||||
refnode_id = validated_data.pop('refnode')
|
refnode_id = validated_data.pop('refnode')
|
||||||
refnode = m.TreeNode.objects.get(pk=refnode_id)
|
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.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
|
|
||||||
import seminar.models as s
|
from seminar.models.pomocne import Obrazek
|
||||||
import seminar.models as m
|
import treenode.models as s
|
||||||
|
import treenode.models as m
|
||||||
from treenode import treelib
|
from treenode import treelib
|
||||||
import treenode.forms as f
|
import treenode.forms as f
|
||||||
import treenode.templatetags as tnltt
|
import treenode.templatetags as tnltt
|
||||||
|
|
@ -300,7 +301,7 @@ class VueTestView(generic.TemplateView):
|
||||||
|
|
||||||
|
|
||||||
class NahrajObrazekKTreeNoduView(LoginRequiredMixin, CreateView):
|
class NahrajObrazekKTreeNoduView(LoginRequiredMixin, CreateView):
|
||||||
model = s.Obrazek
|
model = Obrazek
|
||||||
form_class = f.NahrajObrazekKTreeNoduForm
|
form_class = f.NahrajObrazekKTreeNoduForm
|
||||||
|
|
||||||
def get_initial(self):
|
def get_initial(self):
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ from rest_framework import status
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
from rest_framework.permissions import BasePermission, AllowAny
|
from rest_framework.permissions import BasePermission, AllowAny
|
||||||
from seminar import models as m
|
from treenode import models as m
|
||||||
import treenode.serializers as views
|
import treenode.serializers as views
|
||||||
|
|
||||||
from treenode.permissions import AllowWrite
|
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 = ({}
|
bez_diakritiky = ({}
|
||||||
# FIXME: funguje jen pro český a slovenský text, jinak jsou špatně
|
# FIXME: funguje jen pro český a slovenský text, jinak jsou špatně
|
||||||
# transliterace. Potenciální řešení:
|
# 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 functools import cached_property
|
||||||
from typing import Union, Iterable # TODO: s pythonem 3.10 přepsat na '|'
|
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 django.db.models import Q, Sum
|
||||||
from seminar.utils import resi_v_rocniku
|
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
|
ROCNIK_ZRUSENI_TEMAT = 25
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -18,11 +26,11 @@ class FixedIterator:
|
||||||
|
|
||||||
|
|
||||||
def body_resitelu(
|
def body_resitelu(
|
||||||
za: Union[m.Cislo, m.Rocnik, None] = None,
|
za: Union[Cislo, Rocnik, None] = None,
|
||||||
do: m.Deadline = None,
|
do: Deadline = None,
|
||||||
od: m.Deadline = None,
|
od: Deadline = None,
|
||||||
jen_verejne: bool = True,
|
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
|
null=0 # Výchozí hodnota, pokud pro daného řešitele nejsou body
|
||||||
) -> dict[int, int]:
|
) -> dict[int, int]:
|
||||||
filtr = Q()
|
filtr = Q()
|
||||||
|
|
@ -31,9 +39,9 @@ def body_resitelu(
|
||||||
filtr &= Q(reseni__hodnoceni__deadline_body__verejna_vysledkovka=True)
|
filtr &= Q(reseni__hodnoceni__deadline_body__verejna_vysledkovka=True)
|
||||||
|
|
||||||
# Zjistíme, typ objektu v parametru "za"
|
# 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)
|
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)
|
filtr &= Q(reseni__hodnoceni__deadline_body__cislo=za)
|
||||||
|
|
||||||
if do:
|
if do:
|
||||||
|
|
@ -42,7 +50,7 @@ def body_resitelu(
|
||||||
if od:
|
if od:
|
||||||
filtr &= Q(reseni__hodnoceni__deadline_body__deadline__gte=od.deadline)
|
filtr &= Q(reseni__hodnoceni__deadline_body__deadline__gte=od.deadline)
|
||||||
|
|
||||||
resiteleQuery = m.Resitel.objects.all()
|
resiteleQuery = Resitel.objects.all()
|
||||||
|
|
||||||
if resitele is not None:
|
if resitele is not None:
|
||||||
resitele_id = [r.id for r in resitele]
|
resitele_id = [r.id for r in resitele]
|
||||||
|
|
@ -63,12 +71,12 @@ def body_resitelu(
|
||||||
|
|
||||||
class Vysledkovka(abc.ABC):
|
class Vysledkovka(abc.ABC):
|
||||||
jen_verejne: bool
|
jen_verejne: bool
|
||||||
rocnik: m.Rocnik
|
rocnik: Rocnik
|
||||||
do_deadlinu: m.Deadline
|
do_deadlinu: Deadline
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def aktivni_resitele(self) -> list[m.Resitel]:
|
def aktivni_resitele(self) -> list[Resitel]:
|
||||||
...
|
...
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
|
|
@ -143,20 +151,20 @@ class Vysledkovka(abc.ABC):
|
||||||
|
|
||||||
class VysledkovkaRocniku(Vysledkovka):
|
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.rocnik = rocnik
|
||||||
self.jen_verejne = jen_verejne
|
self.jen_verejne = jen_verejne
|
||||||
deadliny = m.Deadline.objects.filter(cislo__rocnik=rocnik)
|
deadliny = Deadline.objects.filter(cislo__rocnik=rocnik)
|
||||||
if jen_verejne:
|
if jen_verejne:
|
||||||
deadliny = deadliny.filter(verejna_vysledkovka=True)
|
deadliny = deadliny.filter(verejna_vysledkovka=True)
|
||||||
self.do_deadlinu = deadliny.order_by("deadline").last()
|
self.do_deadlinu = deadliny.order_by("deadline").last()
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def aktivni_resitele(self) -> list[m.Resitel]:
|
def aktivni_resitele(self) -> list[Resitel]:
|
||||||
return list(resi_v_rocniku(self.rocnik))
|
return list(resi_v_rocniku(self.rocnik))
|
||||||
|
|
||||||
@cached_property
|
@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. """
|
""" Vrátí všechna čísla daného ročníku. """
|
||||||
if self.jen_verejne:
|
if self.jen_verejne:
|
||||||
return self.rocnik.verejne_vysledkovky_cisla()
|
return self.rocnik.verejne_vysledkovky_cisla()
|
||||||
|
|
@ -164,7 +172,7 @@ class VysledkovkaRocniku(Vysledkovka):
|
||||||
return self.rocnik.cisla.all().order_by('poradi')
|
return self.rocnik.cisla.all().order_by('poradi')
|
||||||
|
|
||||||
@cached_property
|
@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!
|
# TODO: Body jsou decimal!
|
||||||
body_cisla_slovnik = dict()
|
body_cisla_slovnik = dict()
|
||||||
for cislo in self.cisla_rocniku:
|
for cislo in self.cisla_rocniku:
|
||||||
|
|
@ -197,7 +205,7 @@ class VysledkovkaRocniku(Vysledkovka):
|
||||||
radky_vysledkovky = []
|
radky_vysledkovky = []
|
||||||
|
|
||||||
setrizeni_resitele_dict = dict()
|
setrizeni_resitele_dict = dict()
|
||||||
for r in m.Resitel.objects.filter(
|
for r in Resitel.objects.filter(
|
||||||
id__in=self.setrizeni_resitele_id
|
id__in=self.setrizeni_resitele_id
|
||||||
).select_related('osoba'):
|
).select_related('osoba'):
|
||||||
setrizeni_resitele_dict[r.id] = r
|
setrizeni_resitele_dict[r.id] = r
|
||||||
|
|
@ -227,31 +235,31 @@ class VysledkovkaRocniku(Vysledkovka):
|
||||||
class VysledkovkaCisla(Vysledkovka):
|
class VysledkovkaCisla(Vysledkovka):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
cislo: m.Cislo,
|
cislo: Cislo,
|
||||||
jen_verejne: bool = True,
|
jen_verejne: bool = True,
|
||||||
do_deadlinu: m.Deadline = None
|
do_deadlinu: Deadline = None
|
||||||
):
|
):
|
||||||
self.cislo = cislo
|
self.cislo = cislo
|
||||||
self.rocnik = cislo.rocnik
|
self.rocnik = cislo.rocnik
|
||||||
self.jen_verejne = jen_verejne
|
self.jen_verejne = jen_verejne
|
||||||
if do_deadlinu is None:
|
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
|
self.do_deadlinu = do_deadlinu
|
||||||
|
|
||||||
@cached_property
|
@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
|
# TODO možná chytřeji vybírat aktivní řešitele
|
||||||
return list(resi_v_rocniku(self.rocnik))
|
return list(resi_v_rocniku(self.rocnik))
|
||||||
|
|
||||||
@cached_property
|
@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. """
|
""" Vrátí seznam všech problémů s body v daném čísle. """
|
||||||
return m.Problem.objects.filter(
|
return Problem.objects.filter(
|
||||||
hodnoceni__in=m.Hodnoceni.objects.filter(deadline_body__cislo=self.cislo)
|
hodnoceni__in=Hodnoceni.objects.filter(deadline_body__cislo=self.cislo)
|
||||||
).distinct().non_polymorphic().select_related('nadproblem').select_related('nadproblem__nadproblem')
|
).distinct().non_polymorphic().select_related('nadproblem').select_related('nadproblem__nadproblem')
|
||||||
|
|
||||||
@cached_property
|
@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. """
|
""" Vrátí seznam všech problémů, které již nemají nadproblém. """
|
||||||
# hlavní problémy čísla
|
# hlavní problémy čísla
|
||||||
# (mají vlastní sloupeček ve výsledkovce, nemají nadproblém)
|
# (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.
|
# Není cached, protože si myslím, že queryset lze použít ve for jen jednou.
|
||||||
@property
|
@property
|
||||||
def hodnoceni_do_cisla(self):
|
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:
|
if self.jen_verejne:
|
||||||
hodnoceni = hodnoceni.filter(deadline_body__verejna_vysledkovka=True)
|
hodnoceni = hodnoceni.filter(deadline_body__verejna_vysledkovka=True)
|
||||||
return hodnoceni.filter(
|
return hodnoceni.filter(
|
||||||
|
|
@ -347,7 +355,7 @@ class VysledkovkaCisla(Vysledkovka):
|
||||||
return self.sectene_body[2]
|
return self.sectene_body[2]
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def temata_a_spol(self) -> list[m.Problem]:
|
def temata_a_spol(self) -> list[Problem]:
|
||||||
if self.rocnik.rocnik < ROCNIK_ZRUSENI_TEMAT:
|
if self.rocnik.rocnik < ROCNIK_ZRUSENI_TEMAT:
|
||||||
return self.hlavni_problemy
|
return self.hlavni_problemy
|
||||||
else:
|
else:
|
||||||
|
|
@ -358,7 +366,7 @@ class VysledkovkaCisla(Vysledkovka):
|
||||||
return len(self.hlavni_problemy) - len(self.temata_a_spol) > 0
|
return len(self.hlavni_problemy) - len(self.temata_a_spol) > 0
|
||||||
|
|
||||||
@cached_property
|
@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}
|
podproblemy = {hp.id: [] for hp in self.temata_a_spol}
|
||||||
temata_a_spol = set(self.temata_a_spol)
|
temata_a_spol = set(self.temata_a_spol)
|
||||||
podproblemy[-1] = []
|
podproblemy[-1] = []
|
||||||
|
|
@ -381,7 +389,7 @@ class VysledkovkaCisla(Vysledkovka):
|
||||||
return podproblemy
|
return podproblemy
|
||||||
|
|
||||||
@cached_property
|
@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]]
|
return [self.podproblemy[it.id] for it in self.temata_a_spol] + [self.podproblemy[-1]]
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
|
|
@ -411,7 +419,7 @@ class VysledkovkaCisla(Vysledkovka):
|
||||||
radky_vysledkovky = []
|
radky_vysledkovky = []
|
||||||
|
|
||||||
setrizeni_resitele_slovnik = {}
|
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:
|
for r in setrizeni_resitele:
|
||||||
setrizeni_resitele_slovnik[r.id] = r
|
setrizeni_resitele_slovnik[r.id] = r
|
||||||
|
|
@ -462,29 +470,29 @@ class VysledkovkaCisla(Vysledkovka):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def ne_clanek_ne_konfera(problem):
|
def ne_clanek_ne_konfera(problem):
|
||||||
inst = problem.get_real_instance()
|
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):
|
class VysledkovkaDoTeXu(VysledkovkaCisla):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
nejake_cislo: m.Cislo,
|
nejake_cislo: Cislo,
|
||||||
od_vyjma: m.Deadline,
|
od_vyjma: Deadline,
|
||||||
do_vcetne: m.Deadline
|
do_vcetne: Deadline
|
||||||
):
|
):
|
||||||
super().__init__(nejake_cislo, False, do_vcetne)
|
super().__init__(nejake_cislo, False, do_vcetne)
|
||||||
self.od_deadlinu = od_vyjma
|
self.od_deadlinu = od_vyjma
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def problemy(self) -> list[m.Problem]:
|
def problemy(self) -> list[Problem]:
|
||||||
return m.Problem.objects.filter(hodnoceni__in=m.Hodnoceni.objects.filter(
|
return Problem.objects.filter(hodnoceni__in=Hodnoceni.objects.filter(
|
||||||
deadline_body__deadline__gt=self.od_deadlinu.deadline,
|
deadline_body__deadline__gt=self.od_deadlinu.deadline,
|
||||||
deadline_body__deadline__lte=self.do_deadlinu.deadline,
|
deadline_body__deadline__lte=self.do_deadlinu.deadline,
|
||||||
)).distinct().non_polymorphic().select_related('nadproblem').select_related('nadproblem__nadproblem')
|
)).distinct().non_polymorphic().select_related('nadproblem').select_related('nadproblem__nadproblem')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def hodnoceni_do_cisla(self):
|
def hodnoceni_do_cisla(self):
|
||||||
hodnoceni = m.Hodnoceni.objects.prefetch_related(
|
hodnoceni = Hodnoceni.objects.prefetch_related(
|
||||||
'problem', 'reseni', 'reseni__resitele')
|
'problem', 'reseni', 'reseni__resitele')
|
||||||
if self.jen_verejne:
|
if self.jen_verejne:
|
||||||
hodnoceni = hodnoceni.filter(deadline_body__verejna_vysledkovka=True)
|
hodnoceni = hodnoceni.filter(deadline_body__verejna_vysledkovka=True)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue