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'