Browse Source

Merge branch 'develop' of gimli.ms.mff.cuni.cz:/akce/mam/git/mamweb into develop

export_seznamu_prednasek
parent
commit
217d6e9398
  1. 6
      aesop/views.py
  2. 14
      mamweb/settings_common.py
  3. 1
      mamweb/settings_local.py
  4. 1
      mamweb/settings_prod.py
  5. 1
      mamweb/settings_test.py
  6. 18
      mamweb/static/css/mamweb.css
  7. 5
      mamweb/templates/base.html
  8. 16
      mamweb/urls.py
  9. 11
      odevzdavatko/__init__.py
  10. 28
      odevzdavatko/admin.py
  11. 5
      odevzdavatko/apps.py
  12. 218
      odevzdavatko/forms.py
  13. 0
      odevzdavatko/migrations/__init__.py
  14. 0
      odevzdavatko/static/odevzdavatko/cross.png
  15. 0
      odevzdavatko/static/odevzdavatko/dynamic_formsets.js
  16. 0
      odevzdavatko/static/odevzdavatko/plus.png
  17. 8
      odevzdavatko/templates/odevzdavatko/detail.html
  18. 0
      odevzdavatko/templates/odevzdavatko/detail_resitele.html
  19. 2
      odevzdavatko/templates/odevzdavatko/nahraj_reseni.html
  20. 1
      odevzdavatko/templates/odevzdavatko/posli_reseni.html
  21. 4
      odevzdavatko/templates/odevzdavatko/prehled_reseni.html
  22. 0
      odevzdavatko/templates/odevzdavatko/seznam.html
  23. 2
      odevzdavatko/templates/odevzdavatko/tabulka.html
  24. 20
      odevzdavatko/urls.py
  25. 119
      odevzdavatko/views.py
  26. 4
      personalni/__init__.py
  27. 50
      personalni/admin.py
  28. 5
      personalni/apps.py
  29. 223
      personalni/forms.py
  30. 0
      personalni/migrations/__init__.py
  31. 0
      personalni/static/personalni/prihlaska.js
  32. 0
      personalni/templates/personalni/profil/orgorozcestnik.html
  33. 0
      personalni/templates/personalni/profil/resitel.html
  34. 105
      personalni/templates/personalni/udaje/edit.html
  35. 0
      personalni/templates/personalni/udaje/gdpr.html
  36. 123
      personalni/templates/personalni/udaje/prihlaska.html
  37. 0
      personalni/templates/personalni/udaje/prihlaska_field.html
  38. 24
      personalni/urls.py
  39. 306
      personalni/views.py
  40. 3
      requirements.txt
  41. 186
      seminar/admin.py
  42. 139
      seminar/autocomplete_light_registry.py.old
  43. 2
      seminar/migrations/0001_squashed_0098_auto_20210906_0305.py
  44. 2
      seminar/migrations/0084_clanek_cislo.py
  45. 1786
      seminar/models.py
  46. 8
      seminar/models/__init__.py
  47. 22
      seminar/models/base.py
  48. 38
      seminar/models/novinky.py
  49. 191
      seminar/models/odevzdavatko.py
  50. 438
      seminar/models/personalni.py
  51. 67
      seminar/models/pomocne.py
  52. 214
      seminar/models/soustredeni.py
  53. 266
      seminar/models/treenode.py
  54. 650
      seminar/models/tvorba.py
  55. 68
      seminar/templates/seminar/archiv/cislo.html
  56. 8
      seminar/templates/seminar/archiv/rocnik.html
  57. 105
      seminar/templates/seminar/profil/edit.html
  58. 123
      seminar/templates/seminar/profil/prihlaska.html
  59. 8
      seminar/templates/seminar/zadani/AktualniVysledkovka.html
  60. 15
      seminar/templatetags/mam_menu.py
  61. 4
      seminar/testutils.py
  62. 78
      seminar/urls.py
  63. 2
      seminar/utils.py
  64. 2
      seminar/views/__init__.py
  65. 820
      seminar/views/views_all.py
  66. 5
      soustredeni/__init__.py
  67. 43
      soustredeni/admin.py
  68. 5
      soustredeni/apps.py
  69. 0
      soustredeni/migrations/__init__.py
  70. 0
      soustredeni/templates/soustredeni/maily_ucastniku.txt
  71. 0
      soustredeni/templates/soustredeni/seznam_soustredeni.html
  72. 0
      soustredeni/templates/soustredeni/seznam_ucastniku.html
  73. 0
      soustredeni/templates/soustredeni/ucastnici.tex
  74. 35
      soustredeni/urls.py
  75. 55
      soustredeni/views.py
  76. 0
      treenode/__init__.py
  77. 88
      treenode/admin.py
  78. 5
      treenode/apps.py
  79. 14
      treenode/forms.py
  80. 0
      treenode/migrations/__init__.py
  81. 0
      treenode/permissions.py
  82. 2
      treenode/routers.py
  83. 2
      treenode/serializers.py
  84. 0
      treenode/static/treenode/treenode_editor.js
  85. 0
      treenode/templates/treenode/orphanage.html
  86. 0
      treenode/templates/treenode/treenode.html
  87. 0
      treenode/templates/treenode/treenode_add_stub.html
  88. 0
      treenode/templates/treenode/treenode_name.html
  89. 0
      treenode/templates/treenode/treenode_recursive.html
  90. 0
      treenode/templates/treenode/vuetest.html
  91. 2
      treenode/templatetags.py
  92. 2
      treenode/tests.py
  93. 0
      treenode/treelib.py
  94. 18
      treenode/urls.py
  95. 322
      treenode/views.py
  96. 6
      treenode/viewsets.py
  97. 7
      various/context_processors.py
  98. 3
      vysledkovky/__init__.py
  99. 5
      vysledkovky/apps.py
  100. 0
      vysledkovky/migrations/__init__.py

6
aesop/views.py

@ -7,7 +7,7 @@ from django.utils.encoding import force_text
from .utils import default_ovvpfile from .utils import default_ovvpfile
from seminar.models import Rocnik, Soustredeni from seminar.models import Rocnik, Soustredeni
from seminar.views import vysledkovka from vysledkovky import utils
from seminar.utils import aktivniResitele from seminar.utils import aktivniResitele
class ExportIndexView(generic.View): class ExportIndexView(generic.View):
@ -66,8 +66,8 @@ class ExportRocnikView(generic.View):
rocnik = get_object_or_404(Rocnik, prvni_rok=pr, exportovat=True) rocnik = get_object_or_404(Rocnik, prvni_rok=pr, exportovat=True)
cislo = rocnik.posledni_zverejnena_vysledkovka_cislo() cislo = rocnik.posledni_zverejnena_vysledkovka_cislo()
resitele = aktivniResitele(cislo, True) resitele = aktivniResitele(cislo, True)
slovnik_body = vysledkovka.secti_body_za_rocnik(cislo, resitele, False) slovnik_body = utils.secti_body_za_rocnik(cislo, resitele, False)
setrizeni_resitele, body = vysledkovka.setrid_resitele_a_body(slovnik_body) setrizeni_resitele, body = utils.setrid_resitele_a_body(slovnik_body)
of = default_ovvpfile('MaM.rocnik', rocnik) of = default_ovvpfile('MaM.rocnik', rocnik)
of.headers['comment'] = u'MaM-Web export aktivnich resitelu rocniku {rocnik} do cisla {cislo}'.format(rocnik=rocnik, cislo=cislo) of.headers['comment'] = u'MaM-Web export aktivnich resitelu rocniku {rocnik} do cisla {cislo}'.format(rocnik=rocnik, cislo=cislo)

14
mamweb/settings_common.py

@ -79,6 +79,7 @@ TEMPLATES = [
'django.contrib.messages.context_processors.messages', 'django.contrib.messages.context_processors.messages',
'sekizai.context_processors.sekizai', 'sekizai.context_processors.sekizai',
'header_fotky.context_processors.vzhled', 'header_fotky.context_processors.vzhled',
'various.context_processors.rozliseni',
'various.context_processors.april', 'various.context_processors.april',
) )
}, },
@ -109,9 +110,7 @@ INSTALLED_APPS = (
'dal', 'dal',
'dal_select2', 'dal_select2',
'fluent_comments',
'crispy_forms', 'crispy_forms',
'threadedcomments',
'django_comments', 'django_comments',
'django.contrib.flatpages', 'django.contrib.flatpages',
@ -138,6 +137,11 @@ INSTALLED_APPS = (
'various.autentizace', 'various.autentizace',
'api', 'api',
'aesop', 'aesop',
'odevzdavatko',
'vysledkovky',
'personalni',
'soustredeni',
'treenode',
# Admin upravy: # Admin upravy:
@ -216,12 +220,6 @@ REST_FRAMEWORK = {
'PAGE_SIZE': 100 'PAGE_SIZE': 100
} }
# Comments
COMMENTS_APP = 'fluent_comments'
#COMMENTS_APP = 'threadedcomments'
FLUENT_COMMENTS_EXCLUDE_FIELDS = ('name', 'email', 'url', 'title')
# SECURITY WARNING: keep the secret key used in production secret! # SECURITY WARNING: keep the secret key used in production secret!
# Create file 'django.secret' in every install (it is not kept in git) # Create file 'django.secret' in every install (it is not kept in git)

1
mamweb/settings_local.py

@ -97,3 +97,4 @@ LOGGING = {
# E-maily posílat chceme, ale do terminálu :-) # E-maily posílat chceme, ale do terminálu :-)
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
SEND_EMAIL_NOTIFICATIONS = True SEND_EMAIL_NOTIFICATIONS = True
LOCAL_TEST_PROD = "local"

1
mamweb/settings_prod.py

@ -67,3 +67,4 @@ LOGGING['handlers']['registration_error_log']['filename'] = '/home/mam-web/logs/
# E-MAIL NOTIFICATIONS # E-MAIL NOTIFICATIONS
POSLI_MAILOVOU_NOTIFIKACI = True POSLI_MAILOVOU_NOTIFIKACI = True
LOCAL_TEST_PROD = "prod"

1
mamweb/settings_test.py

@ -76,3 +76,4 @@ EMAIL_BACKEND = 'various.mail_prefixer.PrefixingMailBackend'
# TODO Pouze na otestování testu… Zvolit konferu! # TODO Pouze na otestování testu… Zvolit konferu!
# XXX: Je to pole, protože implementační detail backendu. # XXX: Je to pole, protože implementační detail backendu.
TESTOVACI_EMAILOVA_KONFERENCE = ['betatest@mam.mff.cuni.cz'] TESTOVACI_EMAILOVA_KONFERENCE = ['betatest@mam.mff.cuni.cz']
LOCAL_TEST_PROD = "test"

18
mamweb/static/css/mamweb.css

@ -1182,3 +1182,21 @@ div.gdpr {
label[for=id_skola] { label[for=id_skola] {
font-weight: bold; font-weight: bold;
} }
.localweb {
border-left: 20px solid greenyellow;
border-right: 20px solid greenyellow;
}
.localweb .login-bar {
margin-left: -20px;
}
.testweb {
border-left: 20px solid darkorange;
border-right: 20px solid darkorange;
}
.testweb .login-bar {
margin-left: -20px;
}

5
mamweb/templates/base.html

@ -15,9 +15,6 @@
<script src="{% static 'js/jquery-1.11.1.js' %}"></script> <script src="{% static 'js/jquery-1.11.1.js' %}"></script>
<script src="{% static 'js/jquery-3.4.1.js' %}"></script> <script src="{% static 'js/jquery-3.4.1.js' %}"></script>
<link rel="stylesheet" type="text/css" href="{% static 'fluent_comments/css/ajaxcomments.css' %}" />
<script type="text/javascript" src="{% static 'fluent_comments/js/ajaxcomments.js' %}"></script>
{# nastavení MathJaxu, aby nahrazoval i matiku obalenou jednoduchými $ #} {# nastavení MathJaxu, aby nahrazoval i matiku obalenou jednoduchými $ #}
<script type="text/x-mathjax-config"> <script type="text/x-mathjax-config">
MathJax.Hub.Config({ MathJax.Hub.Config({
@ -39,7 +36,7 @@
{% block script %}{% endblock %} {% block script %}{% endblock %}
</head> </head>
<body class='{% if user.is_staff %}org-logged-in{% endif %}'> <body class='{{ LOCAL_TEST_PROD }}web{% if user.is_staff %} org-logged-in{% endif %}'>
{% if user.is_staff %} {% if user.is_staff %}
<div class="login-bar" > <div class="login-bar" >

16
mamweb/urls.py

@ -6,7 +6,7 @@ from django.views.generic.base import TemplateView
from django import views from django import views
from django.urls import path # As per docs. from django.urls import path # As per docs.
from .routers import router from treenode.routers import router
urlpatterns = [ urlpatterns = [
@ -17,24 +17,36 @@ urlpatterns = [
# Seminarova aplikace (ma vlastni podadresare) # Seminarova aplikace (ma vlastni podadresare)
path('', include('seminar.urls')), path('', include('seminar.urls')),
# Odevzdavatko (ma vlastni podadresare)
path('', include('odevzdavatko.urls')),
# Korekturovaci aplikace (ma vlastni podadresare) # Korekturovaci aplikace (ma vlastni podadresare)
path('', include('korektury.urls')), path('', include('korektury.urls')),
# Prednaskova aplikace (ma vlastni podadresare) # Prednaskova aplikace (ma vlastni podadresare)
path('', include('prednasky.urls')), path('', include('prednasky.urls')),
# Soustredkova aplikace (ma vlastni podadresare)
path('', include('soustredeni.urls')),
# Personalni aplikace (ma vlastni podadresare)
# (profil, osobní údaje, ..., ne autentizace, viz dále)
path('', include('personalni.urls')),
# Autentizační aplikace (ma vlastni podadresare) # Autentizační aplikace (ma vlastni podadresare)
path('', include('various.autentizace.urls')), path('', include('various.autentizace.urls')),
# Api (ma vlastni podadresare) (autocomplete apod.) # Api (ma vlastni podadresare) (autocomplete apod.)
path('', include('api.urls')), path('', include('api.urls')),
# treenode (ma vlastni podadresare)
path('', include('treenode.urls')),
# Aesop (ma vlastni podadresare) # Aesop (ma vlastni podadresare)
path('', include('aesop.urls')), path('', include('aesop.urls')),
# Comments (interni i verejne) # Comments (interni i verejne)
path('comments_dj/', include('django_comments.urls')), path('comments_dj/', include('django_comments.urls')),
path('comments_fl/', include('fluent_comments.urls')),
# REST API # REST API
path('api/', include(router.urls)), path('api/', include(router.urls)),

11
odevzdavatko/__init__.py

@ -0,0 +1,11 @@
"""
Obsahuje vše, co se týká odevzdávání (+ nahrávání) a opravování řešení řešitelů.
Slovníček:
Moje řešení = Přehled řešení = Řešení, která odevzdal aktuálního uživatel sám.
Došlá řešení = Tabulka + seznam + detail + ... = Řešení, která poslal někdo jiný.
Poslat řešení = Odevdat řešení. (Tj. řešení se vztahem k aktuálnímu uživateli.)
Nahrát řešení = Nahrání řešení bez vztahu k aktuálnímu uživateli.
TODO: Místo vložit řešení v nahrávání a posílání řešení dát něco jiného?
"""

28
odevzdavatko/admin.py

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

5
odevzdavatko/apps.py

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

218
odevzdavatko/forms.py

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

0
odevzdavatko/migrations/__init__.py

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

Before

Width:  |  Height:  |  Size: 717 B

After

Width:  |  Height:  |  Size: 717 B

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

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

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

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

@ -4,7 +4,7 @@
{% block content %} {% block content %}
{# FIXME: Necopypastovat! Tohle je zkopírované ze static/seminar/dynamic_formsets.js #} {# FIXME: Necopypastovat! Tohle je zkopírované ze static/odevzdavatko/dynamic_formsets.js #}
<script type='text/javascript'> <script type='text/javascript'>
// Credit https://medium.com/all-about-django/adding-forms-dynamically-to-a-django-formset-375f1090c2b0 // Credit https://medium.com/all-about-django/adding-forms-dynamically-to-a-django-formset-375f1090c2b0
function updateElementIndex(el, prefix, ndx) { function updateElementIndex(el, prefix, ndx) {
@ -104,14 +104,14 @@ $(document).ready(function(){
<td>{{ subform.problem }}</td> <td>{{ subform.problem }}</td>
<td>{{ subform.body }}</td> <td>{{ subform.body }}</td>
<td>{{ subform.cislo_body }}</td> <td>{{ subform.cislo_body }}</td>
<td><a href="#" class="smazat_hodnoceni" id="id_{{subform.prefix}}-jsremove"><img src="{% static "seminar/cross.png" %}" alt="Smazat"></a></td> <td><a href="#" class="smazat_hodnoceni" id="id_{{subform.prefix}}-jsremove"><img src="{% static "odevzdavatko/cross.png" %}" alt="Smazat"></a></td>
</tr> </tr>
</tbody> </tbody>
{% endfor %} {% endfor %}
</table> </table>
<a href="#"> <img src="{% static "seminar/plus.png" %}" id="pridat_hodnoceni" alt="Přidat hodnocení"></a> </br> <a href="#"> <img src="{% static "odevzdavatko/plus.png" %}" id="pridat_hodnoceni" alt="Přidat hodnocení"></a> </br>
<input type=submit value="Uložit"></form> <input type=submit value="Uložit"></form>
<table id="empty_form" style="display: none;"> <table id="empty_form" style="display: none;">
@ -119,7 +119,7 @@ $(document).ready(function(){
<td>{{ form.empty_form.problem }}</td> <td>{{ form.empty_form.problem }}</td>
<td>{{ form.empty_form.body }}</td> <td>{{ form.empty_form.body }}</td>
<td>{{ form.empty_form.cislo_body }}</td> <td>{{ form.empty_form.cislo_body }}</td>
<td><a href="#" class="smazat_hodnoceni" id="id_{{subform.prefix}}-jsremove"><img src="{% static "seminar/cross.png" %}" alt="Smazat"></a></td> <td><a href="#" class="smazat_hodnoceni" id="id_{{subform.prefix}}-jsremove"><img src="{% static "odevzdavatko/cross.png" %}" alt="Smazat"></a></td>
</tr> </tr>
</table> </table>

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

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

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

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

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

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

@ -23,9 +23,9 @@
</tr> </tr>
{% for hodn in hodnoceni %} {% for hodn in hodnoceni %}
<tr> <tr>
<td>{{ hodn.reseni.cas_doruceni | date:"d.m.Y H:i"}}</td> <td><a href="{% url 'odevzdavatko_resitel_reseni' hodn.reseni.id %}">{{ hodn.reseni.cas_doruceni | date:"d.m.Y H:i"}}</a></td>
<td id="problem"><span title="{{ hodn.problem.nazev }}">{{ hodn.problem.nazev | zkrat_nazev_problemu }}</span></td> <td id="problem"><span title="{{ hodn.problem.nazev }}">{{ hodn.problem.nazev | zkrat_nazev_problemu }}</span></td>
<td><a href="{% url 'odevzdavatko_resitel_reseni' hodn.reseni.id %}">{{ hodn.body|default_if_none:"---" }}</a></td> <td>{{ hodn.body|default_if_none:"---" }}</td>
<td>{{ hodn.reseni.cas_doruceni | deadline_html }}</td> <td>{{ hodn.reseni.cas_doruceni | deadline_html }}</td>
</tr> </tr>
{% endfor %} {% endfor %}

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

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

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

20
odevzdavatko/urls.py

@ -0,0 +1,20 @@
from django.urls import path
from seminar.utils import org_required, resitel_required, viewMethodSwitch, \
resitel_or_org_required
from . import views
urlpatterns = [
path('org/add_solution', org_required(views.PosliReseniView.as_view()), name='seminar_vloz_reseni'),
path('resitel/nahraj_reseni', resitel_required(views.NahrajReseniView.as_view()), name='seminar_nahraj_reseni'),
path('resitel/odevzdana_reseni/', resitel_or_org_required(views.PrehledOdevzdanychReseni.as_view()), name='seminar_resitel_odevzdana_reseni'),
path('org/reseni/', org_required(views.TabulkaOdevzdanychReseniView.as_view()), name='odevzdavatko_tabulka'),
path('org/reseni/rocnik/<int:rocnik>/', org_required(views.TabulkaOdevzdanychReseniView.as_view()), name='odevzdavatko_tabulka'),
path('org/reseni/<int:problem>/<int:resitel>/', org_required(views.ReseniProblemuView.as_view()), name='odevzdavatko_reseni_resitele_k_problemu'),
path('org/reseni/<int:pk>', org_required(viewMethodSwitch(get=views.DetailReseniView.as_view(), post=views.hodnoceniReseniView)), name='odevzdavatko_detail_reseni'),
path('org/reseni/all', org_required(views.SeznamReseniView.as_view())),
path('org/reseni/akt', org_required(views.SeznamAktualnichReseniView.as_view())),
path('resitel/reseni/<int:pk>', resitel_or_org_required(views.ResitelReseniView.as_view()), name='odevzdavatko_resitel_reseni'),
]

119
seminar/views/odevzdavatko.py → odevzdavatko/views.py

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

4
personalni/__init__.py

@ -0,0 +1,4 @@
"""
Obsahuje vše okolo registrace a osobních údajů (ne přihlášení a změnu hesla).
Také obsahuje rozcestníky a Řešitele s Organizátorem.
"""

50
personalni/admin.py

@ -0,0 +1,50 @@
from django.contrib import admin
from django.contrib.auth.models import Group
from django_reverse_admin import ReverseModelAdmin
import seminar.models as m
@admin.register(m.Osoba)
class OsobaAdmin(admin.ModelAdmin):
actions = ['synchronizuj_maily', 'udelej_orgem']
def synchronizuj_maily(self, request, queryset):
for o in queryset:
if o.user is not None:
u = o.user
u.email = o.email
u.save()
self.message_user(request, "E-maily synchronizovány.")
synchronizuj_maily.short_description = "Synchronizuj vybraným osobám e-maily do uživatelů"
def udelej_orgem(self,request,queryset):
org_group = Group.objects.get(name='org')
print(queryset)
for o in queryset:
user = o.user
print(user)
user.groups.add(org_group)
user.is_staff = True
user.save()
org = m.Organizator.objects.create(osoba=o)
org.save()
udelej_orgem.short_description = "Udělej vybraných osob organizátory"
class OsobaInline(admin.TabularInline):
model = m.Osoba
@admin.register(m.Organizator)
class OrganizatorAdmin(ReverseModelAdmin):
search_fields = ['osoba__jmeno', 'osoba__prijmeni', 'osoba__prezdivka']
inline_type = 'stacked'
inline_reverse = ['osoba']
@admin.register(m.Resitel)
class ResitelAdmin(ReverseModelAdmin):
search_fields = ['osoba__jmeno', 'osoba__prijmeni', 'osoba__prezdivka']
ordering = ('osoba__jmeno','osoba__prijmeni')
inline_type = 'stacked'
inline_reverse = ['osoba']
admin.site.register(m.Skola)
admin.site.register(m.Prijemce)

5
personalni/apps.py

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

223
seminar/forms.py → personalni/forms.py

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

0
personalni/migrations/__init__.py

0
seminar/static/seminar/prihlaska.js → personalni/static/personalni/prihlaska.js

0
seminar/templates/seminar/orgorozcestnik.html → personalni/templates/personalni/profil/orgorozcestnik.html

0
seminar/templates/seminar/profil/resitel.html → personalni/templates/personalni/profil/resitel.html

105
personalni/templates/personalni/udaje/edit.html

@ -0,0 +1,105 @@
{% extends "base.html" %}
{% load staticfiles %}
{% block script %}
<script src="{% static 'personalni/prihlaska.js' %}"></script>
{% endblock %}
<!--
# pro přidání políčka do formuláře je potřeba
# - mít v modelu tu položku, kterou chci upravovat
# - přidat do views (prihlaskaView, resitelEditView)
# - přidat do forms
# - includovat do html
-->
{% block content %}
<h1>
{% block nadpis1a %}{% block nadpis1b %}
Změna osobních údajů
{% endblock %}{% endblock %}
</h1>
<form action="{% url 'seminar_resitel_edit' %}" method="post">
{% csrf_token %}
{{form.non_field_errors}}
<hr>
<h4>
Přihlašovací údaje
</h4>
<table class="form">
{% include "personalni/udaje/prihlaska_field.html" with field=form.username %}
</table>
<hr>
<h4>
Osobní údaje
</h4>
<table class="form">
{% include "personalni/udaje/prihlaska_field.html" with field=form.jmeno %}
{% include "personalni/udaje/prihlaska_field.html" with field=form.prijmeni %}
{% include "personalni/udaje/prihlaska_field.html" with field=form.pohlavi_muz%}
{% include "personalni/udaje/prihlaska_field.html" with field=form.email %}
{% include "personalni/udaje/prihlaska_field.html" with field=form.telefon %}
{% include "personalni/udaje/prihlaska_field.html" with field=form.datum_narozeni %}
</table>
<hr>
<h4>
Bydliště
</h4>
<table class="form">
{% include "personalni/udaje/prihlaska_field.html" with field=form.ulice %}
{% include "personalni/udaje/prihlaska_field.html" with field=form.mesto %}
{% include "personalni/udaje/prihlaska_field.html" with field=form.psc %}
{% include "personalni/udaje/prihlaska_field.html" with field=form.stat %}
{% include "personalni/udaje/prihlaska_field.html" with field=form.stat_text id="id_li_stat_text"%}
</table>
<hr>
<h4>
Škola
</h4>
<table class="form">
{% include "personalni/udaje/prihlaska_field.html" with field=form.skola %}
<tr><td colspan="2" ><button id="id_skola_text_button" type="button">Škola není v seznamu</button></td></tr>
<tr><td id="id_li_skola_vypln" colspan="2">Vyplň prosím celý název a adresu školy.</td></tr>
{% include "personalni/udaje/prihlaska_field.html" with field=form.skola_nazev id="id_li_skola_nazev" %}
{% include "personalni/udaje/prihlaska_field.html" with field=form.skola_adresa id="id_li_skola_adresa" %}
{% include "personalni/udaje/prihlaska_field.html" with field=form.rok_maturity %}
</table>
<hr>
<h4>
Pošta
</h4>
<table class="form">
{% include "personalni/udaje/prihlaska_field.html" with field=form.zasilat %}
{% include "personalni/udaje/prihlaska_field.html" with field=form.zasilat_cislo_emailem %}
</table>
<hr>
<h4>
Zasílání propagačních materiálů
</h4>
<table class="form">
{% include "personalni/udaje/prihlaska_field.html" with field=form.spam %}
</table>
<hr>
<input type="submit" value="Změnit">
</form>
<script>
$("#id_stat").on("change",addrCountryChanged);
$("#id_skola_text_button").on("click",schoolNotInList);
</script>
{% endblock %}

0
seminar/templates/seminar/profil/gdpr.html → personalni/templates/personalni/udaje/gdpr.html

123
personalni/templates/personalni/udaje/prihlaska.html

@ -0,0 +1,123 @@
{% extends "base.html" %}
{% load staticfiles %}
{% block script %}
<script src="{% static 'personalni/prihlaska.js' %}"></script>
{% endblock %}
<!--
# pro přidání políčka do formuláře je potřeba
# - mít v modelu tu položku, kterou chci upravovat
# - přidat do views (prihlaskaView, resitelEditView)
# - přidat do forms
# - includovat do html
-->
{% block content %}
<h1>
{% block nadpis1a %}{% block nadpis1b %}
Přihláška do semináře
{% endblock %}{% endblock %}
</h1>
<p><b>Tučně</b> popsaná pole jsou povinná.</p>
<form action="{% url 'seminar_prihlaska' %}" method="post">
{% csrf_token %}
{{form.non_field_errors}}
<hr>
<h4>
Přihlašovací údaje
</h4>
<table class="form">
{% include "personalni/udaje/prihlaska_field.html" with field=form.username %}
{# {% include "personalni/udaje/prihlaska_field.html" with field=form.password %}#}
{# {% include "personalni/udaje/prihlaska_field.html" with field=form.password_check %}#}
</table>
<hr>
<h4>
Osobní údaje
</h4>
<table class="form">
{% include "personalni/udaje/prihlaska_field.html" with field=form.jmeno %}
{% include "personalni/udaje/prihlaska_field.html" with field=form.prijmeni %}
{% include "personalni/udaje/prihlaska_field.html" with field=form.pohlavi_muz%}
{% include "personalni/udaje/prihlaska_field.html" with field=form.email %}
{% include "personalni/udaje/prihlaska_field.html" with field=form.telefon %}
{% include "personalni/udaje/prihlaska_field.html" with field=form.datum_narozeni %}
</table>
<hr>
<h4>
Bydliště
</h4>
<table class="form">
{% include "personalni/udaje/prihlaska_field.html" with field=form.ulice %}
{% include "personalni/udaje/prihlaska_field.html" with field=form.mesto %}
{% include "personalni/udaje/prihlaska_field.html" with field=form.psc %}
{% include "personalni/udaje/prihlaska_field.html" with field=form.stat %}
{% include "personalni/udaje/prihlaska_field.html" with field=form.stat_text id="id_li_stat_text"%}
</table>
<hr>
<h4>
Škola
</h4>
<table class="form">
{% include "personalni/udaje/prihlaska_field.html" with field=form.skola %}
<tr><td colspan="2" ><button id="id_skola_text_button" type="button">Škola není v seznamu</button></td></tr>
<tr><td id="id_li_skola_vypln" colspan="2">Vyplň prosím celý název a adresu školy.</td></tr>
{% include "personalni/udaje/prihlaska_field.html" with field=form.skola_nazev id="id_li_skola_nazev" %}
{% include "personalni/udaje/prihlaska_field.html" with field=form.skola_adresa id="id_li_skola_adresa" %}
{% include "personalni/udaje/prihlaska_field.html" with field=form.rok_maturity %}
</table>
<hr>
<h4>
Pošta
</h4>
<table class="form">
{% include "personalni/udaje/prihlaska_field.html" with field=form.zasilat %}
{% include "personalni/udaje/prihlaska_field.html" with field=form.zasilat_cislo_emailem %}
</table>
<hr>
<h4>
GDPR
</h4>
{% include "personalni/udaje/gdpr.html" %}
<table class="form">
{% include "personalni/udaje/prihlaska_field.html" with field=form.gdpr %}
</table>
<hr>
<h4>
Zasílání propagačních materiálů
</h4>
<table class="form">
{% include "personalni/udaje/prihlaska_field.html" with field=form.spam %}
</table>
<hr>
<input type="submit" value="Odeslat">
</form>
<script>
$("#id_stat").on("change",addrCountryChanged);
$("#id_skola_text_button").on("click",schoolNotInList);
</script>
{% endblock %}

0
seminar/templates/seminar/profil/prihlaska_field.html → personalni/templates/personalni/udaje/prihlaska_field.html

24
personalni/urls.py

@ -0,0 +1,24 @@
from django.urls import path
from django.contrib.auth.decorators import login_required
from . import views
from seminar.utils import org_required
urlpatterns = [
path(
'org/rozcestnik/',
org_required(views.OrgoRozcestnikView.as_view()),
name='seminar_org_rozcestnik'
),
path('prihlaska/', views.prihlaskaView, name='seminar_prihlaska'),
path(
'resitel/osobni-udaje/',
login_required(views.resitelEditView),
name='seminar_resitel_edit'
),
# Obecný view na profil -- orgům dá rozcestník, řešitelům jejich stránku
path('profil/', views.profilView, name='profil'),
]

306
personalni/views.py

@ -0,0 +1,306 @@
from django.shortcuts import render
from django.urls import reverse
from django.views import generic
from django.db.models import Q
from django.views.decorators.debug import sensitive_post_parameters
from django.views.generic.base import TemplateView
from django.contrib.auth.models import User, Permission, Group
from django.contrib.auth.mixins import LoginRequiredMixin
from django.db import transaction
import seminar.models as s
import seminar.models as m
from .forms import PrihlaskaForm, ProfileEditForm, PoMaturiteProfileEditForm
from datetime import date
import logging
from seminar.views import formularOKView
from various.autentizace.views import LoginView
from various.autentizace.utils import posli_reset_hesla
from django.forms.models import model_to_dict
class OrgoRozcestnikView(TemplateView):
""" Zobrazí organizátorský rozcestník."""
template_name = 'personalni/profil/orgorozcestnik.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['posledni_soustredeni'] = s.Soustredeni.objects.order_by('-datum_konce').first()
nastaveni = s.Nastaveni.objects.first()
aktualni_rocnik = nastaveni.aktualni_rocnik
context['posledni_cislo_url'] = nastaveni.aktualni_cislo.verejne_url()
# TODO možná chceme odkazovat na právě rozpracované číslo, a ne to poslední vydané
# pokud nechceme haluzit kód (= poradi) dalšího čísla, bude asi potřeba jít
# přes treenody (a dát si přitom pozor na MezicisloNode)
neobodovana_reseni = s.Hodnoceni.objects.filter(body__isnull=True)
reseni_mimo_cislo = s.Hodnoceni.objects.filter(cislo_body__isnull=True)
context['pocet_neobodovanych_reseni'] = neobodovana_reseni.count()
context['pocet_reseni_mimo_cislo'] = reseni_mimo_cislo.count()
u = self.request.user
os = s.Osoba.objects.get(user=u)
organizator = s.Organizator.objects.get(osoba=os)
context['muj_pocet_neobodovanych_reseni'] = neobodovana_reseni.filter(Q(problem__garant=organizator) | Q(problem__autor=organizator) | Q(problem__opravovatele__in=[organizator])).distinct().count()
context['muj_pocet_reseni_mimo_cislo'] = reseni_mimo_cislo.filter(Q(problem__garant=organizator) | Q(problem__autor=organizator) | Q(problem__opravovatele__in=[organizator])).count()
#FIXME: přidat stav='STAV_ZADANY'
temata = s.Tema.objects.filter(Q(garant=organizator) | Q(autor=organizator) | Q(opravovatele__in=[organizator]),
rocnik=aktualni_rocnik).distinct()
ulohy = s.Uloha.objects.filter(Q(garant=organizator) | Q(autor=organizator) | Q(opravovatele__in=[organizator]),
cislo_zadani__rocnik=aktualni_rocnik).distinct()
clanky = s.Clanek.objects.filter(Q(garant=organizator) | Q(autor=organizator) | Q(opravovatele__in=[organizator]),
cislo__rocnik=aktualni_rocnik).distinct()
context['temata'] = temata
context['ulohy'] = ulohy
context['clanky'] = clanky
context['organizator'] = organizator
return context
#content_type = 'text/plain; charset=UTF8'
#XXX
class ResitelView(LoginRequiredMixin,generic.DetailView):
model = s.Resitel
template_name = 'personalni/profil/resitel.html'
def get_object(self, queryset=None):
print(self.request.user)
return s.Resitel.objects.get(osoba__user=self.request.user)
### Formulare
# pro přidání políčka do formuláře je potřeba
# - mít v modelu tu položku, kterou chci upravovat
# - přidat do views (prihlaskaView, resitelEditView)
# - přidat do forms
# - includovat do html
def prihlaska_log_gdpr_safe(logger, gdpr_logger, msg, form_data):
msg = "{}, form_hash:{}".format(msg,hash(frozenset(form_data.items)))
logger.warn(msg)
gdpr_logger.warn(msg+", form:{}".format(form_data))
@sensitive_post_parameters('jmeno', 'prijmeni', 'email', 'telefon', 'datum_narozeni', 'ulice', 'mesto', 'psc', 'skola')
def resitelEditView(request):
err_logger = logging.getLogger('seminar.prihlaska.problem')
## Načtení objektů Osoba a Resitel patřících k aktuálně přihlášenému uživateli
u = request.user
osoba_edit = s.Osoba.objects.get(user=u)
if hasattr(osoba_edit,'resitel'):
resitel_edit = osoba_edit.resitel
else:
resitel_edit = None
user_edit = osoba_edit.user
## Vytvoření slovníku, kterým předvyplním formulář
prefill_1=model_to_dict(user_edit)
if resitel_edit:
prefill_2=model_to_dict(resitel_edit)
prefill_1.update(prefill_2)
prefill_3=model_to_dict(osoba_edit)
prefill_1.update(prefill_3)
if 'datum_narozeni' in prefill_1:
prefill_1['datum_narozeni'] = str(prefill_1['datum_narozeni'])
if 'rok_maturity' not in prefill_1 or prefill_1['rok_maturity'] < date.today().year:
form = PoMaturiteProfileEditForm(initial=prefill_1)
else:
form = ProfileEditForm(initial=prefill_1)
## Změna údajů a jejich uložení
if request.method == 'POST':
POST = request.POST.copy()
POST["username"] = osoba_edit.user.username
if 'rok_maturity' not in prefill_1 or prefill_1['rok_maturity'] < date.today().year:
form = PoMaturiteProfileEditForm(POST)
else:
form = ProfileEditForm(POST)
form.username = user_edit.username
if form.is_valid():
## Změny v osobě
fcd = form.cleaned_data
form_hash = hash(frozenset(fcd.items()))
form_logger = logging.getLogger('seminar.prihlaska.form')
form_logger.info("EDIT:" + str(fcd) + str(form_hash)) # TODO možná logovat jinak
osoba_edit.jmeno = fcd['jmeno']
osoba_edit.prijmeni = fcd['prijmeni']
osoba_edit.pohlavi_muz = fcd['pohlavi_muz']
osoba_edit.email = fcd['email']
osoba_edit.telefon = fcd['telefon']
osoba_edit.ulice = fcd['ulice']
osoba_edit.mesto = fcd['mesto']
osoba_edit.psc = fcd['psc']
osoba_edit.datum_narozeni = fcd['datum_narozeni']
## Změny v osobě s podmínkami
if fcd.get('spam',False):
osoba_edit.datum_souhlasu_zasilani = date.today()
if fcd.get('stat','') in ('CZ','SK'):
osoba_edit.stat = fcd['stat']
else:
## Neznámá země
msg = "Unknown country {}".format(fcd['stat_text'])
if resitel_edit:
## Změny v řešiteli
resitel_edit.skola = fcd['skola']
resitel_edit.rok_maturity = fcd['rok_maturity']
resitel_edit.zasilat = fcd['zasilat']
resitel_edit.zasilat_cislo_emailem = fcd['zasilat_cislo_emailem']
if fcd.get('skola'):
resitel_edit.skola = fcd['skola']
else:
# Unknown school - log it
msg = "Unknown school {}, {}".format(fcd['skola_nazev'],fcd['skola_adresa'])
resitel_edit.save()
osoba_edit.save()
return formularOKView(request, text=f'Údaje byly úspěšně uloženy. <a href="{reverse("profil")}">Vrátit se zpět na profil.</a>')
return render(request, 'personalni/udaje/edit.html', {'form': form})
@sensitive_post_parameters('jmeno', 'prijmeni', 'email', 'telefon', 'datum_narozeni', 'ulice', 'mesto', 'psc', 'skola')
def prihlaskaView(request):
generic_logger = logging.getLogger('seminar.prihlaska')
err_logger = logging.getLogger('seminar.prihlaska.problem')
form_logger = logging.getLogger('seminar.prihlaska.form')
if request.method == 'POST':
form = PrihlaskaForm(request.POST)
# TODO vyresit, co se bude v jakych situacich zobrazovat
if form.is_valid():
generic_logger.info("Form valid")
fcd = form.cleaned_data
form_hash = hash(frozenset(fcd.items()))
form_logger.info(str(fcd) + str(form_hash)) # TODO možná logovat jinak
with transaction.atomic():
u = User.objects.create_user(
username=fcd['username'],
email = fcd['email'])
u.save()
resitel_perm = Permission.objects.filter(codename__exact='resitel').first()
u.user_permissions.add(resitel_perm)
resitel_grp = Group.objects.filter(name__exact='resitel').first()
u.groups.add(resitel_grp)
o = s.Osoba(
jmeno = fcd['jmeno'],
prijmeni = fcd['prijmeni'],
pohlavi_muz = fcd['pohlavi_muz'],
email = fcd['email'],
telefon = fcd.get('telefon',''),
datum_narozeni = fcd.get('datum_narozeni',None),
datum_souhlasu_udaje = date.today(),
datum_registrace = date.today(),
ulice = fcd.get('ulice',''),
mesto = fcd.get('mesto',''),
psc = fcd.get('psc',''),
poznamka = str(fcd)
)
if fcd.get('spam',False):
o.datum_souhlasu_zasilani = date.today()
if fcd.get('stat','') in ('CZ','SK'):
o.stat = fcd['stat']
else:
# Unknown country - log it
msg = "Unknown country {}".format(fcd['stat_text'])
err_logger.warn(msg + str(form_hash))
# Dovolujeme doregistraci uživatele pro existující mail, takže naopak chceme doplnit/aktualizovat údaje do stávajícího objektu
try:
orig_osoba = m.Osoba.objects.get(email=fcd['email'])
orig_osoba.poznamka += '\nDOREGISTRACE K EXISTUJÍCÍMU E-MAILU, diff níže.'
except m.Osoba.DoesNotExist:
# Trik: Budeme aktualizovat údaje nové osoby, takže se asi nic nezmění, ale fungovat to bude.
orig_osoba = o
# Porovnání údajů
assert orig_osoba.user is None, "Právě-registrující-se osoba už má Uživatele!"
osoba_attrs = ['jmeno', 'prijmeni', 'pohlavi_muz', 'email', 'telefon', 'datum_narozeni', 'ulice', 'mesto', 'psc', 'stat', 'datum_souhlasu_udaje', 'datum_souhlasu_zasilani', 'datum_registrace']
diffattrs = []
for attr in osoba_attrs:
new = getattr(o, attr)
old = getattr(orig_osoba, attr)
if new != old:
orig_osoba.poznamka += f'\nRozdíl v {attr}: Původní {old}, nový {new}'
diffattrs.append(f'Osoba.{attr}')
setattr(orig_osoba, attr, new)
# Datum registrace chceme původní / nižší:
orig_osoba.datum_registrace = min(orig_osoba.datum_registrace, o.datum_registrace)
# Od této chvíle dál je správná osoba ta "původní", novou podle formuláře si ale zachováme
o, o_form = orig_osoba, o
o.save()
o.user = u
o.save()
# Jednoduchá kvazi-kontrola duplicitních Osob
kolize = m.Osoba.objects.filter(jmeno=o.jmeno, prijmeni=o.prijmeni)
if kolize.count() > 1: # Jednu z nich jsme právě uložili
err_logger.warning(f'Zaregistrovala se osoba s kolizním jménem. ID osob: {[o.id for o in kolize]}')
r = s.Resitel(
rok_maturity = fcd['rok_maturity'],
zasilat = fcd['zasilat'],
zasilat_cislo_emailem = fcd['zasilat_cislo_emailem']
)
if fcd.get('skola'):
r.skola = fcd['skola']
else:
# Unknown school - log it
msg = "Unknown school {}, {}".format(fcd['skola_nazev'],fcd['skola_adresa'])
err_logger.warn(msg + str(form_hash))
# Porovnání údajů u řešitele
try:
orig_resitel = o.resitel
orig_resitel.poznamka += '\nDOREGISTRACE ŘEŠITELE, diff:'
except m.Resitel.DoesNotExist:
# Stejný trik:
orig_resitel = r
resitel_attrs = ['skola', 'rok_maturity', 'zasilat', 'zasilat_cislo_emailem']
for attr in resitel_attrs:
new = getattr(r, attr)
old = getattr(orig_resitel, attr)
if new != old:
orig_resitel.poznamka += f'\nRozdíl v {attr}: Původní {old}, nový {new}'
diffattrs.append(f'Resitel.{attr}')
setattr(orig_resitel, attr, new)
r, r_form = orig_resitel, r
r.osoba = o # Tohle by mělo být bezpečné…
r.save()
if diffattrs: err_logger.warning(f'Different fields when matching Řešitel id {r.id} or Osoba id {o.id}: {diffattrs}')
posli_reset_hesla(u, request)
return formularOKView(request, text='Na tvůj e-mail jsme právě poslali odkaz pro nastavení hesla.')
# if a GET (or any other method) we'll create a blank form
else:
form = PrihlaskaForm()
return render(request, 'personalni/udaje/prihlaska.html', {'form': form})
# Jen hloupé rozhazovátko
def profilView(request):
user = request.user
if user.has_perm('auth.org'):
return OrgoRozcestnikView.as_view()(request)
if user.has_perm('auth.resitel'):
return ResitelView.as_view()(request)
else:
return LoginView.as_view()(request)

3
requirements.txt

@ -34,9 +34,6 @@ django-webpack-loader
django-rest-polymorphic django-rest-polymorphic
# Comments # Comments
akismet==1.0.1
django-fluent-comments==2.1
django-threadedcomments==1.2
django-contrib-comments==1.9.0 django-contrib-comments==1.9.0
# debug tools/extensions # debug tools/extensions

186
seminar/admin.py

@ -1,12 +1,9 @@
from django.contrib import admin from django.contrib import admin
from django.contrib.auth.models import Group
from django.db import models from django.db import models
from django.forms import widgets, ModelForm from django.forms import widgets, ModelForm
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from polymorphic.admin import PolymorphicParentModelAdmin, PolymorphicChildModelAdmin, PolymorphicChildModelFilter from polymorphic.admin import PolymorphicParentModelAdmin, PolymorphicChildModelAdmin, PolymorphicChildModelFilter
from reversion.admin import VersionAdmin
from django_reverse_admin import ReverseModelAdmin
from solo.admin import SingletonModelAdmin from solo.admin import SingletonModelAdmin
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
@ -15,10 +12,7 @@ from seminar.utils import hlavni_problem
# Todo: reversion # Todo: reversion
import seminar.models as m import seminar.models as m
import seminar.treelib as tl
admin.site.register(m.Skola)
admin.site.register(m.Prijemce)
admin.site.register(m.Rocnik) admin.site.register(m.Rocnik)
class CisloForm(ModelForm): class CisloForm(ModelForm):
@ -109,47 +103,6 @@ class CisloAdmin(admin.ModelAdmin):
force_publish.short_description = 'Zveřejnit vybraná čísla a všechny návrhy úloh v nich učinit zadanými' force_publish.short_description = 'Zveřejnit vybraná čísla a všechny návrhy úloh v nich učinit zadanými'
@admin.register(m.Osoba)
class OsobaAdmin(admin.ModelAdmin):
actions = ['synchronizuj_maily', 'udelej_orgem']
def synchronizuj_maily(self, request, queryset):
for o in queryset:
if o.user is not None:
u = o.user
u.email = o.email
u.save()
self.message_user(request, "E-maily synchronizovány.")
synchronizuj_maily.short_description = "Synchronizuj vybraným osobám e-maily do uživatelů"
def udelej_orgem(self,request,queryset):
org_group = Group.objects.get(name='org')
print(queryset)
for o in queryset:
user = o.user
print(user)
user.groups.add(org_group)
user.is_staff = True
user.save()
org = m.Organizator.objects.create(osoba=o)
org.save()
udelej_orgem.short_description = "Udělej vybraných osob organizátory"
class OsobaInline(admin.TabularInline):
model = m.Osoba
@admin.register(m.Organizator)
class OrganizatorAdmin(ReverseModelAdmin):
search_fields = ['osoba__jmeno', 'osoba__prijmeni', 'osoba__prezdivka']
inline_type = 'stacked'
inline_reverse = ['osoba']
@admin.register(m.Resitel)
class ResitelAdmin(ReverseModelAdmin):
search_fields = ['osoba__jmeno', 'osoba__prijmeni', 'osoba__prezdivka']
ordering = ('osoba__jmeno','osoba__prijmeni')
inline_type = 'stacked'
inline_reverse = ['osoba']
@admin.register(m.Problem) @admin.register(m.Problem)
class ProblemAdmin(PolymorphicParentModelAdmin): class ProblemAdmin(PolymorphicParentModelAdmin):
@ -200,147 +153,8 @@ class ResitelInline(admin.TabularInline):
model = m.Resitel model = m.Resitel
extra = 1 extra = 1
class SoustredeniUcastniciInline(admin.TabularInline):
model = m.Soustredeni_Ucastnici
extra = 1
fields = ['resitel','poznamka']
autocomplete_fields = ['resitel']
ordering = ['resitel__osoba__jmeno', 'resitel__osoba__prijmeni']
formfield_overrides = {
models.TextField: {'widget': widgets.TextInput}
}
def get_queryset(self,request):
qs = super().get_queryset(request)
return qs.select_related('resitel','soustredeni')
class SoustredeniOrganizatoriInline(admin.TabularInline):
model = m.Soustredeni.organizatori.through
extra = 1
fields = ['organizator','poznamka']
autocomplete_fields = ['organizator']
ordering = ['organizator__osoba__jmeno','organizator__prijmeni']
formfield_overrides = {
models.TextField: {'widget': widgets.TextInput}
}
def get_queryset(self,request):
qs = super().get_queryset(request)
return qs.select_related('organizator', 'soustredeni')
@admin.register(m.Soustredeni)
class SoustredeniAdmin(admin.ModelAdmin):
model = m.Soustredeni
inline_type = 'tabular'
inlines = [SoustredeniUcastniciInline, SoustredeniOrganizatoriInline]
class PrilohaReseniInline(admin.TabularInline):
model = m.PrilohaReseni
extra = 1
admin.site.register(m.PrilohaReseni)
class Reseni_ResiteleInline(admin.TabularInline):
model = m.Reseni_Resitele
@admin.register(m.Reseni)
class ReseniAdmin(ReverseModelAdmin):
base_model = m.Reseni
inline_type = 'tabular'
# inline_reverse = ['text_cely','resitele'] TODO vrátit zpět a zrychlit dotaz
inline_reverse = ['resitele']
exclude = ['text_zkraceny', 'text_zkraceny_set']
inlines = [PrilohaReseniInline]
# FAIL in template
# inlines = [PrilohaReseniInline,Reseni_ResiteleInline]
admin.site.register(m.Hodnoceni)
admin.site.register(m.Pohadka) admin.site.register(m.Pohadka)
admin.site.register(m.Obrazek) admin.site.register(m.Obrazek)
# Polymorfismus pro stromy
# TODO: Inlines podle https://django-polymorphic.readthedocs.io/en/stable/admin.html
@admin.register(m.TreeNode)
class TreeNodeAdmin(PolymorphicParentModelAdmin):
base_model = m.TreeNode
child_models = [
m.RocnikNode,
m.CisloNode,
m.MezicisloNode,
m.TemaVCisleNode,
m.UlohaZadaniNode,
m.PohadkaNode,
m.UlohaVzorakNode,
m.TextNode,
m.CastNode,
m.OrgTextNode,
]
actions = ['aktualizuj_nazvy']
# XXX: nejspíš je to totální DB HOG, nechcete to použít moc často.
def aktualizuj_nazvy(self, request, queryset):
newqs = queryset.get_real_instances()
for tn in newqs:
tn.aktualizuj_nazev()
tn.save()
self.message_user(request, "Názvy aktualizovány.")
aktualizuj_nazvy.short_description = "Aktualizuj vybraným TreeNodům názvy"
@admin.register(m.RocnikNode)
class RocnikNodeAdmin(PolymorphicChildModelAdmin):
base_model = m.RocnikNode
show_in_index = True
@admin.register(m.CisloNode)
class CisloNodeAdmin(PolymorphicChildModelAdmin):
base_model = m.CisloNode
show_in_index = True
@admin.register(m.MezicisloNode)
class MezicisloNodeAdmin(PolymorphicChildModelAdmin):
base_model = m.MezicisloNode
show_in_index = True
@admin.register(m.TemaVCisleNode)
class TemaVCisleNodeAdmin(PolymorphicChildModelAdmin):
base_model = m.TemaVCisleNode
show_in_index = True
@admin.register(m.UlohaZadaniNode)
class UlohaZadaniNodeAdmin(PolymorphicChildModelAdmin):
base_model = m.UlohaZadaniNode
show_in_index = True
@admin.register(m.PohadkaNode)
class PohadkaNodeAdmin(PolymorphicChildModelAdmin):
base_model = m.PohadkaNode
show_in_index = True
@admin.register(m.UlohaVzorakNode)
class UlohaVzorakNodeAdmin(PolymorphicChildModelAdmin):
base_model = m.UlohaVzorakNode
show_in_index = True
@admin.register(m.TextNode)
class TextNodeAdmin(PolymorphicChildModelAdmin):
base_model = m.TextNode
show_in_index = True
@admin.register(m.CastNode)
class TextNodeAdmin(PolymorphicChildModelAdmin):
base_model = m.CastNode
show_in_index = True
fields = ('nadpis',)
@admin.register(m.OrgTextNode)
class TextNodeAdmin(PolymorphicChildModelAdmin):
base_model = m.OrgTextNode
show_in_index = True
admin.site.register(m.Nastaveni, SingletonModelAdmin) admin.site.register(m.Nastaveni, SingletonModelAdmin)
admin.site.register(m.Novinky) admin.site.register(m.Novinky)

139
seminar/autocomplete_light_registry.py.old

@ -1,139 +0,0 @@
# -*- coding: utf-8 -*-
from autocomplete_light import shortcuts as autocomplete_light
from .models import Skola, Resitel, Problem, Organizator
from taggit.models import Tag
autocomplete_light.register(Tag)
class SkolaAutocomplete(autocomplete_light.AutocompleteModelBase):
model = Skola
search_fields = ['nazev', 'mesto', 'ulice']
split_words = True
limit_choices = 15
attrs = {
# This will set the input placeholder attribute:
'placeholder': 'Škola',
# This will set the yourlabs.Autocomplete.minimumCharacters
# options, the naming conversion is handled by jQuery
'data-autocomplete-minimum-characters': 1,
}
widget_attrs = {
'data-widget-maximum-values': 15,
'class': 'modern-style',
}
autocomplete_light.register(SkolaAutocomplete)
class ResitelAutocomplete(autocomplete_light.AutocompleteModelBase):
model = Resitel
search_fields = ['jmeno', 'prijmeni']
split_words = False
limit_choices = 15
def choice_label(self, resitel):
return "%s, %s (%s)" % (resitel.plne_jmeno(), resitel.mesto, resitel.rok_maturity)
attrs= {
# This will set the input placeholder attribute:
'placeholder': 'Řešitel',
# This will set the yourlabs.Autocomplete.minimumCharacters
# options, the naming conversion is handled by jQuery
'data-autocomplete-minimum-characters': 1,
}
widget_attrs = {
'data-widget-maximum-values': 15,
# Enable modern-style widget !
'class': 'modern-style',
}
autocomplete_light.register(ResitelAutocomplete)
class OrganizatorAutocomplete(autocomplete_light.AutocompleteModelBase):
model = Organizator
search_fields = ['user__first_name', 'user__last_name', 'prezdivka']
split_words = False
limit_choices = 15
def choice_label(self, organizator):
return "%s '%s' %s" % (organizator.user.first_name,
organizator.prezdivka,
organizator.user.last_name)
attrs = {
# This will set the input placeholder attribute:
'placeholder': 'Organizátor',
# This will set the yourlabs.Autocomplete.minimumCharacters
# options, the naming conversion is handled by jQuery
'data-autocomplete-minimum-characters': 1,
}
widget_attrs = {
'data-widget-maximum-values': 15,
# Enable modern-style widget !
'class': 'modern-style',
}
autocomplete_light.register(OrganizatorAutocomplete)
class ProblemAutocomplete(autocomplete_light.AutocompleteModelBase):
model = Problem
search_fields = ['nazev']
split_words = False
limit_choices = 10
def choice_label(self, p):
if p.stav == Problem.STAV_ZADANY:
popisek = ""
try:
popisek = "%s (%s, %s.%s)".format(p.nazev, p.typ, p.cislo_zadani.rocnik.rocnik, p.kod_v_rocniku())
except:
#popisek = "%s (%s, %s.%s)".format(p.nazev, p.typ, p.stav)
popisek = "CHYBA"
return popisek
else:
return "%s (%s, %s)".format(p.nazev, p.typ, p.stav)
attrs = {
# This will set the input placeholder attribute:
'placeholder': 'Problém',
# This will set the yourlabs.Autocomplete.minimumCharacters
# options, the naming conversion is handled by jQuery
'data-autocomplete-minimum-characters': 1,
}
widget_attrs = {
'data-widget-maximum-values': 10,
# Enable modern-style widget !
'class': 'modern-style',
}
#FIXME Nefunguje, nevime proc
#autocomplete_light.register(ProblemAutocomplete)

2
seminar/migrations/0001_squashed_0098_auto_20210906_0305.py

@ -12,7 +12,7 @@ import taggit.managers
from datetime import date from datetime import date
from django.db.models import Q from django.db.models import Q
from seminar.treelib import get_parent from treenode.treelib import get_parent
import datetime as dt import datetime as dt

2
seminar/migrations/0084_clanek_cislo.py

@ -2,7 +2,7 @@
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
from seminar.treelib import get_parent from treenode.treelib import get_parent
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

1786
seminar/models.py

File diff suppressed because it is too large

8
seminar/models/__init__.py

@ -0,0 +1,8 @@
from .tvorba import *
from .odevzdavatko import *
from .base import *
from .personalni import *
from .soustredeni import *
from .pomocne import *
from .treenode import *
from .novinky import *

22
seminar/models/base.py

@ -0,0 +1,22 @@
from django.urls import reverse
from django.db import models
class SeminarModelBase(models.Model):
class Meta:
abstract = True
def verejne(self):
return False
# def get_absolute_url(self):
# return "https://" + str(get_current_site(None)) + self.verejne_url()
def admin_url(self):
model_name = self.__class__.__name__.lower()
return reverse('admin:seminar_{}_change'.format(model_name), args=(self.id, ))
# def verejne_url(self):
# return None

38
seminar/models/novinky.py

@ -0,0 +1,38 @@
from django.db import models
from imagekit.models import ImageSpecField
from imagekit.processors import ResizeToFit
from reversion import revisions as reversion
from . import personalni as pm
@reversion.register(ignore_duplicates=True)
class Novinky(models.Model):
class Meta:
verbose_name = 'Novinka'
verbose_name_plural = 'Novinky'
ordering = ['-datum']
datum = models.DateField(auto_now_add=True)
text = models.TextField('Text novinky', blank=True, null=True)
obrazek = models.ImageField('Obrázek', upload_to='image_novinky/%Y/%m/%d/',
null=True, blank=True)
obrazek_maly = ImageSpecField(source='obrazek',
processors=[
ResizeToFit(350, 200, upscale=False)
],
options={'quality': 95})
autor = models.ForeignKey(pm.Organizator, verbose_name='Autor novinky', null=True,
on_delete=models.SET_NULL)
zverejneno = models.BooleanField('Zveřejněno', default=False)
def __str__(self):
if self.text:
return '[' + str(self.datum) + '] ' + self.text[0:50]
else:
return '[' + str(self.datum) + '] '

191
seminar/models/odevzdavatko.py

@ -0,0 +1,191 @@
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)
## 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)
reseni = models.ForeignKey(Reseni, verbose_name='řešení', on_delete=models.CASCADE)
problem = models.ForeignKey(am.Problem, verbose_name='problém',
related_name='hodnoceni', on_delete=models.PROTECT)
def __str__(self):
return "{}, {}, {}".format(self.problem, self.reseni, self.body)
def generate_filename(self, filename):
return os.path.join(
settings.SEMINAR_RESENI_DIR,
am.aux_generate_filename(self, filename)
)
@reversion.register(ignore_duplicates=True)
class PrilohaReseni(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)

438
seminar/models/personalni.py

@ -0,0 +1,438 @@
# -*- 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, ...)')
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)
# 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)
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, 'Nikam'),
]
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)
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 ."""
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(cislo_body__rocnik__rocnik__lte=25,reseni__in=self.reseni_set.all())
novejsi_hodnoceni = Hodnoceni.objects.filter(reseni__in=self.reseni_set.all()).difference(hodnoceni_do_25_rocniku)
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(cislo_body__rocnik__rocnik__lte=26,reseni__in=self.reseni_set.all())
novejsi_body = body_z_hodnoceni(
Hodnoceni.objects.filter(reseni__in=self.reseni_set.all())
.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
)
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']

67
seminar/models/pomocne.py

@ -0,0 +1,67 @@
# -*- coding: utf-8 -*-
import logging
from django.db import models
from .base import SeminarModelBase
logger = logging.getLogger(__name__)
class Text(SeminarModelBase):
class Meta:
db_table = 'seminar_texty'
verbose_name = 'text'
verbose_name_plural = 'texty'
na_web = models.TextField(
'text na web', blank=True,
help_text='Text ke zveřejnění na webu')
do_cisla = models.TextField(
'text do čísla', blank=True,
help_text='Text ke zveřejnění v čísle')
# má OneToOneField s:
# Reseni (je u něj jako reseni_cele)
# obrázky mají návaznost opačným směrem (vazba z druhé strany)
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
# *Node.save() aktualizuje název *Nodu.
for tn in self.textnode_set.all():
tn.save()
def __str__(self):
return str(self.na_web)[:20]
class Obrazek(SeminarModelBase):
class Meta:
db_table = 'seminar_obrazky'
verbose_name = 'obrázek'
verbose_name_plural = 'obrázky'
# Interní ID
id = models.AutoField(primary_key=True)
na_web = models.ImageField(
'obrázek na web', upload_to='obrazky/%Y/%m/%d/',
null=True, blank=True)
text = models.ForeignKey(
Text, verbose_name='text',
help_text='text, ve kterém se obrázek vyskytuje',
null=False, blank=False, on_delete=models.CASCADE)
do_cisla_barevny = models.FileField(
'barevný obrázek do čísla',
help_text='Barevná verze obrázku do čísla',
upload_to='obrazky/%Y/%m/%d/', blank=True, null=True)
do_cisla_cernobily = models.FileField(
'černobílý obrázek do čísla',
help_text='Černobílá verze obrázku do čísla',
upload_to='obrazky/%Y/%m/%d/', blank=True, null=True)
# TODO placement hint - chci ho tady / pred textem / za textem

214
seminar/models/soustredeni.py

@ -0,0 +1,214 @@
# -*- 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_CHOICES = [
(TYP_JARNI, 'Jarní soustředění'),
(TYP_PODZIMNI, 'Podzimní soustředění'),
(TYP_VIKEND, 'Víkendový sraz'),
]
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

266
seminar/models/treenode.py

@ -0,0 +1,266 @@
# -*- coding: utf-8 -*-
import logging
from django.db import models
from django.urls import reverse
from django.contrib.contenttypes.models import ContentType
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 . import personalni as pm
from .pomocne import Text
logger = logging.getLogger(__name__)
from seminar.models import tvorba as am
class TreeNode(PolymorphicModel):
class Meta:
db_table = "seminar_nodes_treenode"
verbose_name = "TreeNode"
verbose_name_plural = "TreeNody"
# TODO: Nechceme radši jako root vyžadovat přímo RocnikNode?
root = models.ForeignKey('TreeNode',
related_name="potomci_set",
null = True,
blank = False,
on_delete = models.SET_NULL, # Vrcholy s null kořenem jsou sirotci bez ročníku
verbose_name="kořen stromu")
first_child = models.OneToOneField('TreeNode',
related_name='father_of_first',
null = True,
blank = True,
on_delete=models.SET_NULL,
verbose_name="první potomek")
succ = models.OneToOneField('TreeNode',
related_name="prev",
null = True,
blank = True,
on_delete=models.SET_NULL,
verbose_name="další element na stejné úrovni")
nazev = models.TextField("název tohoto node",
help_text = "Tento název se zobrazuje v nabídkách pro výběr vhodného TreeNode",
blank=False,
null=True) # Nezveřejnitelný název na stránky - pouze do adminu
zajimave = models.BooleanField(default = False,
verbose_name = "Zajímavé",
help_text = "Zobrazí se daná věc na rozcestníku témátek")
srolovatelne = models.BooleanField(null = True, blank = True,
verbose_name = "Srolovatelné",
help_text = "Bude na stránce témátka možnost tuto položku skrýt")
def getOdkazStr(self): # String na rozcestník
return self.first_child.getOdkazStr()
def getOdkaz(self): # ID HTML tagu, na který se bude scrollovat #{{self.getOdkaz}}
# Jsem si vědom, že tu potenciálně vznikají kolize.
# Přijdou mi natolik nepravděpodobné, že je neřeším
# Chtěl jsem ale hezké odkazy
string = unidecode(self.getOdkazStr())
returnVal = ""
i = 0
while len(returnVal) < 16: # Max 15 znaků
if i == len(string):
break
if string[i] == " ":
returnVal += "-"
if string[i].isalnum():
returnVal += string[i].lower()
i += 1
return returnVal
def __str__(self):
if self.nazev:
return self.nazev
else:
#TODO: logování
return "Nepojmenovaný Treenode"
def save(self, *args, **kwargs):
self.aktualizuj_nazev()
super().save(*args, **kwargs)
def aktualizuj_nazev(self):
raise NotImplementedError("Pokus o aktualizaci názvu obecného TreeNode místo konkrétní instance")
def get_admin_url(self):
content_type = ContentType.objects.get_for_model(self.__class__)
return reverse("admin:%s_%s_change" % (content_type.app_label, content_type.model), args=(self.id,))
class RocnikNode(TreeNode):
class Meta:
db_table = 'seminar_nodes_rocnik'
verbose_name = 'Ročník (Node)'
verbose_name_plural = 'Ročníky (Node)'
rocnik = models.OneToOneField(am.Rocnik,
on_delete = models.PROTECT, # Pokud chci mazat ročník, musím si Node pořešit ručně
verbose_name = "ročník")
def aktualizuj_nazev(self):
self.nazev = "RocnikNode: "+str(self.rocnik)
class CisloNode(TreeNode):
class Meta:
db_table = 'seminar_nodes_cislo'
verbose_name = 'Číslo (Node)'
verbose_name_plural = 'Čísla (Node)'
cislo = models.OneToOneField(am.Cislo,
on_delete = models.PROTECT, # Pokud chci mazat číslo, musím si Node pořešit ručně
verbose_name = "číslo")
def aktualizuj_nazev(self):
self.nazev = "CisloNode: "+str(self.cislo)
def getOdkazStr(self):
return "Číslo " + str(self.cislo)
class MezicisloNode(TreeNode):
class Meta:
db_table = 'seminar_nodes_mezicislo'
verbose_name = 'Mezičíslo (Node)'
verbose_name_plural = 'Mezičísla (Node)'
# TODO: Využít TreeLib
def aktualizuj_nazev(self):
from treenode.treelib import safe_pred
if safe_pred(self) is not None:
if (self.prev.get_real_instance_class() != CisloNode and
self.prev.get_real_instance_class() != MezicisloNode):
raise ValueError("Předchůdce není číslo!")
posledni = self.prev.cislo
self.nazev = "MezicisloNode: Mezičíslo po čísle"+str(posledni)
elif self.root:
if self.root.get_real_instance_class() != RocnikNode:
raise ValueError("Kořen stromu není ročník!")
rocnik = self.root.rocnik
self.nazev = "MezicisloNode: První mezičíslo ročníku "+str(rocnik)
else:
print("!!!!! Nějaké neidentifikované mezičíslo !!!!!")
self.nazev = "MezicisloNode: Neidentifikovatelné mezičíslo!"
def getOdkazStr(self):
return "Obsah dostupný pouze na webu"
class TemaVCisleNode(TreeNode):
""" Obsahuje příspěvky k tématu v daném čísle """
class Meta:
db_table = 'seminar_nodes_temavcisle'
verbose_name = 'Téma v čísle (Node)'
verbose_name_plural = 'Témata v čísle (Node)'
tema = models.ForeignKey(am.Tema,
on_delete=models.PROTECT, # Pokud chci mazat téma, musím si Node pořešit ručně
verbose_name = "téma v čísle")
def aktualizuj_nazev(self):
self.nazev = "TemaVCisleNode: "+str(self.tema)
def getOdkazStr(self):
return str(self.tema)
class OrgTextNode(TreeNode):
class Meta:
db_table = 'seminar_nodes_orgtextnode'
verbose_name = 'Organizátorský článek (Node)'
verbose_name_plural = 'Organizátorské články (Node)'
organizator = models.ForeignKey(pm.Organizator,
null=False,
blank=False,
on_delete=models.DO_NOTHING,
verbose_name="Organizátor",
)
org_verejny = models.BooleanField(default = True,
verbose_name = "Org je veřejný?",
help_text = "Pokud ano, bude org pod článkem podepsaný",
null=False,
)
def aktualizuj_nazev(self):
return f"OrgTextNode začínající následujícim: {self.first_child.nazev}"
# FIXME!!!
#def getOdkazStr(self):
# return str(self.clanek)
class UlohaZadaniNode(TreeNode):
class Meta:
db_table = 'seminar_nodes_uloha_zadani'
verbose_name = 'Zadání úlohy (Node)'
verbose_name_plural = 'Zadání úloh (Node)'
uloha = models.OneToOneField(am.Uloha,
on_delete=models.PROTECT, # Pokud chci mazat téma, musím si Node pořešit ručně
verbose_name = "úloha",
null=True,
blank=False)
def aktualizuj_nazev(self):
self.nazev = "UlohaZadaniNode: "+str(self.uloha)
def getOdkazStr(self):
return str(self.uloha)
class PohadkaNode(TreeNode):
class Meta:
db_table = 'seminar_nodes_pohadka'
verbose_name = 'Pohádka (Node)'
verbose_name_plural = 'Pohádky (Node)'
pohadka = models.OneToOneField(am.Pohadka,
on_delete=models.PROTECT, # Pokud chci mazat pohádku, musím si Node pořešit ručně
verbose_name = "pohádka",
)
def aktualizuj_nazev(self):
self.nazev = "PohadkaNode: "+str(self.pohadka)
class UlohaVzorakNode(TreeNode):
class Meta:
db_table = 'seminar_nodes_uloha_vzorak'
verbose_name = 'Vzorák úlohy (Node)'
verbose_name_plural = 'Vzoráky úloh (Node)'
uloha = models.OneToOneField(am.Uloha,
on_delete=models.PROTECT, # Pokud chci mazat téma, musím si Node pořešit ručně
verbose_name = "úloha",
null=True,
blank=False)
def aktualizuj_nazev(self):
self.nazev = "UlohaVzorakNode: "+str(self.uloha)
def getOdkazStr(self):
return str(self.uloha)
class TextNode(TreeNode):
class Meta:
db_table = 'seminar_nodes_obsah'
verbose_name = 'Text (Node)'
verbose_name_plural = 'Text (Node)'
text = models.ForeignKey(Text,
on_delete=models.CASCADE,
verbose_name = 'text')
def aktualizuj_nazev(self):
self.nazev = "TextNode: "+str(self.text)
def getOdkazStr(self):
return str(self.text)
class CastNode(TreeNode):
class Meta:
db_table = 'seminar_nodes_cast'
verbose_name = 'Část (Node)'
verbose_name_plural = 'Části (Node)'
nadpis = models.CharField('Nadpis', max_length=100, help_text = 'Nadpis podvěšené části obsahu')
def aktualizuj_nazev(self):
self.nazev = "CastNode: "+str(self.nadpis)
def getOdkazStr(self):
return str(self.nadpis)

650
seminar/models/tvorba.py

@ -0,0 +1,650 @@
# -*- coding: utf-8 -*-
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.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.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 seminar.utils import hlavni_problem
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__)
@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(verejna_vysledkovka=True))
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')
datum_deadline_soustredeni = models.DateField(
'datum deadline soustředění',
blank=True, null=True,
help_text='Datum pro příjem řešení pro účast na soustředění')
datum_preddeadline = models.DateField('datum předdeadline', blank=True, null=True,
help_text='Datum pro příjem řešení, která se otisknou v dalším čísle')
datum_deadline = models.DateField('datum deadline', blank=True, null=True,
help_text='Datum pro příjem řešení úloh zadaných v tomto čísle')
verejne_db = models.BooleanField('číslo zveřejněno',
db_column='verejne', default=False)
verejna_vysledkovka = models.BooleanField(
'zveřejněna výsledkovka',
default=False,
help_text='Je-li false u veřejného čísla, '
'není výsledkovka zatím veřejná.')
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')
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('cislo').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())
text_mailu = 'Ahoj,\n' \
'na adrese {} najdete nejnovější číslo.\n' \
'Vaše M&M\n'.format(odkaz)
# Prijemci e-mailu
emaily = map(lambda r: r.osoba.email, filter(lambda r: r.zasilat_cislo_emailem, aktivniResitele(self)))
if not settings.POSLI_MAILOVOU_NOTIFIKACI:
print("Poslal bych upozornění na tyto adresy: ", " ".join(emaily))
return
email = EmailMessage(
subject=predmet,
body=text_mailu,
from_email=poslat_z_mailu,
bcc=list(emaily)
#bcc = příjemci skryté kopie
)
email.send()
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 clean(self):
# Finální deadline má být až poslední a je povinný, pokud nějaký deadline existuje.
# Existence:
if self.datum_deadline is None and (self.datum_preddeadline is not None or self.datum_deadline_soustredeni is not None):
raise ValidationError({'datum_deadline': "Číslo musí mít finální deadline, pokud má nějaké deadliny"})
if self.datum_deadline is not None:
if self.datum_preddeadline is not None and self.datum_preddeadline > self.datum_deadline:
raise ValidationError({'datum_preddeadline': "Předdeadline musí předcházet finálnímu deadlinu"})
if self.datum_deadline_soustredeni is not None and self.datum_deadline_soustredeni > self.datum_deadline:
raise ValidationError({'datum_deadline_soustredeni': "Soustřeďkový deadline musí předcházet finálnímu deadlinu"})
@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 == 'zadany':
if self.nadproblem:
return self.nadproblem.kod_v_rocniku+".{}".format(self.kod)
return str(self.kod)
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, ))
def hlavni_problem(self):
""" Pro daný problém vrátí jeho nejvyšší nadproblém."""
return hlavni_problem(self)
# 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 == 'zadany':
if self.nadproblem:
return self.nadproblem.kod_v_rocniku+".t{}".format(self.kod)
return "t{}".format(self.kod)
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 == 'zadany':
# 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)
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 == 'zadany':
name="{}.u{}".format(self.cislo_zadani.poradi,self.kod)
if self.nadproblem:
return self.nadproblem.kod_v_rocniku+name
return name
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='poslední vydané číslo',
null=False, on_delete=models.PROTECT)
@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

68
seminar/templates/seminar/archiv/cislo.html

@ -79,73 +79,7 @@
{% endif %} {% endif %}
{% if cislo.verejna_vysledkovka or user.je_org %} {% if cislo.verejna_vysledkovka or user.je_org %}
<table class='vysledkovka'> {% include "vysledkovky/vysledkovka_cisla.html" %}
<tr class='border-b'>
<th class='border-r'>#
<th class='border-r'>Jméno
{% for p in problemy %}
<th class='border-r' id="problem{{ forloop.counter }}">{# <a href="{{ p.verejne_url }}"> #}{{ p.kod_v_rocniku }}{# </a> #}
{# TODELETE #}
{% for podproblemy in podproblemy_iter.next %}
<th class='border-r podproblem{{ forloop.parentloop.counter }} podproblem'>{# <a href="{{ podproblemy.verejne_url }}"> #}{{ podproblemy.kod_v_rocniku }}{# </a> #}
{% endfor %}
{# TODELETE #}
{% endfor %}
{% if ostatni %}<th class='border-r'>Ostatní {% endif %}
{# TODELETE #}
{% for podproblemy in podproblemy_iter.next %}
<th class='border-r podproblem{{ problemy.len }} podproblem'>{# <a href="{{ podproblemy.verejne_url }}"> #}{{ podproblemy.kod_v_rocniku }}{# </a> #}
{% endfor %}
{# TODELETE #}
<th class='border-r'>Za číslo
<th class='border-r'>Za ročník
<th class='border-r'>Odjakživa
{% for rv in radky_vysledkovky %}
<tr>
<td class='border-r'>{% autoescape off %}{{ rv.poradi }}{% endautoescape %}
<th class='border-r'>
{% if rv.titul %}
{{ rv.titul }}<sup>MM</sup>
{% endif %}
{{ rv.resitel.osoba.plne_jmeno }}
{% for b in rv.body_problemy_sezn %}
<td class='border-r'>{{ b }}
{# TODELETE #}
{% for body_podproblemu in rv.body_podproblemy_iter.next %}
<td class='border-r podproblem{{ forloop.parentloop.counter }} podproblem'>{{ body_podproblemu }}
{% endfor %}
{# TODELETE #}
{% endfor %}
<td class='border-r'>{{ rv.body_cislo }}
<td class='border-r'><b>{{ rv.body_rocnik }}</b>
<td class='border-r'>{{ rv.body_celkem_odjakziva }}
</tr>
{% endfor %}
</table>
{# TODELETE #}
<script>
{% for p in problemy %}
$(".podproblem{{ forloop.counter }}").css("display", "none")
$("#problem{{ forloop.counter }}")[0].addEventListener('mouseover', podproblem{{ forloop.counter }})
$("#problem{{ forloop.counter }}")[0].addEventListener('mouseout', podproblem{{ forloop.counter }}end)
function podproblem{{ forloop.counter }}(event) {
$(".podproblem{{ forloop.counter }}").css("display", "")
}
function podproblem{{ forloop.counter }}end(event) {
$(".podproblem{{ forloop.counter }}").css("display", "none")
}
{% endfor %}
</script>
{# TODELETE #}
{% endif %} {% endif %}
{% if not cislo.verejna_vysledkovka and user.je_org %} {% if not cislo.verejna_vysledkovka and user.je_org %}

8
seminar/templates/seminar/archiv/rocnik.html

@ -114,18 +114,14 @@
{% if vysledkovka %} {% if vysledkovka %}
<h2>Výsledková listina</h2> <h2>Výsledková listina</h2>
{% include "seminar/vysledkovka_rocnik.html" %} {% include "vysledkovky/vysledkovka_rocnik.html" %}
{% endif %} {% endif %}
{% if user.je_org %} {% if user.je_org %}
<div class='mam-org-only'> <div class='mam-org-only'>
<a href='vysledkovka.tex' download>Výsledkovka ročníku (LaTeX, včetně neveřejných)</a> <a href='vysledkovka.tex' download>Výsledkovka ročníku (LaTeX, včetně neveřejných)</a>
<h2>Výsledková listina včetně neveřejných bodů</h2> <h2>Výsledková listina včetně neveřejných bodů</h2>
{% with radky_vysledkovky_s_neverejnymi as radky_vysledkovky %} {% include "vysledkovky/vysledkovka_rocnik_neverejna.html" %}
{% with cisla_s_neverejnymi as cisla %}
{% include "seminar/vysledkovka_rocnik.html" %}
{% endwith %}
{% endwith %}
</div> </div>
{% endif %} {% endif %}

105
seminar/templates/seminar/profil/edit.html

@ -1,105 +0,0 @@
{% extends "base.html" %}
{% load staticfiles %}
{% block script %}
<script src="{% static 'seminar/prihlaska.js' %}"></script>
{% endblock %}
<!--
# pro přidání políčka do formuláře je potřeba
# - mít v modelu tu položku, kterou chci upravovat
# - přidat do views (prihlaskaView, resitelEditView)
# - přidat do forms
# - includovat do html
-->
{% block content %}
<h1>
{% block nadpis1a %}{% block nadpis1b %}
Změna osobních údajů
{% endblock %}{% endblock %}
</h1>
<form action="{% url 'seminar_resitel_edit' %}" method="post">
{% csrf_token %}
{{form.non_field_errors}}
<hr>
<h4>
Přihlašovací údaje
</h4>
<table class="form">
{% include "seminar/profil/prihlaska_field.html" with field=form.username %}
</table>
<hr>
<h4>
Osobní údaje
</h4>
<table class="form">
{% include "seminar/profil/prihlaska_field.html" with field=form.jmeno %}
{% include "seminar/profil/prihlaska_field.html" with field=form.prijmeni %}
{% include "seminar/profil/prihlaska_field.html" with field=form.pohlavi_muz%}
{% include "seminar/profil/prihlaska_field.html" with field=form.email %}
{% include "seminar/profil/prihlaska_field.html" with field=form.telefon %}
{% include "seminar/profil/prihlaska_field.html" with field=form.datum_narozeni %}
</table>
<hr>
<h4>
Bydliště
</h4>
<table class="form">
{% include "seminar/profil/prihlaska_field.html" with field=form.ulice %}
{% include "seminar/profil/prihlaska_field.html" with field=form.mesto %}
{% include "seminar/profil/prihlaska_field.html" with field=form.psc %}
{% include "seminar/profil/prihlaska_field.html" with field=form.stat %}
{% include "seminar/profil/prihlaska_field.html" with field=form.stat_text id="id_li_stat_text"%}
</table>
<hr>
<h4>
Škola
</h4>
<table class="form">
{% include "seminar/profil/prihlaska_field.html" with field=form.skola %}
<tr><td colspan="2" ><button id="id_skola_text_button" type="button">Škola není v seznamu</button></td></tr>
<tr><td id="id_li_skola_vypln" colspan="2">Vyplň prosím celý název a adresu školy.</td></tr>
{% include "seminar/profil/prihlaska_field.html" with field=form.skola_nazev id="id_li_skola_nazev" %}
{% include "seminar/profil/prihlaska_field.html" with field=form.skola_adresa id="id_li_skola_adresa" %}
{% include "seminar/profil/prihlaska_field.html" with field=form.rok_maturity %}
</table>
<hr>
<h4>
Pošta
</h4>
<table class="form">
{% include "seminar/profil/prihlaska_field.html" with field=form.zasilat %}
{% include "seminar/profil/prihlaska_field.html" with field=form.zasilat_cislo_emailem %}
</table>
<hr>
<h4>
Zasílání propagačních materiálů
</h4>
<table class="form">
{% include "seminar/profil/prihlaska_field.html" with field=form.spam %}
</table>
<hr>
<input type="submit" value="Změnit">
</form>
<script>
$("#id_stat").on("change",addrCountryChanged);
$("#id_skola_text_button").on("click",schoolNotInList);
</script>
{% endblock %}

123
seminar/templates/seminar/profil/prihlaska.html

@ -1,123 +0,0 @@
{% extends "base.html" %}
{% load staticfiles %}
{% block script %}
<script src="{% static 'seminar/prihlaska.js' %}"></script>
{% endblock %}
<!--
# pro přidání políčka do formuláře je potřeba
# - mít v modelu tu položku, kterou chci upravovat
# - přidat do views (prihlaskaView, resitelEditView)
# - přidat do forms
# - includovat do html
-->
{% block content %}
<h1>
{% block nadpis1a %}{% block nadpis1b %}
Přihláška do semináře
{% endblock %}{% endblock %}
</h1>
<p><b>Tučně</b> popsaná pole jsou povinná.</p>
<form action="{% url 'seminar_prihlaska' %}" method="post">
{% csrf_token %}
{{form.non_field_errors}}
<hr>
<h4>
Přihlašovací údaje
</h4>
<table class="form">
{% include "seminar/profil/prihlaska_field.html" with field=form.username %}
{# {% include "seminar/profil/prihlaska_field.html" with field=form.password %}#}
{# {% include "seminar/profil/prihlaska_field.html" with field=form.password_check %}#}
</table>
<hr>
<h4>
Osobní údaje
</h4>
<table class="form">
{% include "seminar/profil/prihlaska_field.html" with field=form.jmeno %}
{% include "seminar/profil/prihlaska_field.html" with field=form.prijmeni %}
{% include "seminar/profil/prihlaska_field.html" with field=form.pohlavi_muz%}
{% include "seminar/profil/prihlaska_field.html" with field=form.email %}
{% include "seminar/profil/prihlaska_field.html" with field=form.telefon %}
{% include "seminar/profil/prihlaska_field.html" with field=form.datum_narozeni %}
</table>
<hr>
<h4>
Bydliště
</h4>
<table class="form">
{% include "seminar/profil/prihlaska_field.html" with field=form.ulice %}
{% include "seminar/profil/prihlaska_field.html" with field=form.mesto %}
{% include "seminar/profil/prihlaska_field.html" with field=form.psc %}
{% include "seminar/profil/prihlaska_field.html" with field=form.stat %}
{% include "seminar/profil/prihlaska_field.html" with field=form.stat_text id="id_li_stat_text"%}
</table>
<hr>
<h4>
Škola
</h4>
<table class="form">
{% include "seminar/profil/prihlaska_field.html" with field=form.skola %}
<tr><td colspan="2" ><button id="id_skola_text_button" type="button">Škola není v seznamu</button></td></tr>
<tr><td id="id_li_skola_vypln" colspan="2">Vyplň prosím celý název a adresu školy.</td></tr>
{% include "seminar/profil/prihlaska_field.html" with field=form.skola_nazev id="id_li_skola_nazev" %}
{% include "seminar/profil/prihlaska_field.html" with field=form.skola_adresa id="id_li_skola_adresa" %}
{% include "seminar/profil/prihlaska_field.html" with field=form.rok_maturity %}
</table>
<hr>
<h4>
Pošta
</h4>
<table class="form">
{% include "seminar/profil/prihlaska_field.html" with field=form.zasilat %}
{% include "seminar/profil/prihlaska_field.html" with field=form.zasilat_cislo_emailem %}
</table>
<hr>
<h4>
GDPR
</h4>
{% include "seminar/profil/gdpr.html" %}
<table class="form">
{% include "seminar/profil/prihlaska_field.html" with field=form.gdpr %}
</table>
<hr>
<h4>
Zasílání propagačních materiálů
</h4>
<table class="form">
{% include "seminar/profil/prihlaska_field.html" with field=form.spam %}
</table>
<hr>
<input type="submit" value="Odeslat">
</form>
<script>
$("#id_stat").on("change",addrCountryChanged);
$("#id_skola_text_button").on("click",schoolNotInList);
</script>
{% endblock %}

8
seminar/templates/seminar/zadani/AktualniVysledkovka.html

@ -9,7 +9,7 @@
</h1> </h1>
{% if radky_vysledkovky %} {% if radky_vysledkovky %}
{% include "seminar/vysledkovka_rocnik.html" %} {% include "vysledkovky/vysledkovka_rocnik.html" %}
{% else %} {% else %}
<p>V tomto ročníku zatím žádné výsledky nejsou.</p> <p>V tomto ročníku zatím žádné výsledky nejsou.</p>
{% endif %} {% endif %}
@ -22,11 +22,7 @@
{% if user.je_org and vysledkovka_s_neverejnymi %} {% if user.je_org and vysledkovka_s_neverejnymi %}
<div class='mam-org-only'> <div class='mam-org-only'>
<h1>Výsledky včetně neveřejných</h1> <h1>Výsledky včetně neveřejných</h1>
{% with vysledkovka_s_neverejnymi as radky_vysledkovky %} {% include "vysledkovky/vysledkovka_rocnik_neverejna.html" %}
{% with cisla_s_neverejnymi as cisla %}
{% include "seminar/vysledkovka_rocnik.html" %}
{% endwith %}
{% endwith %}
</div> </div>
{% endif %} {% endif %}

15
seminar/templatetags/mam_menu.py

@ -1,15 +0,0 @@
from django import template
from seminar.models import Rocnik
register = template.Library()
@register.inclusion_tag('results.html')
def seminar_rocniky(parser, token):
return {
'rocniky': Rocnik.objects.all()
}
@register.simple_tag
def aktualni_rocniky():
return Rocnik.objects.all()

4
seminar/testutils.py

@ -17,7 +17,7 @@ import seminar.models as m
from django.contrib.flatpages.models import FlatPage from django.contrib.flatpages.models import FlatPage
from django.contrib.sites.models import Site from django.contrib.sites.models import Site
from seminar.treelib import all_children, insert_last_child, all_children_of_type, create_node_after from treenode.treelib import all_children, insert_last_child, all_children_of_type, create_node_after
User = django.contrib.auth.get_user_model() User = django.contrib.auth.get_user_model()
@ -753,7 +753,7 @@ def gen_clanek(rnd, organizatori, resitele):
reseni.text_cely = reseninode reseni.text_cely = reseninode
reseni.save() reseni.save()
from seminar.treelib import insert_last_child, create_child from treenode.treelib import insert_last_child, create_child
insert_last_child(cislonode, reseninode) insert_last_child(cislonode, reseninode)
# Vyrobíme nějaký obsah # Vyrobíme nějaký obsah

78
seminar/urls.py

@ -1,7 +1,6 @@
from django.urls import path, include, re_path from django.urls import path, include, re_path
from django.contrib.auth.decorators import login_required
from . import views from . import views
from .utils import org_required, resitel_required, viewMethodSwitch, resitel_or_org_required from .utils import org_required
urlpatterns = [ urlpatterns = [
# path('aktualni/temata/', views.TemataRozcestnikView), # path('aktualni/temata/', views.TemataRozcestnikView),
@ -18,42 +17,6 @@ urlpatterns = [
path('rocnik/<int:rocnik>/', views.RocnikView.as_view(), name='seminar_rocnik'), path('rocnik/<int:rocnik>/', views.RocnikView.as_view(), name='seminar_rocnik'),
path('cislo/<int:rocnik>.<str:cislo>/', views.CisloView.as_view(), name='seminar_cislo'), path('cislo/<int:rocnik>.<str:cislo>/', views.CisloView.as_view(), name='seminar_cislo'),
path('problem/<int:pk>/', views.problemView, name='seminar_problem'), path('problem/<int:pk>/', views.problemView, name='seminar_problem'),
#path('treenode/<int:pk>/', views.TreeNodeView.as_view(), name='seminar_treenode'),
#path('treenode/<int:pk>/json/', views.TreeNodeJSONView.as_view(), name='seminar_treenode_json'),
#path('treenode/text/<int:pk>/', views.TextWebView.as_view(), name='seminar_textnode_web'),
#path('treenode/editor/pridat/<str:co>/<int:pk>/<str:kam>/', views.TreeNodePridatView.as_view(), name='treenode_pridat'),
#path('treenode/editor/smazat/<int:pk>/', views.TreeNodeSmazatView.as_view(), name='treenode_smazat'),
#path('treenode/editor/odvesitpryc/<int:pk>/', views.TreeNodeOdvesitPrycView.as_view(), name='treenode_odvesitpryc'),
#path('treenode/editor/podvesit/<int:pk>/<str:kam>/', views.TreeNodePodvesitView.as_view(), name='treenode_podvesit'),
#path('treenode/editor/prohodit/<int:pk>/', views.TreeNodeProhoditView.as_view(), name='treenode_prohodit'),
#path('treenode/sirotcinec/', views.SirotcinecView.as_view(), name='seminar_treenode_sirotcinec'),
#path('problem/(?P<pk>\d+)/(?P<prispevek>\d+)/', views.PrispevekView.as_view(), name='seminar_problem_prispevek'),
# Soustredeni
path(
'soustredeni/probehlo/',
views.SoustredeniListView.as_view(),
name='seminar_seznam_soustredeni'
),
path(
'soustredeni/<int:soustredeni>/seznam_ucastniku',
org_required(views.SoustredeniUcastniciView.as_view()),
name='soustredeni_ucastnici'
),
path(
'soustredeni/<int:soustredeni>/maily_ucastniku',
org_required(views.SoustredeniMailyUcastnikuView.as_view()),
name='maily_ucastniku'
),
path(
'soustredeni/<int:soustredeni>/export_ucastniku',
org_required(views.soustredeniUcastniciExportView),
name='soustredeni_ucastnici_export'
),
path(
'soustredeni/<int:soustredeni>/fotogalerie/',
include('galerie.urls')
),
# Zadani # Zadani
# path('aktualni/zadani/', views.AktualniZadaniView.as_view(), name='seminar_aktualni_zadani'), # Dočasně ad-hoc jednoduchá věc. # path('aktualni/zadani/', views.AktualniZadaniView.as_view(), name='seminar_aktualni_zadani'), # Dočasně ad-hoc jednoduchá věc.
@ -102,46 +65,7 @@ urlpatterns = [
'cislo/<int:trocnik>.<str:tcislo>/odmeny/<int:frocnik>.<str:fcislo>/', 'cislo/<int:trocnik>.<str:tcislo>/odmeny/<int:frocnik>.<str:fcislo>/',
org_required(views.OdmenyView.as_view()), org_required(views.OdmenyView.as_view()),
name="seminar_archiv_odmeny"), name="seminar_archiv_odmeny"),
path(
'soustredeni/<int:soustredeni>/obalky.pdf',
org_required(views.soustredeniObalkyView),
name='seminar_soustredeni_obalky'
),
# příprava na nestatický orgorozcestník
path(
'org/rozcestnik/',
org_required(views.OrgoRozcestnikView.as_view()),
name='seminar_org_rozcestnik'
),
path('prihlaska/',views.prihlaskaView, name='seminar_prihlaska'),
path('resitel/odevzdana_reseni/', resitel_or_org_required(views.PrehledOdevzdanychReseni.as_view()), name='seminar_resitel_odevzdana_reseni'),
path(
'resitel/osobni-udaje/',
login_required(views.resitelEditView),
name='seminar_resitel_edit'
),
# Obecný view na profil -- orgům dá rozcestník, řešitelům jejich stránku
path('profil/', views.profilView, name='profil'),
path('org/add_solution', org_required(views.AddSolutionView.as_view()), name='seminar_vloz_reseni'),
path('resitel/nahraj_reseni', resitel_required(views.NahrajReseniView.as_view()), name='seminar_nahraj_reseni'),
re_path(r'^temp/vue/.*$',views.VueTestView.as_view(),name='vue_test_view'),
path('temp/image_upload/', views.NahrajObrazekKTreeNoduView.as_view()),
path('', views.TitulniStranaView.as_view(), name='titulni_strana'), path('', views.TitulniStranaView.as_view(), name='titulni_strana'),
path('jak-resit/', views.JakResitView.as_view(), name='jak_resit'), path('jak-resit/', views.JakResitView.as_view(), name='jak_resit'),
path('org/reseni/', org_required(views.TabulkaOdevzdanychReseniView.as_view()), name='odevzdavatko_tabulka'),
path('org/reseni/rocnik/<int:rocnik>/', org_required(views.TabulkaOdevzdanychReseniView.as_view()), name='odevzdavatko_tabulka'),
path('org/reseni/<int:problem>/<int:resitel>/', org_required(views.ReseniProblemuView.as_view()), name='odevzdavatko_reseni_resitele_k_problemu'),
path('org/reseni/<int:pk>', org_required(viewMethodSwitch(get=views.DetailReseniView.as_view(), post=views.hodnoceniReseniView)), name='odevzdavatko_detail_reseni'),
path('org/reseni/all', org_required(views.SeznamReseniView.as_view())),
path('org/reseni/akt', org_required(views.SeznamAktualnichReseniView.as_view())),
path('resitel/reseni/<int:pk>', resitel_or_org_required(views.ResitelReseniView.as_view()), name='odevzdavatko_resitel_reseni'),
] ]

2
seminar/utils.py

@ -20,7 +20,7 @@ from enum import auto
import logging import logging
import seminar.models as m import seminar.models as m
import seminar.treelib as t import treenode.treelib as t
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

2
seminar/views/__init__.py

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

820
seminar/views/views_all.py

@ -1,35 +1,26 @@
from django.shortcuts import get_object_or_404, render, redirect from django.shortcuts import get_object_or_404, render
from django.http import HttpResponse, HttpResponseRedirect, HttpResponseForbidden, JsonResponse from django.http import HttpResponse
from django.urls import reverse,reverse_lazy from django.urls import reverse
from django.core.exceptions import PermissionDenied, ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.core.mail import send_mail
from django.views import generic from django.views import generic
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.http import Http404,HttpResponseBadRequest,HttpResponseRedirect from django.http import Http404
from django.db.models import Q, Sum, Count from django.db.models import Q, Sum, Count
from django.views.decorators.csrf import ensure_csrf_cookie from django.views.generic.base import RedirectView
from django.views.decorators.debug import sensitive_post_parameters
from django.views.generic.edit import FormView, CreateView
from django.views.generic.base import TemplateView, RedirectView
from django.contrib.auth.models import User, Permission, Group
from django.contrib.auth.mixins import LoginRequiredMixin
from django.db import transaction
from django.core import serializers
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.forms.models import model_to_dict
import seminar.models as s import seminar.models as s
import seminar.models as m import seminar.models as m
from seminar.models import Problem, Cislo, Reseni, Nastaveni, Rocnik, Soustredeni, Organizator, Resitel, Novinky, Soustredeni_Ucastnici, Pohadka, Tema, Clanek, Osoba, Skola # Tohle je stare a chceme se toho zbavit. Pouzivejte s.ToCoChci from seminar.models import Problem, Cislo, Reseni, Nastaveni, Rocnik, Organizator, Resitel, Novinky, Tema, Clanek # Tohle je stare a chceme se toho zbavit. Pouzivejte s.ToCoChci
#from .models import VysledkyZaCislo, VysledkyKCisluZaRocnik, VysledkyKCisluOdjakziva #from .models import VysledkyZaCislo, VysledkyKCisluZaRocnik, VysledkyKCisluOdjakziva
from seminar import utils, treelib from seminar import utils
from seminar.forms import PrihlaskaForm, ProfileEditForm, PoMaturiteProfileEditForm from treenode import treelib
import seminar.forms as f import treenode.templatetags as tnltt
import seminar.templatetags.treenodes as tnltt import treenode.serializers as vr
import seminar.views.views_rest as vr from vysledkovky.utils import body_resitelu
from seminar.views.vysledkovka import vysledkovka_rocniku, vysledkovka_cisla, body_resitelu from vysledkovky.views import vysledkovka_rocniku, vysledkovka_cisla
from datetime import timedelta, date, datetime, MAXYEAR from datetime import date, datetime
from django.utils import timezone from django.utils import timezone
from itertools import groupby from itertools import groupby
from collections import OrderedDict from collections import OrderedDict
@ -40,16 +31,10 @@ import os
import os.path as op import os.path as op
from django.conf import settings from django.conf import settings
import unicodedata import unicodedata
import json
import traceback
import sys
import csv
import logging import logging
import time import time
from seminar.utils import aktivniResitele, resi_v_rocniku, problemy_rocniku, cisla_rocniku, hlavni_problemy_f from seminar.utils import aktivniResitele
from various.autentizace.views import LoginView
from various.autentizace.utils import posli_reset_hesla
# ze starého modelu # ze starého modelu
#def verejna_temata(rocnik): #def verejna_temata(rocnik):
@ -83,279 +68,6 @@ class ObalkovaniView(generic.ListView):
context['cislo'] = self.cislo context['cislo'] = self.cislo
return context return context
class TNLData(object):
def __init__(self,anode,parent=None, index=None):
self.node = anode
self.sernode = vr.TreeNodeSerializer(anode)
self.children = []
self.parent = parent
self.tema_in_path = False
self.index = index
if parent:
self.tema_in_path = parent.tema_in_path
if isinstance(anode, m.TemaVCisleNode):
self.tema_in_path = True
def add_edit_options(self):
self.deletable = tnltt.deletable(self)
self.editable_siblings = tnltt.editableSiblings(self)
self.editable_children = tnltt.editableChildren(self)
self.text_only_subtree = tnltt.textOnlySubtree(self)
self.can_podvesit_za = tnltt.canPodvesitZa(self)
self.can_podvesit_pred = tnltt.canPodvesitPred(self)
self.appendable_children = tnltt.appendableChildren(self)
print("appChld",self.appendable_children)
if self.parent:
self.appendable_siblings = tnltt.appendableChildren(self.parent)
else:
self.appendable_siblings = []
@classmethod
def public_above(cls, anode):
""" Returns output of verejne for closest Rocnik, Cislo or Problem above.
(All of them have method verejne.)"""
parent = anode # chceme začít už od konkrétního node včetně
while True:
rocnik = isinstance(parent, s.RocnikNode)
cislo = isinstance(parent, s.CisloNode)
uloha = (isinstance(parent, s.UlohaVzorakNode) or
isinstance(parent, s.UlohaZadaniNode))
tema = isinstance(parent, s.TemaVCisleNode)
if (rocnik or cislo or uloha or tema) or parent==None:
break
else:
parent = treelib.get_parent(parent)
if rocnik:
return parent.rocnik.verejne()
elif cislo:
return parent.cislo.verejne()
elif uloha:
return parent.uloha.verejne()
elif tema:
return parent.tema.verejne()
elif None:
print("Existuje TreeNode, který není pod číslem, ročníkem, úlohou"
"ani tématem. {}".format(anode))
return False
@classmethod
def all_public_children(cls, anode):
for ch in treelib.all_children(anode):
if TNLData.public_above(ch):
yield ch
else:
continue
@classmethod
def from_treenode(cls, anode, user, parent=None, index=None):
if TNLData.public_above(anode) or user.has_perm('auth.org'):
out = cls(anode,parent,index)
else:
raise PermissionDenied()
if user.has_perm('auth.org'):
enum_children = enumerate(treelib.all_children(anode))
else:
enum_children = enumerate(TNLData.all_public_children(anode))
for (idx,ch) in enum_children:
outitem = cls.from_treenode(ch, user, out, idx)
out.children.append(outitem)
out.add_edit_options()
return out
@classmethod
def from_tnldata_list(cls, tnllist):
"""Vyrobíme virtuální TNL, který nemá obsah, ale má za potomky všechna zadaná TNLData"""
result = cls(None)
for idx, tnl in enumerate(tnllist):
result.children.append(tnl)
tnl.parent = result
tnl.index = idx
result.add_edit_options()
return result
@classmethod
def filter_treenode(cls, treenode, predicate):
tnll = cls._filter_treenode_recursive(treenode, predicate) # TreeNodeList List :-)
return TNLData.from_tnldata_list(tnll)
@classmethod
def _filter_treenode_recursive(cls, treenode, predicate):
if predicate(treenode):
return [cls.from_treenode(treenode)]
else:
found = []
for tn in treelib.all_children(treenode):
result = cls.filter_treenode(tn, predicate)
# Result by v tuhle chvíli měl být seznam TNLDat odpovídající treenodům, jež matchnuly predikát.
for tnl in result:
found.append(tnl)
return found
def to_json(self):
#self.node = anode
#self.children = []
#self.parent = parent
#self.tema_in_path = False
#self.index = index
out = {}
out['node'] = self.sernode.data
out['children'] = [n.to_json() for n in self.children]
out['tema_in_path'] = self.tema_in_path
out['index'] = self.index
out['deletable'] = self.deletable
out['editable_siblings'] = self.editable_siblings
out['editable_children'] = self.editable_children
out['text_only_subtree'] = self.text_only_subtree
out['can_podvesit_za'] = self.can_podvesit_za
out['can_podvesit_pod'] = self.can_podvesit_pred
out['appendable_children'] = self.appendable_children
out['appendable_siblings'] = self.appendable_siblings
return out
def __repr__(self):
return("TNL({})".format(self.node))
class TreeNodeView(generic.DetailView):
model = s.TreeNode
template_name = 'seminar/treenode.html'
def get_context_data(self,**kwargs):
context = super().get_context_data(**kwargs)
context['tnldata'] = TNLData.from_treenode(self.object,self.request.user)
return context
class TreeNodeJSONView(generic.DetailView):
model = s.TreeNode
def get(self,request,*args, **kwargs):
self.object = self.get_object()
data = TNLData.from_treenode(self.object,self.request.user).to_json()
return JsonResponse(data)
class TreeNodePridatView(generic.View):
type_from_str = {
'rocnikNode': m.RocnikNode,
'cisloNode': m.CisloNode,
'castNode': m.CastNode,
'textNode': m.TextNode,
'temaVCisleNode': m.TemaVCisleNode,
'reseniNode': m.ReseniNode,
'ulohaZadaniNode': m.UlohaZadaniNode,
'ulohaVzorakNode': m.UlohaVzorakNode,
'pohadkaNode': m.PohadkaNode,
'orgText': m.OrgTextNode,
}
def post(self, request, *args, **kwargs):
######## FIXME: ROZEPSANE, NEFUNGUJE, DOPSAT !!!!!! ###########
node = s.TreeNode.objects.get(pk=self.kwargs['pk'])
kam = self.kwargs['kam']
co = self.kwargs['co']
typ = self.type_from_str[co]
raise NotImplementedError('Neni to dopsane, dopis to!')
if kam not in ('pred','syn','za'):
raise ValidationError('Přidat lze pouze před nebo za node nebo jako syna')
if co == m.TextNode:
new_obj = m.Text()
new_obj.save()
elif co == m.CastNode:
new_obj = m.CastNode()
new_obj.nadpis = request.POST.get('pridat-castNode-{}-{}'.format(node.id,kam))
new_obj.save()
elif co == m.ReseniNode:
new_obj = m
pass
elif co == m.UlohaZadaniNode:
pass
elif co == m.UlohaReseniNode:
pass
else:
new_obj = None
if kam == 'pred':
pass
if kam == 'syn':
if typ == m.TextNode:
text_obj = m.Text()
text_obj.save()
node = treelib.create_child(node,typ,text=text_obj)
else:
node = treelib.create_child(node,typ)
if kam == 'za':
if typ == m.TextNode:
text_obj = m.Text()
text_obj.save()
node = treelib.create_node_after(node,typ,text=text_obj)
else:
node = treelib.create_node_after(node,typ)
return redirect(node.get_admin_url())
class TreeNodeSmazatView(generic.base.View):
def post(self, request, *args, **kwargs):
node = s.TreeNode.objects.get(pk=self.kwargs['pk'])
if node.first_child:
raise NotImplementedError('Mazání TreeNode se syny není zatím podporováno!')
treelib.disconnect_node(node)
node.delete()
return redirect(request.headers.get('referer'))
class TreeNodeOdvesitPrycView(generic.base.View):
def post(self, request, *args, **kwargs):
node = s.TreeNode.objects.get(pk=self.kwargs['pk'])
treelib.disconnect_node(node)
node.root = None
node.save()
return redirect(request.headers.get('referer'))
class TreeNodePodvesitView(generic.base.View):
def post(self, request, *args, **kwargs):
node = s.TreeNode.objects.get(pk=self.kwargs['pk'])
kam = self.kwargs['kam']
if kam == 'pred':
treelib.lower_node(node)
elif kam == 'za':
raise NotImplementedError('Podvěsit za není zatím podporováno')
return redirect(request.headers.get('referer'))
class TreeNodeProhoditView(generic.base.View):
def post(self, request, *args, **kwargs):
node = s.TreeNode.objects.get(pk=self.kwargs['pk'])
treelib.swap_succ(node)
return redirect(request.headers.get('referer'))
#FIXME ve formulari predat puvodni url a vratit redirect na ni
class SirotcinecView(generic.ListView):
model = s.TreeNode
template_name = 'seminar/orphanage.html'
def get_queryset(self):
return s.TreeNode.objects.not_instance_of(s.RocnikNode).filter(root=None,prev=None,succ=None,father_of_first=None)
# FIXME pouzit Django REST Framework
class TextWebView(generic.DetailView):
model = s.Text
def get(self,request,*args, **kwargs):
self.object = self.get_object()
return JsonResponse(model_to_dict(self.object,exclude='do_cisla'))
# FIXME: Pozor, níž je ještě jeden ProblemView! # FIXME: Pozor, níž je ještě jeden ProblemView!
#class ProblemView(generic.DetailView): #class ProblemView(generic.DetailView):
@ -494,31 +206,34 @@ def ZadaniAktualniVysledkovkaView(request):
nastaveni = get_object_or_404(Nastaveni) nastaveni = get_object_or_404(Nastaveni)
# Aktualni verejna vysledkovka # Aktualni verejna vysledkovka
rocnik = nastaveni.aktualni_rocnik rocnik = nastaveni.aktualni_rocnik
vysledkovka = vysledkovka_rocniku(rocnik) context = vysledkovka_rocniku(
cisla = cisla_rocniku(rocnik) rocnik=rocnik,
request=request,
sneverejnou=True
)
# kdyz neni verejna vysledkovka, tak zobraz starou # kdyz neni verejna vysledkovka, tak zobraz starou
if not vysledkovka or not any(map(lambda it: it.verejna_vysledkovka, cisla)): if len(context['cisla']) == 0:
try: try:
minuly_rocnik = Rocnik.objects.get( minuly_rocnik = Rocnik.objects.get(
prvni_rok=(rocnik.prvni_rok-1)) prvni_rok=(rocnik.prvni_rok-1))
rocnik = minuly_rocnik rocnik = minuly_rocnik
vysledkovka = vysledkovka_rocniku(minuly_rocnik)
cisla = cisla_rocniku(minuly_rocnik) # Přepíšeme prázdnou výsledkovku výsledkovkou z minulého ročníku
context = vysledkovka_rocniku(
rocnik=rocnik,
context=context,
request=request,
sneverejnou=True
)
except ObjectDoesNotExist: except ObjectDoesNotExist:
pass pass
# vysledkovka s neverejnyma vysledkama
vysledkovka_s_neverejnymi = vysledkovka_rocniku(nastaveni.aktualni_rocnik, jen_verejne=False) context['rocnik'] = rocnik
cisla_s_neverejnymi = cisla_rocniku(nastaveni.aktualni_rocnik, jen_verejne=False)
return render( return render(
request, request,
'seminar/zadani/AktualniVysledkovka.html', 'seminar/zadani/AktualniVysledkovka.html',
{ context
'rocnik': rocnik,
'radky_vysledkovky': vysledkovka,
'cisla': cisla,
'vysledkovka_s_neverejnymi': vysledkovka_s_neverejnymi,
'cisla_s_neverejnymi': cisla_s_neverejnymi,
}
) )
@ -664,18 +379,12 @@ class RocnikView(generic.DetailView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
start = time.time() start = time.time()
context = super(RocnikView, self).get_context_data(**kwargs) context = super(RocnikView, self).get_context_data(**kwargs)
context = vysledkovka_rocniku(
# vysledkovka = True zajistí vykreslení, rocnik=context["rocnik"],
# zkontrolovat, kdy se má a nemá vykreslovat context=context,
cisla = cisla_rocniku(context["rocnik"]) request=self.request,
context['vysledkovka'] = any(map(lambda it: it.verejna_vysledkovka, cisla)) sneverejnou=True
if self.request.user.je_org: )
context['cisla_s_neverejnymi'] = cisla_rocniku(context["rocnik"], jen_verejne=False)
context['radky_vysledkovky_s_neverejnymi'] = vysledkovka_rocniku(context["rocnik"], jen_verejne=False)
context['hlavni_problemy_v_rocniku_s_neverejnymi'] = hlavni_problemy_f(problemy_rocniku(context["rocnik"], jen_verejne=False))
context['cisla'] = cisla
context['radky_vysledkovky'] = vysledkovka_rocniku(context["rocnik"])
context['hlavni_problemy_v_rocniku'] = hlavni_problemy_f(problemy_rocniku(context["rocnik"]))
end = time.time() end = time.time()
print("Kontext:", end-start) print("Kontext:", end-start)
@ -836,51 +545,6 @@ def oldObalkovaniView(request, rocnik, cislo):
{'cislo': cislo, 'problemy': problemy, 'reseni': reseni} {'cislo': cislo, 'problemy': problemy, 'reseni': reseni}
) )
### Orgostránky
class OrgoRozcestnikView(TemplateView):
''' Zobrazí organizátorský rozcestník.'''
template_name = 'seminar/orgorozcestnik.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['posledni_soustredeni'] = Soustredeni.objects.order_by('-datum_konce').first()
nastaveni = Nastaveni.objects.first()
aktualni_rocnik = nastaveni.aktualni_rocnik
context['posledni_cislo_url'] = nastaveni.aktualni_cislo.verejne_url()
# TODO možná chceme odkazovat na právě rozpracované číslo, a ne to poslední vydané
# pokud nechceme haluzit kód (= poradi) dalšího čísla, bude asi potřeba jít
# přes treenody (a dát si přitom pozor na MezicisloNode)
neobodovana_reseni = s.Hodnoceni.objects.filter(body__isnull=True)
reseni_mimo_cislo = s.Hodnoceni.objects.filter(cislo_body__isnull=True)
context['pocet_neobodovanych_reseni'] = neobodovana_reseni.count()
context['pocet_reseni_mimo_cislo'] = reseni_mimo_cislo.count()
u = self.request.user
os = s.Osoba.objects.get(user=u)
organizator = s.Organizator.objects.get(osoba=os)
context['muj_pocet_neobodovanych_reseni'] = neobodovana_reseni.filter(Q(problem__garant=organizator) | Q(problem__autor=organizator) | Q(problem__opravovatele__in=[organizator])).distinct().count()
context['muj_pocet_reseni_mimo_cislo'] = reseni_mimo_cislo.filter(Q(problem__garant=organizator) | Q(problem__autor=organizator) | Q(problem__opravovatele__in=[organizator])).count()
#FIXME: přidat stav='STAV_ZADANY'
temata = s.Tema.objects.filter(Q(garant=organizator) | Q(autor=organizator) | Q(opravovatele__in=[organizator]),
rocnik=aktualni_rocnik).distinct()
ulohy = s.Uloha.objects.filter(Q(garant=organizator) | Q(autor=organizator) | Q(opravovatele__in=[organizator]),
cislo_zadani__rocnik=aktualni_rocnik).distinct()
clanky = s.Clanek.objects.filter(Q(garant=organizator) | Q(autor=organizator) | Q(opravovatele__in=[organizator]),
cislo__rocnik=aktualni_rocnik).distinct()
context['temata'] = temata
context['ulohy'] = ulohy
context['clanky'] = clanky
context['organizator'] = organizator
return context
#content_type = 'text/plain; charset=UTF8'
#XXX
### Tituly ### Tituly
@ -911,53 +575,6 @@ def TitulyView(request, rocnik, cislo):
return render(request, 'seminar/archiv/tituly.tex', return render(request, 'seminar/archiv/tituly.tex',
{'resitele': resitele,'jmenovci':jmenovci},content_type="text/plain") {'resitele': resitele,'jmenovci':jmenovci},content_type="text/plain")
### Soustredeni
class SoustredeniListView(generic.ListView):
model = Soustredeni
template_name = 'seminar/soustredeni/seznam_soustredeni.html'
def soustredeniObalkyView(request,soustredeni):
soustredeni = get_object_or_404(Soustredeni,id = soustredeni)
return obalkyView(request,soustredeni.ucastnici.all())
class SoustredeniUcastniciBaseView(generic.ListView):
model = Soustredeni_Ucastnici
def get_queryset(self):
soustredeni = get_object_or_404(
Soustredeni,
pk=self.kwargs["soustredeni"]
)
return Soustredeni_Ucastnici.objects.filter(
soustredeni=soustredeni).select_related('resitel')
class SoustredeniMailyUcastnikuView(SoustredeniUcastniciBaseView):
""" Seznam e-mailů řešitelů oddělených čárkami. """
model = Soustredeni_Ucastnici
template_name = 'seminar/soustredeni/maily_ucastniku.txt'
class SoustredeniUcastniciView(SoustredeniUcastniciBaseView):
""" HTML tabulka účastníků pro tisk. """
model = Soustredeni_Ucastnici
template_name = 'seminar/soustredeni/seznam_ucastniku.html'
def soustredeniUcastniciExportView(request,soustredeni):
soustredeni = get_object_or_404(Soustredeni,id = soustredeni)
ucastnici = Resitel.objects.filter(soustredeni=soustredeni)
response = HttpResponse(content_type='text/csv')
response['Content-Disposition'] = 'attachment; filename="ucastnici.csv"'
writer = csv.writer(response)
writer.writerow(["jmeno", "prijmeni", "rok_maturity", "telefon", "email", "ulice", "mesto", "psc","stat"])
for u in ucastnici:
o = u.osoba
writer.writerow([o.jmeno, o.prijmeni, str(u.rok_maturity), o.telefon, o.email, o.ulice, o.mesto, o.psc, o.stat.name])
return response
### Články ### Články
def group_by_rocnik(clanky): def group_by_rocnik(clanky):
@ -994,7 +611,7 @@ class ClankyResitelView(generic.ListView):
queryset = [] queryset = []
skupiny_clanku = group_by_rocnik(clanky) skupiny_clanku = group_by_rocnik(clanky)
for skupina in skupiny_clanku: for skupina in skupiny_clanku:
skupina.sort(key=lambda clanek: clanek.kod_v_rocniku()) skupina.sort(key=lambda clanek: clanek.kod_v_rocniku)
for clanek in skupina: for clanek in skupina:
queryset.append(clanek) queryset.append(clanek)
return queryset return queryset
@ -1026,361 +643,6 @@ def StavDatabazeView(request):
}) })
class ResitelView(LoginRequiredMixin,generic.DetailView):
model = Resitel
template_name = 'seminar/profil/resitel.html'
def get_object(self, queryset=None):
print(self.request.user)
return Resitel.objects.get(osoba__user=self.request.user)
### Formulare
# pro přidání políčka do formuláře je potřeba
# - mít v modelu tu položku, kterou chci upravovat
# - přidat do views (prihlaskaView, resitelEditView)
# - přidat do forms
# - includovat do html
class AddSolutionView(LoginRequiredMixin, FormView):
template_name = 'seminar/org/vloz_reseni.html'
form_class = f.VlozReseniForm
def form_valid(self, form):
data = form.cleaned_data
nove_reseni = m.Reseni.objects.create(
cas_doruceni=data['cas_doruceni'],
forma=data['forma'],
poznamka=data['poznamka'],
)
nove_reseni.resitele.add(data['resitel'])
nove_reseni.problem.add(data['problem'])
nove_reseni.save()
# Chtěl jsem, aby bylo vidět, že se to uložilo, tak přesměrovávám na profil.
return redirect(reverse('profil'))
class NahrajReseniView(LoginRequiredMixin, CreateView):
model = s.Reseni
template_name = 'seminar/profil/nahraj_reseni.html'
form_class = f.NahrajReseniForm
def get(self, request, *args, **kwargs):
# Zaříznutí starých řešitelů:
# FIXME: Je to tady dost naprasené, mělo by to asi být jinde…
osoba = m.Osoba.objects.get(user=self.request.user)
resitel = osoba.resitel
if resitel.rok_maturity <= m.Nastaveni.get_solo().aktualni_rocnik.prvni_rok:
return render(request, 'universal.html', {
'title': 'Nelze odevzdat',
'error': 'Zdá se, že jsi již odmaturoval/a, a tedy nemůžeš odevzdat do našeho semináře řešení.',
'text': 'Pokud se ti zdá, že to je chyba, napiš nám prosím e-mail. Díky.',
})
return super().get(request, *args, **kwargs)
def get_context_data(self,**kwargs):
data = super().get_context_data(**kwargs)
if self.request.POST:
data['prilohy'] = f.ReseniSPrilohamiFormSet(self.request.POST,self.request.FILES)
else:
data['prilohy'] = f.ReseniSPrilohamiFormSet()
return data
# FIXME prepsat tak, aby form_valid se volalo jen tehdy, kdyz je form i formset validni
# Inspirace: https://stackoverflow.com/questions/41599809/using-a-django-filefield-in-an-inline-formset
def form_valid(self,form):
context = self.get_context_data()
prilohy = context['prilohy']
if not prilohy.is_valid():
return super().form_invalid(form)
with transaction.atomic():
self.object = form.save()
self.object.resitele.add(Resitel.objects.get(osoba__user = self.request.user))
self.object.cas_doruceni = timezone.now()
self.object.forma = s.Reseni.FORMA_UPLOAD
self.object.save()
prilohy.instance = self.object
prilohy.save()
# Pošleme mail opravovatelům a garantovi
# FIXME: Nechat spočítat databázi? Je to pár dotazů (pravděpodobně), takže to za to možná nestojí
prijemci = set()
problemy = []
for prob in form.cleaned_data['problem']:
prijemci.update(prob.opravovatele.all())
if prob.garant is not None:
prijemci.add(prob.garant)
problemy.append(prob)
# FIXME: Možná poslat mail i relevantním orgům nadproblémů?
if len(prijemci) < 1:
logger.warning(f"Pozor, neposílám e-mail nikomu. Problémy: {problemy}")
# FIXME: Víc informativní obsah mailů, možná vč. příloh?
prijemci = map(lambda it: it.osoba.email, prijemci)
resitel = Osoba.objects.get(user = self.request.user)
seznam = "problému " + str(problemy[0]) if len(problemy) == 1 else 'následujícím problémům:\n' + ', \n'.join(map(str, problemy))
seznam_do_subjectu = "problému " + str(problemy[0]) + ("" if len(problemy) == 1 else f" (a dalším { len(problemy) - 1 })")
send_mail(
subject="Nové řešení k " + seznam_do_subjectu,
message=f"Řešitel{ '' if resitel.pohlavi_muz else 'ka' } { resitel } právě nahrál{'' if resitel.pohlavi_muz else 'a' } nové řešení k { seznam }.\n\nHurá do opravování: { self.object.absolute_url() }",
from_email="submitovatko@mam.mff.cuni.cz", # FIXME: Chceme to mít radši tady, nebo v nastavení?
recipient_list=list(prijemci),
)
return formularOKView(self.request, text='Řešení úspěšně odevzdáno')
def prihlaska_log_gdpr_safe(logger, gdpr_logger, msg, form_data):
msg = "{}, form_hash:{}".format(msg,hash(frozenset(form_data.items)))
logger.warn(msg)
gdpr_logger.warn(msg+", form:{}".format(form_data))
from django.forms.models import model_to_dict
@sensitive_post_parameters('jmeno', 'prijmeni', 'email', 'telefon', 'datum_narozeni', 'ulice', 'mesto', 'psc', 'skola')
def resitelEditView(request):
err_logger = logging.getLogger('seminar.prihlaska.problem')
## Načtení objektů Osoba a Resitel patřících k aktuálně přihlášenému uživateli
u = request.user
osoba_edit = Osoba.objects.get(user=u)
if hasattr(osoba_edit,'resitel'):
resitel_edit = osoba_edit.resitel
else:
resitel_edit = None
user_edit = osoba_edit.user
## Vytvoření slovníku, kterým předvyplním formulář
prefill_1=model_to_dict(user_edit)
if resitel_edit:
prefill_2=model_to_dict(resitel_edit)
prefill_1.update(prefill_2)
prefill_3=model_to_dict(osoba_edit)
prefill_1.update(prefill_3)
if 'datum_narozeni' in prefill_1:
prefill_1['datum_narozeni'] = str(prefill_1['datum_narozeni'])
if 'rok_maturity' not in prefill_1 or prefill_1['rok_maturity'] < date.today().year:
form = PoMaturiteProfileEditForm(initial=prefill_1)
else:
form = ProfileEditForm(initial=prefill_1)
## Změna údajů a jejich uložení
if request.method == 'POST':
POST = request.POST.copy()
POST["username"] = osoba_edit.user.username
if 'rok_maturity' not in prefill_1 or prefill_1['rok_maturity'] < date.today().year:
form = PoMaturiteProfileEditForm(POST)
else:
form = ProfileEditForm(POST)
form.username = user_edit.username
if form.is_valid():
## Změny v osobě
fcd = form.cleaned_data
form_hash = hash(frozenset(fcd.items()))
form_logger = logging.getLogger('seminar.prihlaska.form')
form_logger.info("EDIT:" + str(fcd) + str(form_hash)) # TODO možná logovat jinak
osoba_edit.jmeno = fcd['jmeno']
osoba_edit.prijmeni = fcd['prijmeni']
osoba_edit.pohlavi_muz = fcd['pohlavi_muz']
osoba_edit.email = fcd['email']
osoba_edit.telefon = fcd['telefon']
osoba_edit.ulice = fcd['ulice']
osoba_edit.mesto = fcd['mesto']
osoba_edit.psc = fcd['psc']
osoba_edit.datum_narozeni = fcd['datum_narozeni']
## Změny v osobě s podmínkami
if fcd.get('spam',False):
osoba_edit.datum_souhlasu_zasilani = date.today()
if fcd.get('stat','') in ('CZ','SK'):
osoba_edit.stat = fcd['stat']
else:
## Neznámá země
msg = "Unknown country {}".format(fcd['stat_text'])
if resitel_edit:
## Změny v řešiteli
resitel_edit.skola = fcd['skola']
resitel_edit.rok_maturity = fcd['rok_maturity']
resitel_edit.zasilat = fcd['zasilat']
resitel_edit.zasilat_cislo_emailem = fcd['zasilat_cislo_emailem']
if fcd.get('skola'):
resitel_edit.skola = fcd['skola']
else:
# Unknown school - log it
msg = "Unknown school {}, {}".format(fcd['skola_nazev'],fcd['skola_adresa'])
resitel_edit.save()
osoba_edit.save()
return formularOKView(request, text=f'Údaje byly úspěšně uloženy. <a href="{reverse("profil")}">Vrátit se zpět na profil.</a>')
return render(request, 'seminar/profil/edit.html', {'form': form})
@sensitive_post_parameters('jmeno', 'prijmeni', 'email', 'telefon', 'datum_narozeni', 'ulice', 'mesto', 'psc', 'skola')
def prihlaskaView(request):
generic_logger = logging.getLogger('seminar.prihlaska')
err_logger = logging.getLogger('seminar.prihlaska.problem')
form_logger = logging.getLogger('seminar.prihlaska.form')
if request.method == 'POST':
form = PrihlaskaForm(request.POST)
# TODO vyresit, co se bude v jakych situacich zobrazovat
if form.is_valid():
generic_logger.info("Form valid")
fcd = form.cleaned_data
form_hash = hash(frozenset(fcd.items()))
form_logger.info(str(fcd) + str(form_hash)) # TODO možná logovat jinak
with transaction.atomic():
u = User.objects.create_user(
username=fcd['username'],
email = fcd['email'])
u.save()
resitel_perm = Permission.objects.filter(codename__exact='resitel').first()
u.user_permissions.add(resitel_perm)
resitel_grp = Group.objects.filter(name__exact='resitel').first()
u.groups.add(resitel_grp)
o = Osoba(
jmeno = fcd['jmeno'],
prijmeni = fcd['prijmeni'],
pohlavi_muz = fcd['pohlavi_muz'],
email = fcd['email'],
telefon = fcd.get('telefon',''),
datum_narozeni = fcd.get('datum_narozeni',None),
datum_souhlasu_udaje = date.today(),
datum_registrace = date.today(),
ulice = fcd.get('ulice',''),
mesto = fcd.get('mesto',''),
psc = fcd.get('psc',''),
poznamka = str(fcd)
)
if fcd.get('spam',False):
o.datum_souhlasu_zasilani = date.today()
if fcd.get('stat','') in ('CZ','SK'):
o.stat = fcd['stat']
else:
# Unknown country - log it
msg = "Unknown country {}".format(fcd['stat_text'])
err_logger.warn(msg + str(form_hash))
# Dovolujeme doregistraci uživatele pro existující mail, takže naopak chceme doplnit/aktualizovat údaje do stávajícího objektu
try:
orig_osoba = m.Osoba.objects.get(email=fcd['email'])
orig_osoba.poznamka += '\nDOREGISTRACE K EXISTUJÍCÍMU E-MAILU, diff níže.'
except m.Osoba.DoesNotExist:
# Trik: Budeme aktualizovat údaje nové osoby, takže se asi nic nezmění, ale fungovat to bude.
orig_osoba = o
# Porovnání údajů
assert orig_osoba.user is None, "Právě-registrující-se osoba už má Uživatele!"
osoba_attrs = ['jmeno', 'prijmeni', 'pohlavi_muz', 'email', 'telefon', 'datum_narozeni', 'ulice', 'mesto', 'psc', 'stat', 'datum_souhlasu_udaje', 'datum_souhlasu_zasilani', 'datum_registrace']
diffattrs = []
for attr in osoba_attrs:
new = getattr(o, attr)
old = getattr(orig_osoba, attr)
if new != old:
orig_osoba.poznamka += f'\nRozdíl v {attr}: Původní {old}, nový {new}'
diffattrs.append(f'Osoba.{attr}')
setattr(orig_osoba, attr, new)
# Datum registrace chceme původní / nižší:
orig_osoba.datum_registrace = min(orig_osoba.datum_registrace, o.datum_registrace)
# Od této chvíle dál je správná osoba ta "původní", novou podle formuláře si ale zachováme
o, o_form = orig_osoba, o
o.save()
o.user = u
o.save()
# Jednoduchá kvazi-kontrola duplicitních Osob
kolize = m.Osoba.objects.filter(jmeno=o.jmeno, prijmeni=o.prijmeni)
if kolize.count() > 1: # Jednu z nich jsme právě uložili
err_logger.warning(f'Zaregistrovala se osoba s kolizním jménem. ID osob: {[o.id for o in kolize]}')
r = Resitel(
rok_maturity = fcd['rok_maturity'],
zasilat = fcd['zasilat'],
zasilat_cislo_emailem = fcd['zasilat_cislo_emailem']
)
if fcd.get('skola'):
r.skola = fcd['skola']
else:
# Unknown school - log it
msg = "Unknown school {}, {}".format(fcd['skola_nazev'],fcd['skola_adresa'])
err_logger.warn(msg + str(form_hash))
# Porovnání údajů u řešitele
try:
orig_resitel = o.resitel
orig_resitel.poznamka += '\nDOREGISTRACE ŘEŠITELE, diff:'
except m.Resitel.DoesNotExist:
# Stejný trik:
orig_resitel = r
resitel_attrs = ['skola', 'rok_maturity', 'zasilat', 'zasilat_cislo_emailem']
for attr in resitel_attrs:
new = getattr(r, attr)
old = getattr(orig_resitel, attr)
if new != old:
orig_resitel.poznamka += f'\nRozdíl v {attr}: Původní {old}, nový {new}'
diffattrs.append(f'Resitel.{attr}')
setattr(orig_resitel, attr, new)
r, r_form = orig_resitel, r
r.osoba = o # Tohle by mělo být bezpečné…
r.save()
if diffattrs: err_logger.warning(f'Different fields when matching Řešitel id {r.id} or Osoba id {o.id}: {diffattrs}')
posli_reset_hesla(u, request)
return formularOKView(request, text='Na tvůj e-mail jsme právě poslali odkaz pro nastavení hesla.')
# if a GET (or any other method) we'll create a blank form
else:
form = PrihlaskaForm()
return render(request, 'seminar/profil/prihlaska.html', {'form': form})
class VueTestView(generic.TemplateView):
template_name = 'seminar/vuetest.html'
class NahrajObrazekKTreeNoduView(LoginRequiredMixin, CreateView):
model = s.Obrazek
form_class = f.NahrajObrazekKTreeNoduForm
def get_initial(self):
initial = super().get_initial()
initial['na_web'] = self.request.FILES['upload']
return initial
def form_valid(self,form):
print(self.request.headers)
print(self.request.headers['Textid'])
print(form.instance)
print(form)
self.object = form.save(commit=False)
print(self.object.na_web)
self.object.text = m.Text.objects.get(pk=int(self.request.headers['Textid']))
self.object.save()
return JsonResponse({"url":self.object.na_web.url})
# Jen hloupé rozhazovátko
def profilView(request):
user = request.user
if user.has_perm('auth.org'):
return OrgoRozcestnikView.as_view()(request)
if user.has_perm('auth.resitel'):
return ResitelView.as_view()(request)
else:
return LoginView.as_view()(request)
# Interní, nemá se nikdy objevit v urls (jinak to účastníci vytrolí) # Interní, nemá se nikdy objevit v urls (jinak to účastníci vytrolí)
def formularOKView(request, text=''): def formularOKView(request, text=''):
template_name = 'seminar/formular_ok.html' template_name = 'seminar/formular_ok.html'

5
soustredeni/__init__.py

@ -0,0 +1,5 @@
"""
Obsahuje vše ( na přednášky) ohledně soustředění.
TODO stvrzenky?
"""

43
soustredeni/admin.py

@ -0,0 +1,43 @@
from django.contrib import admin
from django.forms import widgets
from django.db import models
from seminar.models import soustredeni as m
class SoustredeniUcastniciInline(admin.TabularInline):
model = m.Soustredeni_Ucastnici
extra = 1
fields = ['resitel','poznamka']
autocomplete_fields = ['resitel']
ordering = ['resitel__osoba__jmeno', 'resitel__osoba__prijmeni']
formfield_overrides = {
models.TextField: {'widget': widgets.TextInput}
}
def get_queryset(self,request):
qs = super().get_queryset(request)
return qs.select_related('resitel','soustredeni')
class SoustredeniOrganizatoriInline(admin.TabularInline):
model = m.Soustredeni.organizatori.through
extra = 1
fields = ['organizator','poznamka']
autocomplete_fields = ['organizator']
ordering = ['organizator__osoba__jmeno','organizator__prijmeni']
formfield_overrides = {
models.TextField: {'widget': widgets.TextInput}
}
def get_queryset(self,request):
qs = super().get_queryset(request)
return qs.select_related('organizator', 'soustredeni')
@admin.register(m.Soustredeni)
class SoustredeniAdmin(admin.ModelAdmin):
model = m.Soustredeni
inline_type = 'tabular'
inlines = [SoustredeniUcastniciInline, SoustredeniOrganizatoriInline]

5
soustredeni/apps.py

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

0
soustredeni/migrations/__init__.py

0
seminar/templates/seminar/soustredeni/maily_ucastniku.txt → soustredeni/templates/soustredeni/maily_ucastniku.txt

0
seminar/templates/seminar/soustredeni/seznam_soustredeni.html → soustredeni/templates/soustredeni/seznam_soustredeni.html

0
seminar/templates/seminar/soustredeni/seznam_ucastniku.html → soustredeni/templates/soustredeni/seznam_ucastniku.html

0
seminar/templates/seminar/soustredeni/ucastnici.tex → soustredeni/templates/soustredeni/ucastnici.tex

35
soustredeni/urls.py

@ -0,0 +1,35 @@
from django.urls import path, include
from . import views
from seminar.utils import org_required
urlpatterns = [
path(
'soustredeni/probehlo/',
views.SoustredeniListView.as_view(),
name='seminar_seznam_soustredeni'
),
path(
'soustredeni/<int:soustredeni>/seznam_ucastniku',
org_required(views.SoustredeniUcastniciView.as_view()),
name='soustredeni_ucastnici'
),
path(
'soustredeni/<int:soustredeni>/maily_ucastniku',
org_required(views.SoustredeniMailyUcastnikuView.as_view()),
name='maily_ucastniku'
),
path(
'soustredeni/<int:soustredeni>/export_ucastniku',
org_required(views.soustredeniUcastniciExportView),
name='soustredeni_ucastnici_export'
),
path(
'soustredeni/<int:soustredeni>/obalky.pdf',
org_required(views.soustredeniObalkyView),
name='seminar_soustredeni_obalky'
),
path(
'soustredeni/<int:soustredeni>/fotogalerie/',
include('galerie.urls')
),
]

55
soustredeni/views.py

@ -0,0 +1,55 @@
from django.shortcuts import get_object_or_404
from django.http import HttpResponse
from django.views import generic
from seminar.models import Soustredeni, Resitel, Soustredeni_Ucastnici # Tohle je stare a chceme se toho zbavit. Pouzivejte s.ToCoChci
import csv
from seminar.views import obalkyView
class SoustredeniListView(generic.ListView):
model = Soustredeni
template_name = 'soustredeni/seznam_soustredeni.html'
def soustredeniObalkyView(request, soustredeni):
soustredeni = get_object_or_404(Soustredeni, id=soustredeni)
return obalkyView(request, soustredeni.ucastnici.all())
class SoustredeniUcastniciBaseView(generic.ListView):
model = Soustredeni_Ucastnici
def get_queryset(self):
soustredeni = get_object_or_404(
Soustredeni,
pk=self.kwargs["soustredeni"]
)
return Soustredeni_Ucastnici.objects.filter(
soustredeni=soustredeni).select_related('resitel')
class SoustredeniMailyUcastnikuView(SoustredeniUcastniciBaseView):
""" Seznam e-mailů řešitelů oddělených čárkami. """
model = Soustredeni_Ucastnici
template_name = 'soustredeni/maily_ucastniku.txt'
class SoustredeniUcastniciView(SoustredeniUcastniciBaseView):
""" HTML tabulka účastníků pro tisk. """
model = Soustredeni_Ucastnici
template_name = 'soustredeni/seznam_ucastniku.html'
def soustredeniUcastniciExportView(request, soustredeni):
soustredeni = get_object_or_404(Soustredeni, id=soustredeni)
ucastnici = Resitel.objects.filter(soustredeni=soustredeni)
response = HttpResponse(content_type='text/csv')
response['Content-Disposition'] = 'attachment; filename="ucastnici.csv"'
writer = csv.writer(response)
writer.writerow(["jmeno", "prijmeni", "rok_maturity", "telefon", "email", "ulice", "mesto", "psc","stat"])
for u in ucastnici:
o = u.osoba
writer.writerow([o.jmeno, o.prijmeni, str(u.rok_maturity), o.telefon, o.email, o.ulice, o.mesto, o.psc, o.stat.name])
return response

0
treenode/__init__.py

88
treenode/admin.py

@ -0,0 +1,88 @@
from django.contrib import admin
from polymorphic.admin import PolymorphicParentModelAdmin, PolymorphicChildModelAdmin, PolymorphicChildModelFilter
import seminar.models as m
# Polymorfismus pro stromy
# TODO: Inlines podle https://django-polymorphic.readthedocs.io/en/stable/admin.html
@admin.register(m.TreeNode)
class TreeNodeAdmin(PolymorphicParentModelAdmin):
base_model = m.TreeNode
child_models = [
m.RocnikNode,
m.CisloNode,
m.MezicisloNode,
m.TemaVCisleNode,
m.UlohaZadaniNode,
m.PohadkaNode,
m.UlohaVzorakNode,
m.TextNode,
m.CastNode,
m.OrgTextNode,
]
actions = ['aktualizuj_nazvy']
# XXX: nejspíš je to totální DB HOG, nechcete to použít moc často.
def aktualizuj_nazvy(self, request, queryset):
newqs = queryset.get_real_instances()
for tn in newqs:
tn.aktualizuj_nazev()
tn.save()
self.message_user(request, "Názvy aktualizovány.")
aktualizuj_nazvy.short_description = "Aktualizuj vybraným TreeNodům názvy"
@admin.register(m.RocnikNode)
class RocnikNodeAdmin(PolymorphicChildModelAdmin):
base_model = m.RocnikNode
show_in_index = True
@admin.register(m.CisloNode)
class CisloNodeAdmin(PolymorphicChildModelAdmin):
base_model = m.CisloNode
show_in_index = True
@admin.register(m.MezicisloNode)
class MezicisloNodeAdmin(PolymorphicChildModelAdmin):
base_model = m.MezicisloNode
show_in_index = True
@admin.register(m.TemaVCisleNode)
class TemaVCisleNodeAdmin(PolymorphicChildModelAdmin):
base_model = m.TemaVCisleNode
show_in_index = True
@admin.register(m.UlohaZadaniNode)
class UlohaZadaniNodeAdmin(PolymorphicChildModelAdmin):
base_model = m.UlohaZadaniNode
show_in_index = True
@admin.register(m.PohadkaNode)
class PohadkaNodeAdmin(PolymorphicChildModelAdmin):
base_model = m.PohadkaNode
show_in_index = True
@admin.register(m.UlohaVzorakNode)
class UlohaVzorakNodeAdmin(PolymorphicChildModelAdmin):
base_model = m.UlohaVzorakNode
show_in_index = True
@admin.register(m.TextNode)
class TextNodeAdmin(PolymorphicChildModelAdmin):
base_model = m.TextNode
show_in_index = True
@admin.register(m.CastNode)
class TextNodeAdmin(PolymorphicChildModelAdmin):
base_model = m.CastNode
show_in_index = True
fields = ('nadpis',)
@admin.register(m.OrgTextNode)
class TextNodeAdmin(PolymorphicChildModelAdmin):
base_model = m.OrgTextNode
show_in_index = True

5
treenode/apps.py

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

14
treenode/forms.py

@ -0,0 +1,14 @@
from django import forms
import seminar.models as m
# pro přidání políčka do formuláře je potřeba
# - mít v modelu tu položku, kterou chci upravovat
# - přidat do views (prihlaskaView, resitelEditView)
# - přidat do forms
# - includovat do html
class NahrajObrazekKTreeNoduForm(forms.ModelForm):
class Meta:
model = m.Obrazek
fields = ('na_web',)

0
treenode/migrations/__init__.py

0
seminar/permissions.py → treenode/permissions.py

2
mamweb/routers.py → treenode/routers.py

@ -1,5 +1,5 @@
from rest_framework import routers from rest_framework import routers
from seminar import viewsets as vs from treenode import viewsets as vs
router = routers.DefaultRouter() router = routers.DefaultRouter()

2
seminar/views/views_rest.py → treenode/serializers.py

@ -2,7 +2,7 @@ from rest_framework import serializers
from rest_polymorphic.serializers import PolymorphicSerializer from rest_polymorphic.serializers import PolymorphicSerializer
import seminar.models as m import seminar.models as m
from seminar import treelib from treenode import treelib
DEFAULT_NODE_DEPTH = 2 DEFAULT_NODE_DEPTH = 2

0
seminar/static/seminar/treenode_editor.js → treenode/static/treenode/treenode_editor.js

0
seminar/templates/seminar/orphanage.html → treenode/templates/treenode/orphanage.html

0
seminar/templates/seminar/treenode.html → treenode/templates/treenode/treenode.html

0
seminar/templates/seminar/treenode_add_stub.html → treenode/templates/treenode/treenode_add_stub.html

0
seminar/templates/seminar/treenode_name.html → treenode/templates/treenode/treenode_name.html

0
seminar/templates/seminar/treenode_recursive.html → treenode/templates/treenode/treenode_recursive.html

0
seminar/templates/seminar/vuetest.html → treenode/templates/treenode/vuetest.html

2
seminar/templatetags/treenodes.py → treenode/templatetags.py

@ -17,7 +17,7 @@ def nodeType(value):
if isinstance(value,UlohaZadaniNode): return "Zadání úlohy" if isinstance(value,UlohaZadaniNode): return "Zadání úlohy"
if isinstance(value,PohadkaNode): return "Pohádka" if isinstance(value,PohadkaNode): return "Pohádka"
### NASLEDUJICI FUNKCE SE POUZIVAJI VE views_all.py V SEKCI PRIPRAVJICI TNLData ### NASLEDUJICI FUNKCE SE POUZIVAJI VE views.py V SEKCI PRIPRAVJICI TNLData
### NEMAZAT, PRESUNOUT S TNLDaty NEKAM BOKEM ### NEMAZAT, PRESUNOUT S TNLDaty NEKAM BOKEM
@register.filter @register.filter

2
seminar/tests_treelib.py → treenode/tests.py

@ -1,5 +1,5 @@
from django.test import TestCase from django.test import TestCase
import seminar.treelib as tl import treenode.treelib as tl
import seminar.models as m import seminar.models as m
class SimpleTreeLibTests(TestCase): class SimpleTreeLibTests(TestCase):

0
seminar/treelib.py → treenode/treelib.py

18
treenode/urls.py

@ -0,0 +1,18 @@
from django.urls import path, re_path
from . import views
urlpatterns = [
#path('treenode/<int:pk>/', views.TreeNodeView.as_view(), name='seminar_treenode'),
#path('treenode/<int:pk>/json/', views.TreeNodeJSONView.as_view(), name='seminar_treenode_json'),
#path('treenode/text/<int:pk>/', views.TextWebView.as_view(), name='seminar_textnode_web'),
#path('treenode/editor/pridat/<str:co>/<int:pk>/<str:kam>/', views.TreeNodePridatView.as_view(), name='treenode_pridat'),
#path('treenode/editor/smazat/<int:pk>/', views.TreeNodeSmazatView.as_view(), name='treenode_smazat'),
#path('treenode/editor/odvesitpryc/<int:pk>/', views.TreeNodeOdvesitPrycView.as_view(), name='treenode_odvesitpryc'),
#path('treenode/editor/podvesit/<int:pk>/<str:kam>/', views.TreeNodePodvesitView.as_view(), name='treenode_podvesit'),
#path('treenode/editor/prohodit/<int:pk>/', views.TreeNodeProhoditView.as_view(), name='treenode_prohodit'),
#path('treenode/sirotcinec/', views.SirotcinecView.as_view(), name='seminar_treenode_sirotcinec'),
#path('problem/(?P<pk>\d+)/(?P<prispevek>\d+)/', views.PrispevekView.as_view(), name='seminar_problem_prispevek'),
re_path(r'^temp/vue/.*$',views.VueTestView.as_view(),name='vue_test_view'),
path('temp/image_upload/', views.NahrajObrazekKTreeNoduView.as_view()),
]

322
treenode/views.py

@ -0,0 +1,322 @@
from django.forms import model_to_dict
from django.shortcuts import redirect
from django.http import JsonResponse
from django.views import generic
from django.views.generic.edit import CreateView
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.exceptions import PermissionDenied
import seminar.models as s
import seminar.models as m
from treenode import treelib
import treenode.forms as f
import treenode.templatetags as tnltt
import treenode.serializers as vr
import logging
logger = logging.getLogger(__name__)
class TNLData(object):
def __init__(self,anode,parent=None, index=None):
self.node = anode
self.sernode = vr.TreeNodeSerializer(anode)
self.children = []
self.parent = parent
self.tema_in_path = False
self.index = index
if parent:
self.tema_in_path = parent.tema_in_path
if isinstance(anode, m.TemaVCisleNode):
self.tema_in_path = True
def add_edit_options(self):
self.deletable = tnltt.deletable(self)
self.editable_siblings = tnltt.editableSiblings(self)
self.editable_children = tnltt.editableChildren(self)
self.text_only_subtree = tnltt.textOnlySubtree(self)
self.can_podvesit_za = tnltt.canPodvesitZa(self)
self.can_podvesit_pred = tnltt.canPodvesitPred(self)
self.appendable_children = tnltt.appendableChildren(self)
print("appChld",self.appendable_children)
if self.parent:
self.appendable_siblings = tnltt.appendableChildren(self.parent)
else:
self.appendable_siblings = []
@classmethod
def public_above(cls, anode):
""" Returns output of verejne for closest Rocnik, Cislo or Problem above.
(All of them have method verejne.)"""
parent = anode # chceme začít už od konkrétního node včetně
while True:
rocnik = isinstance(parent, s.RocnikNode)
cislo = isinstance(parent, s.CisloNode)
uloha = (isinstance(parent, s.UlohaVzorakNode) or
isinstance(parent, s.UlohaZadaniNode))
tema = isinstance(parent, s.TemaVCisleNode)
if (rocnik or cislo or uloha or tema) or parent==None:
break
else:
parent = treelib.get_parent(parent)
if rocnik:
return parent.rocnik.verejne()
elif cislo:
return parent.cislo.verejne()
elif uloha:
return parent.uloha.verejne()
elif tema:
return parent.tema.verejne()
elif None:
print("Existuje TreeNode, který není pod číslem, ročníkem, úlohou"
"ani tématem. {}".format(anode))
return False
@classmethod
def all_public_children(cls, anode):
for ch in treelib.all_children(anode):
if TNLData.public_above(ch):
yield ch
else:
continue
@classmethod
def from_treenode(cls, anode, user, parent=None, index=None):
if TNLData.public_above(anode) or user.has_perm('auth.org'):
out = cls(anode,parent,index)
else:
raise PermissionDenied()
if user.has_perm('auth.org'):
enum_children = enumerate(treelib.all_children(anode))
else:
enum_children = enumerate(TNLData.all_public_children(anode))
for (idx,ch) in enum_children:
outitem = cls.from_treenode(ch, user, out, idx)
out.children.append(outitem)
out.add_edit_options()
return out
@classmethod
def from_tnldata_list(cls, tnllist):
"""Vyrobíme virtuální TNL, který nemá obsah, ale má za potomky všechna zadaná TNLData"""
result = cls(None)
for idx, tnl in enumerate(tnllist):
result.children.append(tnl)
tnl.parent = result
tnl.index = idx
result.add_edit_options()
return result
@classmethod
def filter_treenode(cls, treenode, predicate):
tnll = cls._filter_treenode_recursive(treenode, predicate) # TreeNodeList List :-)
return TNLData.from_tnldata_list(tnll)
@classmethod
def _filter_treenode_recursive(cls, treenode, predicate):
if predicate(treenode):
return [cls.from_treenode(treenode)]
else:
found = []
for tn in treelib.all_children(treenode):
result = cls.filter_treenode(tn, predicate)
# Result by v tuhle chvíli měl být seznam TNLDat odpovídající treenodům, jež matchnuly predikát.
for tnl in result:
found.append(tnl)
return found
def to_json(self):
#self.node = anode
#self.children = []
#self.parent = parent
#self.tema_in_path = False
#self.index = index
out = {}
out['node'] = self.sernode.data
out['children'] = [n.to_json() for n in self.children]
out['tema_in_path'] = self.tema_in_path
out['index'] = self.index
out['deletable'] = self.deletable
out['editable_siblings'] = self.editable_siblings
out['editable_children'] = self.editable_children
out['text_only_subtree'] = self.text_only_subtree
out['can_podvesit_za'] = self.can_podvesit_za
out['can_podvesit_pod'] = self.can_podvesit_pred
out['appendable_children'] = self.appendable_children
out['appendable_siblings'] = self.appendable_siblings
return out
def __repr__(self):
return("TNL({})".format(self.node))
class TreeNodeView(generic.DetailView):
model = s.TreeNode
template_name = 'treenode/treenode.html'
def get_context_data(self,**kwargs):
context = super().get_context_data(**kwargs)
context['tnldata'] = TNLData.from_treenode(self.object,self.request.user)
return context
class TreeNodeJSONView(generic.DetailView):
model = s.TreeNode
def get(self,request,*args, **kwargs):
self.object = self.get_object()
data = TNLData.from_treenode(self.object,self.request.user).to_json()
return JsonResponse(data)
class TreeNodePridatView(generic.View):
type_from_str = {
'rocnikNode': m.RocnikNode,
'cisloNode': m.CisloNode,
'castNode': m.CastNode,
'textNode': m.TextNode,
'temaVCisleNode': m.TemaVCisleNode,
'reseniNode': m.ReseniNode,
'ulohaZadaniNode': m.UlohaZadaniNode,
'ulohaVzorakNode': m.UlohaVzorakNode,
'pohadkaNode': m.PohadkaNode,
'orgText': m.OrgTextNode,
}
def post(self, request, *args, **kwargs):
######## FIXME: ROZEPSANE, NEFUNGUJE, DOPSAT !!!!!! ###########
node = s.TreeNode.objects.get(pk=self.kwargs['pk'])
kam = self.kwargs['kam']
co = self.kwargs['co']
typ = self.type_from_str[co]
raise NotImplementedError('Neni to dopsane, dopis to!')
if kam not in ('pred','syn','za'):
raise ValidationError('Přidat lze pouze před nebo za node nebo jako syna')
if co == m.TextNode:
new_obj = m.Text()
new_obj.save()
elif co == m.CastNode:
new_obj = m.CastNode()
new_obj.nadpis = request.POST.get('pridat-castNode-{}-{}'.format(node.id,kam))
new_obj.save()
elif co == m.ReseniNode:
new_obj = m
pass
elif co == m.UlohaZadaniNode:
pass
elif co == m.UlohaReseniNode:
pass
else:
new_obj = None
if kam == 'pred':
pass
if kam == 'syn':
if typ == m.TextNode:
text_obj = m.Text()
text_obj.save()
node = treelib.create_child(node, typ, text=text_obj)
else:
node = treelib.create_child(node, typ)
if kam == 'za':
if typ == m.TextNode:
text_obj = m.Text()
text_obj.save()
node = treelib.create_node_after(node, typ, text=text_obj)
else:
node = treelib.create_node_after(node, typ)
return redirect(node.get_admin_url())
class TreeNodeSmazatView(generic.base.View):
def post(self, request, *args, **kwargs):
node = s.TreeNode.objects.get(pk=self.kwargs['pk'])
if node.first_child:
raise NotImplementedError('Mazání TreeNode se syny není zatím podporováno!')
treelib.disconnect_node(node)
node.delete()
return redirect(request.headers.get('referer'))
class TreeNodeOdvesitPrycView(generic.base.View):
def post(self, request, *args, **kwargs):
node = s.TreeNode.objects.get(pk=self.kwargs['pk'])
treelib.disconnect_node(node)
node.root = None
node.save()
return redirect(request.headers.get('referer'))
class TreeNodePodvesitView(generic.base.View):
def post(self, request, *args, **kwargs):
node = s.TreeNode.objects.get(pk=self.kwargs['pk'])
kam = self.kwargs['kam']
if kam == 'pred':
treelib.lower_node(node)
elif kam == 'za':
raise NotImplementedError('Podvěsit za není zatím podporováno')
return redirect(request.headers.get('referer'))
class TreeNodeProhoditView(generic.base.View):
def post(self, request, *args, **kwargs):
node = s.TreeNode.objects.get(pk=self.kwargs['pk'])
treelib.swap_succ(node)
return redirect(request.headers.get('referer'))
#FIXME ve formulari predat puvodni url a vratit redirect na ni
class SirotcinecView(generic.ListView):
model = s.TreeNode
template_name = 'treenode/orphanage.html'
def get_queryset(self):
return s.TreeNode.objects.not_instance_of(s.RocnikNode).filter(root=None,prev=None,succ=None,father_of_first=None)
# FIXME pouzit Django REST Framework
class TextWebView(generic.DetailView):
model = s.Text
def get(self,request,*args, **kwargs):
self.object = self.get_object()
return JsonResponse(model_to_dict(self.object,exclude='do_cisla'))
class VueTestView(generic.TemplateView):
template_name = 'treenode/vuetest.html'
class NahrajObrazekKTreeNoduView(LoginRequiredMixin, CreateView):
model = s.Obrazek
form_class = f.NahrajObrazekKTreeNoduForm
def get_initial(self):
initial = super().get_initial()
initial['na_web'] = self.request.FILES['upload']
return initial
def form_valid(self,form):
print(self.request.headers)
print(self.request.headers['Textid'])
print(form.instance)
print(form)
self.object = form.save(commit=False)
print(self.object.na_web)
self.object.text = m.Text.objects.get(pk=int(self.request.headers['Textid']))
self.object.save()
return JsonResponse({"url":self.object.na_web.url})

6
seminar/viewsets.py → treenode/viewsets.py

@ -3,10 +3,10 @@ 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 . import models as m from seminar import models as m
from . import views import treenode.serializers as views
from seminar.permissions import AllowWrite from treenode.permissions import AllowWrite
class PermissionMixin(object): class PermissionMixin(object):
""" Redefines get_permissions so that only organizers can make changes. """ """ Redefines get_permissions so that only organizers can make changes. """

7
various/context_processors.py

@ -1,3 +1,6 @@
from django.conf import settings
def april(req): def april(req):
if 'X-April' in req.headers: if 'X-April' in req.headers:
try: try:
@ -12,3 +15,7 @@ def april(req):
return {'april': today.year} return {'april': today.year}
return {} return {}
def rozliseni(request):
return {"LOCAL_TEST_PROD": settings.LOCAL_TEST_PROD}

3
vysledkovky/__init__.py

@ -0,0 +1,3 @@
"""
Obsahuje výsledkovky a vše, co se týká sčítání bodů.
"""

5
vysledkovky/apps.py

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

0
vysledkovky/migrations/__init__.py

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save