From ef68d3fb750fe838664f2f4acca40f9938acdbe6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=C3=A1=C5=A1=20Havelka?= Date: Fri, 8 Oct 2021 10:36:59 +0200 Subject: [PATCH 01/15] Move odevzdavatko do aplikace odevzdavatko --- mamweb/settings_common.py | 2 +- mamweb/urls.py | 3 + odevzdavatko/__init__.py | 11 + odevzdavatko/admin.py | 28 +++ odevzdavatko/apps.py | 5 + odevzdavatko/forms.py | 218 ++++++++++++++++++ odevzdavatko/migrations/__init__.py | 0 .../static/odevzdavatko}/cross.png | Bin .../static/odevzdavatko}/dynamic_formsets.js | 0 .../static/odevzdavatko}/plus.png | Bin .../templates}/odevzdavatko/detail.html | 8 +- .../odevzdavatko}/nahraj_reseni.html | 2 +- .../templates/odevzdavatko/posli_reseni.html | 1 - .../odevzdavatko/prehled_reseni.html | 0 .../templates}/odevzdavatko/seznam.html | 0 .../templates}/odevzdavatko/tabulka.html | 2 +- odevzdavatko/urls.py | 18 ++ .../odevzdavatko.py => odevzdavatko/views.py | 118 +++++++++- seminar/admin.py | 21 -- seminar/forms.py | 201 +--------------- seminar/models/__init__.py | 2 + seminar/{models.py => models/models_all.py} | 176 +------------- seminar/models/odevzdavatko.py | 188 +++++++++++++++ seminar/urls.py | 14 +- seminar/views/__init__.py | 1 - seminar/views/views_all.py | 114 +-------- 26 files changed, 599 insertions(+), 534 deletions(-) create mode 100644 odevzdavatko/__init__.py create mode 100644 odevzdavatko/admin.py create mode 100644 odevzdavatko/apps.py create mode 100644 odevzdavatko/forms.py create mode 100644 odevzdavatko/migrations/__init__.py rename {seminar/static/seminar => odevzdavatko/static/odevzdavatko}/cross.png (100%) rename {seminar/static/seminar => odevzdavatko/static/odevzdavatko}/dynamic_formsets.js (100%) rename {seminar/static/seminar => odevzdavatko/static/odevzdavatko}/plus.png (100%) rename {seminar/templates/seminar => odevzdavatko/templates}/odevzdavatko/detail.html (92%) rename {seminar/templates/seminar/profil => odevzdavatko/templates/odevzdavatko}/nahraj_reseni.html (98%) rename seminar/templates/seminar/org/vloz_reseni.html => odevzdavatko/templates/odevzdavatko/posli_reseni.html (88%) rename seminar/templates/seminar/odevzdavatko/resitel_prehled.html => odevzdavatko/templates/odevzdavatko/prehled_reseni.html (100%) rename {seminar/templates/seminar => odevzdavatko/templates}/odevzdavatko/seznam.html (100%) rename {seminar/templates/seminar => odevzdavatko/templates}/odevzdavatko/tabulka.html (97%) create mode 100644 odevzdavatko/urls.py rename seminar/views/odevzdavatko.py => odevzdavatko/views.py (73%) create mode 100644 seminar/models/__init__.py rename seminar/{models.py => models/models_all.py} (89%) create mode 100644 seminar/models/odevzdavatko.py diff --git a/mamweb/settings_common.py b/mamweb/settings_common.py index 3c850245..08997bb4 100644 --- a/mamweb/settings_common.py +++ b/mamweb/settings_common.py @@ -138,7 +138,7 @@ INSTALLED_APPS = ( 'various.autentizace', 'api', 'aesop', - + 'odevzdavatko', # Admin upravy: # 'material', diff --git a/mamweb/urls.py b/mamweb/urls.py index 2fdbff45..b47be1cc 100644 --- a/mamweb/urls.py +++ b/mamweb/urls.py @@ -16,6 +16,9 @@ urlpatterns = [ # Seminarova aplikace (ma vlastni podadresare) path('', include('seminar.urls')), + + # Odevzdavatko (ma vlastni podadresare) + path('', include('odevzdavatko.urls')), # Korekturovaci aplikace (ma vlastni podadresare) path('', include('korektury.urls')), diff --git a/odevzdavatko/__init__.py b/odevzdavatko/__init__.py new file mode 100644 index 00000000..a4ee2679 --- /dev/null +++ b/odevzdavatko/__init__.py @@ -0,0 +1,11 @@ +""" +Obsahuje vše, co se týká odevzdávání (+ nahrávání) a opravování řešení řešitelů. + +Slovníček: + Moje řešení = Přehled řešení = Řešení, která odevzdal aktuálního uživatel sám. + Došlá řešení = Tabulka + seznam + detail + ... = Řešení, která poslal někdo jiný. + Poslat řešení = Odevdat mé řešení. (Tj. řešení se vztahem k aktuálnímu uživateli.) + Nahrát řešení = Nahrání řešení bez vztahu k aktuálnímu uživateli. + +TODO: Místo vložit řešení v nahrávání a posílání řešení dát něco jiného? +""" \ No newline at end of file diff --git a/odevzdavatko/admin.py b/odevzdavatko/admin.py new file mode 100644 index 00000000..6048eb36 --- /dev/null +++ b/odevzdavatko/admin.py @@ -0,0 +1,28 @@ +from django.contrib import admin +from django_reverse_admin import ReverseModelAdmin +import seminar.models as m + + +class PrilohaReseniInline(admin.TabularInline): + model = m.PrilohaReseni + extra = 1 + + +class Reseni_ResiteleInline(admin.TabularInline): + model = m.Reseni_Resitele + + +@admin.register(m.Reseni) +class ReseniAdmin(ReverseModelAdmin): + base_model = m.Reseni + inline_type = 'tabular' + # inline_reverse = ['text_cely','resitele'] TODO vrátit zpět a zrychlit dotaz + inline_reverse = ['resitele'] + exclude = ['text_zkraceny', 'text_zkraceny_set'] + inlines = [PrilohaReseniInline] +# FAIL in template +# inlines = [PrilohaReseniInline,Reseni_ResiteleInline] + + +admin.site.register(m.PrilohaReseni) +admin.site.register(m.Hodnoceni) diff --git a/odevzdavatko/apps.py b/odevzdavatko/apps.py new file mode 100644 index 00000000..507c284f --- /dev/null +++ b/odevzdavatko/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class OdevzdavatkoConfig(AppConfig): + name = 'odevzdavatko' diff --git a/odevzdavatko/forms.py b/odevzdavatko/forms.py new file mode 100644 index 00000000..14639b65 --- /dev/null +++ b/odevzdavatko/forms.py @@ -0,0 +1,218 @@ +from django import forms +from dal import autocomplete +from django.forms import formset_factory +from django.forms.models import inlineformset_factory + +from seminar.models import Resitel +import seminar.models as m + +import logging + +# pro přidání políčka do formuláře je potřeba +# - mít v modelu tu položku, kterou chci upravovat +# - přidat do views (prihlaskaView, resitelEditView) +# - přidat do forms +# - includovat do html + +class DateInput(forms.DateInput): + # aby se datum dalo vybírat z kalendáře + input_type = 'date' + + +class PosliReseniForm(forms.Form): + #FIXME jen podproblémy daného problému + problem = forms.ModelChoiceField(label='Problém',queryset=m.Problem.objects.all()) + # to_field_name + #problem = models.ManyToManyField(Problem, verbose_name='problém', help_text='Problém', + # through='Hodnoceni') + + # FIXME pridat vice resitelu + resitel = forms.ModelChoiceField(label="Řešitel", + queryset=Resitel.objects.all(), + widget=autocomplete.ModelSelect2( + url='autocomplete_resitel', + attrs = {'data-placeholder--id': '-1', + 'data-placeholder--text' : '---', + 'data-allow-clear': 'true'}) + ) + + + #resitele = models.ManyToManyField(Resitel, verbose_name='autoři řešení', + # help_text='Seznam autorů řešení', through='Reseni_Resitele') + + cas_doruceni = forms.DateField(widget=DateInput(),label="Čas doručení") + + #cas_doruceni = models.DateTimeField('čas_doručení', default=timezone.now, blank=True) + + forma = forms.ChoiceField(label="Forma řešení",choices = m.Reseni.FORMA_CHOICES) + #forma = models.CharField('forma řešení', max_length=16, choices=FORMA_CHOICES, blank=False, + # default=FORMA_EMAIL) + + poznamka = forms.CharField(label='Neveřejná poznámka', required=False) + #poznamka = models.TextField('neveřejná poznámka', blank=True, + # help_text='Neveřejná poznámka k řešení (plain text)') + + #TODO body do cisla + #TODO prilohy + + ##def __init__(self, *args, **kwargs): + ## super().__init__(*args, **kwargs) + ## #self.fields['favorite_color'] = forms.ChoiceField(choices=[(color.id, color.name) for color in Resitel.objects.all()]) + +class NahrajReseniForm(forms.ModelForm): + class Meta: + model = m.Reseni + fields = ('problem',) + help_texts = {'problem':''} # Nezobrazovat help text ve formuláři + + widgets = {'problem': + autocomplete.ModelSelect2Multiple( + url='autocomplete_problem_odevzdatelny', + attrs = {'data-placeholder--id': '-1', + 'data-placeholder--text' : '---', + 'data-allow-clear': 'true'}, + ) + } + +ReseniSPrilohamiFormSet = inlineformset_factory(m.Reseni,m.PrilohaReseni, + form = NahrajReseniForm, + fields = ('soubor','res_poznamka'), + widgets = {'res_poznamka':forms.TextInput()}, + extra = 1, + can_delete = False, + + ) + + +class JednoHodnoceniForm(forms.ModelForm): + class Meta: + model = m.Hodnoceni + fields = ('problem', 'body', 'cislo_body') + widgets = { + 'problem': autocomplete.ModelSelect2( + url='autocomplete_problem_odevzdatelny', # FIXME: Dovolit i starší? + ) + } + +OhodnoceniReseniFormSet = formset_factory(JednoHodnoceniForm, + extra = 0, + ) + +class PoznamkaReseniForm(forms.ModelForm): + class Meta: + model = m.Reseni + fields = ('poznamka',) + +# FIXME: Ideálně by mělo být součástí třídy níž, ale neumím to udělat +DATE_FORMAT = '%Y-%m-%d' + +class OdevzdavatkoTabulkaFiltrForm(forms.Form): + """Form pro filtrování přehledové odevzdávátkové tabulky + + Inspirováno https://kam.mff.cuni.cz/mffzoom/""" + + # Věci definované níž se importují i ve views pro odevzdávátko (Inspirováno https://docs.djangoproject.com/en/3.1/ref/models/fields/#field-choices) + + RESITELE_RELEVANTNI = 'relevantni' + RESITELE_NEODMATUROVAVSI = 'neodmaturovavsi' + RESITELE_CHOICES = [ + (RESITELE_RELEVANTNI, 'Relevantní řešitelé'), # I.e. nezobrazovat prázdné řádky tabulky + (RESITELE_NEODMATUROVAVSI, 'Všichni bez maturity'), + # Možná: všechny vč. historických? + ] + + PROBLEMY_MOJE = 'moje' + PROBLEMY_LETOSNI = 'letosni' + PROBLEMY_CHOICES = [ + (PROBLEMY_MOJE, 'Moje problémy'), # Letošní problémy, které mají v sobě nebo v nadproblémech přiřazeného daného orga + (PROBLEMY_LETOSNI, 'Všechny letošní'), + # TODO: *hlavní problémy, možná všechny... + # XXX: Chtělo by to i "aktuálně zadané... + ] + + # TODO: Typy problémů (problémy, úlohy, ostatní, všechny)? Jen některá řešení (obodovaná/neobodovaná, víc řešitelů, ...)? + + + @classmethod + def gen_terminy(cls, rocnik=None): + import datetime + from time import strftime + + from django.db.utils import OperationalError + try: + aktualni_rocnik = m.Nastaveni.get_solo().aktualni_rocnik + aktualni_cislo = m.Nastaveni.get_solo().aktualni_cislo + except OperationalError: + # django.db.utils.OperationalError: no such table: seminar_nastaveni + # Nemáme databázi, takže to selhalo. Pro jistotu vrátíme aspoň dvě možnosti, ať to nepadá dál + logger = logging.getLogger(__name__) + logger.error("Rozbitá databáze (před počátečními migracemi?)") + return [('broken', 'Je to rozbitý'), ('fubar', 'Nefunguje to')] + + # FIXME: Tohle je hnusný monkey patch, mělo by to být nějak zahrnuto výš. + if rocnik is not None: + aktualni_rocnik = rocnik + aktualni_cislo = m.Cislo.objects.filter(rocnik=rocnik).order_by('poradi').last() + + result = [] + + for cislo in m.Cislo.objects.filter( + rocnik=aktualni_rocnik, + poradi__lte=aktualni_cislo.poradi, + ).reverse(): # Standardně se řadí od nejnovějšího čísla + # Předem je mi líto kohokoliv, kdo tyhle řádky bude číst... + if cislo.datum_vydani is not None and cislo.datum_vydani <= datetime.date.today(): + result.append(( + strftime(DATE_FORMAT, cislo.datum_vydani.timetuple()), + f"Vydání {cislo.poradi}. čísla")) + if cislo.datum_preddeadline is not None and cislo.datum_preddeadline <= datetime.date.today(): + result.append(( + strftime(DATE_FORMAT, cislo.datum_preddeadline.timetuple()), + f"Předdeadline {cislo.poradi}. čísla")) + if cislo.datum_deadline_soustredeni is not None and cislo.datum_deadline_soustredeni <= datetime.date.today(): + result.append(( + strftime(DATE_FORMAT, cislo.datum_deadline_soustredeni.timetuple()), + f"Sous. deadline {cislo.poradi}. čísla")) + if cislo.datum_deadline is not None and cislo.datum_deadline <= datetime.date.today(): + result.append(( + strftime(DATE_FORMAT, cislo.datum_deadline.timetuple()), + f"Finální deadline {cislo.poradi}. čísla")) + result.append(( + strftime(DATE_FORMAT, datetime.date.today().timetuple()), f"Dnes")) + + return result + + @classmethod + def gen_initial(cls, rocnik=None): + terminy = cls.gen_terminy(rocnik) + initial = { + 'resitele': cls.RESITELE_RELEVANTNI, + 'problemy': cls.PROBLEMY_MOJE, + # Pokud chceme neaktuální ročník, tak nás nejspíš zajímají všechna řešení… + 'reseni_od': terminy[-2] if rocnik is None else terminy[0], + 'reseni_do': terminy[-1], + 'neobodovane': False, + } + return initial + + def __init__(self, *args, rocnik=None, **kwargs): + if 'initial' not in kwargs: + super().__init__(initial=self.gen_initial(rocnik), *args, **kwargs) + else: + super().__init__(*args, **kwargs) + # choices jako parametr Select widgetu neumí brát callable, jen iterable, takže si pro jednoduchost můžu rovnou uložit výsledek sem... + # A "sem" znamená do libovolné metody, protože jinak se jedná o kód, který django spustí při inicializaci a protože potřebujeme databázi, tak by spadnul při vyrábění testdat... + self.terminy = self.gen_terminy(rocnik) + self.fields['reseni_od'].widget = forms.Select(choices=self.gen_terminy(rocnik)) + # Pokud chceme neaktuální ročník, tak nás nejspíš zajímají všechna řešení… + self.fields['reseni_od'].initial = self.terminy[-2] if rocnik is None else self.terminy[0] + self.fields['reseni_do'].widget = forms.Select(choices=self.gen_terminy(rocnik)) + self.fields['reseni_do'].initial = self.terminy[-1] + + # NOTE: Initial definuji pro jednotlivé fieldy, aby to bylo tady a nebylo potřeba to řešit ve views... + resitele = forms.ChoiceField(choices=RESITELE_CHOICES) + problemy = forms.ChoiceField(choices=PROBLEMY_CHOICES) + + reseni_od = forms.DateField(input_formats=[DATE_FORMAT]) + reseni_do = forms.DateField(input_formats=[DATE_FORMAT]) + neobodovane = forms.BooleanField(required=False) diff --git a/odevzdavatko/migrations/__init__.py b/odevzdavatko/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/seminar/static/seminar/cross.png b/odevzdavatko/static/odevzdavatko/cross.png similarity index 100% rename from seminar/static/seminar/cross.png rename to odevzdavatko/static/odevzdavatko/cross.png diff --git a/seminar/static/seminar/dynamic_formsets.js b/odevzdavatko/static/odevzdavatko/dynamic_formsets.js similarity index 100% rename from seminar/static/seminar/dynamic_formsets.js rename to odevzdavatko/static/odevzdavatko/dynamic_formsets.js diff --git a/seminar/static/seminar/plus.png b/odevzdavatko/static/odevzdavatko/plus.png similarity index 100% rename from seminar/static/seminar/plus.png rename to odevzdavatko/static/odevzdavatko/plus.png diff --git a/seminar/templates/seminar/odevzdavatko/detail.html b/odevzdavatko/templates/odevzdavatko/detail.html similarity index 92% rename from seminar/templates/seminar/odevzdavatko/detail.html rename to odevzdavatko/templates/odevzdavatko/detail.html index 04e8c550..35f5c8c5 100644 --- a/seminar/templates/seminar/odevzdavatko/detail.html +++ b/odevzdavatko/templates/odevzdavatko/detail.html @@ -4,7 +4,7 @@ {% block content %} -{# FIXME: Necopypastovat! Tohle je zkopírované ze static/seminar/dynamic_formsets.js #} +{# FIXME: Necopypastovat! Tohle je zkopírované ze static/odevzdavatko/dynamic_formsets.js #} + {% 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 100% rename from seminar/templates/seminar/odevzdavatko/resitel_prehled.html rename to odevzdavatko/templates/odevzdavatko/prehled_reseni.html 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 a5e3c350..f7a00a76 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..477beb2b --- /dev/null +++ b/odevzdavatko/urls.py @@ -0,0 +1,18 @@ +from django.urls import path + +from seminar.utils import org_required, resitel_required, viewMethodSwitch, \ + resitel_or_org_required +from . import views + +urlpatterns = [ + path('org/add_solution', org_required(views.PosliReseniView.as_view()), name='seminar_vloz_reseni'), + path('resitel/nahraj_reseni', resitel_required(views.NahrajReseniView.as_view()), name='seminar_nahraj_reseni'), + path('resitel/odevzdana_reseni/', resitel_or_org_required(views.PrehledOdevzdanychReseni.as_view()), name='seminar_resitel_odevzdana_reseni'), + + path('org/reseni/', org_required(views.TabulkaOdevzdanychReseniView.as_view()), name='odevzdavatko_tabulka'), + path('org/reseni/rocnik//', 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())), +] diff --git a/seminar/views/odevzdavatko.py b/odevzdavatko/views.py similarity index 73% rename from seminar/views/odevzdavatko.py rename to odevzdavatko/views.py index ce86958c..7d38a25a 100644 --- a/seminar/views/odevzdavatko.py +++ b/odevzdavatko/views.py @@ -1,8 +1,10 @@ -from django.views.generic import ListView, DetailView, FormView +from django.contrib.auth.mixins import LoginRequiredMixin +from django.core.mail import send_mail +from django.utils import timezone +from django.views.generic import ListView, DetailView, FormView, CreateView from django.views.generic.list import MultipleObjectTemplateResponseMixin,MultipleObjectMixin from django.views.generic.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 @@ -13,9 +15,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__) @@ -41,7 +44,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): @@ -161,7 +164,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() @@ -203,7 +206,7 @@ class ReseniProblemuView(MultipleObjectTemplateResponseMixin, MultipleObjectMixi ## XXX: https://docs.djangoproject.com/en/3.1/topics/class-based-views/mixins/#avoid-anything-more-complex 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']) @@ -227,7 +230,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}) @@ -266,7 +269,7 @@ def hodnoceniReseniView(request, pk, *args, **kwargs): 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: @@ -292,7 +295,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): @@ -301,3 +304,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/seminar/admin.py b/seminar/admin.py index 3ee14541..9b31627e 100644 --- a/seminar/admin.py +++ b/seminar/admin.py @@ -230,27 +230,6 @@ class SoustredeniAdmin(admin.ModelAdmin): 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) diff --git a/seminar/forms.py b/seminar/forms.py index 19ed8d9f..6c4ad7a1 100644 --- a/seminar/forms.py +++ b/seminar/forms.py @@ -3,10 +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 +from .models import Skola, Resitel, Osoba import seminar.models as m from datetime import date @@ -222,205 +220,8 @@ class PoMaturiteProfileEditForm(ProfileEditForm): 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/seminar/models/__init__.py b/seminar/models/__init__.py new file mode 100644 index 00000000..2c869390 --- /dev/null +++ b/seminar/models/__init__.py @@ -0,0 +1,2 @@ +from .models_all import * +from .odevzdavatko import * diff --git a/seminar/models.py b/seminar/models/models_all.py similarity index 89% rename from seminar/models.py rename to seminar/models/models_all.py index d4c3ad81..bf749303 100644 --- a/seminar/models.py +++ b/seminar/models/models_all.py @@ -9,7 +9,6 @@ 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 @@ -316,6 +315,7 @@ class Resitel(SeminarModelBase): 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) @@ -362,6 +362,7 @@ class Resitel(SeminarModelBase): # - 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) @@ -399,6 +400,7 @@ class Resitel(SeminarModelBase): 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()) @@ -1084,101 +1086,6 @@ class Uloha(Problem): 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 - - 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""" @@ -1204,45 +1111,6 @@ def generate_filename_konfera(self, filename): ) ## -def generate_filename(self, filename): - return os.path.join( - settings.SEMINAR_RESENI_DIR, - aux_generate_filename(self, filename) - ) - - -@reversion.register(ignore_duplicates=True) -class PrilohaReseni(SeminarModelBase): - - class Meta: - db_table = 'seminar_priloha_reseni' - verbose_name = 'Příloha řešení' - verbose_name_plural = 'Přílohy řešení' - ordering = ['reseni', 'vytvoreno'] - - # Interní ID - id = models.AutoField(primary_key = True) - - reseni = models.ForeignKey(Reseni, verbose_name='řešení', related_name='prilohy', - on_delete=models.CASCADE) - - vytvoreno = models.DateTimeField('vytvořeno', default=timezone.now, blank=True, editable=False) - - soubor = models.FileField('soubor', upload_to = generate_filename) - - poznamka = models.TextField('neveřejná poznámka', blank=True, - help_text='Neveřejná poznámka k příloze řešení (plain text), např. o původu') - - res_poznamka = models.TextField('poznámka řešitele', blank=True, - help_text='Poznámka k příloze řešení, např. co daný soubor obsahuje') - - def __str__(self): - return str(self.soubor) - - def split(self): - "Vrátí cestu rozsekanou po složkách. To se hodí v templatech" - # Věřím, že tohle funguje, případně použít os.path nebo pathlib. - return self.soubor.url.split('/') class Pohadka(SeminarModelBase): @@ -1385,29 +1253,6 @@ class Konfera(Problem): 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): @@ -1705,21 +1550,6 @@ class CastNode(TreeNode): def getOdkazStr(self): return str(self.nadpis) -class ReseniNode(TreeNode): - class Meta: - db_table = 'seminar_nodes_otistene_reseni' - verbose_name = 'Otištěné řešení (Node)' - verbose_name_plural = 'Otištěná řešení (Node)' - reseni = models.ForeignKey(Reseni, - on_delete=models.PROTECT, - verbose_name = 'reseni') - - def aktualizuj_nazev(self): - self.nazev = "ReseniNode: "+str(self.reseni) - - def getOdkazStr(self): - return str(self.reseni) - @reversion.register(ignore_duplicates=True) class Nastaveni(SingletonModel): diff --git a/seminar/models/odevzdavatko.py b/seminar/models/odevzdavatko.py new file mode 100644 index 00000000..ce6f88a7 --- /dev/null +++ b/seminar/models/odevzdavatko.py @@ -0,0 +1,188 @@ +import os + +import reversion + +from django.contrib.sites.shortcuts import get_current_site +from django.db import models +from django.db.models import Sum +from django.urls import reverse_lazy +from django.utils import timezone +from django.conf import settings + +from seminar.models import models_all as am + + +@reversion.register(ignore_duplicates=True) +class Reseni(am.SeminarModelBase): + + class Meta: + db_table = 'seminar_reseni' + verbose_name = 'Řešení' + verbose_name_plural = 'Řešení' + #ordering = ['-problem', 'resitele'] # FIXME: Takhle to chceme, ale nefunguje to. + ordering = ['-cas_doruceni'] + + # Interní ID + id = models.AutoField(primary_key = True) + + # Ke každé dvojici řešní a problém existuje nanejvýš jedno hodnocení, doplnění vazby. + problem = models.ManyToManyField(am.Problem, verbose_name='problém', help_text='Problém', + through='Hodnoceni') + + resitele = models.ManyToManyField(am.Resitel, verbose_name='autoři řešení', + help_text='Seznam autorů řešení', through='Reseni_Resitele') + + + cas_doruceni = models.DateTimeField('čas_doručení', default=timezone.now, blank=True) + + FORMA_PAPIR = 'papir' + FORMA_EMAIL = 'email' + FORMA_UPLOAD = 'upload' + FORMA_CHOICES = [ + (FORMA_PAPIR, 'Papírové řešení'), + (FORMA_EMAIL, 'Emailem'), + (FORMA_UPLOAD, 'Upload přes web'), + ] + forma = models.CharField('forma řešení', max_length=16, choices=FORMA_CHOICES, blank=False, + default=FORMA_EMAIL) + + text_cely = models.OneToOneField('ReseniNode', verbose_name='Plná verze textu řešení', + blank=True, null=True, related_name="reseni_cely_set", + on_delete=models.PROTECT) + + poznamka = models.TextField('neveřejná poznámka', blank=True, + help_text='Neveřejná poznámka k řešení (plain text)') + + zverejneno = models.BooleanField('řešení zveřejněno', default=False, + help_text='Udává, zda je řešení zveřejněno') + + def verejne_url(self): + return str(reverse_lazy('odevzdavatko_detail_reseni', args=[self.id])) + + def absolute_url(self): + return "https://" + str(get_current_site(None)) + self.verejne_url() + + # má OneToOneField s: + # Konfera + + # má ForeignKey s: + # Hodnoceni + + def sum_body(self): + return self.hodnoceni_set.all().aggregate(Sum('body'))["body__sum"] + + def __str__(self): + return "{}({}): {}({})".format(self.resitele.first(),len(self.resitele.all()), self.problem.first() ,len(self.problem.all())) + # NOTE: Potenciální DB HOG (bez select_related) + +## Pravdepodobne uz nebude potreba: +# def save(self, *args, **kwargs): +# if ((self.cislo_body is None) and (self.problem.cislo_reseni) and +# (self.problem.typ == Problem.TYP_ULOHA)): +# self.cislo_body = self.problem.cislo_reseni +# super(Reseni, self).save(*args, **kwargs) + +class Hodnoceni(am.SeminarModelBase): + class Meta: + db_table = 'seminar_hodnoceni' + verbose_name = 'Hodnocení' + verbose_name_plural = 'Hodnocení' + + # Interní ID + id = models.AutoField(primary_key = True) + + + body = models.DecimalField(max_digits=8, decimal_places=1, verbose_name='body', + blank=True, null=True) + + cislo_body = models.ForeignKey(am.Cislo, verbose_name='číslo pro body', + related_name='hodnoceni', blank=True, null=True, on_delete=models.PROTECT) + + reseni = models.ForeignKey(Reseni, verbose_name='řešení', on_delete=models.CASCADE) + + problem = models.ForeignKey(am.Problem, verbose_name='problém', + related_name='hodnoceni', on_delete=models.PROTECT) + + def __str__(self): + return "{}, {}, {}".format(self.problem, self.reseni, self.body) + +def generate_filename(self, filename): + return os.path.join( + settings.SEMINAR_RESENI_DIR, + am.aux_generate_filename(self, filename) + ) + + +@reversion.register(ignore_duplicates=True) +class PrilohaReseni(am.SeminarModelBase): + + class Meta: + db_table = 'seminar_priloha_reseni' + verbose_name = 'Příloha řešení' + verbose_name_plural = 'Přílohy řešení' + ordering = ['reseni', 'vytvoreno'] + + # Interní ID + id = models.AutoField(primary_key = True) + + reseni = models.ForeignKey(Reseni, verbose_name='řešení', related_name='prilohy', + on_delete=models.CASCADE) + + vytvoreno = models.DateTimeField('vytvořeno', default=timezone.now, blank=True, editable=False) + + soubor = models.FileField('soubor', upload_to = generate_filename) + + poznamka = models.TextField('neveřejná poznámka', blank=True, + help_text='Neveřejná poznámka k příloze řešení (plain text), např. o původu') + + res_poznamka = models.TextField('poznámka řešitele', blank=True, + help_text='Poznámka k příloze řešení, např. co daný soubor obsahuje') + + def __str__(self): + return str(self.soubor) + + def split(self): + "Vrátí cestu rozsekanou po složkách. To se hodí v templatech" + # Věřím, že tohle funguje, případně použít os.path nebo pathlib. + return self.soubor.url.split('/') + + +# Vazebna tabulka. Mozna se generuje automaticky. +@reversion.register(ignore_duplicates=True) +class Reseni_Resitele(models.Model): + + class Meta: + db_table = 'seminar_reseni_resitele' + verbose_name = 'Řešení řešitelů' + verbose_name_plural = 'Řešení řešitelů' + ordering = ['reseni', 'resitele'] + + # Interní ID + id = models.AutoField(primary_key = True) + + resitele = models.ForeignKey(am.Resitel, verbose_name='řešitel', on_delete=models.PROTECT) + + reseni = models.ForeignKey(Reseni, verbose_name='řešení', on_delete=models.CASCADE) + + # podil - jakou merou se ktery resitel podilel na danem reseni + # - pouziti v budoucnu, pokud by resitele nemeli dostat vsichni stejne bodu za spolecne reseni + + def __str__(self): + return '{} od {}'.format(self.reseni, self.resitel) + # NOTE: Poteciální DB HOG bez select_related + +class ReseniNode(am.TreeNode): + class Meta: + db_table = 'seminar_nodes_otistene_reseni' + verbose_name = 'Otištěné řešení (Node)' + verbose_name_plural = 'Otištěná řešení (Node)' + reseni = models.ForeignKey(Reseni, + on_delete=models.PROTECT, + verbose_name = 'reseni') + + def aktualizuj_nazev(self): + self.nazev = "ReseniNode: "+str(self.reseni) + + def getOdkazStr(self): + return str(self.reseni) + diff --git a/seminar/urls.py b/seminar/urls.py index 8a6aacf4..4a94e27b 100644 --- a/seminar/urls.py +++ b/seminar/urls.py @@ -1,7 +1,7 @@ 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), @@ -116,8 +116,6 @@ urlpatterns = [ 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), @@ -127,19 +125,9 @@ urlpatterns = [ # 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())), ] diff --git a/seminar/views/__init__.py b/seminar/views/__init__.py index 785102e5..22c60734 100644 --- a/seminar/views/__init__.py +++ b/seminar/views/__init__.py @@ -1,6 +1,5 @@ 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 56e2aaae..d5e3e54c 100644 --- a/seminar/views/views_all.py +++ b/seminar/views/views_all.py @@ -1,26 +1,22 @@ 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.http import HttpResponse, JsonResponse +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.edit import CreateView from django.views.generic.base import TemplateView, RedirectView from django.contrib.auth.models import User, Permission, Group from django.contrib.auth.mixins import LoginRequiredMixin from django.db import transaction -from django.core import serializers from django.core.exceptions import PermissionDenied -from django.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, Soustredeni, Organizator, Resitel, Novinky, Soustredeni_Ucastnici, Tema, Clanek, Osoba # Tohle je stare a chceme se toho zbavit. Pouzivejte s.ToCoChci #from .models import VysledkyZaCislo, VysledkyKCisluZaRocnik, VysledkyKCisluOdjakziva from seminar import utils, treelib from seminar.forms import PrihlaskaForm, ProfileEditForm, PoMaturiteProfileEditForm @@ -29,7 +25,7 @@ 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 datetime import date, datetime from django.utils import timezone from itertools import groupby from collections import OrderedDict @@ -40,14 +36,11 @@ 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 seminar.utils import aktivniResitele, problemy_rocniku, cisla_rocniku, hlavni_problemy_f from various.autentizace.views import LoginView from various.autentizace.utils import posli_reset_hesla @@ -1040,97 +1033,6 @@ class ResitelView(LoginRequiredMixin,generic.DetailView): # - 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) From 61b6f4bfd9d4d6ee6124c34baf70b7a9629e75c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=C3=A1=C5=A1=20Havelka?= Date: Thu, 7 Oct 2021 19:30:28 +0200 Subject: [PATCH 02/15] Move vysledkovek do aplikace vysledkovky --- aesop/views.py | 6 +- mamweb/settings_common.py | 2 + .../seminar/archiv/cislo-normal.html | 29 +------- seminar/templates/seminar/archiv/cislo.html | 68 +------------------ seminar/templates/seminar/archiv/rocnik.html | 8 +-- .../seminar/zadani/AktualniVysledkovka.html | 8 +-- seminar/views/views_all.py | 54 +++++++-------- vysledkovky/__init__.py | 3 + vysledkovky/apps.py | 5 ++ vysledkovky/migrations/__init__.py | 0 .../vysledkovky/vysledkovka_cisla.html | 66 ++++++++++++++++++ .../vysledkovky}/vysledkovka_rocnik.html | 0 .../vysledkovka_rocnik_neverejna.html | 1 + .../vysledkovka.py => vysledkovky/utils.py | 25 +++---- vysledkovky/views.py | 37 ++++++++++ 15 files changed, 159 insertions(+), 153 deletions(-) create mode 100644 vysledkovky/__init__.py create mode 100644 vysledkovky/apps.py create mode 100644 vysledkovky/migrations/__init__.py create mode 100644 vysledkovky/templates/vysledkovky/vysledkovka_cisla.html rename {seminar/templates/seminar => vysledkovky/templates/vysledkovky}/vysledkovka_rocnik.html (100%) create mode 100644 vysledkovky/templates/vysledkovky/vysledkovka_rocnik_neverejna.html rename seminar/views/vysledkovka.py => vysledkovky/utils.py (97%) create mode 100644 vysledkovky/views.py 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 08997bb4..19f80548 100644 --- a/mamweb/settings_common.py +++ b/mamweb/settings_common.py @@ -139,6 +139,8 @@ INSTALLED_APPS = ( 'api', 'aesop', 'odevzdavatko', + 'vysledkovky', + # Admin upravy: # 'material', diff --git a/seminar/templates/seminar/archiv/cislo-normal.html b/seminar/templates/seminar/archiv/cislo-normal.html index e23da09c..4d4a9da0 100644 --- a/seminar/templates/seminar/archiv/cislo-normal.html +++ b/seminar/templates/seminar/archiv/cislo-normal.html @@ -58,34 +58,7 @@ {% endif %} {% if cislo.verejna_vysledkovka or user.is_staff %} - - - - - {% endfor %} -
# - Jméno #} - {# problémy by měly být veřejné, když je veřejná výsledkovka #} -{# {% for p in problemy %} - {{ p.kod_v_rocniku }} - {% endfor %} - Za číslo - Za ročník - Odjakživa - {% for rv in radky_vysledkovky %} -
{% autoescape off %}{{ rv.poradi }}{% endautoescape %} - - {% if rv.resitel.titul != "" %} - {{ rv.resitel.titul }}MM - {% endif %} - {{ rv.resitel.osoba.plne_jmeno }} - {% for b in rv.hlavni_problemy_body %} - {{ b }} - {% endfor %} - {{ rv.body_cislo }} - {{ rv.body_rocnik }} - {{ rv.body_celkem_odjakziva }} -
+ {% include "vysledkovky/vysledkovka_cisla.html" %} {% endif %} {% if not cislo.verejna_vysledkovka and user.is_staff %} 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 aa2f1dbc..7f71037f 100644 --- a/seminar/templates/seminar/archiv/rocnik.html +++ b/seminar/templates/seminar/archiv/rocnik.html @@ -72,17 +72,13 @@ {% endif %}

Výsledková listina

- {% include "seminar/vysledkovka_rocnik.html" %} + {% include "vysledkovky/vysledkovka_rocnik.html" %} {% endif %} {% if user.je_org %}

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/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/views/views_all.py b/seminar/views/views_all.py index d5e3e54c..a646d98f 100644 --- a/seminar/views/views_all.py +++ b/seminar/views/views_all.py @@ -23,7 +23,8 @@ from seminar.forms import PrihlaskaForm, ProfileEditForm, PoMaturiteProfileEditF 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 vysledkovky.utils import body_resitelu +from vysledkovky.views import vysledkovka_rocniku, vysledkovka_cisla from datetime import date, datetime from django.utils import timezone @@ -487,31 +488,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 ) @@ -655,18 +659,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) 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 From 0cd1c3ef1a4f509ce50949d22f05a3e62cb4254d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=C3=A1=C5=A1=20Havelka?= Date: Fri, 8 Oct 2021 18:06:02 +0200 Subject: [PATCH 03/15] Move personalni do aplikace personalni --- mamweb/settings_common.py | 1 + mamweb/urls.py | 4 + personalni/__init__.py | 4 + personalni/admin.py | 48 ++ personalni/apps.py | 5 + personalni/forms.py | 221 ++++++++ personalni/migrations/__init__.py | 0 .../static/personalni}/prihlaska.js | 0 .../personalni/profil}/orgorozcestnik.html | 0 .../templates/personalni}/profil/resitel.html | 0 .../templates/personalni/udaje/edit.html | 105 ++++ .../templates/personalni/udaje}/gdpr.html | 0 .../templates/personalni/udaje/prihlaska.html | 123 +++++ .../personalni/udaje}/prihlaska_field.html | 0 personalni/urls.py | 24 + personalni/views.py | 306 +++++++++++ seminar/admin.py | 44 -- seminar/forms.py | 213 -------- seminar/models/__init__.py | 2 + seminar/models/base.py | 22 + seminar/models/models_all.py | 476 +----------------- seminar/models/odevzdavatko.py | 6 +- seminar/models/personalni.py | 438 ++++++++++++++++ seminar/templates/seminar/profil/edit.html | 105 ---- .../templates/seminar/profil/prihlaska.html | 123 ----- seminar/urls.py | 18 - seminar/views/views_all.py | 288 +---------- 27 files changed, 1329 insertions(+), 1247 deletions(-) create mode 100644 personalni/__init__.py create mode 100644 personalni/admin.py create mode 100644 personalni/apps.py create mode 100644 personalni/forms.py create mode 100644 personalni/migrations/__init__.py rename {seminar/static/seminar => personalni/static/personalni}/prihlaska.js (100%) rename {seminar/templates/seminar => personalni/templates/personalni/profil}/orgorozcestnik.html (100%) rename {seminar/templates/seminar => personalni/templates/personalni}/profil/resitel.html (100%) create mode 100644 personalni/templates/personalni/udaje/edit.html rename {seminar/templates/seminar/profil => personalni/templates/personalni/udaje}/gdpr.html (100%) create mode 100644 personalni/templates/personalni/udaje/prihlaska.html rename {seminar/templates/seminar/profil => personalni/templates/personalni/udaje}/prihlaska_field.html (100%) create mode 100644 personalni/urls.py create mode 100644 personalni/views.py create mode 100644 seminar/models/base.py create mode 100644 seminar/models/personalni.py delete mode 100644 seminar/templates/seminar/profil/edit.html delete mode 100644 seminar/templates/seminar/profil/prihlaska.html diff --git a/mamweb/settings_common.py b/mamweb/settings_common.py index 19f80548..c7146455 100644 --- a/mamweb/settings_common.py +++ b/mamweb/settings_common.py @@ -140,6 +140,7 @@ INSTALLED_APPS = ( 'aesop', 'odevzdavatko', 'vysledkovky', + 'personalni', # Admin upravy: diff --git a/mamweb/urls.py b/mamweb/urls.py index b47be1cc..ef0aa449 100644 --- a/mamweb/urls.py +++ b/mamweb/urls.py @@ -26,6 +26,10 @@ urlpatterns = [ # Prednaskova aplikace (ma vlastni podadresare) path('', include('prednasky.urls')), + # Personalni aplikace (ma vlastni podadresare) + # (profil, osobní údaje, ..., ne autentizace, viz dále) + path('', include('personalni.urls')), + # Autentizační aplikace (ma vlastni podadresare) path('', include('various.autentizace.urls')), 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..113fb99f --- /dev/null +++ b/personalni/admin.py @@ -0,0 +1,48 @@ +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" + +@admin.register(m.Organizator) +class OrganizatorAdmin(admin.ModelAdmin): + search_fields = ['osoba__jmeno', 'osoba__prijmeni', 'osoba__prezdivka'] + +class OsobaInline(admin.TabularInline): + model = m.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/personalni/forms.py b/personalni/forms.py new file mode 100644 index 00000000..78f1f11a --- /dev/null +++ b/personalni/forms.py @@ -0,0 +1,221 @@ +from django import forms +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 seminar.models import Skola, Resitel, Osoba + +from datetime import date +import logging + +# pro přidání políčka do formuláře je potřeba +# - mít v modelu tu položku, kterou chci upravovat +# - přidat do views (prihlaskaView, resitelEditView) +# - přidat do forms +# - includovat do html + +class DateInput(forms.DateInput): + # aby se datum dalo vybírat z kalendáře + input_type = 'date' + +class 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,}$" + + +class PrihlaskaForm(PasswordResetForm): + username = forms.CharField(label='Přihlašovací jméno', + max_length=256, + required=True, + help_text='Tímto jménem se následně budeš přihlašovat pro odevzdání řešení a další činnosti v semináři') + + jmeno = forms.CharField(label='Jméno', max_length=256, required=True) + prijmeni = forms.CharField(label='Příjmení', max_length=256, required=True) + pohlavi_muz = forms.ChoiceField(label='Pohlaví', + choices = ((True,'muž'),(False,'žena')), required=True) + email = forms.EmailField(label='E-mail',max_length=256, required=True) + telefon = forms.CharField(widget=TelInput(),label='Telefon',max_length=256, required=False) + datum_narozeni = forms.DateField(widget=DateInput(),label='Datum narození', required=False) + ulice = forms.CharField(label='Ulice a číslo popisné', max_length=256, required=False) + mesto = forms.CharField(label='Město', max_length=256, required=False) + psc = forms.CharField(label='PSČ', max_length=32, required=False) + stat = forms.ChoiceField(label='Stát', + choices = (('CZ', 'Česká Republika'), + ('SK', 'Slovenská Republika'), + ('other', 'Jiné')), + required=False) + stat_text = forms.CharField(label='Stát', max_length=256, required=False) + + skola = forms.ModelChoiceField(label="Škola", + queryset=Skola.objects.all(), + widget=autocomplete.ModelSelect2( + url='autocomplete_skola', + attrs = {'data-placeholder--id': '-1', + 'data-placeholder--text' : '---', + 'data-allow-clear': 'true'}) + ,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) + +# trida = forms.CharField(label='Třída',max_length=10, required=True) + + rok_maturity = forms.IntegerField( + label='Rok maturity', + min_value=date.today().year, + max_value=date.today().year+8, + required=True) + zasilat = forms.ChoiceField(label='Kam zasílat čísla a řešení',choices = Resitel.ZASILAT_CHOICES, required=True) + zasilat_cislo_emailem = forms.BooleanField(label='Chci dostávat e-mailem upozornění na vydání nového čísla', required=False) + + gdpr = forms.BooleanField(label='Souhlasím se zpracováním osobních údajů', required=True) + spam = forms.BooleanField(label='Souhlasím se zasíláním materiálů od MFF UK', required=False) + + def clean_username(self): + err_logger = logging.getLogger('seminar.prihlaska.problem') + username = self.cleaned_data.get('username') + try: + User.objects.get(username=username) + msg = "Username {} exists".format(username) + err_logger.info(msg) + raise forms.ValidationError('Přihlašovací jméno je již použito') + + except ObjectDoesNotExist: + pass + return username + + def clean_email(self): + err_logger = logging.getLogger('seminar.prihlaska.problem') + email = self.cleaned_data.get('email') + try: + osoba = Osoba.objects.get(email=email) + msg = "Email {} exists".format(email) + if osoba.user is not None: + err_logger.info(msg) + raise forms.ValidationError('E-mail je již použit') + else: + msg += ', but currently has no User, so allowing registration.' + err_logger.info(msg) + + except ObjectDoesNotExist: + pass + return email + + + def clean(self): + super().clean() + + err_logger = logging.getLogger('seminar.prihlaska.problem') + + data = self.cleaned_data + if data.get('stat') != 'other' and data.get('stat_text') != '': + self.add_error('stat',forms.ValidationError('Nelze mít vybraný stát z menu a zároven zapsaný textem')) + if data.get('skola') and (data.get('skola_nazev') or data.get('skola_adresa')): + self.add_error('skola',forms.ValidationError('Pokud je škola v seznamu, nevypisujte ji ručně, pokud není, zrušte výběr ze seznamu (křížek vpravo)')) + if not data.get('skola'): + if data.get('skola_nazev')=='' and data.get('skola_adresa')=='': + self.add_error('skola',forms.ValidationError('Je nutné vyplnit školu')) + elif data.get('skola_nazev')=='': + self.add_error('skola_nazev',forms.ValidationError('Je nutné vyplnit název školy')) + elif data.get('skola_adresa')=='': + self.add_error('skola_adresa',forms.ValidationError('Je nutné vyplnit adresu školy')) + + +class ProfileEditForm(forms.Form): + username = forms.CharField(label='Přihlašovací jméno', + max_length=256, + required=False, + disabled=True) + + jmeno = forms.CharField(label='Jméno', max_length=256, required=True) + prijmeni = forms.CharField(label='Příjmení', max_length=256, required=True) + pohlavi_muz = forms.ChoiceField(label='Pohlaví', + choices = ((True,'muž'),(False,'žena')), required=True) + email = forms.EmailField(label='E-mail',max_length=256, required=True) + telefon = forms.CharField(widget=TelInput(),label='Telefon',max_length=256, required=False) + datum_narozeni = forms.DateField(widget=DateInput(),label='Datum narození', required=False) + ulice = forms.CharField(label='Ulice', max_length=256, required=False) + mesto = forms.CharField(label='Město', max_length=256, required=False) + psc = forms.CharField(label='PSČ', max_length=32, required=False) + stat = forms.ChoiceField(label='Stát', + choices = (('CZ', 'Česká republika'), + ('SK', 'Slovenská republika'), + ('other', 'Jiné')), + required=False) + stat_text = forms.CharField(label='Stát', max_length=256, required=False) + + skola = forms.ModelChoiceField(label="Škola", + queryset=Skola.objects.all(), + widget=autocomplete.ModelSelect2( + url='autocomplete_skola', + attrs = {'data-placeholder--id': '-1', + 'data-placeholder--text' : '---', + 'data-allow-clear': 'true'}) + ,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) + +# trida = forms.CharField(label='Třída',max_length=10, required=True) + + rok_maturity = forms.IntegerField( + label='Rok maturity', + min_value=date.today().year, + max_value=date.today().year+8, + required=True) + zasilat = forms.ChoiceField(label='Kam zasílat čísla a řešení',choices = Resitel.ZASILAT_CHOICES, required=True) + zasilat_cislo_emailem = forms.BooleanField(label='Chci dostávat email s upozorněním na vydání nového čísla', required=False) + + spam = forms.BooleanField(label='Souhlasím se zasíláním materiálů od MFF UK', required=False) +# def clean_username(self): +# err_logger = logging.getLogger('seminar.prihlaska.problem') +# username = self.cleaned_data.get('username') +# try: +# User.objects.get(username=username) +# msg = "Username {} exists".format(username) +# err_logger.info(msg) +# raise forms.ValidationError('Přihlašovací jméno je již použito') +# +# except ObjectDoesNotExist: +# pass +# return username +# + def clean_email(self): + err_logger = logging.getLogger('seminar.prihlaska.problem') + email = self.cleaned_data.get('email') + try: + Osoba.objects.exclude(user__username=self.username).get(email=email) + msg = "Email {} exists (in edit)".format(email) + err_logger.info(msg) + raise forms.ValidationError('Email je již použit') + + except ObjectDoesNotExist: + pass + return email + #def clean(self): + # super().clean() + # + # err_logger = logging.getLogger('seminar.prihlaska.problem') + + # data = self.cleaned_data + # if data.get('password') != data.get('password_check'): + # self.add_error('password_check',forms.ValidationError('Hesla se neshodují')) + # if data.get('stat') != '' and data.get('stat_text') != '': + # self.add_error('stat',forms.ValidationError('Nelze mít vybraný stát z menu a zároven zapsaný textem')) + # if data.get('skola') and (data.get('skola_nazev') or data.get('skola_adresa')): + # self.add_error('skola',forms.ValidationError('Pokud je škola v seznamu, nevypisujte ji ručně, pokud není, zrušte výběr ze seznamu (křížek vpravo)')) + # if not data.get('skola'): + # if data.get('skola_nazev')=='' and data.get('skola_adresa')=='': + # self.add_error('skola',forms.ValidationError('Je nutné vyplnit školu')) + # elif data.get('skola_nazev')=='': + # self.add_error('skola_nazev',forms.ValidationError('Je nutné vyplnit název školy')) + # 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) 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/seminar/admin.py b/seminar/admin.py index 9b31627e..98a65db2 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 @@ -17,8 +14,6 @@ from seminar.utils import hlavni_problem 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): @@ -105,45 +100,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" - -@admin.register(m.Organizator) -class OrganizatorAdmin(admin.ModelAdmin): - search_fields = ['osoba__jmeno', 'osoba__prijmeni', 'osoba__prezdivka'] - -class OsobaInline(admin.TabularInline): - model = m.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): diff --git a/seminar/forms.py b/seminar/forms.py index 6c4ad7a1..704084f7 100644 --- a/seminar/forms.py +++ b/seminar/forms.py @@ -1,225 +1,12 @@ from django import forms -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 .models import Skola, Resitel, Osoba import seminar.models as m -from datetime import date -import logging - # pro přidání políčka do formuláře je potřeba # - mít v modelu tu položku, kterou chci upravovat # - přidat do views (prihlaskaView, resitelEditView) # - přidat do forms # - includovat do html -class DateInput(forms.DateInput): - # aby se datum dalo vybírat z kalendáře - input_type = 'date' - -class 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,}$" - - -class PrihlaskaForm(PasswordResetForm): - username = forms.CharField(label='Přihlašovací jméno', - max_length=256, - required=True, - help_text='Tímto jménem se následně budeš přihlašovat pro odevzdání řešení a další činnosti v semináři') - - jmeno = forms.CharField(label='Jméno', max_length=256, required=True) - prijmeni = forms.CharField(label='Příjmení', max_length=256, required=True) - pohlavi_muz = forms.ChoiceField(label='Pohlaví', - choices = ((True,'muž'),(False,'žena')), required=True) - email = forms.EmailField(label='E-mail',max_length=256, required=True) - telefon = forms.CharField(widget=TelInput(),label='Telefon',max_length=256, required=False) - datum_narozeni = forms.DateField(widget=DateInput(),label='Datum narození', required=False) - ulice = forms.CharField(label='Ulice a číslo popisné', max_length=256, required=False) - mesto = forms.CharField(label='Město', max_length=256, required=False) - psc = forms.CharField(label='PSČ', max_length=32, required=False) - stat = forms.ChoiceField(label='Stát', - choices = (('CZ', 'Česká Republika'), - ('SK', 'Slovenská Republika'), - ('other', 'Jiné')), - required=False) - stat_text = forms.CharField(label='Stát', max_length=256, required=False) - - skola = forms.ModelChoiceField(label="Škola", - queryset=Skola.objects.all(), - widget=autocomplete.ModelSelect2( - url='autocomplete_skola', - attrs = {'data-placeholder--id': '-1', - 'data-placeholder--text' : '---', - 'data-allow-clear': 'true'}) - ,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) - -# trida = forms.CharField(label='Třída',max_length=10, required=True) - - rok_maturity = forms.IntegerField( - label='Rok maturity', - min_value=date.today().year, - max_value=date.today().year+8, - required=True) - zasilat = forms.ChoiceField(label='Kam zasílat čísla a řešení',choices = Resitel.ZASILAT_CHOICES, required=True) - zasilat_cislo_emailem = forms.BooleanField(label='Chci dostávat e-mailem upozornění na vydání nového čísla', required=False) - - gdpr = forms.BooleanField(label='Souhlasím se zpracováním osobních údajů', required=True) - spam = forms.BooleanField(label='Souhlasím se zasíláním materiálů od MFF UK', required=False) - - def clean_username(self): - err_logger = logging.getLogger('seminar.prihlaska.problem') - username = self.cleaned_data.get('username') - try: - User.objects.get(username=username) - msg = "Username {} exists".format(username) - err_logger.info(msg) - raise forms.ValidationError('Přihlašovací jméno je již použito') - - except ObjectDoesNotExist: - pass - return username - - def clean_email(self): - err_logger = logging.getLogger('seminar.prihlaska.problem') - email = self.cleaned_data.get('email') - try: - osoba = Osoba.objects.get(email=email) - msg = "Email {} exists".format(email) - if osoba.user is not None: - err_logger.info(msg) - raise forms.ValidationError('E-mail je již použit') - else: - msg += ', but currently has no User, so allowing registration.' - err_logger.info(msg) - - except ObjectDoesNotExist: - pass - return email - - - def clean(self): - super().clean() - - err_logger = logging.getLogger('seminar.prihlaska.problem') - - data = self.cleaned_data - if data.get('stat') != 'other' and data.get('stat_text') != '': - self.add_error('stat',forms.ValidationError('Nelze mít vybraný stát z menu a zároven zapsaný textem')) - if data.get('skola') and (data.get('skola_nazev') or data.get('skola_adresa')): - self.add_error('skola',forms.ValidationError('Pokud je škola v seznamu, nevypisujte ji ručně, pokud není, zrušte výběr ze seznamu (křížek vpravo)')) - if not data.get('skola'): - if data.get('skola_nazev')=='' and data.get('skola_adresa')=='': - self.add_error('skola',forms.ValidationError('Je nutné vyplnit školu')) - elif data.get('skola_nazev')=='': - self.add_error('skola_nazev',forms.ValidationError('Je nutné vyplnit název školy')) - elif data.get('skola_adresa')=='': - self.add_error('skola_adresa',forms.ValidationError('Je nutné vyplnit adresu školy')) - - -class ProfileEditForm(forms.Form): - username = forms.CharField(label='Přihlašovací jméno', - max_length=256, - required=False, - disabled=True) - - jmeno = forms.CharField(label='Jméno', max_length=256, required=True) - prijmeni = forms.CharField(label='Příjmení', max_length=256, required=True) - pohlavi_muz = forms.ChoiceField(label='Pohlaví', - choices = ((True,'muž'),(False,'žena')), required=True) - email = forms.EmailField(label='E-mail',max_length=256, required=True) - telefon = forms.CharField(widget=TelInput(),label='Telefon',max_length=256, required=False) - datum_narozeni = forms.DateField(widget=DateInput(),label='Datum narození', required=False) - ulice = forms.CharField(label='Ulice', max_length=256, required=False) - mesto = forms.CharField(label='Město', max_length=256, required=False) - psc = forms.CharField(label='PSČ', max_length=32, required=False) - stat = forms.ChoiceField(label='Stát', - choices = (('CZ', 'Česká republika'), - ('SK', 'Slovenská republika'), - ('other', 'Jiné')), - required=False) - stat_text = forms.CharField(label='Stát', max_length=256, required=False) - - skola = forms.ModelChoiceField(label="Škola", - queryset=Skola.objects.all(), - widget=autocomplete.ModelSelect2( - url='autocomplete_skola', - attrs = {'data-placeholder--id': '-1', - 'data-placeholder--text' : '---', - 'data-allow-clear': 'true'}) - ,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) - -# trida = forms.CharField(label='Třída',max_length=10, required=True) - - rok_maturity = forms.IntegerField( - label='Rok maturity', - min_value=date.today().year, - max_value=date.today().year+8, - required=True) - zasilat = forms.ChoiceField(label='Kam zasílat čísla a řešení',choices = Resitel.ZASILAT_CHOICES, required=True) - zasilat_cislo_emailem = forms.BooleanField(label='Chci dostávat email s upozorněním na vydání nového čísla', required=False) - - spam = forms.BooleanField(label='Souhlasím se zasíláním materiálů od MFF UK', required=False) -# def clean_username(self): -# err_logger = logging.getLogger('seminar.prihlaska.problem') -# username = self.cleaned_data.get('username') -# try: -# User.objects.get(username=username) -# msg = "Username {} exists".format(username) -# err_logger.info(msg) -# raise forms.ValidationError('Přihlašovací jméno je již použito') -# -# except ObjectDoesNotExist: -# pass -# return username -# - def clean_email(self): - err_logger = logging.getLogger('seminar.prihlaska.problem') - email = self.cleaned_data.get('email') - try: - Osoba.objects.exclude(user__username=self.username).get(email=email) - msg = "Email {} exists (in edit)".format(email) - err_logger.info(msg) - raise forms.ValidationError('Email je již použit') - - except ObjectDoesNotExist: - pass - return email - #def clean(self): - # super().clean() - # - # err_logger = logging.getLogger('seminar.prihlaska.problem') - - # data = self.cleaned_data - # if data.get('password') != data.get('password_check'): - # self.add_error('password_check',forms.ValidationError('Hesla se neshodují')) - # if data.get('stat') != '' and data.get('stat_text') != '': - # self.add_error('stat',forms.ValidationError('Nelze mít vybraný stát z menu a zároven zapsaný textem')) - # if data.get('skola') and (data.get('skola_nazev') or data.get('skola_adresa')): - # self.add_error('skola',forms.ValidationError('Pokud je škola v seznamu, nevypisujte ji ručně, pokud není, zrušte výběr ze seznamu (křížek vpravo)')) - # if not data.get('skola'): - # if data.get('skola_nazev')=='' and data.get('skola_adresa')=='': - # self.add_error('skola',forms.ValidationError('Je nutné vyplnit školu')) - # elif data.get('skola_nazev')=='': - # self.add_error('skola_nazev',forms.ValidationError('Je nutné vyplnit název školy')) - # 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 NahrajObrazekKTreeNoduForm(forms.ModelForm): class Meta: diff --git a/seminar/models/__init__.py b/seminar/models/__init__.py index 2c869390..9694f84c 100644 --- a/seminar/models/__init__.py +++ b/seminar/models/__init__.py @@ -1,2 +1,4 @@ from .models_all import * from .odevzdavatko import * +from .base import * +from .personalni 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/models_all.py b/seminar/models/models_all.py index bf749303..cb7c14e7 100644 --- a/seminar/models/models_all.py +++ b/seminar/models/models_all.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- import os -import random import subprocess import pathlib import tempfile @@ -8,21 +7,17 @@ import logging from django.contrib.sites.shortcuts import get_current_site from django.db import models -from django.contrib import auth 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.urls import reverse 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 imagekit.models import ImageSpecField +from imagekit.processors import ResizeToFit from django.utils.functional import cached_property -from django_countries.fields import CountryField from solo.models import SingletonModel from taggit.managers import TaggableManager @@ -39,393 +34,11 @@ 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) +from . import personalni as pm - 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() +from .base import SeminarModelBase +logger = logging.getLogger(__name__) @reversion.register(ignore_duplicates=True) @@ -703,59 +316,6 @@ class Cislo(SeminarModelBase): 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): @@ -783,10 +343,10 @@ class Soustredeni(SeminarModelBase): 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í', + 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(Organizator, + organizatori = models.ManyToManyField(pm.Organizator, verbose_name='Organizátoři soustředění', help_text='Seznam organizátorů soustředění', through='Soustredeni_Organizatori') @@ -865,15 +425,15 @@ class Problem(SeminarModelBase,PolymorphicModel): 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', + 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(Organizator, verbose_name='garant zadaného problému', + 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(Organizator, verbose_name='opravovatelé', + 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='', @@ -1126,7 +686,7 @@ class Pohadka(SeminarModelBase): id = models.AutoField(primary_key=True) autor = models.ForeignKey( - Organizator, + pm.Organizator, verbose_name="Autor pohádky", # Při nahrávání z TeXu není vyplnění vyžadováno, v adminu je @@ -1171,7 +731,7 @@ class Soustredeni_Ucastnici(SeminarModelBase): # Interní ID id = models.AutoField(primary_key = True) - resitel = models.ForeignKey(Resitel, verbose_name='řešitel', on_delete=models.PROTECT) + 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) @@ -1196,7 +756,7 @@ class Soustredeni_Organizatori(SeminarModelBase): # Interní ID id = models.AutoField(primary_key = True) - organizator = models.ForeignKey(Organizator, verbose_name='organizátor', + organizator = models.ForeignKey(pm.Organizator, verbose_name='organizátor', on_delete=models.PROTECT) soustredeni = models.ForeignKey(Soustredeni, verbose_name='soustředění', @@ -1225,7 +785,7 @@ class Konfera(Problem): 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', + 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í', @@ -1266,7 +826,7 @@ class Konfery_Ucastnici(models.Model): # Interní ID id = models.AutoField(primary_key = True) - resitel = models.ForeignKey(Resitel, verbose_name='řešitel', on_delete=models.PROTECT) + resitel = models.ForeignKey(pm.Resitel, verbose_name='řešitel', on_delete=models.PROTECT) konfera = models.ForeignKey(Konfera, verbose_name='konfera', on_delete=models.CASCADE) @@ -1452,7 +1012,7 @@ class OrgTextNode(TreeNode): verbose_name = 'Organizátorský článek (Node)' verbose_name_plural = 'Organizátorské články (Node)' - organizator = models.ForeignKey(Organizator, + organizator = models.ForeignKey(pm.Organizator, null=False, blank=False, on_delete=models.DO_NOTHING, @@ -1598,7 +1158,7 @@ class Novinky(models.Model): ], options={'quality': 95}) - autor = models.ForeignKey(Organizator, verbose_name='Autor novinky', null=True, + autor = models.ForeignKey(pm.Organizator, verbose_name='Autor novinky', null=True, on_delete=models.SET_NULL) zverejneno = models.BooleanField('Zveřejněno', default=False) diff --git a/seminar/models/odevzdavatko.py b/seminar/models/odevzdavatko.py index ce6f88a7..f922e19a 100644 --- a/seminar/models/odevzdavatko.py +++ b/seminar/models/odevzdavatko.py @@ -10,6 +10,8 @@ from django.utils import timezone from django.conf import settings from seminar.models import models_all as am +from seminar.models import personalni as pm +from seminar.models.base import SeminarModelBase @reversion.register(ignore_duplicates=True) @@ -29,7 +31,7 @@ class Reseni(am.SeminarModelBase): problem = models.ManyToManyField(am.Problem, verbose_name='problém', help_text='Problém', through='Hodnoceni') - resitele = models.ManyToManyField(am.Resitel, verbose_name='autoři řešení', + resitele = models.ManyToManyField(pm.Resitel, verbose_name='autoři řešení', help_text='Seznam autorů řešení', through='Reseni_Resitele') @@ -160,7 +162,7 @@ class Reseni_Resitele(models.Model): # Interní ID id = models.AutoField(primary_key = True) - resitele = models.ForeignKey(am.Resitel, verbose_name='řešitel', on_delete=models.PROTECT) + 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) 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/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/urls.py b/seminar/urls.py index 4a94e27b..8cc333e0 100644 --- a/seminar/urls.py +++ b/seminar/urls.py @@ -1,5 +1,4 @@ from django.urls import path, include, re_path -from django.contrib.auth.decorators import login_required from . import views from .utils import org_required @@ -107,23 +106,6 @@ urlpatterns = [ 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/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'), re_path(r'^temp/vue/.*$',views.VueTestView.as_view(),name='vue_test_view'), path('temp/image_upload/', views.NahrajObrazekKTreeNoduView.as_view()), diff --git a/seminar/views/views_all.py b/seminar/views/views_all.py index a646d98f..edc0b71a 100644 --- a/seminar/views/views_all.py +++ b/seminar/views/views_all.py @@ -1,3 +1,4 @@ +from django.forms import model_to_dict from django.shortcuts import get_object_or_404, render, redirect from django.http import HttpResponse, JsonResponse from django.urls import reverse @@ -6,20 +7,16 @@ from django.views import generic from django.utils.translation import ugettext as _ from django.http import Http404 from django.db.models import Q, Sum, Count -from django.views.decorators.debug import sensitive_post_parameters from django.views.generic.edit import CreateView -from django.views.generic.base import TemplateView, RedirectView -from django.contrib.auth.models import User, Permission, Group +from django.views.generic.base import RedirectView from django.contrib.auth.mixins import LoginRequiredMixin -from django.db import transaction from django.core.exceptions import PermissionDenied import seminar.models as s import seminar.models as m -from seminar.models import Problem, Cislo, Reseni, Nastaveni, Rocnik, Soustredeni, Organizator, Resitel, Novinky, Soustredeni_Ucastnici, Tema, Clanek, Osoba # Tohle je stare a chceme se toho zbavit. Pouzivejte s.ToCoChci +from seminar.models import Problem, Cislo, Reseni, Nastaveni, Rocnik, Soustredeni, Organizator, Resitel, Novinky, Soustredeni_Ucastnici, Tema, Clanek # 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 @@ -41,9 +38,7 @@ import csv import logging import time -from seminar.utils import aktivniResitele, 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): @@ -825,51 +820,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 @@ -1014,15 +964,6 @@ def StavDatabazeView(request): 'jmena_zen': utils.histogram([r.osoba.jmeno for r in zeny]), }) - -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 @@ -1031,216 +972,6 @@ class ResitelView(LoginRequiredMixin,generic.DetailView): # - 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)) - -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' @@ -1268,17 +999,6 @@ class NahrajObrazekKTreeNoduView(LoginRequiredMixin, CreateView): 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' From 1b3a04be144eb3aae36afef7089dac712f43268b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=C3=A1=C5=A1=20Havelka?= Date: Fri, 8 Oct 2021 18:58:00 +0200 Subject: [PATCH 04/15] Move soustredeni do aplikace soustredeni --- mamweb/settings_common.py | 1 + mamweb/urls.py | 5 +- seminar/admin.py | 35 --- seminar/models/__init__.py | 1 + seminar/models/models_all.py | 194 ---------------- seminar/models/soustredeni.py | 214 ++++++++++++++++++ seminar/urls.py | 31 --- seminar/views/views_all.py | 50 +--- soustredeni/__init__.py | 5 + soustredeni/admin.py | 43 ++++ soustredeni/apps.py | 5 + soustredeni/migrations/__init__.py | 0 .../soustredeni/maily_ucastniku.txt | 0 .../soustredeni/seznam_soustredeni.html | 0 .../soustredeni/seznam_ucastniku.html | 0 .../templates}/soustredeni/ucastnici.tex | 0 soustredeni/urls.py | 35 +++ soustredeni/views.py | 55 +++++ 18 files changed, 364 insertions(+), 310 deletions(-) create mode 100644 seminar/models/soustredeni.py create mode 100644 soustredeni/__init__.py create mode 100644 soustredeni/admin.py create mode 100644 soustredeni/apps.py create mode 100644 soustredeni/migrations/__init__.py rename {seminar/templates/seminar => soustredeni/templates}/soustredeni/maily_ucastniku.txt (100%) rename {seminar/templates/seminar => soustredeni/templates}/soustredeni/seznam_soustredeni.html (100%) rename {seminar/templates/seminar => soustredeni/templates}/soustredeni/seznam_ucastniku.html (100%) rename {seminar/templates/seminar => soustredeni/templates}/soustredeni/ucastnici.tex (100%) create mode 100644 soustredeni/urls.py create mode 100644 soustredeni/views.py diff --git a/mamweb/settings_common.py b/mamweb/settings_common.py index c7146455..718d0a3a 100644 --- a/mamweb/settings_common.py +++ b/mamweb/settings_common.py @@ -141,6 +141,7 @@ INSTALLED_APPS = ( 'odevzdavatko', 'vysledkovky', 'personalni', + 'soustredeni', # Admin upravy: diff --git a/mamweb/urls.py b/mamweb/urls.py index ef0aa449..3a0eb5d2 100644 --- a/mamweb/urls.py +++ b/mamweb/urls.py @@ -22,10 +22,13 @@ urlpatterns = [ # Korekturovaci aplikace (ma vlastni podadresare) path('', include('korektury.urls')), - + # Prednaskova aplikace (ma vlastni podadresare) path('', include('prednasky.urls')), + # Soustredkova aplikace (ma vlastni podadresare) + path('', include('soustredeni.urls')), + # Personalni aplikace (ma vlastni podadresare) # (profil, osobní údaje, ..., ne autentizace, viz dále) path('', include('personalni.urls')), diff --git a/seminar/admin.py b/seminar/admin.py index 98a65db2..15e60c28 100644 --- a/seminar/admin.py +++ b/seminar/admin.py @@ -150,41 +150,6 @@ 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] - admin.site.register(m.Pohadka) admin.site.register(m.Obrazek) diff --git a/seminar/models/__init__.py b/seminar/models/__init__.py index 9694f84c..714e2caa 100644 --- a/seminar/models/__init__.py +++ b/seminar/models/__init__.py @@ -2,3 +2,4 @@ from .models_all import * from .odevzdavatko import * from .base import * from .personalni import * +from .soustredeni import * diff --git a/seminar/models/models_all.py b/seminar/models/models_all.py index cb7c14e7..129b1d1b 100644 --- a/seminar/models/models_all.py +++ b/seminar/models/models_all.py @@ -317,68 +317,6 @@ class Cislo(SeminarModelBase): raise ValidationError({'datum_deadline_soustredeni': "Soustřeďkový deadline musí předcházet finálnímu deadlinu"}) -@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(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) # Pozor na následující řádek. *Nekrmit, asi kouše!* class Problem(SeminarModelBase,PolymorphicModel): @@ -658,20 +596,6 @@ def aux_generate_filename(self, filename): 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) - ) - -## - class Pohadka(SeminarModelBase): """Kus pohádky před/za úlohou v čísle""" @@ -718,124 +642,6 @@ class Pohadka(SeminarModelBase): # 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(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 - - - -@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(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 class Obrazek(SeminarModelBase): class Meta: diff --git a/seminar/models/soustredeni.py b/seminar/models/soustredeni.py new file mode 100644 index 00000000..06a87ece --- /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 models_all 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/urls.py b/seminar/urls.py index 8cc333e0..8f6a04e5 100644 --- a/seminar/urls.py +++ b/seminar/urls.py @@ -28,32 +28,6 @@ urlpatterns = [ #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. path('aktualni/zadani/', views.AktualniZadaniView, name='seminar_aktualni_zadani'), @@ -101,11 +75,6 @@ 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' - ), re_path(r'^temp/vue/.*$',views.VueTestView.as_view(),name='vue_test_view'), path('temp/image_upload/', views.NahrajObrazekKTreeNoduView.as_view()), diff --git a/seminar/views/views_all.py b/seminar/views/views_all.py index edc0b71a..8dc8c0ee 100644 --- a/seminar/views/views_all.py +++ b/seminar/views/views_all.py @@ -14,7 +14,7 @@ from django.core.exceptions import PermissionDenied import seminar.models as s import seminar.models as m -from seminar.models import Problem, Cislo, Reseni, Nastaveni, Rocnik, Soustredeni, Organizator, Resitel, Novinky, Soustredeni_Ucastnici, Tema, Clanek # 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 import seminar.forms as f @@ -34,7 +34,6 @@ import os import os.path as op from django.conf import settings import unicodedata -import csv import logging import time @@ -850,53 +849,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): 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 From 543265c7afe1ac0fe9749dc390b61c7aac66f8d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=C3=A1=C5=A1=20Havelka?= Date: Fri, 8 Oct 2021 19:05:11 +0200 Subject: [PATCH 05/15] Move pomocne z models_all.py --- seminar/models/__init__.py | 1 + seminar/models/models_all.py | 52 +--------------------------- seminar/models/pomocne.py | 67 ++++++++++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+), 51 deletions(-) create mode 100644 seminar/models/pomocne.py diff --git a/seminar/models/__init__.py b/seminar/models/__init__.py index 714e2caa..cb55c941 100644 --- a/seminar/models/__init__.py +++ b/seminar/models/__init__.py @@ -3,3 +3,4 @@ from .odevzdavatko import * from .base import * from .personalni import * from .soustredeni import * +from .pomocne import * diff --git a/seminar/models/models_all.py b/seminar/models/models_all.py index 129b1d1b..64dde9a1 100644 --- a/seminar/models/models_all.py +++ b/seminar/models/models_all.py @@ -37,6 +37,7 @@ from seminar.utils import aktivniResitele from . import personalni as pm from .base import SeminarModelBase +from .pomocne import Text logger = logging.getLogger(__name__) @@ -507,32 +508,7 @@ class Clanek(Problem): 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' @@ -643,32 +619,6 @@ class Pohadka(SeminarModelBase): pass -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" 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 From 7954d6d59f25c3905745efa3089f7b8944ca30e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=C3=A1=C5=A1=20Havelka?= Date: Mon, 1 Nov 2021 23:50:41 +0100 Subject: [PATCH 06/15] =?UTF-8?q?Delete=20star=C3=BD=20autocomplete?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- seminar/autocomplete_light_registry.py.old | 139 --------------------- 1 file changed, 139 deletions(-) delete mode 100644 seminar/autocomplete_light_registry.py.old 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) - - From babfd9c25d725d439430dc20c51c9363df60092c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=C3=A1=C5=A1=20Havelka?= Date: Sun, 7 Nov 2021 10:25:34 +0100 Subject: [PATCH 07/15] Move treenode do aplikace treenode --- mamweb/settings_common.py | 1 + mamweb/urls.py | 5 +- seminar/admin.py | 84 ----- .../0001_squashed_0098_auto_20210906_0305.py | 2 +- seminar/migrations/0084_clanek_cislo.py | 2 +- seminar/models/models_all.py | 4 +- seminar/testutils.py | 6 +- seminar/urls.py | 13 - seminar/utils.py | 2 +- seminar/views/__init__.py | 1 - seminar/views/views_all.py | 184 +--------- treenode/__init__.py | 0 treenode/admin.py | 88 +++++ treenode/apps.py | 5 + {seminar => treenode}/forms.py | 0 treenode/migrations/__init__.py | 0 {seminar => treenode}/permissions.py | 0 {mamweb => treenode}/routers.py | 2 +- .../views_rest.py => treenode/serializers.py | 2 +- .../static/treenode}/treenode_editor.js | 0 .../templates/treenode}/orphanage.html | 0 .../templates/treenode}/treenode.html | 0 .../treenode}/treenode_add_stub.html | 0 .../templates/treenode}/treenode_name.html | 0 .../treenode}/treenode_recursive.html | 0 .../templates/treenode}/vuetest.html | 0 .../treenodes.py => treenode/templatetags.py | 2 +- seminar/tests_treelib.py => treenode/tests.py | 2 +- {seminar => treenode}/treelib.py | 0 treenode/urls.py | 18 + treenode/views.py | 322 ++++++++++++++++++ {seminar => treenode}/viewsets.py | 6 +- 32 files changed, 459 insertions(+), 292 deletions(-) create mode 100644 treenode/__init__.py create mode 100644 treenode/admin.py create mode 100644 treenode/apps.py rename {seminar => treenode}/forms.py (100%) create mode 100644 treenode/migrations/__init__.py rename {seminar => treenode}/permissions.py (100%) rename {mamweb => treenode}/routers.py (94%) rename seminar/views/views_rest.py => treenode/serializers.py (99%) rename {seminar/static/seminar => treenode/static/treenode}/treenode_editor.js (100%) rename {seminar/templates/seminar => treenode/templates/treenode}/orphanage.html (100%) rename {seminar/templates/seminar => treenode/templates/treenode}/treenode.html (100%) rename {seminar/templates/seminar => treenode/templates/treenode}/treenode_add_stub.html (100%) rename {seminar/templates/seminar => treenode/templates/treenode}/treenode_name.html (100%) rename {seminar/templates/seminar => treenode/templates/treenode}/treenode_recursive.html (100%) rename {seminar/templates/seminar => treenode/templates/treenode}/vuetest.html (100%) rename seminar/templatetags/treenodes.py => treenode/templatetags.py (98%) rename seminar/tests_treelib.py => treenode/tests.py (98%) rename {seminar => treenode}/treelib.py (100%) create mode 100644 treenode/urls.py create mode 100644 treenode/views.py rename {seminar => treenode}/viewsets.py (98%) diff --git a/mamweb/settings_common.py b/mamweb/settings_common.py index 718d0a3a..fa070a86 100644 --- a/mamweb/settings_common.py +++ b/mamweb/settings_common.py @@ -142,6 +142,7 @@ INSTALLED_APPS = ( 'vysledkovky', 'personalni', 'soustredeni', + 'treenode', # Admin upravy: diff --git a/mamweb/urls.py b/mamweb/urls.py index 3a0eb5d2..593bddde 100644 --- a/mamweb/urls.py +++ b/mamweb/urls.py @@ -6,7 +6,7 @@ from django.views.generic.base import TemplateView from django import views from django.urls import path # As per docs. -from .routers import router +from treenode.routers import router urlpatterns = [ @@ -39,6 +39,9 @@ urlpatterns = [ # Api (ma vlastni podadresare) (autocomplete apod.) path('', include('api.urls')), + # treenode (ma vlastni podadresare) + path('', include('treenode.urls')), + # Aesop (ma vlastni podadresare) path('', include('aesop.urls')), diff --git a/seminar/admin.py b/seminar/admin.py index 15e60c28..aa1310b7 100644 --- a/seminar/admin.py +++ b/seminar/admin.py @@ -12,7 +12,6 @@ from seminar.utils import hlavni_problem # Todo: reversion import seminar.models as m -import seminar.treelib as tl admin.site.register(m.Rocnik) @@ -153,88 +152,5 @@ class ResitelInline(admin.TabularInline): 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/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/models_all.py b/seminar/models/models_all.py index 64dde9a1..032139ee 100644 --- a/seminar/models/models_all.py +++ b/seminar/models/models_all.py @@ -25,7 +25,7 @@ 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 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é) @@ -728,7 +728,7 @@ class MezicisloNode(TreeNode): # TODO: Využít TreeLib def aktualizuj_nazev(self): - from seminar.treelib import safe_pred + 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): 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 8f6a04e5..182bbe7e 100644 --- a/seminar/urls.py +++ b/seminar/urls.py @@ -17,16 +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'), # Zadani # path('aktualni/zadani/', views.AktualniZadaniView.as_view(), name='seminar_aktualni_zadani'), # Dočasně ad-hoc jednoduchá věc. @@ -76,9 +66,6 @@ urlpatterns = [ org_required(views.OdmenyView.as_view()), name="seminar_archiv_odmeny"), - 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'), ] 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 22c60734..8db4424b 100644 --- a/seminar/views/__init__.py +++ b/seminar/views/__init__.py @@ -1,5 +1,4 @@ from .views_all import * -from .views_rest import * # Dočsasné views from .docasne import * diff --git a/seminar/views/views_all.py b/seminar/views/views_all.py index 8dc8c0ee..fe0d42de 100644 --- a/seminar/views/views_all.py +++ b/seminar/views/views_all.py @@ -1,25 +1,22 @@ -from django.forms import model_to_dict -from django.shortcuts import get_object_or_404, render, redirect -from django.http import HttpResponse, JsonResponse +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 from django.db.models import Q, Sum, Count -from django.views.generic.edit import CreateView from django.views.generic.base import RedirectView -from django.contrib.auth.mixins import LoginRequiredMixin from django.core.exceptions import PermissionDenied import seminar.models as s import seminar.models as m from seminar.models 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 -import seminar.forms as f -import seminar.templatetags.treenodes as tnltt -import seminar.views.views_rest as vr +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 @@ -209,141 +206,6 @@ class TNLData(object): 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): @@ -916,40 +778,6 @@ def StavDatabazeView(request): 'jmena_zen': utils.histogram([r.osoba.jmeno for r in zeny]), }) -### 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 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}) - # Interní, nemá se nikdy objevit v urls (jinak to účastníci vytrolí) def formularOKView(request, text=''): 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/seminar/forms.py b/treenode/forms.py similarity index 100% rename from seminar/forms.py rename to treenode/forms.py 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. """ From 970352322c057965c698ba734b5de7eb0e592556 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=C3=A1=C5=A1=20Havelka?= Date: Sun, 7 Nov 2021 10:47:39 +0100 Subject: [PATCH 08/15] =?UTF-8?q?Move=20treenode=20modely=20do=20vlastn?= =?UTF-8?q?=C3=ADho=20souboru?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- seminar/models/__init__.py | 1 + seminar/models/models_all.py | 257 +------------------------------ seminar/models/odevzdavatko.py | 3 +- seminar/models/treenode.py | 266 +++++++++++++++++++++++++++++++++ 4 files changed, 274 insertions(+), 253 deletions(-) create mode 100644 seminar/models/treenode.py diff --git a/seminar/models/__init__.py b/seminar/models/__init__.py index cb55c941..a78eccf7 100644 --- a/seminar/models/__init__.py +++ b/seminar/models/__init__.py @@ -4,3 +4,4 @@ from .base import * from .personalni import * from .soustredeni import * from .pomocne import * +from .treenode import * diff --git a/seminar/models/models_all.py b/seminar/models/models_all.py index 032139ee..47777bb4 100644 --- a/seminar/models/models_all.py +++ b/seminar/models/models_all.py @@ -12,7 +12,6 @@ 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.contrib.contenttypes.models import ContentType from django.utils.text import get_valid_filename from imagekit.models import ImageSpecField from imagekit.processors import ResizeToFit @@ -23,7 +22,7 @@ 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 roman from seminar.utils import hlavni_problem from treenode import treelib @@ -37,7 +36,6 @@ from seminar.utils import aktivniResitele from . import personalni as pm from .base import SeminarModelBase -from .pomocne import Text logger = logging.getLogger(__name__) @@ -304,6 +302,7 @@ class Cislo(SeminarModelBase): 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): @@ -480,6 +479,7 @@ class Tema(Problem): 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) @@ -557,7 +557,8 @@ class Uloha(Problem): pass def cislo_node(self): - zadani_node = self.ulohazadaninode + zadani_node = self.ulohazadaninode + from seminar.models.treenode import CisloNode return treelib.get_upper_node_of_type(zadani_node, CisloNode) @@ -619,254 +620,6 @@ class Pohadka(SeminarModelBase): pass -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 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(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(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) - - @reversion.register(ignore_duplicates=True) class Nastaveni(SingletonModel): diff --git a/seminar/models/odevzdavatko.py b/seminar/models/odevzdavatko.py index f922e19a..e450712e 100644 --- a/seminar/models/odevzdavatko.py +++ b/seminar/models/odevzdavatko.py @@ -11,6 +11,7 @@ from django.conf import settings from seminar.models import models_all as am from seminar.models import personalni as pm +from seminar.models import treenode as tm from seminar.models.base import SeminarModelBase @@ -173,7 +174,7 @@ class Reseni_Resitele(models.Model): return '{} od {}'.format(self.reseni, self.resitel) # NOTE: Poteciální DB HOG bez select_related -class ReseniNode(am.TreeNode): +class ReseniNode(tm.TreeNode): class Meta: db_table = 'seminar_nodes_otistene_reseni' verbose_name = 'Otištěné řešení (Node)' diff --git a/seminar/models/treenode.py b/seminar/models/treenode.py new file mode 100644 index 00000000..012fd097 --- /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 models_all 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) From 4009aedff09cfb9c360c9d36916b4c4874a684b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=C3=A1=C5=A1=20Havelka?= Date: Sun, 7 Nov 2021 11:13:55 +0100 Subject: [PATCH 09/15] =?UTF-8?q?Move=20rozd=C4=9Blen=C3=AD=20tvorby=20a?= =?UTF-8?q?=20novinek=20v=20models?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- seminar/models/__init__.py | 3 +- seminar/models/novinky.py | 38 +++++++++++++++++++++ seminar/models/odevzdavatko.py | 2 +- seminar/models/soustredeni.py | 2 +- seminar/models/treenode.py | 2 +- seminar/models/{models_all.py => tvorba.py} | 34 ------------------ 6 files changed, 43 insertions(+), 38 deletions(-) create mode 100644 seminar/models/novinky.py rename seminar/models/{models_all.py => tvorba.py} (95%) diff --git a/seminar/models/__init__.py b/seminar/models/__init__.py index a78eccf7..34712ee4 100644 --- a/seminar/models/__init__.py +++ b/seminar/models/__init__.py @@ -1,7 +1,8 @@ -from .models_all import * +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/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 index e450712e..51288b30 100644 --- a/seminar/models/odevzdavatko.py +++ b/seminar/models/odevzdavatko.py @@ -9,7 +9,7 @@ from django.urls import reverse_lazy from django.utils import timezone from django.conf import settings -from seminar.models import models_all as am +from seminar.models import tvorba as am from seminar.models import personalni as pm from seminar.models import treenode as tm from seminar.models.base import SeminarModelBase diff --git a/seminar/models/soustredeni.py b/seminar/models/soustredeni.py index 06a87ece..1d984948 100644 --- a/seminar/models/soustredeni.py +++ b/seminar/models/soustredeni.py @@ -11,7 +11,7 @@ from django.conf import settings from . import personalni as pm from .base import SeminarModelBase -from seminar.models import models_all as am +from seminar.models import tvorba as am logger = logging.getLogger(__name__) diff --git a/seminar/models/treenode.py b/seminar/models/treenode.py index 012fd097..50261d1a 100644 --- a/seminar/models/treenode.py +++ b/seminar/models/treenode.py @@ -15,7 +15,7 @@ from .pomocne import Text logger = logging.getLogger(__name__) -from seminar.models import models_all as am +from seminar.models import tvorba as am class TreeNode(PolymorphicModel): class Meta: diff --git a/seminar/models/models_all.py b/seminar/models/tvorba.py similarity index 95% rename from seminar/models/models_all.py rename to seminar/models/tvorba.py index 47777bb4..866f2cbf 100644 --- a/seminar/models/models_all.py +++ b/seminar/models/tvorba.py @@ -13,8 +13,6 @@ 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 imagekit.models import ImageSpecField -from imagekit.processors import ResizeToFit from django.utils.functional import cached_property from solo.models import SingletonModel @@ -645,35 +643,3 @@ class Nastaveni(SingletonModel): 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(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) + '] ' From 137ad8863c1bd05be3315a22977c55770ef85b97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=C3=A1=C5=A1=20Havelka?= Date: Sun, 7 Nov 2021 11:16:15 +0100 Subject: [PATCH 10/15] =?UTF-8?q?Zm=C4=9Bna=20importu=20(odstran=C4=9Bn=20?= =?UTF-8?q?proxy=20import)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- seminar/models/odevzdavatko.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/seminar/models/odevzdavatko.py b/seminar/models/odevzdavatko.py index 51288b30..343f92f0 100644 --- a/seminar/models/odevzdavatko.py +++ b/seminar/models/odevzdavatko.py @@ -12,11 +12,11 @@ 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.base import SeminarModelBase +from seminar.models import base as bm @reversion.register(ignore_duplicates=True) -class Reseni(am.SeminarModelBase): +class Reseni(bm.SeminarModelBase): class Meta: db_table = 'seminar_reseni' @@ -85,7 +85,7 @@ class Reseni(am.SeminarModelBase): # self.cislo_body = self.problem.cislo_reseni # super(Reseni, self).save(*args, **kwargs) -class Hodnoceni(am.SeminarModelBase): +class Hodnoceni(bm.SeminarModelBase): class Meta: db_table = 'seminar_hodnoceni' verbose_name = 'Hodnocení' @@ -117,7 +117,7 @@ def generate_filename(self, filename): @reversion.register(ignore_duplicates=True) -class PrilohaReseni(am.SeminarModelBase): +class PrilohaReseni(bm.SeminarModelBase): class Meta: db_table = 'seminar_priloha_reseni' From 27776130e00d789eb65bbe4509388ea8c9683907 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=C3=A1=C5=A1=20Havelka?= Date: Sun, 7 Nov 2021 11:51:12 +0100 Subject: [PATCH 11/15] =?UTF-8?q?Delete=20zbytky=20po=20star=C3=A9m=20zp?= =?UTF-8?q?=C5=AFsobu=20=C5=99e=C5=A1en=C3=AD=20men=C3=AD=C4=8Dka?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- seminar/templatetags/mam_menu.py | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 seminar/templatetags/mam_menu.py 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() From 2bcbf34d8e66de7c2aa9369d9eedf49e0265b061 Mon Sep 17 00:00:00 2001 From: "Tomas \"Jethro\" Pokorny" Date: Mon, 22 Nov 2021 19:40:09 +0100 Subject: [PATCH 12/15] Makefile | fix sync_prod_flatpages Chybela aktivace venvu. --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index bc272aa8..49a888dc 100644 --- a/Makefile +++ b/Makefile @@ -102,7 +102,7 @@ deploy_prod: venv_check sync_prod_flatpages: venv_check @echo Downloading current version of flatpages from mamweb-prod. ssh mam-web@gimli.ms.mff.cuni.cz \ - "cd /akce/mam/www/mamweb-prod; ./manage.py dumpdata flatpages --indent=2 > flat.json" + "cd /akce/mam/www/mamweb-prod; . env/bin/activate; ./manage.py dumpdata flatpages --indent=2 > flat.json" rsync -ave ssh mam-web@gimli.ms.mff.cuni.cz:/akce/mam/www/mamweb-prod/flat.json ./flat.json @echo "Applying downloaded flatpages." ./manage.py loaddata flat.json From 21817389eb89d2879b3a2e4d4aacb71cf1c38018 Mon Sep 17 00:00:00 2001 From: "Tomas \"Jethro\" Pokorny" Date: Mon, 22 Nov 2021 19:44:10 +0100 Subject: [PATCH 13/15] =?UTF-8?q?Fix=20#1181=20-=20zve=C5=99ejn=C4=9Bn?= =?UTF-8?q?=C3=A9=20=C4=8D=C3=ADslo=20mus=C3=AD=20m=C3=ADt=20datum=20vyd?= =?UTF-8?q?=C3=A1n=C3=AD.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- seminar/admin.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/seminar/admin.py b/seminar/admin.py index 9cecb1b6..d05fbfec 100644 --- a/seminar/admin.py +++ b/seminar/admin.py @@ -58,6 +58,10 @@ class CisloForm(ModelForm): if errors: errors.append(ValidationError(mark_safe( 'Pokud chceš učinit všechny problémy, co nejsou zadané ani vyřešené, zadanými a číslo zveřejnit, můžeš to udělat pomocí akce v seznamu čísel'))) + if self.cleaned_data.get('datum_vydani') == None: + self.add_error('datum_vydani','Číslo určené ke zveřejnění nemá nastavené datum vydání') + + if errors: raise ValidationError(errors) return self.cleaned_data From 0271b087b133e621526c9e3c03d6b347cc895c5a Mon Sep 17 00:00:00 2001 From: "Tomas \"Jethro\" Pokorny" Date: Mon, 22 Nov 2021 19:50:55 +0100 Subject: [PATCH 14/15] =?UTF-8?q?Fix=20#1047=20=20-=20nepou=C5=BE=C3=ADvan?= =?UTF-8?q?=C3=BD=20soubor=20smaz=C3=A1n.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../seminar/archiv/cislo-normal.html | 97 ------------------- 1 file changed, 97 deletions(-) delete mode 100644 seminar/templates/seminar/archiv/cislo-normal.html diff --git a/seminar/templates/seminar/archiv/cislo-normal.html b/seminar/templates/seminar/archiv/cislo-normal.html deleted file mode 100644 index e23da09c..00000000 --- a/seminar/templates/seminar/archiv/cislo-normal.html +++ /dev/null @@ -1,97 +0,0 @@ -{% extends "seminar/archiv/base_cisla.html" %} - -{# {% block content %} -
- -

- {% block nadpis1a %}{% block nadpis1b %} - Číslo {{ cislo }} - {% endblock %}{% endblock %} -

- - {% if cislo.pdf %} -

Číslo v pdf - {% endif %} -

Ročník {{ cislo.rocnik }} - - {% if v_cisle_zadane %} -

Zadané problémy

- - {% endif %} - - {% if resene_problemy %} -

Řešené problémy

- - {% endif %} - - {% if user.is_staff %} -
-

Orgovské odkazy

- -
- {% endif %} - - {% if cislo.verejna_vysledkovka %} -

Výsledkovka

- {% else %} - {% if user.is_staff %} -
-

Výsledkovka (neveřejná)

- {% endif %} - {% endif %} - - {% if cislo.verejna_vysledkovka or user.is_staff %} - - - - - {% endfor %} -
# - Jméno #} - {# problémy by měly být veřejné, když je veřejná výsledkovka #} -{# {% for p in problemy %} - {{ p.kod_v_rocniku }} - {% endfor %} - Za číslo - Za ročník - Odjakživa - {% for rv in radky_vysledkovky %} -
{% autoescape off %}{{ rv.poradi }}{% endautoescape %} - - {% if rv.resitel.titul != "" %} - {{ rv.resitel.titul }}MM - {% endif %} - {{ rv.resitel.osoba.plne_jmeno }} - {% for b in rv.hlavni_problemy_body %} - {{ b }} - {% endfor %} - {{ rv.body_cislo }} - {{ rv.body_rocnik }} - {{ rv.body_celkem_odjakziva }} -
- {% endif %} - - {% if not cislo.verejna_vysledkovka and user.is_staff %} -
- {% endif %} - -
-{% endblock content %} #} - From bea7399fe277c2acc2927e49295fe296cb0704c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=C3=A1=C5=A1=20Havelka?= Date: Mon, 22 Nov 2021 20:11:50 +0100 Subject: [PATCH 15/15] Fix merge --- .../templates}/odevzdavatko/detail_resitele.html | 0 odevzdavatko/urls.py | 2 ++ personalni/admin.py | 10 ++++++---- seminar/models/tvorba.py | 7 ++++++- 4 files changed, 14 insertions(+), 5 deletions(-) rename {seminar/templates/seminar => odevzdavatko/templates}/odevzdavatko/detail_resitele.html (100%) diff --git a/seminar/templates/seminar/odevzdavatko/detail_resitele.html b/odevzdavatko/templates/odevzdavatko/detail_resitele.html similarity index 100% rename from seminar/templates/seminar/odevzdavatko/detail_resitele.html rename to odevzdavatko/templates/odevzdavatko/detail_resitele.html diff --git a/odevzdavatko/urls.py b/odevzdavatko/urls.py index 477beb2b..bb04f06a 100644 --- a/odevzdavatko/urls.py +++ b/odevzdavatko/urls.py @@ -15,4 +15,6 @@ urlpatterns = [ 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/personalni/admin.py b/personalni/admin.py index 113fb99f..36a4ae6e 100644 --- a/personalni/admin.py +++ b/personalni/admin.py @@ -30,13 +30,15 @@ class OsobaAdmin(admin.ModelAdmin): org.save() udelej_orgem.short_description = "Udělej vybraných osob organizátory" -@admin.register(m.Organizator) -class OrganizatorAdmin(admin.ModelAdmin): - search_fields = ['osoba__jmeno', 'osoba__prijmeni', 'osoba__prezdivka'] - 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'] diff --git a/seminar/models/tvorba.py b/seminar/models/tvorba.py index 866f2cbf..d123e021 100644 --- a/seminar/models/tvorba.py +++ b/seminar/models/tvorba.py @@ -72,7 +72,12 @@ class Rocnik(SeminarModelBase): 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)