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 %}
+
+
+
+{% 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 %}
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 %}
+
+
+ {% for reseni in object_list %}
+ {% ifchanged reseni.resitele %}
+ {% if not forloop.first %}
+
+ {% endif %}
+ {% for resitel in reseni.resitele.all %}{{resitel.osoba}},{% endfor %}
+
+ {% endifchanged %}
+
+ - Celkem {{reseni.hodnoceni__body__sum}} bodů z {{reseni.hodnoceni__count}} hodnocení
+
+ {% for h in reseni.hodnoceni_set.all %}
+ - {{ h.problem }}: {{ h.body }}b
+ {% endfor %}
+
+
+ {% 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}}
+
+ {% for cislo in tematko.cisla %}
+ - {{cislo.0.0}}
+
+ {% for odkaz in cislo.1 %}
+ - {{odkaz.0}}
+ {% endfor %}
+
+ {% endfor %}
+
+{% 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')