diff --git a/mamweb/settings_local.py b/mamweb/settings_local.py index 517772ee..5a2aa969 100644 --- a/mamweb/settings_local.py +++ b/mamweb/settings_local.py @@ -87,3 +87,5 @@ LOGGING = { # set to 'DEBUG' for EXTRA verbose output # LOGGING['handlers']['console']['level'] = 'INFO' + +EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' diff --git a/requirements.txt b/requirements.txt index f2fd4306..22f8e43c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -27,6 +27,7 @@ django-crispy-forms django-imagekit django-polymorphic django-sitetree +django_reverse_admin # Comments akismet==1.0.1 diff --git a/seminar/admin.py b/seminar/admin.py index e524a19d..6a9dd815 100644 --- a/seminar/admin.py +++ b/seminar/admin.py @@ -1,20 +1,33 @@ from django.contrib import admin from polymorphic.admin import PolymorphicParentModelAdmin, PolymorphicChildModelAdmin, PolymorphicChildModelFilter +from reversion.admin import VersionAdmin +from django_reverse_admin import ReverseModelAdmin # Todo: reversion import seminar.models as m -admin.site.register(m.Osoba) admin.site.register(m.Skola) admin.site.register(m.Prijemce) -admin.site.register(m.Resitel) admin.site.register(m.Rocnik) admin.site.register(m.Cislo) admin.site.register(m.Organizator) admin.site.register(m.Soustredeni) +@admin.register(m.Osoba) +class OsobaAdmin(admin.ModelAdmin): + actions = ['synchronizuj_maily'] + + 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ů" + @admin.register(m.Problem) class ProblemAdmin(PolymorphicParentModelAdmin): base_model = m.Problem @@ -39,11 +52,35 @@ class UlohaAdmin(PolymorphicChildModelAdmin): base_model = m.Uloha show_in_index = True - +class TextAdminInline(admin.TabularInline): + model = m.Text + exclude = ['text_zkraceny_set','text_zkraceny'] admin.site.register(m.Text) -admin.site.register(m.Reseni) -admin.site.register(m.Hodnoceni) + +class ResitelInline(admin.TabularInline): + model = m.Resitel + extra = 1 +admin.site.register(m.Resitel) + +class PrilohaReseniInline(admin.TabularInline): + model = m.PrilohaReseni + extra = 1 admin.site.register(m.PrilohaReseni) + +class Reseni_ResiteleInline(admin.TabularInline): + model = m.Reseni_Resitele + +@admin.register(m.Reseni) +class ReseniAdmin(ReverseModelAdmin): + base_model = m.Reseni + inline_type = 'tabular' + inline_reverse = ['text_cely','resitele'] + exclude = ['text_zkraceny', 'text_zkraceny_set'] + inlines = [PrilohaReseniInline] +# FAIL in template +# inlines = [PrilohaReseniInline,Reseni_ResiteleInline] + +admin.site.register(m.Hodnoceni) admin.site.register(m.Pohadka) admin.site.register(m.Konfera) admin.site.register(m.Obrazek) @@ -68,6 +105,17 @@ class TreeNodeAdmin(PolymorphicParentModelAdmin): m.TextNode, ] + actions = ['aktualizuj_nazvy'] + + # XXX: nejspíš je to totální DB HOG, nechcete to použít moc často. + def aktualizuj_nazvy(self, request, queryset): + newqs = queryset.get_real_instances() + for tn in newqs: + tn.aktualizuj_nazev() + tn.save() + self.message_user(request, "Názvy aktualizovány.") + aktualizuj_nazvy.short_description = "Aktualizuj vybraným TreeNodům názvy" + @admin.register(m.RocnikNode) class RocnikNodeAdmin(PolymorphicChildModelAdmin): base_model = m.RocnikNode diff --git a/seminar/forms.py b/seminar/forms.py index 42d3c2d7..b28beeb9 100644 --- a/seminar/forms.py +++ b/seminar/forms.py @@ -121,3 +121,92 @@ class PrihlaskaForm(forms.Form): 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 EditForm(forms.Form): + username = forms.CharField(label='Přihlašovací jméno', + max_length=256, + required=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(label='Telefon',max_length=256, required=False) + datum_narozeni = forms.DateField(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) + 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.get(email=email) +# msg = "Email {} exists".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')) diff --git a/seminar/migrations/0072_auto_20191204_2257.py b/seminar/migrations/0072_auto_20191204_2257.py new file mode 100644 index 00000000..f96b670a --- /dev/null +++ b/seminar/migrations/0072_auto_20191204_2257.py @@ -0,0 +1,23 @@ +# Generated by Django 2.2.7 on 2019-12-04 21:57 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('seminar', '0071_remove_nastaveni_aktualni_rocnik'), + ] + + operations = [ + migrations.AddField( + model_name='treenode', + name='srolovatelne', + field=models.BooleanField(blank=True, help_text='Bude na stránce témátka možnost tuto položku skrýt', null=True, verbose_name='Srolovatelné'), + ), + migrations.AddField( + model_name='treenode', + name='zajimave', + field=models.BooleanField(default=False, help_text='Zobrazí se daná věc na rozcestníku témátek', verbose_name='Zajímavé'), + ), + ] diff --git a/seminar/migrations/0073_copy_osoba_email_to_user_email.py b/seminar/migrations/0073_copy_osoba_email_to_user_email.py new file mode 100644 index 00000000..3b280209 --- /dev/null +++ b/seminar/migrations/0073_copy_osoba_email_to_user_email.py @@ -0,0 +1,22 @@ +# Generated by Django 2.2.9 on 2020-01-15 21:28 + +from django.db import migrations + +def copy_mails(apps, schema_editor): + Osoba = apps.get_model('seminar', 'Osoba') + + for o in Osoba.objects.all(): + if o.user is not None: + u = o.user + u.email = o.email + u.save() + +class Migration(migrations.Migration): + + dependencies = [ + ('seminar', '0072_auto_20191204_2257'), + ] + + operations = [ + migrations.RunPython(copy_mails, migrations.RunPython.noop) + ] diff --git a/seminar/models.py b/seminar/models.py index d1bdd08c..31eec46a 100644 --- a/seminar/models.py +++ b/seminar/models.py @@ -21,9 +21,9 @@ from taggit.managers import TaggableManager from reversion import revisions as reversion -from seminar.utils import roman +from seminar.utils import roman, FirstTagParser # Pro získání úryvku z TextNode -from unidecode import unidecode +from unidecode import unidecode # Používám pro získání ID odkazu (ještě je to někde po někom zakomentované) from polymorphic.models import PolymorphicModel @@ -130,6 +130,17 @@ class Osoba(SeminarModelBase): 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. @@ -620,7 +631,7 @@ class Problem(SeminarModelBase,PolymorphicModel): id = models.AutoField(primary_key = True) # Název - nazev = models.CharField('název', max_length=256) + nazev = models.CharField('název', max_length=256) # Zveřejnitelný na stránky # Problém má podproblémy nadproblem = models.ForeignKey('self', verbose_name='nadřazený problém', @@ -785,8 +796,11 @@ class Text(SeminarModelBase): for tn in self.textnode_set.all(): tn.save() - - + def __str__(self): + parser = FirstTagParser() + parser.feed(str(self.na_web)) + return parser.firstTag + class Uloha(Problem): class Meta: db_table = 'seminar_ulohy' @@ -885,7 +899,7 @@ class Reseni(SeminarModelBase): # Konfera def __str__(self): - return "{}: {}".format(self.resitel.osoba.plne_jmeno(), self.problem.nazev) + return "{}({}): {}({})".format(self.resitele.first(),len(self.resitele.all()), self.problem.first() ,len(self.problem.all())) # NOTE: Potenciální DB HOG (bez select_related) ## Pravdepodobne uz nebude potreba: @@ -1239,8 +1253,15 @@ class TreeNode(PolymorphicModel): on_delete=models.SET_NULL, verbose_name="další element na stejné úrovni") nazev = models.TextField("název tohoto node", - help_text = "Tento název se zobrazuje v nabídkách pro výběr vhodného TreeNode", - blank=False, null=True) + help_text = "Tento název se zobrazuje v nabídkách pro výběr vhodného TreeNode", + blank=False, + null=True) # Nezveřejnitelný název na stránky - pouze do adminu + zajimave = models.BooleanField(default = False, + verbose_name = "Zajímavé", + help_text = "Zobrazí se daná věc na rozcestníku témátek") + srolovatelne = models.BooleanField(null = True, blank = True, + verbose_name = "Srolovatelné", + help_text = "Bude na stránce témátka možnost tuto položku skrýt") def print_tree(self,indent=0): print("{}TreeNode({})".format(" "*indent,self.id)) @@ -1248,7 +1269,27 @@ class TreeNode(PolymorphicModel): self.first_child.print_tree(indent=indent+2) if self.succ: self.succ.print_tree(indent=indent) - + + def getOdkazStr(self): # String na rozcestník + return self.first_child.getOdkazStr() + + def getOdkaz(self): # ID HTML tagu, na který se bude scrollovat #{{self.getOdkaz}} + # Jsem si vědom, že tu potenciálně vznikají kolize. + # Přijdou mi natolik nepravděpodobné, že je neřeším + # Chtěl jsem ale hezké odkazy + string = unidecode(self.getOdkazStr()) + returnVal = "" + i = 0 + while len(returnVal) < 16: # Max 15 znaků + if i == len(string): + break + if string[i] == " ": + returnVal += "-" + if string[i].isalnum(): + returnVal += string[i].lower() + i += 1 + return returnVal + def __str__(self): if self.nazev: return self.nazev @@ -1260,6 +1301,9 @@ class TreeNode(PolymorphicModel): self.aktualizuj_nazev() super().save(*args, **kwargs) + def aktualizuj_nazev(self): + raise NotImplementedError("Pokus o aktualizaci názvu obecného TreeNode místo konkrétní instance") + class RocnikNode(TreeNode): class Meta: db_table = 'seminar_nodes_rocnik' @@ -1284,6 +1328,9 @@ class CisloNode(TreeNode): def aktualizuj_nazev(self): self.nazev = "CisloNode: "+str(self.cislo) + def getOdkazStr(self): + return "Číslo " + str(self.cislo) + class MezicisloNode(TreeNode): class Meta: db_table = 'seminar_nodes_mezicislo' @@ -1305,6 +1352,8 @@ class MezicisloNode(TreeNode): else: print("!!!!! Nějaké neidentifikované mezičíslo !!!!!") self.nazev = "MezicisloNode: Neidentifikovatelné mezičíslo!" + def getOdkazStr(self): + return "Obsah dostupný pouze na webu" class TemaVCisleNode(TreeNode): """ Obsahuje příspěvky k tématu v daném čísle """ @@ -1319,6 +1368,9 @@ class TemaVCisleNode(TreeNode): def aktualizuj_nazev(self): self.nazev = "TemaVCisleNode: "+str(self.tema) + def getOdkazStr(self): + return str(self.tema) + class KonferaNode(TreeNode): class Meta: db_table = 'seminar_nodes_konfera' @@ -1347,6 +1399,10 @@ class ClanekNode(TreeNode): def aktualizuj_nazev(self): self.nazev = "ClanekNode: "+str(self.clanek) + def getOdkazStr(self): + return str(self.clanek) + + class UlohaZadaniNode(TreeNode): class Meta: db_table = 'seminar_nodes_uloha_zadani' @@ -1361,6 +1417,10 @@ class UlohaZadaniNode(TreeNode): def aktualizuj_nazev(self): self.nazev = "UlohaZadaniNode: "+str(self.uloha) + def getOdkazStr(self): + return str(self.uloha) + + class PohadkaNode(TreeNode): class Meta: db_table = 'seminar_nodes_pohadka' @@ -1388,6 +1448,10 @@ class UlohaVzorakNode(TreeNode): def aktualizuj_nazev(self): self.nazev = "UlohaVzorakNode: "+str(self.uloha) + def getOdkazStr(self): + return str(self.uloha) + + class TextNode(TreeNode): class Meta: db_table = 'seminar_nodes_obsah' @@ -1400,6 +1464,10 @@ class TextNode(TreeNode): def aktualizuj_nazev(self): self.nazev = "TextNode: "+str(self.text) + def getOdkazStr(self): + return str(self.text) + + ## FIXME: Logiku přesunout do views. #class VysledkyBase(SeminarModelBase): # @@ -1491,6 +1559,7 @@ class Nastaveni(SingletonModel): aktualni_cislo = models.ForeignKey(Cislo, verbose_name='poslední vydané číslo', null=False, on_delete=models.PROTECT) + @property def aktualni_rocnik(self): return self.aktualni_cislo.rocnik @@ -1534,3 +1603,35 @@ class Novinky(models.Model): return '[' + str(self.datum) + '] ' + self.text[0:50] else: return '[' + str(self.datum) + '] ' + + + + +# FIXME: Tohle nepatří do aplikace 'seminar' +# Nefunkční alternativa vestavěného Usera, který má jméno a mail v přidružené Osobě +# from django.contrib.auth.models import User as Django_User +# +# class Uzivatel(Django_User): +# class Meta: +# proxy = True +# +# @property +# def first_name(self): +# osoby = Osoba.objects.filter(user=self) +# if len(osoby) == 0: +# return None +# return osoby.first().krestni_jmeno +# +# @property +# def last_name(self): +# osoby = Osoba.objects.filter(user=self) +# if len(osoby) == 0: +# return None +# return osoby.first().prijmeni +# +# @property +# def email(self): +# osoby = Osoba.objects.filter(user=self) +# if len(osoby) == 0: +# return None +# return osoby.first().email diff --git a/seminar/templates/seminar/edit.html b/seminar/templates/seminar/edit.html new file mode 100644 index 00000000..3f3e0d99 --- /dev/null +++ b/seminar/templates/seminar/edit.html @@ -0,0 +1,78 @@ +{% extends "seminar/zadani/base.html" %} +{% load staticfiles %} + +{% block script %} + + {{form.media}} + +{% endblock %} +{% block content %} +

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

+
+ {% csrf_token %} + {{form.non_field_errors}} + + +
+ +{% endblock %} + diff --git a/seminar/templates/seminar/login.html b/seminar/templates/seminar/login.html index 88cd364f..6319ecc0 100644 --- a/seminar/templates/seminar/login.html +++ b/seminar/templates/seminar/login.html @@ -8,20 +8,13 @@ Přihlášení {% endblock %}{% endblock %} -{% if login_error %} -{{login_error}} -{% endif %}
{% csrf_token %} - {{form.non_field_errors}}
    -
  • - {% include "seminar/prihlaska_field.html" with field=form.username %} -
  • -
  • - {% include "seminar/prihlaska_field.html" with field=form.password %} -
  • + {{ form.as_ul }}
+ {# Django si posílá jméno další stránky jako obsah formuláře a výchozí hodnota (mi přišlo, že) nejde změnit... #} +
diff --git a/seminar/templates/seminar/logout.html b/seminar/templates/seminar/logout.html new file mode 100644 index 00000000..ab41a8c8 --- /dev/null +++ b/seminar/templates/seminar/logout.html @@ -0,0 +1,18 @@ +{% extends "seminar/zadani/base.html" %} +{% load staticfiles %} + + +{% block content %} +

+ {% block nadpis1a %}{% block nadpis1b %} + Odhlášení + {% endblock %}{% endblock %} +

+ +Byl jsi úspěšně odhlášen +{# Tohle by se asi mělo udělat přes kontext (title), ale kašlu na to, stejně je to jen jednojazyčná stránka #} + +{# TODO: odkaz na znovupřihlášení? #} + +{% endblock %} + diff --git a/seminar/templates/seminar/org/obalkovani.html b/seminar/templates/seminar/org/obalkovani.html new file mode 100644 index 00000000..fa130bc7 --- /dev/null +++ b/seminar/templates/seminar/org/obalkovani.html @@ -0,0 +1,30 @@ +{% extends "base.html" %} + +{% block content %} +

+ {% block nadpis1a %}{% block nadpis1b %} + Obálkování {{ cislo }} + {% endblock %}{% endblock %} +

+ + {% endif %} +

{% for resitel in reseni.resitele.all %}{{resitel.osoba}},{% endfor %}

+ + + +{% endblock content %} diff --git a/seminar/templates/seminar/tematka/rozcestnik.html b/seminar/templates/seminar/tematka/rozcestnik.html new file mode 100644 index 00000000..b13d6075 --- /dev/null +++ b/seminar/templates/seminar/tematka/rozcestnik.html @@ -0,0 +1,14 @@ +{% for tematko in tematka %} +

{{tematko.nazev}}

+

{{tematko.abstrakt}}

+ +{% endfor %} diff --git a/seminar/templates/seminar/tematka/toaletak.html b/seminar/templates/seminar/tematka/toaletak.html new file mode 100644 index 00000000..8b556c6c --- /dev/null +++ b/seminar/templates/seminar/tematka/toaletak.html @@ -0,0 +1 @@ +Stránká témátka diff --git a/seminar/testutils.py b/seminar/testutils.py index 204c0ea6..f378e725 100644 --- a/seminar/testutils.py +++ b/seminar/testutils.py @@ -380,7 +380,8 @@ def gen_temata(rnd, rocniky, rocnik_cisla, organizatori): kod=str(n), # atributy třídy Téma tema_typ=rnd.choice(Tema.TEMA_CHOICES)[0], - rocnik=rocnik + rocnik=rocnik, + abstrakt = "Abstrakt tematka {}".format(n) ) konec_tematu = min(rnd.randint(ci, 7), len(cisla)) for i in range(ci, konec_tematu+1): diff --git a/seminar/urls.py b/seminar/urls.py index c37d1357..57e447f3 100644 --- a/seminar/urls.py +++ b/seminar/urls.py @@ -8,6 +8,9 @@ from django.contrib.auth import views as auth_views staff_member_required = user_passes_test(lambda u: u.is_staff) urlpatterns = [ + path('aktualni/temata/', views.TemataRozcestnikView), + path('/t/', views.TematkoView), + # REDIRECTy path('jak-resit/', RedirectView.as_view(url='/co-je-MaM/jak-resit/')), @@ -86,24 +89,30 @@ urlpatterns = [ path('stav', staff_member_required(views.StavDatabazeView), name='stav_databaze'), path('cislo/./obalkovani', - staff_member_required(views.obalkovaniView), name='seminar_cislo_resitel_obalkovani'), + staff_member_required(views.ObalkovaniView.as_view()), name='seminar_cislo_resitel_obalkovani'), path('cislo/./tex-download.json', staff_member_required(views.texDownloadView), name='seminar_tex_download'), path('soustredeni//obalky.pdf', staff_member_required(views.soustredeniObalkyView), name='seminar_soustredeni_obalky'), - path('tex-upload/login/', views.LoginView, name='seminar_login'), + path('tex-upload/login/', views.TeXUploadLoginView, name='seminar_login'), path( 'tex-upload/', staff_member_required(views.texUploadView), name='seminar_tex_upload' ), + path('org/vloz_body//', + staff_member_required(views.VlozBodyView.as_view()),name='seminar_org_vlozbody'), path('auth/prihlaska/',views.prihlaskaView, name='seminar_prihlaska'), - path('auth/login/', views.loginView, name='login'), - path('auth/logout/', views.logoutView, name='logout'), + path('auth/login/', views.LoginView.as_view(), name='login'), + path('auth/logout/', views.LogoutView.as_view(), name='logout'), path('auth/resitel/', views.ResitelView.as_view(), name='seminar_resitel'), path('autocomplete/skola/',views.SkolaAutocomplete.as_view(), name='autocomplete_skola'), - path('auth/reset_password', views.resetPasswordView, name='reset_password'), + path('auth/reset_password/', views.PasswordResetView.as_view(), name='reset_password'), + path('auth/change_password/', views.PasswordChangeView.as_view(), name='change_password'), + path('auth/reset_password_done/', views.PasswordResetDoneView.as_view(), name='reset_password_done'), + path('auth/reset_password_confirm///', views.PasswordResetConfirmView.as_view(), name='password_reset_confirm'), + path('auth/reset_password_complete/', views.PasswordResetCompleteView.as_view(), name='reset_password_complete'), path('auth/resitel_edit', views.resitelEditView, name='seminar_resitel_edit'), path('', views.TitulniStranaView.as_view(), name='titulni_strana'), diff --git a/seminar/utils.py b/seminar/utils.py index 75092384..d910a5b6 100644 --- a/seminar/utils.py +++ b/seminar/utils.py @@ -2,9 +2,18 @@ import datetime from django.contrib.auth.decorators import user_passes_test +from html.parser import HTMLParser staff_member_required = user_passes_test(lambda u: u.is_staff) +class FirstTagParser(HTMLParser): + def __init__(self, *args, **kwargs): + self.firstTag = None + super().__init__(*args, **kwargs) + def handle_data(self, data): + if self.firstTag == None: + self.firstTag = data + def histogram(seznam): d = {} for i in seznam: diff --git a/seminar/views.py b/seminar/views.py index ba11c9b4..b9afc630 100644 --- a/seminar/views.py +++ b/seminar/views.py @@ -2,24 +2,26 @@ from django.shortcuts import get_object_or_404, render from django.http import HttpResponse, HttpResponseRedirect, HttpResponseForbidden, JsonResponse -from django.urls import reverse +from django.urls import reverse,reverse_lazy from django.core.exceptions import PermissionDenied, ObjectDoesNotExist from django.views import generic from django.utils.translation import ugettext as _ from django.http import Http404,HttpResponseBadRequest,HttpResponseRedirect -from django.db.models import Q +from django.db.models import Q, Sum, Count from django.views.decorators.csrf import ensure_csrf_cookie from django.contrib.auth import authenticate, login, get_user_model, logout +from django.contrib.auth import views as auth_views from django.contrib.auth.models import User from django.contrib.auth.mixins import LoginRequiredMixin from django.db import transaction from dal import autocomplete -from .models import Problem, Cislo, Reseni, Nastaveni, Rocnik, Soustredeni, Organizator, Resitel, Novinky, Soustredeni_Ucastnici, Pohadka, Tema, Clanek, Osoba, Skola +import seminar.models as s +from .models import Problem, Cislo, Reseni, Nastaveni, Rocnik, Soustredeni, Organizator, Resitel, Novinky, Soustredeni_Ucastnici, Pohadka, Tema, Clanek, Osoba, Skola # Tohle je stare a chceme se toho zbavit. Pouzivejte s.ToCoChci #from .models import VysledkyZaCislo, VysledkyKCisluZaRocnik, VysledkyKCisluOdjakziva from . import utils from .unicodecsv import UnicodeWriter -from .forms import PrihlaskaForm, LoginForm +from .forms import PrihlaskaForm, LoginForm, EditForm from datetime import timedelta, date, datetime from django.utils import timezone @@ -43,6 +45,45 @@ def verejna_temata(rocnik): """ return Problem.objects.filter(typ=Problem.TYP_TEMA, cislo_zadani__rocnik=rocnik, cislo_zadani__verejne_db=True).order_by('kod') +def temata_v_rocniku(rocnik): + return Problem.objects.filter(typ=Problem.TYP_TEMA, rocnik=rocnik) + +def get_problemy_k_tematu(tema): + return Problemy.objects.filter(nadproblem = tema) + + +class VlozBodyView(generic.ListView): + template_name = 'seminar/org/vloz_body.html' + + def get_queryset(self): + self.tema = get_object_or_404(Problem,id=self.kwargs['tema']) + print(self.tema) + self.problemy = Problem.objects.filter(nadproblem = self.tema) + print(self.problemy) + self.reseni = Reseni.objects.filter(problem__in=self.problemy) + print(self.reseni) + return self.reseni + + +class ObalkovaniView(generic.ListView): + template_name = 'seminar/org/obalkovani.html' + + def get_queryset(self): + rocnik = get_object_or_404(Rocnik,rocnik=self.kwargs['rocnik']) + cislo = get_object_or_404(Cislo,rocnik=rocnik,poradi=self.kwargs['cislo']) + self.cislo = cislo + self.hodnoceni = s.Hodnoceni.objects.filter(cislo_body=cislo) + self.reseni = Reseni.objects.filter(hodnoceni__in = self.hodnoceni).annotate(Sum('hodnoceni__body')).annotate(Count('hodnoceni')).order_by('resitele__osoba') + return self.reseni + + def get_context_data(self, **kwargs): + context = super(ObalkovaniView, self).get_context_data(**kwargs) + print(self.cislo) + context['cislo'] = self.cislo + return context + + + def AktualniZadaniView(request): nastaveni = get_object_or_404(Nastaveni) @@ -73,6 +114,99 @@ def ZadaniTemataView(request): } ) +# TODO Napsat tuto funkci znovu rekurzivně podle Jethrorad. Potom se podívat, jak lehce se dá modifikovat pro Rozcestník. Pokud lehce, rozšířit ji. Pokud složitě - použít tuhle +def vytahniZLesaSeznam(tematko, koren, pouze_zajimave=False): + returnVal = [] + + stack = [] + stack.append((koren.first_child, 0, False)) #Tuple of node, depth and relevance + + while len(stack) > 0: + wn, wd, wr = stack.pop() + + if wn.succ != None: + stack.append((wn.succ, wd, wr)) + if isinstance(wn, s.TemaVCisleNode): + print("TEMA") + print(wn.tema.id) + print(tematko.id) + if wn.tema.id == tematko.id: + returnVal.append((posledni_cislo, 0)) + print("PRIDANO") + wr = True + wd = 1 + + if wn.srolovatelne: + tagOpen = s.Text(na_web = "Otevírací srolovací tag") + tagOpenNode = s.TextNode(text = tagOpen) + tagClose = s.Text(na_web = "Zavírací srolovací tag") + tagCloseNode = s.TextNode(text = tagClose) + stack.append((tagCloseNode, wd, True)) + + if wn.first_child != None: + stack.append((wn.first_child, wd + 1, wr)) + + if isinstance(wn, s.CisloNode): + posledni_cislo = wn + print(wn) + + if wr: + print("ZAJIMAVE") + if pouze_zajimave: + if not wn.zajimave: + continue + returnVal.append((wn, wd)) + return returnVal + +def TematkoView(request, rocnik, tematko): + nastaveni = s.Nastaveni.objects.first() + rocnik_object = s.Rocnik.objects.filter(rocnik=rocnik) + tematko_object = s.Tema.objects.filter(rocnik=rocnik_object[0], kod=tematko) + seznam = vytahniZLesaSeznam(tematko_object[0], nastaveni.aktualni_rocnik().rocniknode) + for node, depth in seznam: + if node.isinstance(node, s.KonferaNode): + raise Exception("Not implemented yet") + if node.isinstance(node, s.PohadkaNode): # Mohu ignorovat, má pod sebou + pass + + return render(request, 'seminar/tematka/toaletak.html', {}) + + +def TemataRozcestnikView(request): + print("=============================================") + nastaveni = s.Nastaveni.objects.first() + tematka_objects = s.Tema.objects.filter(rocnik=nastaveni.aktualni_rocnik()) + tematka = [] #List tematka obsahuje pro kazde tematko object a list vsech TemaVCisleNodu - implementované pomocí slovníku + for tematko_object in tematka_objects: + print("AKTUALNI TEMATKO") + print(tematko_object.id) + odkazy = vytahniZLesaSeznam(tematko_object, nastaveni.aktualni_rocnik().rocniknode, pouze_zajimave = True) #Odkazy jsou tuply (node, depth) v listu + print(odkazy) + cisla = [] # List tuplů (nazev cisla, list odkazů) + vcisle = [] + cislo = None + for odkaz in odkazy: + if odkaz[1] == 0: + if cislo != None: + cisla.append((cislo, vcisle)) + cislo = (odkaz[0].getOdkazStr(), odkaz[0].getOdkaz()) + vcisle = [] + else: + print(odkaz[0].getOdkaz()) + vcisle.append((odkaz[0].getOdkazStr(), odkaz[0].getOdkaz())) + if cislo != None: + cisla.append((cislo, vcisle)) + + print(cisla) + tematka.append({ + "kod" : tematko_object.kod, + "nazev" : tematko_object.nazev, + "abstrakt" : tematko_object.abstrakt, + "obrazek": tematko_object.obrazek, + "cisla" : cisla + }) + return render(request, 'seminar/tematka/rozcestnik.html', {"tematka": tematka, "rocnik" : nastaveni.aktualni_rocnik().rocnik}) + #def ZadaniAktualniVysledkovkaView(request): # nastaveni = get_object_or_404(Nastaveni) @@ -672,7 +806,7 @@ def obalkyView(request,resitele): return response -def obalkovaniView(request, rocnik, cislo): +def oldObalkovaniView(request, rocnik, cislo): rocnik = Rocnik.objects.get(rocnik=rocnik) cislo = Cislo.objects.get(rocnik=rocnik, cislo=cislo) @@ -809,7 +943,7 @@ def StavDatabazeView(request): @ensure_csrf_cookie -def LoginView(request): +def TeXUploadLoginView(request): """Pro přihlášení při nahrávání z texu""" q = request.POST # nastavení cookie csrftoken @@ -1016,8 +1150,6 @@ class ResitelView(LoginRequiredMixin,generic.DetailView): return Resitel.objects.get(osoba__user=self.request.user) ## Formulare -def resitelEditView(request): - pass def resetPasswordView(request): pass @@ -1054,6 +1186,59 @@ def prihlaska_log_gdpr_safe(logger, gdpr_logger, msg, form_data): logger.warn(msg) gdpr_logger.warn(msg+", form:{}".format(form_data)) +from django.forms.models import model_to_dict +def resitelEditView(request): + err_logger = logging.getLogger('seminar.prihlaska.problem') + ## Načtení objektu Osoba a Resitel, patrici k aktuálně přihlášenému uživately + u = request.user + osoba_edit = Osoba.objects.get(user=u) + resitel_edit = osoba_edit.resitel + user_edit = osoba_edit.user + ## Vytvoření slovníku, kterým předvyplním formulář + prefill_1=model_to_dict(user_edit) + prefill_2=model_to_dict(resitel_edit) + prefill_3=model_to_dict(osoba_edit) + prefill_1.update(prefill_2) + prefill_1.update(prefill_3) + form = EditForm(initial=prefill_1) + ## Změna údajů a jejich uložení + if request.method == 'POST': + form = EditForm(request.POST) + if form.is_valid(): + ## Změny v osobě + fcd = form.cleaned_data + 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'] + ## 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']) + + ## Změny v řešiteli + resitel_edit.skola = fcd['skola'] + resitel_edit.rok_maturity = fcd['rok_maturity'] + resitel_edit.zasilat = fcd['zasilat'] + 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 HttpResponseRedirect('/thanks/') + else: + ## Stránka před odeslaním formuláře = předvyplněný formulář + return render(request, 'seminar/edit.html', {'form': form}) def prihlaskaView(request): generic_logger = logging.getLogger('seminar.prihlaska') @@ -1159,3 +1344,46 @@ class SkolaAutocomplete(autocomplete.Select2QuerySetView): # Q(user__last_name__isstartswith=query)) # # return qs + +# FIXME: Tohle asi vlastně vůbec nepatří do aplikace 'seminar' +class LoginView(auth_views.LoginView): + # Jen vezmeme vestavěný a dáme mu vhodný template a přesměrovací URL + template_name = 'seminar/login.html' + + # Přesměrovací URL má být v kontextu: + def get_context_data(self, **kwargs): + ctx = super().get_context_data(**kwargs) + ctx['next'] = reverse('titulni_strana') + return ctx + +class LogoutView(auth_views.LogoutView): + # Jen vezmeme vestavěný a dáme mu vhodný template a přesměrovací URL + template_name = 'seminar/logout.html' + # Pavel: Vůbec nevím, proč to s _lazy funguje, ale bez toho to bylo rozbité. + next_page = reverse_lazy('titulni_strana') + +# "Chci resetovat heslo" +class PasswordResetView(auth_views.PasswordResetView): + #template_name = 'seminar/password_reset.html' + # TODO: vlastní email_template_name a subject_template_name a html_email_template_name + success_url = reverse_lazy('reset_password_done') + from_email = 'login@mam.mff.cuni.cz' + +# "Poslali jsme e-mail (pokud bylo kam))" +class PasswordResetDoneView(auth_views.PasswordResetDoneView): + #template_name = 'seminar/password_reset_done.html' + pass + +# "Vymysli si heslo" +class PasswordResetConfirmView(auth_views.PasswordResetConfirmView): + #template_name = 'seminar/password_confirm_done.html' + success_url = reverse_lazy('reset_password_complete') + +# "Heslo se asi změnilo." +class PasswordResetCompleteView(auth_views.PasswordResetCompleteView): + #template_name = 'seminar/password_complete_done.html' + pass + +class PasswordChangeView(auth_views.PasswordChangeView): + #template_name = 'seminar/password_change.html' + success_url = reverse_lazy('titulni_strana')