diff --git a/aesop/views.py b/aesop/views.py index c46ab605..d815b5d5 100644 --- a/aesop/views.py +++ b/aesop/views.py @@ -7,7 +7,7 @@ from django.utils.encoding import force_text from .utils import default_ovvpfile from seminar.models import Rocnik, Soustredeni -from seminar.views import vysledkovka +from vysledkovky import utils from seminar.utils import aktivniResitele class ExportIndexView(generic.View): @@ -66,8 +66,8 @@ class ExportRocnikView(generic.View): rocnik = get_object_or_404(Rocnik, prvni_rok=pr, exportovat=True) cislo = rocnik.posledni_zverejnena_vysledkovka_cislo() resitele = aktivniResitele(cislo, True) - slovnik_body = vysledkovka.secti_body_za_rocnik(cislo, resitele, False) - setrizeni_resitele, body = vysledkovka.setrid_resitele_a_body(slovnik_body) + slovnik_body = utils.secti_body_za_rocnik(cislo, resitele, False) + setrizeni_resitele, body = utils.setrid_resitele_a_body(slovnik_body) 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) diff --git a/mamweb/settings_common.py b/mamweb/settings_common.py index 3c850245..ce028bc2 100644 --- a/mamweb/settings_common.py +++ b/mamweb/settings_common.py @@ -79,6 +79,7 @@ TEMPLATES = [ 'django.contrib.messages.context_processors.messages', 'sekizai.context_processors.sekizai', 'header_fotky.context_processors.vzhled', + 'various.context_processors.rozliseni', 'various.context_processors.april', ) }, @@ -109,9 +110,7 @@ INSTALLED_APPS = ( 'dal', 'dal_select2', - 'fluent_comments', 'crispy_forms', - 'threadedcomments', 'django_comments', 'django.contrib.flatpages', @@ -138,6 +137,11 @@ INSTALLED_APPS = ( 'various.autentizace', 'api', 'aesop', + 'odevzdavatko', + 'vysledkovky', + 'personalni', + 'soustredeni', + 'treenode', # Admin upravy: @@ -216,12 +220,6 @@ REST_FRAMEWORK = { '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! # Create file 'django.secret' in every install (it is not kept in git) diff --git a/mamweb/settings_local.py b/mamweb/settings_local.py index 0aadd27e..85058c9f 100644 --- a/mamweb/settings_local.py +++ b/mamweb/settings_local.py @@ -97,3 +97,4 @@ LOGGING = { # E-maily posílat chceme, ale do terminálu :-) EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' SEND_EMAIL_NOTIFICATIONS = True +LOCAL_TEST_PROD = "local" diff --git a/mamweb/settings_prod.py b/mamweb/settings_prod.py index 0374d88b..e674ab05 100644 --- a/mamweb/settings_prod.py +++ b/mamweb/settings_prod.py @@ -67,3 +67,4 @@ LOGGING['handlers']['registration_error_log']['filename'] = '/home/mam-web/logs/ # E-MAIL NOTIFICATIONS POSLI_MAILOVOU_NOTIFIKACI = True +LOCAL_TEST_PROD = "prod" diff --git a/mamweb/settings_test.py b/mamweb/settings_test.py index f8fd844e..365664d2 100644 --- a/mamweb/settings_test.py +++ b/mamweb/settings_test.py @@ -76,3 +76,4 @@ EMAIL_BACKEND = 'various.mail_prefixer.PrefixingMailBackend' # TODO Pouze na otestování testu… Zvolit konferu! # XXX: Je to pole, protože implementační detail backendu. TESTOVACI_EMAILOVA_KONFERENCE = ['betatest@mam.mff.cuni.cz'] +LOCAL_TEST_PROD = "test" diff --git a/mamweb/static/css/mamweb.css b/mamweb/static/css/mamweb.css index 8b19fb80..26baccbd 100644 --- a/mamweb/static/css/mamweb.css +++ b/mamweb/static/css/mamweb.css @@ -1182,3 +1182,21 @@ div.gdpr { label[for=id_skola] { 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; +} diff --git a/mamweb/templates/base.html b/mamweb/templates/base.html index dfd8362e..85cb9e12 100644 --- a/mamweb/templates/base.html +++ b/mamweb/templates/base.html @@ -15,9 +15,6 @@ - - - {# nastavení MathJaxu, aby nahrazoval i matiku obalenou jednoduchými $ #} + {% endblock %} {% block content %} diff --git a/seminar/templates/seminar/org/vloz_reseni.html b/odevzdavatko/templates/odevzdavatko/posli_reseni.html similarity index 88% rename from seminar/templates/seminar/org/vloz_reseni.html rename to odevzdavatko/templates/odevzdavatko/posli_reseni.html index fa537cd9..2a6a2566 100644 --- a/seminar/templates/seminar/org/vloz_reseni.html +++ b/odevzdavatko/templates/odevzdavatko/posli_reseni.html @@ -3,7 +3,6 @@ {% block script %} {{form.media}} - {% endblock %} {% block content %} diff --git a/seminar/templates/seminar/odevzdavatko/resitel_prehled.html b/odevzdavatko/templates/odevzdavatko/prehled_reseni.html similarity index 79% rename from seminar/templates/seminar/odevzdavatko/resitel_prehled.html rename to odevzdavatko/templates/odevzdavatko/prehled_reseni.html index 773aa2a3..ae3d45a9 100644 --- a/seminar/templates/seminar/odevzdavatko/resitel_prehled.html +++ b/odevzdavatko/templates/odevzdavatko/prehled_reseni.html @@ -23,9 +23,9 @@ {% for hodn in hodnoceni %} - {{ hodn.reseni.cas_doruceni | date:"d.m.Y H:i"}} + {{ hodn.reseni.cas_doruceni | date:"d.m.Y H:i"}} {{ hodn.problem.nazev | zkrat_nazev_problemu }} - {{ hodn.body|default_if_none:"---" }} + {{ hodn.body|default_if_none:"---" }} {{ hodn.reseni.cas_doruceni | deadline_html }} {% endfor %} diff --git a/seminar/templates/seminar/odevzdavatko/seznam.html b/odevzdavatko/templates/odevzdavatko/seznam.html similarity index 100% rename from seminar/templates/seminar/odevzdavatko/seznam.html rename to odevzdavatko/templates/odevzdavatko/seznam.html diff --git a/seminar/templates/seminar/odevzdavatko/tabulka.html b/odevzdavatko/templates/odevzdavatko/tabulka.html similarity index 97% rename from seminar/templates/seminar/odevzdavatko/tabulka.html rename to odevzdavatko/templates/odevzdavatko/tabulka.html index 4e4be2ec..c56611ef 100644 --- a/seminar/templates/seminar/odevzdavatko/tabulka.html +++ b/odevzdavatko/templates/odevzdavatko/tabulka.html @@ -4,7 +4,7 @@ {% block content %} -
+ {{ filtr.resitele }} {{ filtr.problemy }} Od: {{ filtr.reseni_od }} diff --git a/odevzdavatko/urls.py b/odevzdavatko/urls.py new file mode 100644 index 00000000..bb04f06a --- /dev/null +++ b/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//', org_required(views.TabulkaOdevzdanychReseniView.as_view()), name='odevzdavatko_tabulka'), + path('org/reseni///', org_required(views.ReseniProblemuView.as_view()), name='odevzdavatko_reseni_resitele_k_problemu'), + path('org/reseni/', 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/', resitel_or_org_required(views.ResitelReseniView.as_view()), name='odevzdavatko_resitel_reseni'), +] diff --git a/seminar/views/odevzdavatko.py b/odevzdavatko/views.py similarity index 74% rename from seminar/views/odevzdavatko.py rename to odevzdavatko/views.py index b3e1947d..aa95af2f 100644 --- a/seminar/views/odevzdavatko.py +++ b/odevzdavatko/views.py @@ -1,9 +1,12 @@ from django.core.exceptions import PermissionDenied from django.views.generic import ListView, DetailView, FormView +from django.contrib.auth.mixins import LoginRequiredMixin +from django.core.mail import send_mail +from django.utils import timezone +from django.views.generic import ListView, DetailView, FormView, CreateView from django.views.generic.list import MultipleObjectTemplateResponseMixin,MultipleObjectMixin from django.views.generic.base import View -from django.views.generic.detail import SingleObjectMixin -from django.shortcuts import redirect, get_object_or_404 +from django.shortcuts import redirect, get_object_or_404, render from django.urls import reverse from django.db import transaction from django.db.models import Q @@ -14,9 +17,10 @@ from itertools import groupby import logging import seminar.models as m -import seminar.forms as f -from seminar.forms import OdevzdavatkoTabulkaFiltrForm as FiltrForm -from seminar.utils import aktivniResitele, resi_v_rocniku, deadline +from . import forms as f +from .forms import OdevzdavatkoTabulkaFiltrForm as FiltrForm +from seminar.utils import resi_v_rocniku, deadline +from seminar.views import formularOKView logger = logging.getLogger(__name__) @@ -42,7 +46,7 @@ class SouhrnReseni: class TabulkaOdevzdanychReseniView(ListView): - template_name = 'seminar/odevzdavatko/tabulka.html' + template_name = 'odevzdavatko/tabulka.html' model = m.Hodnoceni def inicializuj_osy_tabulky(self): @@ -166,7 +170,7 @@ class TabulkaOdevzdanychReseniView(ListView): # Velmi silně inspirováno zdrojáky, FIXME: Nedá se to udělat smysluplněji? class ReseniProblemuView(MultipleObjectTemplateResponseMixin, MultipleObjectMixin, View): model = m.Reseni - template_name = 'seminar/odevzdavatko/seznam.html' + template_name = 'odevzdavatko/seznam.html' def get_queryset(self): qs = super().get_queryset() @@ -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 class DetailReseniView(DetailView): model = m.Reseni - template_name = 'seminar/odevzdavatko/detail.html' + template_name = 'odevzdavatko/detail.html' def aktualni_hodnoceni(self): self.reseni = get_object_or_404(m.Reseni, id=self.kwargs['pk']) @@ -232,7 +236,7 @@ class DetailReseniView(DetailView): def hodnoceniReseniView(request, pk, *args, **kwargs): reseni = get_object_or_404(m.Reseni, pk=pk) - template_name = 'seminar/odevzdavatko/detail.html' + template_name = 'odevzdavatko/detail.html' form_class = f.OhodnoceniReseniFormSet success_url = reverse('odevzdavatko_detail_reseni', kwargs={'pk': pk}) @@ -271,7 +275,7 @@ def hodnoceniReseniView(request, pk, *args, **kwargs): class ResitelReseniView(DetailView): model = m.Reseni - template_name = 'seminar/odevzdavatko/detail_resitele.html' + template_name = 'odevzdavatko/detail_resitele.html' def aktualni_hodnoceni(self): self.reseni = get_object_or_404(m.Reseni, id=self.kwargs['pk']) @@ -299,7 +303,7 @@ class ResitelReseniView(DetailView): class PrehledOdevzdanychReseni(ListView): model = m.Hodnoceni - template_name = 'seminar/odevzdavatko/resitel_prehled.html' + template_name = 'odevzdavatko/prehled_reseni.html' def get_queryset(self): if not self.request.user.is_authenticated: @@ -325,7 +329,7 @@ class PrehledOdevzdanychReseni(ListView): class SeznamReseniView(ListView): model = m.Reseni - template_name = 'seminar/odevzdavatko/seznam.html' + template_name = 'odevzdavatko/seznam.html' class SeznamAktualnichReseniView(SeznamReseniView): def get_queryset(self): @@ -334,3 +338,94 @@ class SeznamAktualnichReseniView(SeznamReseniView): resitele = resi_v_rocniku(akt_rocnik) qs = qs.filter(resitele__in=resitele) # FIXME: Najde řešení i ze starých ročníků, která odevzdal alespoň jeden aktuální řešitel return qs + + +class PosliReseniView(LoginRequiredMixin, FormView): + template_name = 'odevzdavatko/posli_reseni.html' + form_class = f.PosliReseniForm + + def form_valid(self, form): + data = form.cleaned_data + nove_reseni = m.Reseni.objects.create( + cas_doruceni=data['cas_doruceni'], + forma=data['forma'], + poznamka=data['poznamka'], + ) + nove_reseni.resitele.add(data['resitel']) + nove_reseni.problem.add(data['problem']) + nove_reseni.save() + # Chtěl jsem, aby bylo vidět, že se to uložilo, tak přesměrovávám na profil. + return redirect(reverse('profil')) + + +class NahrajReseniView(LoginRequiredMixin, CreateView): + model = m.Reseni + template_name = 'odevzdavatko/nahraj_reseni.html' + form_class = f.NahrajReseniForm + + def get(self, request, *args, **kwargs): + # Zaříznutí starých řešitelů: + # FIXME: Je to tady dost naprasené, mělo by to asi být jinde… + osoba = m.Osoba.objects.get(user=self.request.user) + resitel = osoba.resitel + if resitel.rok_maturity <= m.Nastaveni.get_solo().aktualni_rocnik.prvni_rok: + return render(request, 'universal.html', { + 'title': 'Nelze odevzdat', + 'error': 'Zdá se, že jsi již odmaturoval/a, a tedy nemůžeš odevzdat do našeho semináře řešení.', + 'text': 'Pokud se ti zdá, že to je chyba, napiš nám prosím e-mail. Díky.', + }) + return super().get(request, *args, **kwargs) + + def get_context_data(self,**kwargs): + data = super().get_context_data(**kwargs) + if self.request.POST: + data['prilohy'] = f.ReseniSPrilohamiFormSet(self.request.POST,self.request.FILES) + else: + data['prilohy'] = f.ReseniSPrilohamiFormSet() + return data + + # FIXME prepsat tak, aby form_valid se volalo jen tehdy, kdyz je form i formset validni + # Inspirace: https://stackoverflow.com/questions/41599809/using-a-django-filefield-in-an-inline-formset + def form_valid(self,form): + context = self.get_context_data() + prilohy = context['prilohy'] + if not prilohy.is_valid(): + return super().form_invalid(form) + with transaction.atomic(): + self.object = form.save() + self.object.resitele.add(m.Resitel.objects.get(osoba__user = self.request.user)) + self.object.cas_doruceni = timezone.now() + self.object.forma = m.Reseni.FORMA_UPLOAD + self.object.save() + + prilohy.instance = self.object + prilohy.save() + + # Pošleme mail opravovatelům a garantovi + # FIXME: Nechat spočítat databázi? Je to pár dotazů (pravděpodobně), takže to za to možná nestojí + prijemci = set() + problemy = [] + for prob in form.cleaned_data['problem']: + prijemci.update(prob.opravovatele.all()) + if prob.garant is not None: + prijemci.add(prob.garant) + problemy.append(prob) + # FIXME: Možná poslat mail i relevantním orgům nadproblémů? + if len(prijemci) < 1: + logger.warning(f"Pozor, neposílám e-mail nikomu. Problémy: {problemy}") + # FIXME: Víc informativní obsah mailů, možná vč. příloh? + prijemci = map(lambda it: it.osoba.email, prijemci) + + resitel = m.Osoba.objects.get(user = self.request.user) + + seznam = "problému " + str(problemy[0]) if len(problemy) == 1 else 'následujícím problémům:\n' + ', \n'.join(map(str, problemy)) + seznam_do_subjectu = "problému " + str(problemy[0]) + ("" if len(problemy) == 1 else f" (a dalším { len(problemy) - 1 })") + + send_mail( + subject="Nové řešení k " + seznam_do_subjectu, + message=f"Řešitel{ '' if resitel.pohlavi_muz else 'ka' } { resitel } právě nahrál{'' if resitel.pohlavi_muz else 'a' } nové řešení k { seznam }.\n\nHurá do opravování: { self.object.absolute_url() }", + from_email="submitovatko@mam.mff.cuni.cz", # FIXME: Chceme to mít radši tady, nebo v nastavení? + recipient_list=list(prijemci), + ) + + return formularOKView(self.request, text='Řešení úspěšně odevzdáno') diff --git a/personalni/__init__.py b/personalni/__init__.py new file mode 100644 index 00000000..65912bec --- /dev/null +++ b/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. +""" \ No newline at end of file diff --git a/personalni/admin.py b/personalni/admin.py new file mode 100644 index 00000000..36a4ae6e --- /dev/null +++ b/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) diff --git a/personalni/apps.py b/personalni/apps.py new file mode 100644 index 00000000..359f220c --- /dev/null +++ b/personalni/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class PersonalniConfig(AppConfig): + name = 'personalni' diff --git a/seminar/forms.py b/personalni/forms.py similarity index 51% rename from seminar/forms.py rename to personalni/forms.py index 19ed8d9f..78f1f11a 100644 --- a/seminar/forms.py +++ b/personalni/forms.py @@ -3,11 +3,8 @@ from dal import autocomplete from django.contrib.auth.forms import PasswordResetForm from django.core.exceptions import ObjectDoesNotExist from django.contrib.auth.models import User -from django.forms import formset_factory -from django.forms.models import inlineformset_factory -from .models import Skola, Resitel, Osoba, Problem -import seminar.models as m +from seminar.models import Skola, Resitel, Osoba from datetime import date import logging @@ -19,13 +16,13 @@ import logging # - includovat do html class DateInput(forms.DateInput): - # aby se datum dalo vybírat z kalendáře - input_type = 'date' - + # aby se datum dalo vybírat z kalendáře + input_type = 'date' + class TelInput(forms.TextInput): - # tohle je možná k niřemu, ale alepsoň to mění input type a nic to nekazí - input_type = 'tel' - input_pattern="^[+]?[()/0-9. -]{9,}$" + # tohle je možná k niřemu, ale alepsoň to mění input type a nic to nekazí + input_type = 'tel' + input_pattern="^[+]?[()/0-9. -]{9,}$" class PrihlaskaForm(PasswordResetForm): @@ -58,7 +55,7 @@ class PrihlaskaForm(PasswordResetForm): attrs = {'data-placeholder--id': '-1', 'data-placeholder--text' : '---', 'data-allow-clear': 'true'}) - ,required=False) + ,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) @@ -156,7 +153,7 @@ class ProfileEditForm(forms.Form): attrs = {'data-placeholder--id': '-1', 'data-placeholder--text' : '---', 'data-allow-clear': 'true'}) - ,required=False) + ,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) @@ -217,210 +214,8 @@ class ProfileEditForm(forms.Form): # elif data.get('skola_adresa')=='': # self.add_error('skola_adresa',forms.ValidationError('Je nutné vyplnit adresu školy')) + class PoMaturiteProfileEditForm(ProfileEditForm): rok_maturity = forms.IntegerField( label='Rok maturity', required=True) - -class VlozReseniForm(forms.Form): - #FIXME jen podproblémy daného problému - problem = forms.ModelChoiceField(label='Problém',queryset=m.Problem.objects.all()) - # to_field_name - #problem = models.ManyToManyField(Problem, verbose_name='problém', help_text='Problém', - # through='Hodnoceni') - - # FIXME pridat vice resitelu - resitel = forms.ModelChoiceField(label="Řešitel", - queryset=Resitel.objects.all(), - widget=autocomplete.ModelSelect2( - url='autocomplete_resitel', - attrs = {'data-placeholder--id': '-1', - 'data-placeholder--text' : '---', - 'data-allow-clear': 'true'}) - ) - - - #resitele = models.ManyToManyField(Resitel, verbose_name='autoři řešení', - # help_text='Seznam autorů řešení', through='Reseni_Resitele') - - cas_doruceni = forms.DateField(widget=DateInput(),label="Čas doručení") - - #cas_doruceni = models.DateTimeField('čas_doručení', default=timezone.now, blank=True) - - forma = forms.ChoiceField(label="Forma řešení",choices = m.Reseni.FORMA_CHOICES) - #forma = models.CharField('forma řešení', max_length=16, choices=FORMA_CHOICES, blank=False, - # default=FORMA_EMAIL) - - poznamka = forms.CharField(label='Neveřejná poznámka', required=False) - #poznamka = models.TextField('neveřejná poznámka', blank=True, - # help_text='Neveřejná poznámka k řešení (plain text)') - - #TODO body do cisla - #TODO prilohy - - ##def __init__(self, *args, **kwargs): - ## super().__init__(*args, **kwargs) - ## #self.fields['favorite_color'] = forms.ChoiceField(choices=[(color.id, color.name) for color in Resitel.objects.all()]) - -class NahrajReseniForm(forms.ModelForm): - class Meta: - model = m.Reseni - fields = ('problem',) - help_texts = {'problem':''} # Nezobrazovat help text ve formuláři - - widgets = {'problem': - autocomplete.ModelSelect2Multiple( - url='autocomplete_problem_odevzdatelny', - attrs = {'data-placeholder--id': '-1', - 'data-placeholder--text' : '---', - 'data-allow-clear': 'true'}, - ) - } - -ReseniSPrilohamiFormSet = inlineformset_factory(m.Reseni,m.PrilohaReseni, - form = NahrajReseniForm, - fields = ('soubor','res_poznamka'), - widgets = {'res_poznamka':forms.TextInput()}, - extra = 1, - can_delete = False, - - ) - -class NahrajObrazekKTreeNoduForm(forms.ModelForm): - class Meta: - model = m.Obrazek - fields = ('na_web',) - - -class JednoHodnoceniForm(forms.ModelForm): - class Meta: - model = m.Hodnoceni - fields = ('problem', 'body', 'cislo_body') - widgets = { - 'problem': autocomplete.ModelSelect2( - url='autocomplete_problem_odevzdatelny', # FIXME: Dovolit i starší? - ) - } - -OhodnoceniReseniFormSet = formset_factory(JednoHodnoceniForm, - extra = 0, - ) - -class PoznamkaReseniForm(forms.ModelForm): - class Meta: - model = m.Reseni - fields = ('poznamka',) - -# FIXME: Ideálně by mělo být součástí třídy níž, ale neumím to udělat -DATE_FORMAT = '%Y-%m-%d' - -class OdevzdavatkoTabulkaFiltrForm(forms.Form): - """Form pro filtrování přehledové odevzdávátkové tabulky - - Inspirováno https://kam.mff.cuni.cz/mffzoom/""" - - # Věci definované níž se importují i ve views pro odevzdávátko (Inspirováno https://docs.djangoproject.com/en/3.1/ref/models/fields/#field-choices) - - RESITELE_RELEVANTNI = 'relevantni' - RESITELE_NEODMATUROVAVSI = 'neodmaturovavsi' - RESITELE_CHOICES = [ - (RESITELE_RELEVANTNI, 'Relevantní řešitelé'), # I.e. nezobrazovat prázdné řádky tabulky - (RESITELE_NEODMATUROVAVSI, 'Všichni bez maturity'), - # Možná: všechny vč. historických? - ] - - PROBLEMY_MOJE = 'moje' - PROBLEMY_LETOSNI = 'letosni' - PROBLEMY_CHOICES = [ - (PROBLEMY_MOJE, 'Moje problémy'), # Letošní problémy, které mají v sobě nebo v nadproblémech přiřazeného daného orga - (PROBLEMY_LETOSNI, 'Všechny letošní'), - # TODO: *hlavní problémy, možná všechny... - # XXX: Chtělo by to i "aktuálně zadané... - ] - - # TODO: Typy problémů (problémy, úlohy, ostatní, všechny)? Jen některá řešení (obodovaná/neobodovaná, víc řešitelů, ...)? - - - @classmethod - def gen_terminy(cls, rocnik=None): - import datetime - from time import strftime - - from django.db.utils import OperationalError - try: - aktualni_rocnik = m.Nastaveni.get_solo().aktualni_rocnik - aktualni_cislo = m.Nastaveni.get_solo().aktualni_cislo - except OperationalError: - # django.db.utils.OperationalError: no such table: seminar_nastaveni - # Nemáme databázi, takže to selhalo. Pro jistotu vrátíme aspoň dvě možnosti, ať to nepadá dál - logger = logging.getLogger(__name__) - logger.error("Rozbitá databáze (před počátečními migracemi?)") - return [('broken', 'Je to rozbitý'), ('fubar', 'Nefunguje to')] - - # FIXME: Tohle je hnusný monkey patch, mělo by to být nějak zahrnuto výš. - if rocnik is not None: - aktualni_rocnik = rocnik - aktualni_cislo = m.Cislo.objects.filter(rocnik=rocnik).order_by('poradi').last() - - result = [] - - for cislo in m.Cislo.objects.filter( - rocnik=aktualni_rocnik, - poradi__lte=aktualni_cislo.poradi, - ).reverse(): # Standardně se řadí od nejnovějšího čísla - # Předem je mi líto kohokoliv, kdo tyhle řádky bude číst... - if cislo.datum_vydani is not None and cislo.datum_vydani <= datetime.date.today(): - result.append(( - strftime(DATE_FORMAT, cislo.datum_vydani.timetuple()), - f"Vydání {cislo.poradi}. čísla")) - if cislo.datum_preddeadline is not None and cislo.datum_preddeadline <= datetime.date.today(): - result.append(( - strftime(DATE_FORMAT, cislo.datum_preddeadline.timetuple()), - f"Předdeadline {cislo.poradi}. čísla")) - if cislo.datum_deadline_soustredeni is not None and cislo.datum_deadline_soustredeni <= datetime.date.today(): - result.append(( - strftime(DATE_FORMAT, cislo.datum_deadline_soustredeni.timetuple()), - f"Sous. deadline {cislo.poradi}. čísla")) - if cislo.datum_deadline is not None and cislo.datum_deadline <= datetime.date.today(): - result.append(( - strftime(DATE_FORMAT, cislo.datum_deadline.timetuple()), - f"Finální deadline {cislo.poradi}. čísla")) - result.append(( - strftime(DATE_FORMAT, datetime.date.today().timetuple()), f"Dnes")) - - return result - - @classmethod - def gen_initial(cls, rocnik=None): - terminy = cls.gen_terminy(rocnik) - initial = { - 'resitele': cls.RESITELE_RELEVANTNI, - 'problemy': cls.PROBLEMY_MOJE, - # Pokud chceme neaktuální ročník, tak nás nejspíš zajímají všechna řešení… - 'reseni_od': terminy[-2] if rocnik is None else terminy[0], - 'reseni_do': terminy[-1], - 'neobodovane': False, - } - return initial - - def __init__(self, *args, rocnik=None, **kwargs): - if 'initial' not in kwargs: - super().__init__(initial=self.gen_initial(rocnik), *args, **kwargs) - else: - super().__init__(*args, **kwargs) - # choices jako parametr Select widgetu neumí brát callable, jen iterable, takže si pro jednoduchost můžu rovnou uložit výsledek sem... - # A "sem" znamená do libovolné metody, protože jinak se jedná o kód, který django spustí při inicializaci a protože potřebujeme databázi, tak by spadnul při vyrábění testdat... - self.terminy = self.gen_terminy(rocnik) - self.fields['reseni_od'].widget = forms.Select(choices=self.gen_terminy(rocnik)) - # Pokud chceme neaktuální ročník, tak nás nejspíš zajímají všechna řešení… - self.fields['reseni_od'].initial = self.terminy[-2] if rocnik is None else self.terminy[0] - self.fields['reseni_do'].widget = forms.Select(choices=self.gen_terminy(rocnik)) - self.fields['reseni_do'].initial = self.terminy[-1] - - # NOTE: Initial definuji pro jednotlivé fieldy, aby to bylo tady a nebylo potřeba to řešit ve views... - resitele = forms.ChoiceField(choices=RESITELE_CHOICES) - problemy = forms.ChoiceField(choices=PROBLEMY_CHOICES) - - reseni_od = forms.DateField(input_formats=[DATE_FORMAT]) - reseni_do = forms.DateField(input_formats=[DATE_FORMAT]) - neobodovane = forms.BooleanField(required=False) diff --git a/personalni/migrations/__init__.py b/personalni/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/seminar/static/seminar/prihlaska.js b/personalni/static/personalni/prihlaska.js similarity index 100% rename from seminar/static/seminar/prihlaska.js rename to personalni/static/personalni/prihlaska.js diff --git a/seminar/templates/seminar/orgorozcestnik.html b/personalni/templates/personalni/profil/orgorozcestnik.html similarity index 100% rename from seminar/templates/seminar/orgorozcestnik.html rename to personalni/templates/personalni/profil/orgorozcestnik.html diff --git a/seminar/templates/seminar/profil/resitel.html b/personalni/templates/personalni/profil/resitel.html similarity index 100% rename from seminar/templates/seminar/profil/resitel.html rename to personalni/templates/personalni/profil/resitel.html diff --git a/personalni/templates/personalni/udaje/edit.html b/personalni/templates/personalni/udaje/edit.html new file mode 100644 index 00000000..e39f5144 --- /dev/null +++ b/personalni/templates/personalni/udaje/edit.html @@ -0,0 +1,105 @@ +{% extends "base.html" %} +{% load staticfiles %} + +{% block script %} + +{% endblock %} + + + +{% block content %} +

+ {% block nadpis1a %}{% block nadpis1b %} + Změna osobních údajů + {% endblock %}{% endblock %} +

+ + {% csrf_token %} + {{form.non_field_errors}} + +
+ +

+ Přihlašovací údaje +

+ + {% include "personalni/udaje/prihlaska_field.html" with field=form.username %} +
+ +
+ +

+ Osobní údaje +

+ + {% 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 %} +
+ +
+ +

+ Bydliště +

+ + {% 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"%} +
+ +
+ +

+ Škola +

+ + {% include "personalni/udaje/prihlaska_field.html" with field=form.skola %} + + + {% 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 %} +
Vyplň prosím celý název a adresu školy.
+ +
+ +

+ Pošta +

+ + {% include "personalni/udaje/prihlaska_field.html" with field=form.zasilat %} + {% include "personalni/udaje/prihlaska_field.html" with field=form.zasilat_cislo_emailem %} +
+ +
+ +

+ Zasílání propagačních materiálů +

+ + {% include "personalni/udaje/prihlaska_field.html" with field=form.spam %} +
+ +
+ + + + +{% endblock %} diff --git a/seminar/templates/seminar/profil/gdpr.html b/personalni/templates/personalni/udaje/gdpr.html similarity index 100% rename from seminar/templates/seminar/profil/gdpr.html rename to personalni/templates/personalni/udaje/gdpr.html diff --git a/personalni/templates/personalni/udaje/prihlaska.html b/personalni/templates/personalni/udaje/prihlaska.html new file mode 100644 index 00000000..a0cbea15 --- /dev/null +++ b/personalni/templates/personalni/udaje/prihlaska.html @@ -0,0 +1,123 @@ +{% extends "base.html" %} +{% load staticfiles %} + +{% block script %} + +{% endblock %} + + + +{% block content %} +

+ {% block nadpis1a %}{% block nadpis1b %} + Přihláška do semináře + {% endblock %}{% endblock %} +

+ +

Tučně popsaná pole jsou povinná.

+ +
+ {% csrf_token %} + {{form.non_field_errors}} + + +
+

+ Přihlašovací údaje +

+ + {% 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 %}#} +
+ +
+ +

+ Osobní údaje +

+ + {% 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 %} +
+ +
+ +

+ Bydliště +

+ + {% 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"%} +
+ +
+ +

+ Škola +

+ + {% include "personalni/udaje/prihlaska_field.html" with field=form.skola %} + + + {% 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 %} +
Vyplň prosím celý název a adresu školy.
+ +
+ +

+ Pošta +

+ + {% include "personalni/udaje/prihlaska_field.html" with field=form.zasilat %} + {% include "personalni/udaje/prihlaska_field.html" with field=form.zasilat_cislo_emailem %} +
+
+ +

+ GDPR +

+ {% include "personalni/udaje/gdpr.html" %} + + {% include "personalni/udaje/prihlaska_field.html" with field=form.gdpr %} +
+ +
+ +

+ Zasílání propagačních materiálů +

+ + {% include "personalni/udaje/prihlaska_field.html" with field=form.spam %} +
+ + + +
+ + +
+ + + +{% endblock %} diff --git a/seminar/templates/seminar/profil/prihlaska_field.html b/personalni/templates/personalni/udaje/prihlaska_field.html similarity index 100% rename from seminar/templates/seminar/profil/prihlaska_field.html rename to personalni/templates/personalni/udaje/prihlaska_field.html diff --git a/personalni/urls.py b/personalni/urls.py new file mode 100644 index 00000000..73a6f720 --- /dev/null +++ b/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'), + +] diff --git a/personalni/views.py b/personalni/views.py new file mode 100644 index 00000000..9ae3c239 --- /dev/null +++ b/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. Vrátit se zpět na profil.') + + 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) diff --git a/requirements.txt b/requirements.txt index abd4f584..3a411faa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -34,9 +34,6 @@ django-webpack-loader django-rest-polymorphic # Comments -akismet==1.0.1 -django-fluent-comments==2.1 -django-threadedcomments==1.2 django-contrib-comments==1.9.0 # debug tools/extensions diff --git a/seminar/admin.py b/seminar/admin.py index d05fbfec..73bb5ff0 100644 --- a/seminar/admin.py +++ b/seminar/admin.py @@ -1,12 +1,9 @@ from django.contrib import admin -from django.contrib.auth.models import Group from django.db import models from django.forms import widgets, ModelForm from django.core.exceptions import ValidationError from polymorphic.admin import PolymorphicParentModelAdmin, PolymorphicChildModelAdmin, PolymorphicChildModelFilter -from reversion.admin import VersionAdmin -from django_reverse_admin import ReverseModelAdmin from solo.admin import SingletonModelAdmin from django.utils.safestring import mark_safe @@ -15,10 +12,7 @@ from seminar.utils import hlavni_problem # Todo: reversion 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) 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' -@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) class ProblemAdmin(PolymorphicParentModelAdmin): @@ -200,147 +153,8 @@ class ResitelInline(admin.TabularInline): model = m.Resitel 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.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.Novinky) diff --git a/seminar/autocomplete_light_registry.py.old b/seminar/autocomplete_light_registry.py.old deleted file mode 100644 index 64590adf..00000000 --- a/seminar/autocomplete_light_registry.py.old +++ /dev/null @@ -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) - - diff --git a/seminar/migrations/0001_squashed_0098_auto_20210906_0305.py b/seminar/migrations/0001_squashed_0098_auto_20210906_0305.py index aedd460c..a95f9c1b 100644 --- a/seminar/migrations/0001_squashed_0098_auto_20210906_0305.py +++ b/seminar/migrations/0001_squashed_0098_auto_20210906_0305.py @@ -12,7 +12,7 @@ import taggit.managers from datetime import date from django.db.models import Q -from seminar.treelib import get_parent +from treenode.treelib import get_parent import datetime as dt diff --git a/seminar/migrations/0084_clanek_cislo.py b/seminar/migrations/0084_clanek_cislo.py index 7a211fa6..ffc6a29d 100644 --- a/seminar/migrations/0084_clanek_cislo.py +++ b/seminar/migrations/0084_clanek_cislo.py @@ -2,7 +2,7 @@ from django.db import migrations, models import django.db.models.deletion -from seminar.treelib import get_parent +from treenode.treelib import get_parent import logging logger = logging.getLogger(__name__) diff --git a/seminar/models.py b/seminar/models.py deleted file mode 100644 index b04b31fa..00000000 --- a/seminar/models.py +++ /dev/null @@ -1,1786 +0,0 @@ -# -*- coding: utf-8 -*- -import os -import random -import subprocess -import pathlib -import tempfile -import logging - -from django.contrib.sites.shortcuts import get_current_site -from django.db import models -from django.contrib import auth -from django.db.models import Sum -from django.utils import timezone -from django.conf import settings -from django.utils.encoding import force_text -from django.utils.text import slugify -from django.urls import reverse, reverse_lazy -from django.core.cache import cache -from django.core.exceptions import ObjectDoesNotExist, ValidationError -from django.contrib.contenttypes.models import ContentType -from django.utils.text import get_valid_filename -from imagekit.models import ImageSpecField, ProcessedImageField -from imagekit.processors import ResizeToFit, Transpose -from django.utils.functional import cached_property - -from django_countries.fields import CountryField -from solo.models import SingletonModel -from taggit.managers import TaggableManager - -from reversion import revisions as reversion - -from seminar.utils import roman, FirstTagParser # Pro získání úryvku z TextNode -from seminar.utils import hlavni_problem -from seminar 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 - -logger = logging.getLogger(__name__) - -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 - -@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 ZŠ.""" - if self.rok_maturity is None: - return '' - rozdil = 5 - (self.rok_maturity - rocnik.prvni_rok) - if rozdil >= 1: - return str(rozdil) - else: - return 'Z' + str(rozdil + 9) - - def vsechny_body(self): - "Spočítá body odjakživa." - vsechna_reseni = self.reseni_set.all() - 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. - 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 - - 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 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 verejna_cisla(self): - vc = [c for c in self.cisla.all() if c.verejne()] - vc.sort(key=lambda c: c.poradi) - return vc - - 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 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…') - 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) -class Organizator(SeminarModelBase): -# zmena dedicnosti z models.Model na SeminarModelBase, potencialni vznik bugu - - 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'] - -@reversion.register(ignore_duplicates=True) -class Soustredeni(SeminarModelBase): - - class Meta: - db_table = 'seminar_soustredeni' - verbose_name = 'Soustředění' - verbose_name_plural = 'Soustředění' - ordering = ['-rocnik__rocnik', '-datum_zacatku'] - - # Interní ID - id = models.AutoField(primary_key = True) - - rocnik = models.ForeignKey(Rocnik, verbose_name='ročník', related_name='soustredeni', - on_delete=models.PROTECT) - - datum_zacatku = models.DateField('datum začátku', blank=True, null=True, - help_text='První den soustředění') - - datum_konce = models.DateField('datum konce', blank=True, null=True, - help_text='Poslední den soustředění') - - verejne_db = models.BooleanField('soustředění zveřejněno', db_column='verejne', default=False) - - misto = models.CharField('místo soustředění', max_length=256, blank=True, default='', - help_text='Místo (název obce, volitelně též objektu') - - ucastnici = models.ManyToManyField(Resitel, verbose_name='účastníci soustředění', - help_text='Seznam účastníků soustředění', through='Soustredeni_Ucastnici') - - organizatori = models.ManyToManyField(Organizator, - verbose_name='Organizátoři soustředění', - help_text='Seznam organizátorů soustředění', - through='Soustredeni_Organizatori') - - text = models.TextField('text k soustředění (HTML)', blank=True, default='') - - TYP_JARNI = 'jarni' - TYP_PODZIMNI = 'podzimni' - TYP_VIKEND = 'vikend' - TYP_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) -# Pozor na následující řádek. *Nekrmit, asi kouše!* -class Problem(SeminarModelBase,PolymorphicModel): - - class Meta: - # Není abstraktní, protože se na něj jinak nedají dělat ForeignKeys. - # TODO: Udělat to polymorfní (pomocí django-polymorphic), abychom dostali - # po těch vazbách přímo tu úlohu/témátko vč. fieldů, které nejsou součástí - # modelu Problem? - - #abstract = True - db_table = 'seminar_problemy' - verbose_name = 'Problém' - verbose_name_plural = 'Problémy' - ordering = ['nazev'] - - # Interní ID - id = models.AutoField(primary_key = True) - - # Název - nazev = models.CharField('název', max_length=256) # Zveřejnitelný na stránky - - # Problém má podproblémy - nadproblem = models.ForeignKey('self', verbose_name='nadřazený problém', - related_name='podproblem', null=True, blank=True, - on_delete=models.SET_NULL) - - STAV_NAVRH = 'navrh' - STAV_ZADANY = 'zadany' - STAV_VYRESENY = 'vyreseny' - STAV_SMAZANY = 'smazany' - STAV_CHOICES = [ - (STAV_NAVRH, 'Návrh'), - (STAV_ZADANY, 'Zadaný'), - (STAV_VYRESENY, 'Vyřešený'), - (STAV_SMAZANY, 'Smazaný'), - ] - stav = models.CharField('stav problému', max_length=32, choices=STAV_CHOICES, blank=False, default=STAV_NAVRH) - # Téma je taky Problém, takže má stavy, "zadané" témátko je aktuálně otevřené a dá se k němu něco poslat (řešení nebo článek) - - zamereni = TaggableManager(verbose_name='zaměření', - help_text='Zaměření M/F/I/O problému, příp. další tagy', blank=True) - - poznamka = models.TextField('org poznámky (HTML)', blank=True, - help_text='Neveřejný návrh úlohy, návrh řešení, text zadání, poznámky ...') - - autor = models.ForeignKey(Organizator, verbose_name='autor problému', - related_name='autor_problemu_%(class)s', null=True, blank=True, - on_delete=models.SET_NULL) - - garant = models.ForeignKey(Organizator, verbose_name='garant zadaného problému', - related_name='garant_problemu_%(class)s', null=True, blank=True, - on_delete=models.SET_NULL) - - opravovatele = models.ManyToManyField(Organizator, verbose_name='opravovatelé', - blank=True, related_name='opravovatele_%(class)s') - - kod = models.CharField('lokální kód', max_length=32, blank=True, default='', - help_text='Číslo/kód úlohy v čísle nebo kód tématu/článku/seriálu v ročníku') - - - vytvoreno = models.DateTimeField('vytvořeno', default=timezone.now, blank=True, editable=False) - - - def __str__(self): - return self.nazev - - # Implicitini implementace, jednotlivé dědící třídy si přepíšou - @cached_property - def kod_v_rocniku(self): - if self.stav == 'zadany': - if self.nadproblem: - return self.nadproblem.kod_v_rocniku+".{}".format(self.kod) - return str(self.kod) - return '' - -# 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 '' - - 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 = [] - 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 '' - - def node(self): - return None - -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 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 '' - - 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 - return treelib.get_upper_node_of_type(zadani_node, CisloNode) - -@reversion.register(ignore_duplicates=True) -class Reseni(SeminarModelBase): - - class Meta: - db_table = 'seminar_reseni' - verbose_name = 'Řešení' - verbose_name_plural = 'Řešení' - #ordering = ['-problem', 'resitele'] # FIXME: Takhle to chceme, ale nefunguje to. - ordering = ['-cas_doruceni'] - - # Interní ID - id = models.AutoField(primary_key = True) - - # Ke každé dvojici řešní a problém existuje nanejvýš jedno hodnocení, doplnění vazby. - problem = models.ManyToManyField(Problem, verbose_name='problém', help_text='Problém', - through='Hodnoceni') - - resitele = models.ManyToManyField(Resitel, verbose_name='autoři řešení', - help_text='Seznam autorů řešení', through='Reseni_Resitele') - - - cas_doruceni = models.DateTimeField('čas_doručení', default=timezone.now, blank=True) - - FORMA_PAPIR = 'papir' - FORMA_EMAIL = 'email' - FORMA_UPLOAD = 'upload' - FORMA_CHOICES = [ - (FORMA_PAPIR, 'Papírové řešení'), - (FORMA_EMAIL, 'Emailem'), - (FORMA_UPLOAD, 'Upload přes web'), - ] - forma = models.CharField('forma řešení', max_length=16, choices=FORMA_CHOICES, blank=False, - default=FORMA_EMAIL) - - text_cely = models.OneToOneField('ReseniNode', verbose_name='Plná verze textu řešení', - blank=True, null=True, related_name="reseni_cely_set", - on_delete=models.PROTECT) - - poznamka = models.TextField('neveřejná poznámka', blank=True, - help_text='Neveřejná poznámka k řešení (plain text)') - - zverejneno = models.BooleanField('řešení zveřejněno', default=False, - help_text='Udává, zda je řešení zveřejněno') - - def verejne_url(self): - return str(reverse_lazy('odevzdavatko_detail_reseni', args=[self.id])) - - def absolute_url(self): - return "https://" + str(get_current_site(None)) + self.verejne_url() - - # má OneToOneField s: - # Konfera - - # má ForeignKey s: - # Hodnoceni - # ReseniNode - - def sum_body(self): - return self.hodnoceni_set.all().aggregate(Sum('body'))["body__sum"] - - def __str__(self): - return "{}({}): {}({})".format(self.resitele.first(),len(self.resitele.all()), self.problem.first() ,len(self.problem.all())) - # NOTE: Potenciální DB HOG (bez select_related) - -## Pravdepodobne uz nebude potreba: -# def save(self, *args, **kwargs): -# if ((self.cislo_body is None) and (self.problem.cislo_reseni) and -# (self.problem.typ == Problem.TYP_ULOHA)): -# self.cislo_body = self.problem.cislo_reseni -# super(Reseni, self).save(*args, **kwargs) - -class Hodnoceni(SeminarModelBase): - class Meta: - db_table = 'seminar_hodnoceni' - verbose_name = 'Hodnocení' - verbose_name_plural = 'Hodnocení' - - # Interní ID - id = models.AutoField(primary_key = True) - - - body = models.DecimalField(max_digits=8, decimal_places=1, verbose_name='body', - blank=True, null=True) - - cislo_body = models.ForeignKey(Cislo, verbose_name='číslo pro body', - related_name='hodnoceni', blank=True, null=True, on_delete=models.PROTECT) - - reseni = models.ForeignKey(Reseni, verbose_name='řešení', on_delete=models.CASCADE) - - problem = models.ForeignKey(Problem, verbose_name='problém', - related_name='hodnoceni', on_delete=models.PROTECT) - - def __str__(self): - return "{}, {}, {}".format(self.problem, self.reseni, self.body) - - - -def aux_generate_filename(self, filename): - """Pomocná funkce generující ošetřený název souboru v adresáři s datem""" - 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) - -# Django neumí jednoduše serializovat partial nebo třídu s __call__ -# (https://docs.djangoproject.com/en/1.8/topics/migrations/), -# neprojdou pak migrace. Takže rozlišení funkcí generujících názvy souboru -# podle adresáře řešíme takto. - -## -def generate_filename_konfera(self, filename): - return os.path.join( - settings.SEMINAR_KONFERY_DIR, - aux_generate_filename(self, filename) - ) - -## -def generate_filename(self, filename): - return os.path.join( - settings.SEMINAR_RESENI_DIR, - aux_generate_filename(self, filename) - ) - - -@reversion.register(ignore_duplicates=True) -class PrilohaReseni(SeminarModelBase): - - class Meta: - db_table = 'seminar_priloha_reseni' - verbose_name = 'Příloha řešení' - verbose_name_plural = 'Přílohy řešení' - ordering = ['reseni', 'vytvoreno'] - - # Interní ID - id = models.AutoField(primary_key = True) - - reseni = models.ForeignKey(Reseni, verbose_name='řešení', related_name='prilohy', - on_delete=models.CASCADE) - - vytvoreno = models.DateTimeField('vytvořeno', default=timezone.now, blank=True, editable=False) - - soubor = models.FileField('soubor', upload_to = generate_filename) - - poznamka = models.TextField('neveřejná poznámka', blank=True, - help_text='Neveřejná poznámka k příloze řešení (plain text), např. o původu') - - res_poznamka = models.TextField('poznámka řešitele', blank=True, - help_text='Poznámka k příloze řešení, např. co daný soubor obsahuje') - - def __str__(self): - return str(self.soubor) - - def split(self): - "Vrátí cestu rozsekanou po složkách. To se hodí v templatech" - # Věřím, že tohle funguje, případně použít os.path nebo pathlib. - return self.soubor.url.split('/') - - -class Pohadka(SeminarModelBase): - """Kus pohádky před/za úlohou v čísle""" - - class Meta: - db_table = 'seminar_pohadky' - verbose_name = 'Pohádka' - verbose_name_plural = 'Pohádky' - ordering = ['vytvoreno'] - - # Interní ID - id = models.AutoField(primary_key=True) - - autor = models.ForeignKey( - Organizator, - verbose_name="Autor pohádky", - - # Při nahrávání z TeXu není vyplnění vyžadováno, v adminu je - null=True, - blank=False, - on_delete=models.SET_NULL - ) - - vytvoreno = models.DateTimeField( - 'Vytvořeno', - default=timezone.now, - blank=True, - editable=False - ) - - # má OneToOneField s: - # PohadkaNode - - def __str__(self): - 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 Soustredeni_Ucastnici(SeminarModelBase): -# zmena dedicnosti z models.Model na SeminarModelBase, potencialni vznik bugu - - class Meta: - db_table = 'seminar_soustredeni_ucastnici' - verbose_name = 'Účast na soustředění' - verbose_name_plural = 'Účasti na soustředění' - ordering = ['soustredeni', 'resitel'] - - # Interní ID - id = models.AutoField(primary_key = True) - - resitel = models.ForeignKey(Resitel, verbose_name='řešitel', on_delete=models.PROTECT) - - soustredeni = models.ForeignKey(Soustredeni, verbose_name='soustředění', - on_delete=models.PROTECT) - - poznamka = models.TextField('neveřejná poznámka', blank=True, - help_text='Neveřejná poznámka k účasti (plain text)') - - def __str__(self): - return '{} na {}'.format(self.resitel, self.soustredeni) - # NOTE: Poteciální DB HOG bez select_related - -@reversion.register(ignore_duplicates=True) -class Soustredeni_Organizatori(SeminarModelBase): -# zmena dedicnosti z models.Model na SeminarModelBase, potencialni vznik bugu - - class Meta: - db_table = 'seminar_soustredeni_organizatori' - verbose_name = 'Účast organizátorů na soustředění' - verbose_name_plural = 'Účasti organizátorů na soustředění' - ordering = ['soustredeni', 'organizator'] - - # Interní ID - id = models.AutoField(primary_key = True) - - organizator = models.ForeignKey(Organizator, verbose_name='organizátor', - on_delete=models.PROTECT) - - soustredeni = models.ForeignKey(Soustredeni, verbose_name='soustředění', - on_delete=models.PROTECT) - - poznamka = models.TextField('neveřejná poznámka', blank=True, - help_text='Neveřejná poznámka k účasti organizátora (plain text)') - - def __str__(self): - return '{} na {}'.format(self.organizator, self.soustredeni) - # NOTE: Poteciální DB HOG bez select_related - - - -@reversion.register(ignore_duplicates=True) -class Konfera(Problem): - class Meta: - db_table = 'seminar_konfera' - verbose_name = 'Konfera' - verbose_name_plural = 'Konfery' - - anotace = models.TextField('anotace', blank=True, - help_text='Popis, o čem bude konfera.') - - abstrakt = models.TextField('abstrakt', blank=True, - help_text='Abstrakt konfery tak, jak byl uveden ve sborníku') - - # FIXME: Umíme omezit jen na účastníky daného soustřeďka? - ucastnici = models.ManyToManyField(Resitel, verbose_name='účastníci konfery', - help_text='Seznam účastníků konfery', through='Konfery_Ucastnici') - - soustredeni = models.ForeignKey(Soustredeni, verbose_name='soustředění', - related_name='konfery', on_delete = models.SET_NULL, null=True) - - TYP_VELETRH = 'veletrh' - TYP_PREZENTACE = 'prezentace' - TYP_CHOICES = [ - (TYP_VELETRH, 'Veletrh (postery)'), - (TYP_PREZENTACE, 'Prezentace (přednáška)'), - ] - typ_prezentace = models.CharField('typ prezentace', max_length=16, choices=TYP_CHOICES, - blank=False, default=TYP_VELETRH) - - prezentace = models.FileField('prezentace',help_text = 'Prezentace nebo fotka posteru', - upload_to = generate_filename_konfera, blank=True) - - materialy = models.FileField('materialy', - help_text = 'Další materiály ke konfeře zabalené do jednoho souboru', - upload_to = generate_filename_konfera, blank=True) - - def __str__(self): - return "{}: ({})".format(self.nazev, self.soustredeni) - - def cislo_node(self): - return None - -# Vazebna tabulka. Mozna se generuje automaticky. -@reversion.register(ignore_duplicates=True) -class Reseni_Resitele(models.Model): - - class Meta: - db_table = 'seminar_reseni_resitele' - verbose_name = 'Řešení řešitelů' - verbose_name_plural = 'Řešení řešitelů' - ordering = ['reseni', 'resitele'] - - # Interní ID - id = models.AutoField(primary_key = True) - - resitele = models.ForeignKey(Resitel, verbose_name='řešitel', on_delete=models.PROTECT) - - reseni = models.ForeignKey(Reseni, verbose_name='řešení', on_delete=models.CASCADE) - - # podil - jakou merou se ktery resitel podilel na danem reseni - # - pouziti v budoucnu, pokud by resitele nemeli dostat vsichni stejne bodu za spolecne reseni - - def __str__(self): - return '{} od {}'.format(self.reseni, self.resitel) - # NOTE: Poteciální DB HOG bez select_related - -@reversion.register(ignore_duplicates=True) -class Konfery_Ucastnici(models.Model): - - class Meta: - db_table = 'seminar_konfery_ucastnici' - verbose_name = 'Účast na konfeře' - verbose_name_plural = 'Účasti na konfeře' - ordering = ['konfera', 'resitel'] - - # Interní ID - id = models.AutoField(primary_key = True) - - resitel = models.ForeignKey(Resitel, verbose_name='řešitel', on_delete=models.PROTECT) - - konfera = models.ForeignKey(Konfera, verbose_name='konfera', on_delete=models.CASCADE) - - poznamka = models.TextField('neveřejná poznámka', blank=True, - help_text='Neveřejná poznámka k účasti (plain text)') - - def __str__(self): - return '{} na {}'.format(self.resitel, self.konfera) - # NOTE: Poteciální DB HOG bez select_related - -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 - -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(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(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 seminar.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(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(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(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(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(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) - -class ReseniNode(TreeNode): - class Meta: - db_table = 'seminar_nodes_otistene_reseni' - verbose_name = 'Otištěné řešení (Node)' - verbose_name_plural = 'Otištěná řešení (Node)' - reseni = models.ForeignKey(Reseni, - on_delete=models.PROTECT, - verbose_name = 'reseni') - - def aktualizuj_nazev(self): - self.nazev = "ReseniNode: "+str(self.reseni) - - def getOdkazStr(self): - return str(self.reseni) - - -@reversion.register(ignore_duplicates=True) -class Nastaveni(SingletonModel): - - 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 - - -@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(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) + '] ' diff --git a/seminar/models/__init__.py b/seminar/models/__init__.py new file mode 100644 index 00000000..34712ee4 --- /dev/null +++ b/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 * diff --git a/seminar/models/base.py b/seminar/models/base.py new file mode 100644 index 00000000..77c857a3 --- /dev/null +++ b/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 + diff --git a/seminar/models/novinky.py b/seminar/models/novinky.py new file mode 100644 index 00000000..f6ce4161 --- /dev/null +++ b/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) + '] ' diff --git a/seminar/models/odevzdavatko.py b/seminar/models/odevzdavatko.py new file mode 100644 index 00000000..343f92f0 --- /dev/null +++ b/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) + diff --git a/seminar/models/personalni.py b/seminar/models/personalni.py new file mode 100644 index 00000000..fc1ac53b --- /dev/null +++ b/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 ZŠ.""" + if self.rok_maturity is None: + return '' + rozdil = 5 - (self.rok_maturity - rocnik.prvni_rok) + if rozdil >= 1: + return str(rozdil) + else: + return 'Z' + str(rozdil + 9) + + def vsechny_body(self): + "Spočítá body odjakživa." + vsechna_reseni = self.reseni_set.all() + from .odevzdavatko import Hodnoceni + vsechna_hodnoceni = Hodnoceni.objects.filter( + reseni__in=vsechna_reseni) + return sum(h.body for h in list(vsechna_hodnoceni) if h.body is not None) + + + def get_titul(self, body=None): + "Vrati titul jako řetězec." + + # Nejprve si zadefinujeme titul + from enum import Enum + from functools import total_ordering + @total_ordering + class Titul(Enum): + """ Třída reprezentující možné tituly. Hodnoty jsou dvojice (dolní hranice, stringifikace). """ + nic = (0, '') + bc = (20, 'Bc.') + mgr = (50, 'Mgr.') + dr = (100, 'Dr.') + doc = (200, 'Doc.') + prof = (500, 'Prof.') + akad = (1000, 'Akad.') + + def __lt__(self, other): + return True if self.value[0] < other.value[0] else False + def __eq__(self, other): # Měla by být implicitní, ale klidně explicitně. + return True if self.value[0] == other.value[0] else False + + def __str__(self): + return self.value[1] + + @classmethod + def z_bodu(cls, body): + aktualni = cls.nic + # TODO: ověřit, že to funguje + for titul in cls: # Kdyžtak použít __members__.items() + if titul.value[0] <= body: + aktualni = titul + else: + break + return aktualni + + # Hledáme body v databázi + # V listopadu 2020 jsme se na filosofické schůzce shodli o změně hranic titulů: + # - body z 25. ročníku a dříve byly shledány dvakrát hodnotnějšími + # - proto se započítávají dvojnásobně a byly posunuté hranice titulů + # - staré tituly se ale nemají odebrat, pokud řešitel v t.č. minulém (26.) ročníku měl titul, má ho mít pořád. + from .odevzdavatko import Hodnoceni + hodnoceni_do_25_rocniku = Hodnoceni.objects.filter(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'] diff --git a/seminar/models/pomocne.py b/seminar/models/pomocne.py new file mode 100644 index 00000000..eab52e70 --- /dev/null +++ b/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 diff --git a/seminar/models/soustredeni.py b/seminar/models/soustredeni.py new file mode 100644 index 00000000..1d984948 --- /dev/null +++ b/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 diff --git a/seminar/models/treenode.py b/seminar/models/treenode.py new file mode 100644 index 00000000..50261d1a --- /dev/null +++ b/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) diff --git a/seminar/models/tvorba.py b/seminar/models/tvorba.py new file mode 100644 index 00000000..d123e021 --- /dev/null +++ b/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 '' + +# 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 '' + + 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 '' + + 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 '' + + 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 diff --git a/seminar/templates/seminar/archiv/cislo.html b/seminar/templates/seminar/archiv/cislo.html index 1f18d4b5..13a505ac 100644 --- a/seminar/templates/seminar/archiv/cislo.html +++ b/seminar/templates/seminar/archiv/cislo.html @@ -79,73 +79,7 @@ {% endif %} {% if cislo.verejna_vysledkovka or user.je_org %} - - - - - {% endfor %} -
# - Jméno - {% for p in problemy %} - {# #}{{ p.kod_v_rocniku }}{# #} - - {# TODELETE #} - {% for podproblemy in podproblemy_iter.next %} - {# #}{{ podproblemy.kod_v_rocniku }}{# #} - {% endfor %} - {# TODELETE #} - - {% endfor %} - {% if ostatni %}Ostatní {% endif %} - - {# TODELETE #} - {% for podproblemy in podproblemy_iter.next %} - {# #}{{ podproblemy.kod_v_rocniku }}{# #} - {% endfor %} - {# TODELETE #} - - - Za číslo - Za ročník - Odjakživa - {% for rv in radky_vysledkovky %} -
{% autoescape off %}{{ rv.poradi }}{% endautoescape %} - - {% if rv.titul %} - {{ rv.titul }}MM - {% endif %} - {{ rv.resitel.osoba.plne_jmeno }} - {% for b in rv.body_problemy_sezn %} - {{ b }} - - {# TODELETE #} - {% for body_podproblemu in rv.body_podproblemy_iter.next %} - {{ body_podproblemu }} - {% endfor %} - {# TODELETE #} - - {% endfor %} - {{ rv.body_cislo }} - {{ rv.body_rocnik }} - {{ rv.body_celkem_odjakziva }} -
- - {# TODELETE #} - - {# TODELETE #} - + {% include "vysledkovky/vysledkovka_cisla.html" %} {% endif %} {% if not cislo.verejna_vysledkovka and user.je_org %} diff --git a/seminar/templates/seminar/archiv/rocnik.html b/seminar/templates/seminar/archiv/rocnik.html index 01a9f5ac..d5c67e9c 100644 --- a/seminar/templates/seminar/archiv/rocnik.html +++ b/seminar/templates/seminar/archiv/rocnik.html @@ -114,18 +114,14 @@ {% if vysledkovka %}

Výsledková listina

- {% include "seminar/vysledkovka_rocnik.html" %} + {% include "vysledkovky/vysledkovka_rocnik.html" %} {% endif %} {% if user.je_org %}
Výsledkovka ročníku (LaTeX, včetně neveřejných)

Výsledková listina včetně neveřejných bodů

- {% with radky_vysledkovky_s_neverejnymi as radky_vysledkovky %} - {% with cisla_s_neverejnymi as cisla %} - {% include "seminar/vysledkovka_rocnik.html" %} - {% endwith %} - {% endwith %} + {% include "vysledkovky/vysledkovka_rocnik_neverejna.html" %}
{% endif %} diff --git a/seminar/templates/seminar/profil/edit.html b/seminar/templates/seminar/profil/edit.html deleted file mode 100644 index 9f94090e..00000000 --- a/seminar/templates/seminar/profil/edit.html +++ /dev/null @@ -1,105 +0,0 @@ -{% extends "base.html" %} -{% load staticfiles %} - -{% block script %} - -{% endblock %} - - - -{% block content %} -

- {% block nadpis1a %}{% block nadpis1b %} - Změna osobních údajů - {% endblock %}{% endblock %} -

-
- {% csrf_token %} - {{form.non_field_errors}} - -
- -

- Přihlašovací údaje -

- - {% include "seminar/profil/prihlaska_field.html" with field=form.username %} -
- -
- -

- Osobní údaje -

- - {% 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 %} -
- -
- -

- Bydliště -

- - {% 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"%} -
- -
- -

- Škola -

- - {% include "seminar/profil/prihlaska_field.html" with field=form.skola %} - - - {% 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 %} -
Vyplň prosím celý název a adresu školy.
- -
- -

- Pošta -

- - {% include "seminar/profil/prihlaska_field.html" with field=form.zasilat %} - {% include "seminar/profil/prihlaska_field.html" with field=form.zasilat_cislo_emailem %} -
- -
- -

- Zasílání propagačních materiálů -

- - {% include "seminar/profil/prihlaska_field.html" with field=form.spam %} -
- -
- - -
- -{% endblock %} diff --git a/seminar/templates/seminar/profil/prihlaska.html b/seminar/templates/seminar/profil/prihlaska.html deleted file mode 100644 index 423e93d9..00000000 --- a/seminar/templates/seminar/profil/prihlaska.html +++ /dev/null @@ -1,123 +0,0 @@ -{% extends "base.html" %} -{% load staticfiles %} - -{% block script %} - -{% endblock %} - - - -{% block content %} -

- {% block nadpis1a %}{% block nadpis1b %} - Přihláška do semináře - {% endblock %}{% endblock %} -

- -

Tučně popsaná pole jsou povinná.

- -
- {% csrf_token %} - {{form.non_field_errors}} - - -
-

- Přihlašovací údaje -

- - {% 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 %}#} -
- -
- -

- Osobní údaje -

- - {% 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 %} -
- -
- -

- Bydliště -

- - {% 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"%} -
- -
- -

- Škola -

- - {% include "seminar/profil/prihlaska_field.html" with field=form.skola %} - - - {% 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 %} -
Vyplň prosím celý název a adresu školy.
- -
- -

- Pošta -

- - {% include "seminar/profil/prihlaska_field.html" with field=form.zasilat %} - {% include "seminar/profil/prihlaska_field.html" with field=form.zasilat_cislo_emailem %} -
-
- -

- GDPR -

- {% include "seminar/profil/gdpr.html" %} - - {% include "seminar/profil/prihlaska_field.html" with field=form.gdpr %} -
- -
- -

- Zasílání propagačních materiálů -

- - {% include "seminar/profil/prihlaska_field.html" with field=form.spam %} -
- - - -
- - -
- - - -{% endblock %} diff --git a/seminar/templates/seminar/zadani/AktualniVysledkovka.html b/seminar/templates/seminar/zadani/AktualniVysledkovka.html index 15c856d9..4ea2bffe 100644 --- a/seminar/templates/seminar/zadani/AktualniVysledkovka.html +++ b/seminar/templates/seminar/zadani/AktualniVysledkovka.html @@ -9,7 +9,7 @@ {% if radky_vysledkovky %} - {% include "seminar/vysledkovka_rocnik.html" %} + {% include "vysledkovky/vysledkovka_rocnik.html" %} {% else %}

V tomto ročníku zatím žádné výsledky nejsou.

{% endif %} @@ -22,11 +22,7 @@ {% if user.je_org and vysledkovka_s_neverejnymi %}

Výsledky včetně neveřejných

- {% with vysledkovka_s_neverejnymi as radky_vysledkovky %} - {% with cisla_s_neverejnymi as cisla %} - {% include "seminar/vysledkovka_rocnik.html" %} - {% endwith %} - {% endwith %} + {% include "vysledkovky/vysledkovka_rocnik_neverejna.html" %}
{% endif %} diff --git a/seminar/templatetags/mam_menu.py b/seminar/templatetags/mam_menu.py deleted file mode 100644 index 9ae7ac68..00000000 --- a/seminar/templatetags/mam_menu.py +++ /dev/null @@ -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() diff --git a/seminar/testutils.py b/seminar/testutils.py index 019f66ac..a66dea4b 100644 --- a/seminar/testutils.py +++ b/seminar/testutils.py @@ -17,7 +17,7 @@ import seminar.models as m from django.contrib.flatpages.models import FlatPage 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() @@ -752,8 +752,8 @@ def gen_clanek(rnd, organizatori, resitele): # Bude to celý text reseni.text_cely = reseninode 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) # Vyrobíme nějaký obsah diff --git a/seminar/urls.py b/seminar/urls.py index 5b71d2b3..182bbe7e 100644 --- a/seminar/urls.py +++ b/seminar/urls.py @@ -1,7 +1,6 @@ from django.urls import path, include, re_path -from django.contrib.auth.decorators import login_required from . import views -from .utils import org_required, resitel_required, viewMethodSwitch, resitel_or_org_required +from .utils import org_required urlpatterns = [ # path('aktualni/temata/', views.TemataRozcestnikView), @@ -18,42 +17,6 @@ urlpatterns = [ path('rocnik//', views.RocnikView.as_view(), name='seminar_rocnik'), path('cislo/./', views.CisloView.as_view(), name='seminar_cislo'), path('problem//', views.problemView, name='seminar_problem'), - #path('treenode//', views.TreeNodeView.as_view(), name='seminar_treenode'), - #path('treenode//json/', views.TreeNodeJSONView.as_view(), name='seminar_treenode_json'), - #path('treenode/text//', views.TextWebView.as_view(), name='seminar_textnode_web'), - #path('treenode/editor/pridat////', views.TreeNodePridatView.as_view(), name='treenode_pridat'), - #path('treenode/editor/smazat//', views.TreeNodeSmazatView.as_view(), name='treenode_smazat'), - #path('treenode/editor/odvesitpryc//', views.TreeNodeOdvesitPrycView.as_view(), name='treenode_odvesitpryc'), - #path('treenode/editor/podvesit///', views.TreeNodePodvesitView.as_view(), name='treenode_podvesit'), - #path('treenode/editor/prohodit//', views.TreeNodeProhoditView.as_view(), name='treenode_prohodit'), - #path('treenode/sirotcinec/', views.SirotcinecView.as_view(), name='seminar_treenode_sirotcinec'), - #path('problem/(?P\d+)/(?P\d+)/', views.PrispevekView.as_view(), name='seminar_problem_prispevek'), - - # Soustredeni - path( - 'soustredeni/probehlo/', - views.SoustredeniListView.as_view(), - name='seminar_seznam_soustredeni' - ), - path( - 'soustredeni//seznam_ucastniku', - org_required(views.SoustredeniUcastniciView.as_view()), - name='soustredeni_ucastnici' - ), - path( - 'soustredeni//maily_ucastniku', - org_required(views.SoustredeniMailyUcastnikuView.as_view()), - name='maily_ucastniku' - ), - path( - 'soustredeni//export_ucastniku', - org_required(views.soustredeniUcastniciExportView), - name='soustredeni_ucastnici_export' - ), - path( - 'soustredeni//fotogalerie/', - include('galerie.urls') - ), # Zadani # path('aktualni/zadani/', views.AktualniZadaniView.as_view(), name='seminar_aktualni_zadani'), # Dočasně ad-hoc jednoduchá věc. @@ -102,46 +65,7 @@ urlpatterns = [ 'cislo/./odmeny/./', org_required(views.OdmenyView.as_view()), name="seminar_archiv_odmeny"), - path( - '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('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//', org_required(views.TabulkaOdevzdanychReseniView.as_view()), name='odevzdavatko_tabulka'), - path('org/reseni///', org_required(views.ReseniProblemuView.as_view()), name='odevzdavatko_reseni_resitele_k_problemu'), - path('org/reseni/', 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/', resitel_or_org_required(views.ResitelReseniView.as_view()), name='odevzdavatko_resitel_reseni'), ] diff --git a/seminar/utils.py b/seminar/utils.py index 6d0a0b72..bb4cdfe7 100644 --- a/seminar/utils.py +++ b/seminar/utils.py @@ -20,7 +20,7 @@ from enum import auto import logging import seminar.models as m -import seminar.treelib as t +import treenode.treelib as t logger = logging.getLogger(__name__) diff --git a/seminar/views/__init__.py b/seminar/views/__init__.py index 785102e5..8db4424b 100644 --- a/seminar/views/__init__.py +++ b/seminar/views/__init__.py @@ -1,6 +1,4 @@ from .views_all import * -from .views_rest import * -from .odevzdavatko import * # Dočsasné views from .docasne import * diff --git a/seminar/views/views_all.py b/seminar/views/views_all.py index 85a7f9f2..1b012bd3 100644 --- a/seminar/views/views_all.py +++ b/seminar/views/views_all.py @@ -1,35 +1,26 @@ -from django.shortcuts import get_object_or_404, render, redirect -from django.http import HttpResponse, HttpResponseRedirect, HttpResponseForbidden, JsonResponse -from django.urls import reverse,reverse_lazy -from django.core.exceptions import PermissionDenied, ObjectDoesNotExist -from django.core.mail import send_mail +from django.shortcuts import get_object_or_404, render +from django.http import HttpResponse +from django.urls import reverse +from django.core.exceptions import ObjectDoesNotExist from django.views import generic from django.utils.translation import ugettext as _ -from django.http import Http404,HttpResponseBadRequest,HttpResponseRedirect +from django.http import Http404 from django.db.models import Q, Sum, Count -from django.views.decorators.csrf import ensure_csrf_cookie -from django.views.decorators.debug import sensitive_post_parameters -from django.views.generic.edit import FormView, CreateView -from django.views.generic.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.views.generic.base import RedirectView from django.core.exceptions import PermissionDenied -from django.forms.models import model_to_dict import seminar.models as s import seminar.models as m -from seminar.models import Problem, Cislo, Reseni, Nastaveni, Rocnik, Soustredeni, Organizator, Resitel, Novinky, Soustredeni_Ucastnici, Pohadka, Tema, Clanek, Osoba, Skola # Tohle je stare a chceme se toho zbavit. Pouzivejte s.ToCoChci +from seminar.models import Problem, Cislo, Reseni, Nastaveni, Rocnik, Organizator, Resitel, Novinky, Tema, Clanek # Tohle je stare a chceme se toho zbavit. Pouzivejte s.ToCoChci #from .models import VysledkyZaCislo, VysledkyKCisluZaRocnik, VysledkyKCisluOdjakziva -from seminar import utils, treelib -from seminar.forms import PrihlaskaForm, ProfileEditForm, PoMaturiteProfileEditForm -import seminar.forms as f -import seminar.templatetags.treenodes as tnltt -import seminar.views.views_rest as vr -from seminar.views.vysledkovka import vysledkovka_rocniku, vysledkovka_cisla, body_resitelu - -from datetime import timedelta, date, datetime, MAXYEAR +from seminar import utils +from treenode import treelib +import treenode.templatetags as tnltt +import treenode.serializers as vr +from vysledkovky.utils import body_resitelu +from vysledkovky.views import vysledkovka_rocniku, vysledkovka_cisla + +from datetime import date, datetime from django.utils import timezone from itertools import groupby from collections import OrderedDict @@ -40,16 +31,10 @@ import os import os.path as op from django.conf import settings import unicodedata -import json -import traceback -import sys -import csv import logging import time -from seminar.utils import aktivniResitele, resi_v_rocniku, problemy_rocniku, cisla_rocniku, hlavni_problemy_f -from various.autentizace.views import LoginView -from various.autentizace.utils import posli_reset_hesla +from seminar.utils import aktivniResitele # ze starého modelu #def verejna_temata(rocnik): @@ -83,279 +68,6 @@ class ObalkovaniView(generic.ListView): context['cislo'] = self.cislo 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! #class ProblemView(generic.DetailView): @@ -494,31 +206,34 @@ def ZadaniAktualniVysledkovkaView(request): nastaveni = get_object_or_404(Nastaveni) # Aktualni verejna vysledkovka rocnik = nastaveni.aktualni_rocnik - vysledkovka = vysledkovka_rocniku(rocnik) - cisla = cisla_rocniku(rocnik) + context = vysledkovka_rocniku( + rocnik=rocnik, + request=request, + sneverejnou=True + ) + # 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: minuly_rocnik = Rocnik.objects.get( prvni_rok=(rocnik.prvni_rok-1)) 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: pass - # vysledkovka s neverejnyma vysledkama - vysledkovka_s_neverejnymi = vysledkovka_rocniku(nastaveni.aktualni_rocnik, jen_verejne=False) - cisla_s_neverejnymi = cisla_rocniku(nastaveni.aktualni_rocnik, jen_verejne=False) + + context['rocnik'] = rocnik return render( request, 'seminar/zadani/AktualniVysledkovka.html', - { - 'rocnik': rocnik, - 'radky_vysledkovky': vysledkovka, - 'cisla': cisla, - 'vysledkovka_s_neverejnymi': vysledkovka_s_neverejnymi, - 'cisla_s_neverejnymi': cisla_s_neverejnymi, - } + context ) @@ -664,18 +379,12 @@ class RocnikView(generic.DetailView): def get_context_data(self, **kwargs): start = time.time() context = super(RocnikView, self).get_context_data(**kwargs) - - # vysledkovka = True zajistí vykreslení, - # zkontrolovat, kdy se má a nemá vykreslovat - cisla = cisla_rocniku(context["rocnik"]) - context['vysledkovka'] = any(map(lambda it: it.verejna_vysledkovka, cisla)) - 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"])) + context = vysledkovka_rocniku( + rocnik=context["rocnik"], + context=context, + request=self.request, + sneverejnou=True + ) end = time.time() print("Kontext:", end-start) @@ -836,51 +545,6 @@ def oldObalkovaniView(request, rocnik, cislo): {'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 @@ -911,53 +575,6 @@ def TitulyView(request, rocnik, cislo): return render(request, 'seminar/archiv/tituly.tex', {'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 def group_by_rocnik(clanky): @@ -994,7 +611,7 @@ class ClankyResitelView(generic.ListView): queryset = [] skupiny_clanku = group_by_rocnik(clanky) 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: queryset.append(clanek) 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. Vrátit se zpět na profil.') - - 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í) def formularOKView(request, text=''): template_name = 'seminar/formular_ok.html' diff --git a/soustredeni/__init__.py b/soustredeni/__init__.py new file mode 100644 index 00000000..a9f1f263 --- /dev/null +++ b/soustredeni/__init__.py @@ -0,0 +1,5 @@ +""" +Obsahuje vše (až na přednášky) ohledně soustředění. + +TODO stvrzenky? +""" \ No newline at end of file diff --git a/soustredeni/admin.py b/soustredeni/admin.py new file mode 100644 index 00000000..11cb8d1d --- /dev/null +++ b/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] + diff --git a/soustredeni/apps.py b/soustredeni/apps.py new file mode 100644 index 00000000..5d282341 --- /dev/null +++ b/soustredeni/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class SoustredeniConfig(AppConfig): + name = 'soustredeni' diff --git a/soustredeni/migrations/__init__.py b/soustredeni/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/seminar/templates/seminar/soustredeni/maily_ucastniku.txt b/soustredeni/templates/soustredeni/maily_ucastniku.txt similarity index 100% rename from seminar/templates/seminar/soustredeni/maily_ucastniku.txt rename to soustredeni/templates/soustredeni/maily_ucastniku.txt diff --git a/seminar/templates/seminar/soustredeni/seznam_soustredeni.html b/soustredeni/templates/soustredeni/seznam_soustredeni.html similarity index 100% rename from seminar/templates/seminar/soustredeni/seznam_soustredeni.html rename to soustredeni/templates/soustredeni/seznam_soustredeni.html diff --git a/seminar/templates/seminar/soustredeni/seznam_ucastniku.html b/soustredeni/templates/soustredeni/seznam_ucastniku.html similarity index 100% rename from seminar/templates/seminar/soustredeni/seznam_ucastniku.html rename to soustredeni/templates/soustredeni/seznam_ucastniku.html diff --git a/seminar/templates/seminar/soustredeni/ucastnici.tex b/soustredeni/templates/soustredeni/ucastnici.tex similarity index 100% rename from seminar/templates/seminar/soustredeni/ucastnici.tex rename to soustredeni/templates/soustredeni/ucastnici.tex diff --git a/soustredeni/urls.py b/soustredeni/urls.py new file mode 100644 index 00000000..9cbd2e1d --- /dev/null +++ b/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//seznam_ucastniku', + org_required(views.SoustredeniUcastniciView.as_view()), + name='soustredeni_ucastnici' + ), + path( + 'soustredeni//maily_ucastniku', + org_required(views.SoustredeniMailyUcastnikuView.as_view()), + name='maily_ucastniku' + ), + path( + 'soustredeni//export_ucastniku', + org_required(views.soustredeniUcastniciExportView), + name='soustredeni_ucastnici_export' + ), + path( + 'soustredeni//obalky.pdf', + org_required(views.soustredeniObalkyView), + name='seminar_soustredeni_obalky' + ), + path( + 'soustredeni//fotogalerie/', + include('galerie.urls') + ), +] diff --git a/soustredeni/views.py b/soustredeni/views.py new file mode 100644 index 00000000..99ec0b03 --- /dev/null +++ b/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 diff --git a/treenode/__init__.py b/treenode/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/treenode/admin.py b/treenode/admin.py new file mode 100644 index 00000000..d2ff4409 --- /dev/null +++ b/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 + + diff --git a/treenode/apps.py b/treenode/apps.py new file mode 100644 index 00000000..95139dc6 --- /dev/null +++ b/treenode/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class TreenodeConfig(AppConfig): + name = 'treenode' diff --git a/treenode/forms.py b/treenode/forms.py new file mode 100644 index 00000000..704084f7 --- /dev/null +++ b/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',) diff --git a/treenode/migrations/__init__.py b/treenode/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/seminar/permissions.py b/treenode/permissions.py similarity index 100% rename from seminar/permissions.py rename to treenode/permissions.py diff --git a/mamweb/routers.py b/treenode/routers.py similarity index 94% rename from mamweb/routers.py rename to treenode/routers.py index 003dd5a6..9a7aba58 100644 --- a/mamweb/routers.py +++ b/treenode/routers.py @@ -1,5 +1,5 @@ from rest_framework import routers -from seminar import viewsets as vs +from treenode import viewsets as vs router = routers.DefaultRouter() diff --git a/seminar/views/views_rest.py b/treenode/serializers.py similarity index 99% rename from seminar/views/views_rest.py rename to treenode/serializers.py index ef49b3cc..eedb03b1 100644 --- a/seminar/views/views_rest.py +++ b/treenode/serializers.py @@ -2,7 +2,7 @@ from rest_framework import serializers from rest_polymorphic.serializers import PolymorphicSerializer import seminar.models as m -from seminar import treelib +from treenode import treelib DEFAULT_NODE_DEPTH = 2 diff --git a/seminar/static/seminar/treenode_editor.js b/treenode/static/treenode/treenode_editor.js similarity index 100% rename from seminar/static/seminar/treenode_editor.js rename to treenode/static/treenode/treenode_editor.js diff --git a/seminar/templates/seminar/orphanage.html b/treenode/templates/treenode/orphanage.html similarity index 100% rename from seminar/templates/seminar/orphanage.html rename to treenode/templates/treenode/orphanage.html diff --git a/seminar/templates/seminar/treenode.html b/treenode/templates/treenode/treenode.html similarity index 100% rename from seminar/templates/seminar/treenode.html rename to treenode/templates/treenode/treenode.html diff --git a/seminar/templates/seminar/treenode_add_stub.html b/treenode/templates/treenode/treenode_add_stub.html similarity index 100% rename from seminar/templates/seminar/treenode_add_stub.html rename to treenode/templates/treenode/treenode_add_stub.html diff --git a/seminar/templates/seminar/treenode_name.html b/treenode/templates/treenode/treenode_name.html similarity index 100% rename from seminar/templates/seminar/treenode_name.html rename to treenode/templates/treenode/treenode_name.html diff --git a/seminar/templates/seminar/treenode_recursive.html b/treenode/templates/treenode/treenode_recursive.html similarity index 100% rename from seminar/templates/seminar/treenode_recursive.html rename to treenode/templates/treenode/treenode_recursive.html diff --git a/seminar/templates/seminar/vuetest.html b/treenode/templates/treenode/vuetest.html similarity index 100% rename from seminar/templates/seminar/vuetest.html rename to treenode/templates/treenode/vuetest.html diff --git a/seminar/templatetags/treenodes.py b/treenode/templatetags.py similarity index 98% rename from seminar/templatetags/treenodes.py rename to treenode/templatetags.py index d3da23ce..e5efe701 100644 --- a/seminar/templatetags/treenodes.py +++ b/treenode/templatetags.py @@ -17,7 +17,7 @@ def nodeType(value): if isinstance(value,UlohaZadaniNode): return "Zadání úlohy" 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 @register.filter diff --git a/seminar/tests_treelib.py b/treenode/tests.py similarity index 98% rename from seminar/tests_treelib.py rename to treenode/tests.py index 3245d0a6..32a77196 100644 --- a/seminar/tests_treelib.py +++ b/treenode/tests.py @@ -1,5 +1,5 @@ from django.test import TestCase -import seminar.treelib as tl +import treenode.treelib as tl import seminar.models as m class SimpleTreeLibTests(TestCase): diff --git a/seminar/treelib.py b/treenode/treelib.py similarity index 100% rename from seminar/treelib.py rename to treenode/treelib.py diff --git a/treenode/urls.py b/treenode/urls.py new file mode 100644 index 00000000..60dc88ad --- /dev/null +++ b/treenode/urls.py @@ -0,0 +1,18 @@ +from django.urls import path, re_path +from . import views + +urlpatterns = [ + #path('treenode//', views.TreeNodeView.as_view(), name='seminar_treenode'), + #path('treenode//json/', views.TreeNodeJSONView.as_view(), name='seminar_treenode_json'), + #path('treenode/text//', views.TextWebView.as_view(), name='seminar_textnode_web'), + #path('treenode/editor/pridat////', views.TreeNodePridatView.as_view(), name='treenode_pridat'), + #path('treenode/editor/smazat//', views.TreeNodeSmazatView.as_view(), name='treenode_smazat'), + #path('treenode/editor/odvesitpryc//', views.TreeNodeOdvesitPrycView.as_view(), name='treenode_odvesitpryc'), + #path('treenode/editor/podvesit///', views.TreeNodePodvesitView.as_view(), name='treenode_podvesit'), + #path('treenode/editor/prohodit//', views.TreeNodeProhoditView.as_view(), name='treenode_prohodit'), + #path('treenode/sirotcinec/', views.SirotcinecView.as_view(), name='seminar_treenode_sirotcinec'), + #path('problem/(?P\d+)/(?P\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()), +] diff --git a/treenode/views.py b/treenode/views.py new file mode 100644 index 00000000..2c300263 --- /dev/null +++ b/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}) diff --git a/seminar/viewsets.py b/treenode/viewsets.py similarity index 98% rename from seminar/viewsets.py rename to treenode/viewsets.py index 7e2ea63a..16dce6d6 100644 --- a/seminar/viewsets.py +++ b/treenode/viewsets.py @@ -3,10 +3,10 @@ from rest_framework import status from rest_framework.response import Response from django.core.exceptions import PermissionDenied from rest_framework.permissions import BasePermission, AllowAny -from . import models as m -from . import views +from seminar import models as m +import treenode.serializers as views -from seminar.permissions import AllowWrite +from treenode.permissions import AllowWrite class PermissionMixin(object): """ Redefines get_permissions so that only organizers can make changes. """ diff --git a/various/context_processors.py b/various/context_processors.py index c0be2975..b9fbd1c3 100644 --- a/various/context_processors.py +++ b/various/context_processors.py @@ -1,3 +1,6 @@ +from django.conf import settings + + def april(req): if 'X-April' in req.headers: try: @@ -12,3 +15,7 @@ def april(req): return {'april': today.year} return {} + +def rozliseni(request): + return {"LOCAL_TEST_PROD": settings.LOCAL_TEST_PROD} + diff --git a/vysledkovky/__init__.py b/vysledkovky/__init__.py new file mode 100644 index 00000000..a420da01 --- /dev/null +++ b/vysledkovky/__init__.py @@ -0,0 +1,3 @@ +""" +Obsahuje výsledkovky a vše, co se týká sčítání bodů. +""" \ No newline at end of file diff --git a/vysledkovky/apps.py b/vysledkovky/apps.py new file mode 100644 index 00000000..7221864c --- /dev/null +++ b/vysledkovky/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class VysledkovkyConfig(AppConfig): + name = 'vysledkovky' diff --git a/vysledkovky/migrations/__init__.py b/vysledkovky/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/vysledkovky/templates/vysledkovky/vysledkovka_cisla.html b/vysledkovky/templates/vysledkovky/vysledkovka_cisla.html new file mode 100644 index 00000000..adeb9067 --- /dev/null +++ b/vysledkovky/templates/vysledkovky/vysledkovka_cisla.html @@ -0,0 +1,66 @@ + + + + + {% endfor %} +
# + Jméno + {% for p in problemy %} + {# #}{{ p.kod_v_rocniku }}{# #} + + {# TODELETE #} + {% for podproblemy in podproblemy_iter.next %} + {# #}{{ podproblemy.kod_v_rocniku }}{# #} + {% endfor %} + {# TODELETE #} + + {% endfor %} + {% if ostatni %}Ostatní {% endif %} + + {# TODELETE #} + {% for podproblemy in podproblemy_iter.next %} + {# #}{{ podproblemy.kod_v_rocniku }}{# #} + {% endfor %} + {# TODELETE #} + + + Za číslo + Za ročník + Odjakživa + {% for rv in radky_vysledkovky %} +
{% autoescape off %}{{ rv.poradi }}{% endautoescape %} + + {% if rv.titul %} + {{ rv.titul }}MM + {% endif %} + {{ rv.resitel.osoba.plne_jmeno }} + {% for b in rv.body_problemy_sezn %} + {{ b }} + + {# TODELETE #} + {% for body_podproblemu in rv.body_podproblemy_iter.next %} + {{ body_podproblemu }} + {% endfor %} + {# TODELETE #} + + {% endfor %} + {{ rv.body_cislo }} + {{ rv.body_rocnik }} + {{ rv.body_celkem_odjakziva }} +
+ +{# TODELETE #} + +{# TODELETE #} diff --git a/seminar/templates/seminar/vysledkovka_rocnik.html b/vysledkovky/templates/vysledkovky/vysledkovka_rocnik.html similarity index 100% rename from seminar/templates/seminar/vysledkovka_rocnik.html rename to vysledkovky/templates/vysledkovky/vysledkovka_rocnik.html diff --git a/vysledkovky/templates/vysledkovky/vysledkovka_rocnik_neverejna.html b/vysledkovky/templates/vysledkovky/vysledkovka_rocnik_neverejna.html new file mode 100644 index 00000000..82871f07 --- /dev/null +++ b/vysledkovky/templates/vysledkovky/vysledkovka_rocnik_neverejna.html @@ -0,0 +1 @@ +{% include "vysledkovky/vysledkovka_rocnik.html" with radky_vysledkovky=radky_vysledkovky_s_neverejnymi cisla=cisla_s_neverejnymi %} \ No newline at end of file diff --git a/seminar/views/vysledkovka.py b/vysledkovky/utils.py similarity index 97% rename from seminar/views/vysledkovka.py rename to vysledkovky/utils.py index 182943b0..e637b587 100644 --- a/seminar/views/vysledkovka.py +++ b/vysledkovky/utils.py @@ -2,7 +2,6 @@ import seminar.models as m from django.db.models import Q, Sum, Count from seminar.utils import aktivniResitele, resi_v_rocniku, cisla_rocniku, hlavni_problem, hlavni_problemy_f, problemy_cisla, podproblemy_v_cislu import time -### Výsledky ROCNIK_ZRUSENI_TEMAT = 25 @@ -142,7 +141,7 @@ def setrid_resitele_a_body(slov_resitel_body): setrizene_body = [dvojice[1] for dvojice in slov_resitel_body] return setrizeni_resitele_id, setrizene_body -def vysledkovka_rocniku(rocnik, jen_verejne=True): +def data_vysledkovky_rocniku(rocnik, jen_verejne=True): """ Přebírá ročník (např. context["rocnik"]) a vrací výsledkovou listinu ve formě vhodné pro šablonu "seminar/vysledkovka_rocniku.html" """ @@ -197,7 +196,7 @@ def vysledkovka_rocniku(rocnik, jen_verejne=True): end = time.time() print("Vysledkovka rocniku",end-start) - return radky_vysledkovky + return radky_vysledkovky, cisla class RadekVysledkovkyCisla(object): """Obsahuje věci, které se hodí vědět při konstruování výsledkovky. @@ -372,9 +371,7 @@ class FixedIterator: # TODELETE -def vysledkovka_cisla(cislo, context=None): - if context is None: - context = {} +def data_vysledkovky_cisla(cislo): problemy = problemy_cisla(cislo) hlavni_problemy = hlavni_problemy_f(problemy) ## TODO možná chytřeji vybírat aktivní řešitele @@ -453,13 +450,11 @@ def vysledkovka_cisla(cislo, context=None): i += 1 # vytahané informace předáváme do kontextu - context['cislo'] = cislo - context['radky_vysledkovky'] = radky_vysledkovky - context['problemy'] = temata_a_spol - context['ostatni'] = je_nejake_ostatni pt = [podproblemy[it.id] for it in temata_a_spol]+[podproblemy[-1]] - context['podproblemy'] = pt - context['podproblemy_iter'] = FixedIterator(pt.__iter__()) # TODELETE - #context['v_cisle_zadane'] = TODO - #context['resene_problemy'] = resene_problemy - return context + return ( + radky_vysledkovky, + temata_a_spol, + je_nejake_ostatni, + pt, + FixedIterator(pt.__iter__()) + ) diff --git a/vysledkovky/views.py b/vysledkovky/views.py new file mode 100644 index 00000000..e5307bba --- /dev/null +++ b/vysledkovky/views.py @@ -0,0 +1,37 @@ +from .utils import data_vysledkovky_cisla, \ + data_vysledkovky_rocniku + + +def vysledkovka_cisla(cislo, context=None): + if context is None: + context = {} + context['cislo'] = cislo + + ( + context['radky_vysledkovky'], + context['problemy'], + context['ostatni'], + context['podproblemy'], + context['podproblemy_iter'] + ) = data_vysledkovky_cisla(cislo) + return context + + +def vysledkovka_rocniku(rocnik, context=None, request=None, sneverejnou=False): + if context is None: + context = {} + + ( + context['radky_vysledkovky'], + context['cisla'] + ) = data_vysledkovky_rocniku(rocnik) + + context['vysledkovka'] = len(context['cisla']) != 0 + + if sneverejnou and request and request.user.je_org: + ( + context['radky_vysledkovky_s_neverejnymi'], + context['cisla_s_neverejnymi'] + ) = data_vysledkovky_rocniku(rocnik, jen_verejne=False) + + return context