Browse Source

Merge remote-tracking branch 'Gimli/data_migrations' into test

middleware_test
Pavel "LEdoian" Turinsky 4 years ago
parent
commit
3408183070
  1. 817
      data/sitetree_new.json
  2. 4
      korektury/urls.py
  3. 5
      mamweb/routers.py
  4. 3
      mamweb/settings_common.py
  5. 2
      mamweb/settings_local.py
  6. 4
      mamweb/static/css/mamweb.css
  7. 17
      seminar/admin.py
  8. 29
      seminar/forms.py
  9. 19
      seminar/migrations/0090_auto_20201110_1958.py
  10. 18
      seminar/migrations/0091_resitel_zasilat_cislo_emailem.py
  11. 139
      seminar/models.py
  12. 2
      seminar/templates/seminar/archiv/cislo.html
  13. 15
      seminar/templates/seminar/archiv/odmeny.html
  14. 47
      seminar/templates/seminar/odevzdavatko/detail.html
  15. 11
      seminar/templates/seminar/odevzdavatko/seznam.html
  16. 36
      seminar/templates/seminar/odevzdavatko/tabulka.html
  17. 12
      seminar/templates/seminar/profil/edit.html
  18. 2
      seminar/templates/seminar/profil/nahraj_reseni.html
  19. 11
      seminar/templates/seminar/profil/prihlaska.html
  20. 27
      seminar/templatetags/utils.py
  21. 2
      seminar/testutils.py
  22. 21
      seminar/urls.py
  23. 14
      seminar/utils.py
  24. 1
      seminar/views/__init__.py
  25. 2
      seminar/views/autocomplete.py
  26. 129
      seminar/views/odevzdavatko.py
  27. 142
      seminar/views/views_all.py
  28. 165
      seminar/views/views_rest.py
  29. 115
      seminar/viewsets.py
  30. 5
      vue_frontend/src/components/AddNewNode.vue
  31. 21
      vue_frontend/src/components/CastNode.vue
  32. 18
      vue_frontend/src/components/TextNode.vue
  33. 40
      vue_frontend/src/components/TreeNode.vue
  34. 2
      vue_frontend/src/components/TreeNodeRoot.vue
  35. 90
      vue_frontend/src/components/UlohaVzorakNode.vue
  36. 100
      vue_frontend/src/components/UlohaZadaniNode.vue
  37. 2
      vue_frontend/src/router/index.js
  38. 5
      vue_frontend/vue.config.js

817
data/sitetree_new.json

File diff suppressed because one or more lines are too long

4
korektury/urls.py

@ -3,8 +3,8 @@ from seminar.utils import org_required
from . import views from . import views
urlpatterns = [ urlpatterns = [
path('korektury/', org_required(views.KorekturyAktualniListView.as_view()), name='korektury-list'), path('korektury/', org_required(views.KorekturyAktualniListView.as_view()), name='korektury_list'),
path('korektury/zastarale/', org_required(views.KorekturyZastaraleListView.as_view()), name='korektury-list'), path('korektury/zastarale/', org_required(views.KorekturyZastaraleListView.as_view()), name='korektury_stare_list'),
path('korektury/<int:pdf>/', org_required(views.KorekturyView.as_view()), name='korektury'), path('korektury/<int:pdf>/', org_required(views.KorekturyView.as_view()), name='korektury'),
path('korektury/help/', org_required(views.KorekturyHelpView.as_view()), name='korektury-help'), path('korektury/help/', org_required(views.KorekturyHelpView.as_view()), name='korektury-help'),
] ]

5
mamweb/routers.py

@ -4,7 +4,12 @@ from seminar import viewsets as vs
router = routers.DefaultRouter() router = routers.DefaultRouter()
router.register(r'ulohavzoraknode', vs.UlohaVzorakNodeViewSet,basename='ulohavzoraknode') router.register(r'ulohavzoraknode', vs.UlohaVzorakNodeViewSet,basename='ulohavzoraknode')
router.register(r'reseninode', vs.ReseniNodeViewSet,basename='reseninode')
router.register(r'text', vs.TextViewSet) router.register(r'text', vs.TextViewSet)
router.register(r'textnode', vs.TextNodeViewSet) router.register(r'textnode', vs.TextNodeViewSet)
router.register(r'castnode', vs.CastNodeViewSet) router.register(r'castnode', vs.CastNodeViewSet)
router.register(r'problem', vs.ProblemViewSet, basename='problem')
router.register(r'uloha', vs.UlohaViewSet, basename='uloha')
router.register(r'reseni', vs.ReseniViewSet, basename='reseni')
router.register(r'ulohazadaninode', vs.UlohaZadaniNodeViewSet)

3
mamweb/settings_common.py

@ -295,6 +295,9 @@ LOGGING = {
}, },
} }
# Permissions for uploads
FILE_UPLOAD_PERMISSIONS = 0o0644
# MaM specific # MaM specific
SEMINAR_RESENI_DIR = os.path.join('reseni') SEMINAR_RESENI_DIR = os.path.join('reseni')

2
mamweb/settings_local.py

@ -28,7 +28,7 @@ INTERNAL_IPS = ['127.0.0.1']
TEMPLATES[0]['OPTIONS']['debug'] = True TEMPLATES[0]['OPTIONS']['debug'] = True
ALLOWED_HOSTS = ['127.0.0.1'] ALLOWED_HOSTS = ['127.0.0.1', '192.168.43.34']
# Database # Database
# https://docs.djangoproject.com/en/1.7/ref/settings/#databases # https://docs.djangoproject.com/en/1.7/ref/settings/#databases

4
mamweb/static/css/mamweb.css

@ -347,6 +347,10 @@ div.zadani_azad_termin {
bottom: 0px; bottom: 0px;
} }
#footer p.license a {
color: #333;
}
p.license-mobile { p.license-mobile {
display: none; display: none;
} }

17
seminar/admin.py

@ -1,4 +1,5 @@
from django.contrib import admin from django.contrib import admin
from django.contrib.auth.models import Permission
from polymorphic.admin import PolymorphicParentModelAdmin, PolymorphicChildModelAdmin, PolymorphicChildModelFilter from polymorphic.admin import PolymorphicParentModelAdmin, PolymorphicChildModelAdmin, PolymorphicChildModelFilter
from reversion.admin import VersionAdmin from reversion.admin import VersionAdmin
@ -18,7 +19,7 @@ admin.site.register(m.Soustredeni)
@admin.register(m.Osoba) @admin.register(m.Osoba)
class OsobaAdmin(admin.ModelAdmin): class OsobaAdmin(admin.ModelAdmin):
actions = ['synchronizuj_maily'] actions = ['synchronizuj_maily', 'udelej_orgem']
def synchronizuj_maily(self, request, queryset): def synchronizuj_maily(self, request, queryset):
for o in queryset: for o in queryset:
@ -29,6 +30,20 @@ class OsobaAdmin(admin.ModelAdmin):
self.message_user(request, "E-maily synchronizovány.") self.message_user(request, "E-maily synchronizovány.")
synchronizuj_maily.short_description = "Synchronizuj vybraným osobám e-maily do uživatelů" synchronizuj_maily.short_description = "Synchronizuj vybraným osobám e-maily do uživatelů"
def udelej_orgem(self,request,queryset):
org_perm = Permission.objects.filter(codename__exact='org').first()
print(queryset)
for o in queryset:
user = o.user
user.user_permissions.add(org_perm)
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.Problem) @admin.register(m.Problem)
class ProblemAdmin(PolymorphicParentModelAdmin): class ProblemAdmin(PolymorphicParentModelAdmin):
base_model = m.Problem base_model = m.Problem

29
seminar/forms.py

@ -10,6 +10,21 @@ import seminar.models as m
from datetime import date from datetime import date
import logging 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 LoginForm(forms.Form): class LoginForm(forms.Form):
username = forms.CharField(label='Přihlašovací jméno', username = forms.CharField(label='Přihlašovací jméno',
max_length=256, max_length=256,
@ -42,8 +57,8 @@ class PrihlaskaForm(forms.Form):
pohlavi_muz = forms.ChoiceField(label='Pohlaví', pohlavi_muz = forms.ChoiceField(label='Pohlaví',
choices = ((True,'muž'),(False,'žena')), required=True) choices = ((True,'muž'),(False,'žena')), required=True)
email = forms.EmailField(label='E-mail',max_length=256, required=True) email = forms.EmailField(label='E-mail',max_length=256, required=True)
telefon = forms.CharField(label='Telefon',max_length=256, required=False) telefon = forms.CharField(widget=TelInput(),label='Telefon',max_length=256, required=False)
datum_narozeni = forms.DateField(label='Datum narození', required=False) datum_narozeni = forms.DateField(widget=DateInput(),label='Datum narození', required=False)
ulice = forms.CharField(label='Ulice', max_length=256, required=False) ulice = forms.CharField(label='Ulice', max_length=256, required=False)
mesto = forms.CharField(label='Město', 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) psc = forms.CharField(label='PSČ', max_length=32, required=False)
@ -74,6 +89,8 @@ class PrihlaskaForm(forms.Form):
max_value=date.today().year+8, max_value=date.today().year+8,
required=True) required=True)
zasilat = forms.ChoiceField(label='Kam zasílat čísla a řešení',choices = Resitel.ZASILAT_CHOICES, 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 emailem upozornění na vydání nového čísla', required=True)
gdpr = forms.BooleanField(label='Souhlasím se zpracováním osobních údajů', required=True) 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) spam = forms.BooleanField(label='Souhlasím se zasíláním materiálů od MFF UK', required=False)
@ -135,8 +152,8 @@ class ProfileEditForm(forms.Form):
pohlavi_muz = forms.ChoiceField(label='Pohlaví', pohlavi_muz = forms.ChoiceField(label='Pohlaví',
choices = ((True,'muž'),(False,'žena')), required=True) choices = ((True,'muž'),(False,'žena')), required=True)
email = forms.EmailField(label='E-mail',max_length=256, required=True) email = forms.EmailField(label='E-mail',max_length=256, required=True)
telefon = forms.CharField(label='Telefon',max_length=256, required=False) telefon = forms.CharField(widget=TelInput(),label='Telefon',max_length=256, required=False)
datum_narozeni = forms.DateField(label='Datum narození', required=False) datum_narozeni = forms.DateField(widget=DateInput(),label='Datum narození', required=False)
ulice = forms.CharField(label='Ulice', max_length=256, required=False) ulice = forms.CharField(label='Ulice', max_length=256, required=False)
mesto = forms.CharField(label='Město', 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) psc = forms.CharField(label='PSČ', max_length=32, required=False)
@ -167,6 +184,8 @@ class ProfileEditForm(forms.Form):
max_value=date.today().year+8, max_value=date.today().year+8,
required=True) required=True)
zasilat = forms.ChoiceField(label='Kam zasílat čísla a řešení',choices = Resitel.ZASILAT_CHOICES, 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=True)
spam = forms.BooleanField(label='Souhlasím se zasíláním materiálů od MFF UK', required=False) spam = forms.BooleanField(label='Souhlasím se zasíláním materiálů od MFF UK', required=False)
# def clean_username(self): # def clean_username(self):
# err_logger = logging.getLogger('seminar.prihlaska.problem') # err_logger = logging.getLogger('seminar.prihlaska.problem')
@ -234,7 +253,7 @@ class VlozReseniForm(forms.Form):
#resitele = models.ManyToManyField(Resitel, verbose_name='autoři řešení', #resitele = models.ManyToManyField(Resitel, verbose_name='autoři řešení',
# help_text='Seznam autorů řešení', through='Reseni_Resitele') # help_text='Seznam autorů řešení', through='Reseni_Resitele')
cas_doruceni = forms.DateField(label="Čas doručení") cas_doruceni = forms.DateField(widget=DateInput(),label="Čas doručení")
#cas_doruceni = models.DateTimeField('čas_doručení', default=timezone.now, blank=True) #cas_doruceni = models.DateTimeField('čas_doručení', default=timezone.now, blank=True)

19
seminar/migrations/0090_auto_20201110_1958.py

@ -0,0 +1,19 @@
# Generated by Django 2.2.12 on 2020-11-10 18:58
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('seminar', '0089_cislo_datum_preddeadline'),
]
operations = [
migrations.AlterField(
model_name='textnode',
name='text',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='seminar.Text', verbose_name='text'),
),
]

18
seminar/migrations/0091_resitel_zasilat_cislo_emailem.py

@ -0,0 +1,18 @@
# Generated by Django 2.2.17 on 2020-12-01 19:53
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('seminar', '0090_auto_20201110_1958'),
]
operations = [
migrations.AddField(
model_name='resitel',
name='zasilat_cislo_emailem',
field=models.BooleanField(default=False, help_text='True pokud chce řešitel dostávat číslo emailem', verbose_name='zasílat číslo emailem'),
),
]

139
seminar/models.py

@ -260,8 +260,10 @@ class Resitel(SeminarModelBase):
(ZASILAT_DO_SKOLY, 'Do školy'), (ZASILAT_DO_SKOLY, 'Do školy'),
(ZASILAT_NIKAM, 'Nikam'), (ZASILAT_NIKAM, 'Nikam'),
] ]
zasilat = models.CharField('kam zasílat', max_length=32, choices=ZASILAT_CHOICES, blank=False, default=ZASILAT_DOMU) 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, poznamka = models.TextField('neveřejná poznámka', blank=True,
help_text='Neveřejná poznámka k řešiteli (plain text)') help_text='Neveřejná poznámka k řešiteli (plain text)')
@ -309,25 +311,106 @@ class Resitel(SeminarModelBase):
return sum(h.body for h in list(vsechna_hodnoceni)) return sum(h.body for h in list(vsechna_hodnoceni))
def get_titul(self, celkove_body=None): def get_titul(self, body=None):
"Vrati titul" "Vrati titul jako řetězec."
if celkove_body is None:
celkove_body = self.vsechny_body() # 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
if celkove_body < 10: def __str__(self):
return '' return self.value[1]
elif celkove_body < 20:
return 'Bc.' @classmethod
elif celkove_body < 50: def z_bodu(cls, body):
return 'Mgr.' aktualni = cls.nic
elif celkove_body < 100: # TODO: ověřit, že to funguje
return 'Dr.' for titul in cls: # Kdyžtak použít __members__.items()
elif celkove_body < 200: if titul.value[0] <= body:
return 'Doc.' aktualni = titul
elif celkove_body < 500: else:
return 'Prof.' break
return aktualni
# Hledáme body v databázi
# V listopadu 2020 jsme se na filosofické schůzce shodli o změně hranic titulů:
# - body z 25. ročníku a dříve byly shledány dvakrát hodnotnějšími
# - proto se započítávají dvojnásobně a byly posunuté hranice titulů
# - staré tituly se ale nemají odebrat, pokud řešitel v t.č. minulém (26.) ročníku měl titul, má ho mít pořád.
hodnoceni_do_25_rocniku = Hodnoceni.objects.filter(cislo_body__rocnik__rocnik__lte=25,reseni__in=self.reseni_set.all())
novejsi_hodnoceni = Hodnoceni.objects.filter(reseni__in=self.reseni_set.all()).difference(hodnoceni_do_25_rocniku)
def body_z_hodnoceni(hh : list):
return sum(h.body for h in hh)
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: else:
return 'Akad.' return Titul.akad
hodnoceni_do_26_rocniku = Hodnoceni.objects.filter(cislo_body__rocnik__rocnik__lte=26,reseni__in=self.reseni_set.all())
novejsi_body = body_z_hodnoceni(
Hodnoceni.objects.filter(reseni__in=self.reseni_set.all())
.difference(hodnoceni_do_26_rocniku)
)
starsi_body = body_z_hodnoceni(hodnoceni_do_26_rocniku)
if body is not None:
# Ještě z toho vybereme ty správně staré body
novejsi_body = max(0, body - starsi_body)
starsi_body = min(starsi_body, body)
# Titul pro 26. ročník
stary_titul = titul_do_26_rocniku(starsi_body)
# Titul podle aktuálních pravidel
novy_titul = Titul.z_bodu(logicke_body)
if novejsi_body == 0:
# Žádné nové body -- titul podle starých pravidel
return str(stary_titul)
return str(max(novy_titul, stary_titul))
def __str__(self): def __str__(self):
return self.osoba.plne_jmeno() return self.osoba.plne_jmeno()
@ -745,15 +828,18 @@ class Problem(SeminarModelBase,PolymorphicModel):
def verejne(self): def verejne(self):
# aktuálně podle stavu problému # aktuálně podle stavu problému
# FIXME pro některé problémy možná chceme override # FIXME pro některé problémy možná chceme override
# FIXME vrací veřejnost čistě problému, nezávisle na čísle, ve kterém je.
# Je to tak správně?
stav_verejny = False stav_verejny = False
if self.stav == 'zadany' or self.stav == 'vyreseny': if self.stav == 'zadany' or self.stav == 'vyreseny':
stav_verejny = True stav_verejny = True
return stav_verejny
cislo_verejne = False #cislo_verejne = False
if (self.cislo_zadani and self.cislo_zadani.verejne()): #if (self.cislo_zadani and self.cislo_zadani.verejne()):
cislo_verejne = True # cislo_verejne = True
return (stav_verejny and cislo_verejne) #return (stav_verejny and cislo_verejne)
verejne.boolean = True verejne.boolean = True
def verejne_url(self): def verejne_url(self):
@ -991,7 +1077,7 @@ def aux_generate_filename(self, filename):
unidecode(filename.replace('/', '-').replace('\0', '')) unidecode(filename.replace('/', '-').replace('\0', ''))
) )
datedir = timezone.now().strftime('%Y-%m') datedir = timezone.now().strftime('%Y-%m')
fname = "{}_{}".format( fname = "{}/{}".format(
timezone.now().strftime('%Y-%m-%d-%H:%M'), timezone.now().strftime('%Y-%m-%d-%H:%M'),
clean) clean)
return os.path.join(datedir, fname) return os.path.join(datedir, fname)
@ -1044,6 +1130,11 @@ class PrilohaReseni(SeminarModelBase):
def __str__(self): def __str__(self):
return str(self.soubor) return str(self.soubor)
def split(self):
"Vrátí cestu rozsekanou po složkách. To se hodí v templatech"
# Věřím, že tohle funguje, případně použít os.path nebo pathlib.
return self.soubor.url.split('/')
class Pohadka(SeminarModelBase): class Pohadka(SeminarModelBase):
"""Kus pohádky před/za úlohou v čísle""" """Kus pohádky před/za úlohou v čísle"""
@ -1480,7 +1571,7 @@ class TextNode(TreeNode):
verbose_name = 'Text (Node)' verbose_name = 'Text (Node)'
verbose_name_plural = 'Text (Node)' verbose_name_plural = 'Text (Node)'
text = models.ForeignKey(Text, text = models.ForeignKey(Text,
on_delete=models.PROTECT, on_delete=models.CASCADE,
verbose_name = 'text') verbose_name = 'text')
def aktualizuj_nazev(self): def aktualizuj_nazev(self):
@ -1513,7 +1604,7 @@ class ReseniNode(TreeNode):
verbose_name = 'reseni') verbose_name = 'reseni')
def aktualizuj_nazev(self): def aktualizuj_nazev(self):
self.nazev = "OtisteneReseniNode: "+str(self.reseni) self.nazev = "ReseniNode: "+str(self.reseni)
def getOdkazStr(self): def getOdkazStr(self):
return str(self.reseni) return str(self.reseni)

2
seminar/templates/seminar/archiv/cislo.html

@ -45,6 +45,7 @@
<li><a href="tituly.tex">Tituly (TeX)</a></li> <li><a href="tituly.tex">Tituly (TeX)</a></li>
<li><a href="vysledkovka.tex">Výsledkovka (TeX)</a></li> <li><a href="vysledkovka.tex">Výsledkovka (TeX)</a></li>
<li><a href="obalkovani">Obálkování</a></li> <li><a href="obalkovani">Obálkování</a></li>
<li><a href="odmeny/{{prevcislo.rocnik.rocnik}}.{{prevcislo.poradi}}/">Odměny</a></li>
</ul> </ul>
</div> </div>
{% endif %} {% endif %}
@ -75,6 +76,7 @@
{% for p in problemy %} {% for p in problemy %}
<th class='border-r'><a href="{{ p.verejne_url }}">{{ p.kod_v_rocniku }}</a> <th class='border-r'><a href="{{ p.verejne_url }}">{{ p.kod_v_rocniku }}</a>
{% endfor %} {% endfor %}
{% if ostatni %}<th class='border-r'>Ostatní {% endif %}
<th class='border-r'>Za číslo <th class='border-r'>Za číslo
<th class='border-r'>Za ročník <th class='border-r'>Za ročník
<th class='border-r'>Odjakživa <th class='border-r'>Odjakživa

15
seminar/templates/seminar/archiv/odmeny.html

@ -0,0 +1,15 @@
{% extends "base.html" %}
{% block content %}
<h1>
{% block nadpis1a %}{% block nadpis1b %}
Odměny {{ cislo }}
{% endblock %}{% endblock %}
</h1>
<ul>
{% for z in zmeny %}
<li> {{z.jmeno}}: {{z.ftitul}} &rarr; {{z.ttitul}}</li>
{% endfor %}
</ul>
{% endblock content %}

47
seminar/templates/seminar/odevzdavatko/detail.html

@ -0,0 +1,47 @@
{% extends "base.html" %}
{% block content %}
<p>Řešené problémy: {{ object.problem.all | join:", " }}</p>
<p>Řešitelé: {{ object.resitele.all | join:", " }}</p>
{# https://docs.djangoproject.com/en/3.1/ref/models/instances/#django.db.models.Model.get_FOO_display #}
<p>Forma: {{ object.get_forma_display }}, doručeno {{ object.cas_doruceni }}</p>
{# Soubory: #}
<h3>Přílohy:</h3>
{% if object.prilohy.all %}
<table>
<tr><th>Soubor</th><th>Řešitelova poznámka</th><th>Datum</th></tr>
{% for priloha in object.prilohy.all %}
<tr>
<td><a href="{{ priloha.soubor.url }}" download>{{ priloha.split | last }}</a></td>
<td>{{ priloha.res_poznamka }}</td>
<td>{{ priloha.vytvoreno }}</td></tr>
{# TODO: Orgo-poznámka, ideálně jako formulář #}
{% endfor %}
</table>
{% else %}
<p>Žádné přílohy</p>
{% endif %}
{# Hodnocení: #}
{# FIXME: Udělat jako formulář #}
<h3>Hodnocení:</h3>
{% if object.hodnoceni_set.all %}
<table>
<tr><th>Problém</th><th>Body</th><th>Číslo pro body</th></tr>
{% for h in object.hodnoceni_set.all %}
<tr>
<td>{{ h.problem }}</a></td>
<td>{{ h.body }}</td>
<td>{{ h.cislo_body }}</td></tr>
{% endfor %}
</table>
{% else %}
<p>Ještě nebylo hodnoceno</p>
{% endif %}
{% endblock %}

11
seminar/templates/seminar/odevzdavatko/seznam.html

@ -0,0 +1,11 @@
{% extends "base.html" %}
{% block content %}
<ul>
{% for obj in object_list %}
<li><a href="{% url 'odevzdavatko_detail_reseni' pk=obj.id %}">{{ obj }}</a> ({{ obj.get_forma_display }} {{ obj.cas_doruceni }})
{% endfor %}
</ul>
{% endblock %}

36
seminar/templates/seminar/odevzdavatko/tabulka.html

@ -0,0 +1,36 @@
{% extends "base.html" %}
{% load utils %} {# Možná by mohlo být někde výš v hierarchii templatů... #}
{% block content %}
<table>
<tr>
<td></td> {# Prázdná buňka v levém horním rohu #}
{% for p in problemy %}
<th>
{# TODO: Přehled řešení k problému, odkázaný odsud? #}
{{ p }}
</th>
{% endfor %}
</tr>
{% for resitel,hodnoty in radky%}
<tr>
<td>
{# TODO: Chceme mít view i na řešení konkrétního řešitele ke všem problémům? #}
{{ resitel }}
</td>
{% for hodn in hodnoty %}
<td>
{% if hodn %}
<a href="{% url 'odevzdavatko_reseni_resitele_k_problemu' problem=hodn.problem_id resitel=hodn.resitel_id %}">
{{ hodn.pocet_reseni }} řeš.<br>{{ hodn.body }} b<br>{{ hodn.posledni_odevzdani|kratke_datum|default_if_none:"Nikdy"|default:"???"}}
</a>
{% endif %}
</td>
{% endfor %}
</tr>
{% endfor %}
</table>
{% endblock %}

12
seminar/templates/seminar/profil/edit.html

@ -6,6 +6,17 @@
{{form.media}} {{form.media}}
<script src="{% static 'seminar/prihlaska.js' %}"></script> <script src="{% static 'seminar/prihlaska.js' %}"></script>
{% endblock %} {% endblock %}
<!--
# 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
-->
{% block content %} {% block content %}
<h1> <h1>
{% block nadpis1a %}{% block nadpis1b %} {% block nadpis1a %}{% block nadpis1b %}
@ -73,6 +84,7 @@
</h4> </h4>
<table class="form"> <table class="form">
{% include "seminar/profil/prihlaska_field.html" with field=form.zasilat %} {% include "seminar/profil/prihlaska_field.html" with field=form.zasilat %}
{% include "seminar/profil/prihlaska_field.html" with field=form.zasilat_cislo_emailem %}
</table> </table>
<hr> <hr>

2
seminar/templates/seminar/profil/nahraj_reseni.html

@ -53,7 +53,7 @@
</td> </td>
<td {% if field.help_text %} class="field-with-comment"{% endif %}> <td {% if field.help_text %} class="field-with-comment"{% endif %}>
{{ field }} {{ field }}
<span class="field-comment">{{ field.help_text|safe }}</span>> <span class="field-comment">{{ field.help_text|safe }}</span>
</td> </td>
<td><span class="field-error">{{ field.errors }}</span></td> <td><span class="field-error">{{ field.errors }}</span></td>

11
seminar/templates/seminar/profil/prihlaska.html

@ -7,6 +7,16 @@
<script src="{% static 'seminar/prihlaska.js' %}"></script> <script src="{% static 'seminar/prihlaska.js' %}"></script>
{% endblock %} {% endblock %}
<!--
# 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
-->
{% block content %} {% block content %}
<h1> <h1>
{% block nadpis1a %}{% block nadpis1b %} {% block nadpis1a %}{% block nadpis1b %}
@ -77,6 +87,7 @@
</h4> </h4>
<table class="form"> <table class="form">
{% include "seminar/profil/prihlaska_field.html" with field=form.zasilat %} {% include "seminar/profil/prihlaska_field.html" with field=form.zasilat %}
{% include "seminar/profil/prihlaska_field.html" with field=form.zasilat_cislo_emailem %}
</table> </table>
<hr> <hr>

27
seminar/templatetags/utils.py

@ -0,0 +1,27 @@
from django import template
from datetime import datetime, timedelta
from pytz import timezone
from mamweb.settings import TIME_ZONE
import logging
register = template.Library()
logger = logging.getLogger(__name__)
@register.filter(name='kratke_datum', expects_localtime=True)
def kratke_datum(dt):
# None dává None, ne-datum dává False, aby se daly použít filtry typu "default".
if dt is None:
return None
if not isinstance(dt, datetime):
logger.warning(f"Špatné volání filtru {__name__}: {dt}")
return False
naive_now = datetime.now()
tz = timezone(TIME_ZONE)
now = tz.localize(naive_now)
delta = now - dt
if delta <= timedelta(days=1):
return dt.strftime("%k:%M")
if delta <= timedelta(days=365): # Timedelta neumí vyjádřit 1 rok
return dt.strftime("%d. %m.")
return dt.strftime("%d. %m. %Y")

2
seminar/testutils.py

@ -200,6 +200,8 @@ def gen_organizatori(rnd, osoby, last_rocnik):
os.user = user os.user = user
os.save() os.save()
os.user.user_permissions.add(org_perm) os.user.user_permissions.add(org_perm)
os.user.is_staff = True
os.user.save()
organizatori.append(Organizator.objects.create(osoba=os, organizatori.append(Organizator.objects.create(osoba=os,
organizuje_od=od, organizuje_do=do, strucny_popis_organizatora = popis_orga)) organizuje_od=od, organizuje_do=do, strucny_popis_organizatora = popis_orga))
return organizatori return organizatori

21
seminar/urls.py

@ -13,8 +13,8 @@ urlpatterns = [
path('co-je-MaM/organizatori/organizovali/', views.CojemamOrganizatoriStariView.as_view(), name='stari_organizatori'), path('co-je-MaM/organizatori/organizovali/', views.CojemamOrganizatoriStariView.as_view(), name='stari_organizatori'),
# Archiv # Archiv
path('archiv/rocniky/', views.ArchivView.as_view()), path('archiv/rocniky/', views.ArchivView.as_view(), name="seninar_archiv_rocniky"),
path('archiv/temata/', views.ArchivTemataView.as_view()), path('archiv/temata/', views.ArchivTemataView.as_view(), name="seninar_archiv_temata"),
path('rocnik/<int:rocnik>/', views.RocnikView.as_view(), name='seminar_rocnik'), path('rocnik/<int:rocnik>/', views.RocnikView.as_view(), name='seminar_rocnik'),
path('cislo/<int:rocnik>.<str:cislo>/', views.CisloView.as_view(), name='seminar_cislo'), path('cislo/<int:rocnik>.<str:cislo>/', views.CisloView.as_view(), name='seminar_cislo'),
@ -90,17 +90,17 @@ urlpatterns = [
name='seminar_rocnik_vysledkovka' name='seminar_rocnik_vysledkovka'
), ),
path( path(
'cislo/<int:rocnik>.<int:cislo>/vysledkovka.tex', 'cislo/<int:rocnik>.<str:cislo>/vysledkovka.tex',
org_required(views.CisloVysledkovkaView.as_view()), org_required(views.CisloVysledkovkaView.as_view()),
name='seminar_cislo_vysledkovka' name='seminar_cislo_vysledkovka'
), ),
path( path(
'cislo/<int:rocnik>.<int:cislo>/obalky.pdf', 'cislo/<int:rocnik>.<str:cislo>/obalky.pdf',
org_required(views.cisloObalkyView), org_required(views.cisloObalkyView),
name='seminar_cislo_obalky' name='seminar_cislo_obalky'
), ),
path( path(
'cislo/<int:rocnik>.<int:cislo>/tituly.tex', 'cislo/<int:rocnik>.<str:cislo>/tituly.tex',
org_required(views.TitulyView), org_required(views.TitulyView),
name='seminar_cislo_titul' name='seminar_cislo_titul'
), ),
@ -110,10 +110,14 @@ urlpatterns = [
name='stav_databaze' name='stav_databaze'
), ),
path( path(
'cislo/<int:rocnik>.<int:cislo>/obalkovani', 'cislo/<int:rocnik>.<str:cislo>/obalkovani',
org_required(views.ObalkovaniView.as_view()), org_required(views.ObalkovaniView.as_view()),
name='seminar_cislo_resitel_obalkovani' name='seminar_cislo_resitel_obalkovani'
), ),
path(
'cislo/<int:trocnik>.<str:tcislo>/odmeny/<int:frocnik>.<str:fcislo>/',
org_required(views.OdmenyView.as_view()),
name="seminar_archiv_odmeny"),
path( path(
'soustredeni/<int:soustredeni>/obalky.pdf', 'soustredeni/<int:soustredeni>/obalky.pdf',
org_required(views.soustredeniObalkyView), org_required(views.soustredeniObalkyView),
@ -168,5 +172,10 @@ urlpatterns = [
# org_member_required(views.OrganizatorAutocomplete.as_view()), # org_member_required(views.OrganizatorAutocomplete.as_view()),
# name='seminar_autocomplete_organizator') # name='seminar_autocomplete_organizator')
path('temp/reseni/', org_required(views.TabulkaOdevzdanychReseniView.as_view()), name='odevzdavatko_tabulka'),
path('temp/reseni/<int:problem>/<int:resitel>/', org_required(views.ReseniProblemuView.as_view()), name='odevzdavatko_reseni_resitele_k_problemu'),
path('temp/reseni/<int:pk>', org_required(views.DetailReseniView.as_view()), name='odevzdavatko_detail_reseni'),
path('temp/reseni/all', org_required(views.SeznamReseniView.as_view())),
path('temp/reseni/akt', org_required(views.SeznamAktualnichReseniView.as_view())),
] ]

14
seminar/utils.py

@ -148,16 +148,12 @@ def resi_v_rocniku(rocnik, cislo=None):
if cislo is None: if cislo is None:
# filtrujeme pouze podle ročníku # filtrujeme pouze podle ročníku
letosni_reseni = m.Reseni.objects.filter(hodnoceni__cislo_body__rocnik=rocnik) return m.Resitel.objects.filter(rok_maturity__gte=rocnik.druhy_rok(),
reseni__hodnoceni__cislo_body__rocnik=rocnik).distinct()
else: # filtrujeme podle ročníku i čísla else: # filtrujeme podle ročníku i čísla
letosni_reseni = m.Reseni.objects.filter(hodnoceni__cislo_body__rocnik=rocnik, return m.Resitel.objects.filter(rok_maturity__gte=rocnik.druhy_rok(),
hodnoceni__cislo_body__poradi__lte=cislo.poradi) reseni__hodnoceni__cislo_body__rocnik=rocnik,
reseni__hodnoceni__cislo_body__poradi__lte=cislo.poradi).distinct()
# vygenerujeme queryset řešitelů, co letos něco poslali
letosni_resitele = m.Resitel.objects.none()
for reseni in letosni_reseni:
letosni_resitele = letosni_resitele | reseni.resitele.filter(rok_maturity__gte=rocnik.druhy_rok())
return letosni_resitele.distinct()
def aktivniResitele(cislo, pouze_letosni=False): def aktivniResitele(cislo, pouze_letosni=False):

1
seminar/views/__init__.py

@ -1,3 +1,4 @@
from .views_all import * from .views_all import *
from .autocomplete import * from .autocomplete import *
from .views_rest import * from .views_rest import *
from .odevzdavatko import *

2
seminar/views/autocomplete.py

@ -34,7 +34,9 @@ class OdevzdatelnyProblemAutocomplete(autocomplete.Select2QuerySetView):
rocnik = nastaveni.aktualni_rocnik rocnik = nastaveni.aktualni_rocnik
temata = m.Tema.objects.filter(rocnik=rocnik, stav=m.Problem.STAV_ZADANY) temata = m.Tema.objects.filter(rocnik=rocnik, stav=m.Problem.STAV_ZADANY)
ulohy = m.Uloha.objects.filter(cislo_deadline__rocnik = rocnik) ulohy = m.Uloha.objects.filter(cislo_deadline__rocnik = rocnik)
clanky = m.Clanek.objects.filter(cislo__rocnik = rocnik, stav=m.Problem.STAV_ZADANY) # FIXME: Je tohle to, co chceme?
ulohy.union(temata) ulohy.union(temata)
ulohy.union(clanky)
qs = ulohy qs = ulohy
if self.q: if self.q:
qs = qs.filter( qs = qs.filter(

129
seminar/views/odevzdavatko.py

@ -0,0 +1,129 @@
from django.views.generic import ListView, DetailView
from django.views.generic.base import TemplateView
from dataclasses import dataclass
import datetime
import seminar.models as m
from seminar.utils import aktivniResitele, resi_v_rocniku
# Co chceme?
# - "Tabulku" aktuální řešitelé x zveřejněné problémy, v buňkách počet řešení
# - TabulkaOdevzdanychReseniView
# - Detail konkrétního problému a řešitele -- přehled všech řešení odevzdaných k tomuto problému
# - ReseniProblemuView
# - Detail konkrétního řešení -- všechny soubory, datum, ...
# - DetailReseniView
#
# Taky se může hodit:
# - Tabulka všech řešitelů x všech problémů?
@dataclass
class SouhrnReseni:
"""Dataclass reprezentující data o odevzdaných řešeních pro zobrazení v tabulce."""
pocet_reseni : int
posledni_odevzdani : datetime.datetime
body : float
class TabulkaOdevzdanychReseniView(ListView):
template_name = 'seminar/odevzdavatko/tabulka.html'
model = m.Hodnoceni
def get_queryset(self):
# FIXME: Tenhle blok nemůže být přímo ve třídě, protože před vyrobením databáze neexistuje Nastavení.
self.akt_rocnik = m.Nastaveni.get_solo().aktualni_rocnik # .get_solo() vrátí tu jedinou instanci, asi...
self.resitele = resi_v_rocniku(self.akt_rocnik)
# NOTE: Protože řešení odkazuje přímo na Problém a QuerySet na Hodnocení je nepolymorfní, musíme porovnávat taky s nepolymorfními Problémy.
self.zadane_problemy = m.Problem.objects.filter(stav=m.Problem.STAV_ZADANY).non_polymorphic()
qs = super().get_queryset()
qs = qs.filter(problem__in=self.zadane_problemy).select_related('reseni', 'problem').prefetch_related('reseni__resitele__osoba')
return qs
def get_context_data(self, *args, **kwargs):
# FIXME: Tenhle blok nemůže být přímo ve třídě, protože před vyrobením databáze neexistuje Nastavení.
self.akt_rocnik = m.Nastaveni.get_solo().aktualni_rocnik # .get_solo() vrátí tu jedinou instanci, asi...
self.resitele = resi_v_rocniku(self.akt_rocnik)
# NOTE: Protože řešení odkazuje přímo na Problém a QuerySet na Hodnocení je nepolymorfní, musíme porovnávat taky s nepolymorfními Problémy.
self.zadane_problemy = m.Problem.objects.filter(stav=m.Problem.STAV_ZADANY).non_polymorphic()
ctx = super().get_context_data(*args, **kwargs)
ctx['problemy'] = self.zadane_problemy
ctx['resitele'] = self.resitele
tabulka = dict()
def pridej_reseni(problem, resitel, body, cas):
if problem not in tabulka:
tabulka[problem] = dict()
if resitel not in tabulka[problem]:
tabulka[problem][resitel] = SouhrnReseni(pocet_reseni=1, posledni_odevzdani=cas, body=body)
else:
tabulka[problem][resitel].posledni_odevzdani = max(tabulka[problem][resitel].posledni_odevzdani, cas)
tabulka[problem][resitel].body = max(tabulka[problem][resitel].body, body,
key=lambda x: x if x is not None else -1 # None je malé číslo
# FIXME: Možná dává smysl i mít None jako velké číslo -- jakože "TODO: zadat body"
)
tabulka[problem][resitel].pocet_reseni += 1
# Pro jednoduchost template si ještě poznamenáme ID problému a řešitele
tabulka[problem][resitel].problem_id = problem.id
tabulka[problem][resitel].resitel_id = resitel.id
for hodnoceni in self.get_queryset():
for resitel in hodnoceni.reseni.resitele.all():
pridej_reseni(hodnoceni.problem, resitel, hodnoceni.body, hodnoceni.reseni.cas_doruceni)
hodnoty = []
for resitel in self.resitele:
resiteluv_radek = []
for problem in self.zadane_problemy:
if problem in tabulka and resitel in tabulka[problem]:
resiteluv_radek.append(tabulka[problem][resitel])
else:
resiteluv_radek.append(None)
hodnoty.append(resiteluv_radek)
ctx['radky'] = list(zip(self.resitele, hodnoty))
return ctx
class ReseniProblemuView(ListView):
model = m.Reseni
template_name = 'seminar/odevzdavatko/seznam.html'
def get_queryset(self):
qs = super().get_queryset()
resitel_id = self.kwargs['resitel']
if resitel_id is None:
raise ValueError("Nemám řešitele!")
problem_id = self.kwargs['problem']
if problem_id is None:
raise ValueError("Nemám problém! (To je problém!)")
resitel = m.Resitel.objects.get(id=resitel_id)
problem = m.Problem.objects.get(id=problem_id)
qs = qs.filter(
problem__in=[problem],
resitele__in=[resitel],
)
return qs
# Kontext automaticky?
class DetailReseniView(DetailView):
model = m.Reseni
template_name = 'seminar/odevzdavatko/detail.html'
# To je všechno? Najde se to podle pk...
# Přehled všech řešení kvůli debugování
class SeznamReseniView(ListView):
model = m.Reseni
template_name = 'seminar/odevzdavatko/seznam.html'
class SeznamAktualnichReseniView(SeznamReseniView):
def get_queryset(self):
qs = super().get_queryset()
akt_rocnik = m.Nastaveni.get_solo().aktualni_rocnik # .get_solo() vrátí tu jedinou instanci, asi...
resitele = resi_v_rocniku(akt_rocnik)
qs = qs.filter(resitele__in=resitele) # FIXME: Najde řešení i ze starých ročníků, která odevzdal alespoň jeden aktuální řešitel
return qs

142
seminar/views/views_all.py

@ -1,4 +1,4 @@
# coding:utf-8
from django.shortcuts import get_object_or_404, render, redirect from django.shortcuts import get_object_or_404, render, redirect
from django.http import HttpResponse, HttpResponseRedirect, HttpResponseForbidden, JsonResponse from django.http import HttpResponse, HttpResponseRedirect, HttpResponseForbidden, JsonResponse
@ -17,6 +17,7 @@ from django.contrib.auth.models import User, Permission
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.db import transaction from django.db import transaction
from django.core import serializers from django.core import serializers
from django.core.exceptions import PermissionDenied
from django.forms.models import model_to_dict from django.forms.models import model_to_dict
import seminar.models as s import seminar.models as s
@ -120,15 +121,57 @@ class TNLData(object):
self.appendable_siblings = tnltt.appendableChildren(self.parent) self.appendable_siblings = tnltt.appendableChildren(self.parent)
else: else:
self.appendable_siblings = [] self.appendable_siblings = []
@classmethod
def public_above(cls, anode):
""" Returns output of verejne for closest Rocnik, Cislo or Problem above.
(All of them have method verejne.)"""
parent = anode # chceme začít už od konkrétního node včetně
while True:
rocnik = isinstance(parent, s.RocnikNode)
cislo = isinstance(parent, s.CisloNode)
uloha = (isinstance(parent, s.UlohaVzorakNode) or
isinstance(parent, s.UlohaZadaniNode))
tema = isinstance(parent, s.TemaVCisleNode)
if (rocnik or cislo or uloha or tema) or parent==None:
break
else:
parent = treelib.get_parent(parent)
if rocnik:
return parent.rocnik.verejne()
elif cislo:
return parent.cislo.verejne()
elif uloha:
return parent.uloha.verejne()
elif tema:
return parent.tema.verejne()
elif None:
print("Existuje TreeNode, který není pod číslem, ročníkem, úlohou"
"ani tématem. {}".format(anode))
return False
@classmethod
def all_public_children(cls, anode):
for ch in treelib.all_children(anode):
if TNLData.public_above(ch):
yield ch
else:
continue
@classmethod @classmethod
def from_treenode(cls,anode,parent=None,index=None): def from_treenode(cls, anode, user, parent=None, index=None):
if TNLData.public_above(anode) or user.has_perm('auth.org'):
out = cls(anode,parent,index) out = cls(anode,parent,index)
for (idx,ch) in enumerate(treelib.all_children(anode)): else:
# FIXME přidat filtrování na veřejnost raise PermissionDenied()
outitem = cls.from_treenode(ch,out,idx)
if user.has_perm('auth.org'):
enum_children = enumerate(treelib.all_children(anode))
else:
enum_children = enumerate(TNLData.all_public_children(anode))
for (idx,ch) in enum_children:
outitem = cls.from_treenode(ch, user, out, idx)
out.children.append(outitem) out.children.append(outitem)
out.add_edit_options() out.add_edit_options()
return out return out
@ -195,7 +238,7 @@ class TreeNodeView(generic.DetailView):
def get_context_data(self,**kwargs): def get_context_data(self,**kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['tnldata'] = TNLData.from_treenode(self.object) context['tnldata'] = TNLData.from_treenode(self.object,self.request.user)
return context return context
class TreeNodeJSONView(generic.DetailView): class TreeNodeJSONView(generic.DetailView):
@ -203,7 +246,7 @@ class TreeNodeJSONView(generic.DetailView):
def get(self,request,*args, **kwargs): def get(self,request,*args, **kwargs):
self.object = self.get_object() self.object = self.get_object()
data = TNLData.from_treenode(self.object).to_json() data = TNLData.from_treenode(self.object,self.request.user).to_json()
return JsonResponse(data) return JsonResponse(data)
@ -332,6 +375,7 @@ class ProblemView(generic.DetailView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
user = self.request.user
# Teď potřebujeme doplnit tnldata do kontextu. # Teď potřebujeme doplnit tnldata do kontextu.
# Ošklivý type switch, hezčí by bylo udělat to polymorfni. FIXME. # Ošklivý type switch, hezčí by bylo udělat to polymorfni. FIXME.
if False: if False:
@ -339,11 +383,11 @@ class ProblemView(generic.DetailView):
pass pass
elif isinstance(self.object, s.Clanek) or isinstance(self.object, s.Konfera): elif isinstance(self.object, s.Clanek) or isinstance(self.object, s.Konfera):
# Tyhle Problémy mají ŘešeníNode # Tyhle Problémy mají ŘešeníNode
context['tnldata'] = TNLData.from_treenode(self.object.reseninode) context['tnldata'] = TNLData.from_treenode(self.object.reseninode,user)
elif isinstance(self.object, s.Uloha): elif isinstance(self.object, s.Uloha):
# FIXME: Teď vždycky zobrazujeme i vzorák! Možná by bylo hezčí/lepší mít to stejně jako pro Téma: procházet jen dosažitelné z Ročníku / čísla / whatever # FIXME: Teď vždycky zobrazujeme i vzorák! Možná by bylo hezčí/lepší mít to stejně jako pro Téma: procházet jen dosažitelné z Ročníku / čísla / whatever
tnl_zadani = TNLData.from_treenode(self.object.ulohazadaninode) tnl_zadani = TNLData.from_treenode(self.object.ulohazadaninode,user)
tnl_vzorak = TNLData.from_treenode(self.object.ulohavzoraknode) tnl_vzorak = TNLData.from_treenode(self.object.ulohavzoraknode,user)
context['tnldata'] = TNLData.from_tnldata_list([tnl_zadani, tnl_vzorak]) context['tnldata'] = TNLData.from_tnldata_list([tnl_zadani, tnl_vzorak])
elif isinstance(self.object, s.Tema): elif isinstance(self.object, s.Tema):
rocniknode = self.object.rocnik.rocniknode rocniknode = self.object.rocnik.rocniknode
@ -945,19 +989,31 @@ def secti_body_za_cislo(cislo, aktivni_resitele, hlavni_problemy=None):
if hlavni_problemy is None: if hlavni_problemy is None:
hlavni_problemy = hlavni_problemy_cisla(cislo) hlavni_problemy = hlavni_problemy_cisla(cislo)
def ne_clanek_ne_konfera(problem):
return not(isinstance(problem.get_real_instance(), m.Clanek) or isinstance(problem.get_real_instance(), m.Konfera))
temata_a_spol = list(filter(ne_clanek_ne_konfera, hlavni_problemy))
def cosi(problem):
return problem.id
hlavni_problemy_slovnik = {} hlavni_problemy_slovnik = {}
for hp in hlavni_problemy: for hp in temata_a_spol:
hlavni_problemy_slovnik[hp.id] = {} hlavni_problemy_slovnik[hp.id] = {}
hlavni_problemy_slovnik[-1] = {}
# zakládání prázdných záznamů pro řešitele # zakládání prázdných záznamů pro řešitele
cislobody = {} cislobody = {}
for ar in aktivni_resitele: for ar in aktivni_resitele:
# řešitele převedeme na řetězec pomocí unikátního id # řešitele převedeme na řetězec pomocí unikátního id
cislobody[ar.id] = "" cislobody[ar.id] = ""
for hp in hlavni_problemy: for hp in temata_a_spol:
slovnik = hlavni_problemy_slovnik[hp.id] slovnik = hlavni_problemy_slovnik[hp.id]
slovnik[ar.id] = "" slovnik[ar.id] = ""
hlavni_problemy_slovnik[-1][ar.id] = ""
# vezmeme všechna řešení s body do daného čísla # vezmeme všechna řešení s body do daného čísla
reseni_do_cisla = Reseni.objects.prefetch_related('problem', 'resitele', reseni_do_cisla = Reseni.objects.prefetch_related('problem', 'resitele',
'hodnoceni_set').filter(hodnoceni__cislo_body=cislo) 'hodnoceni_set').filter(hodnoceni__cislo_body=cislo)
@ -969,7 +1025,10 @@ def secti_body_za_cislo(cislo, aktivni_resitele, hlavni_problemy=None):
# řešení může řešit více problémů # řešení může řešit více problémů
for prob in list(reseni.problem.all()): for prob in list(reseni.problem.all()):
nadproblem = hlavni_problem(prob) nadproblem = hlavni_problem(prob)
if ne_clanek_ne_konfera(nadproblem):
nadproblem_slovnik = hlavni_problemy_slovnik[nadproblem.id] nadproblem_slovnik = hlavni_problemy_slovnik[nadproblem.id]
else:
nadproblem_slovnik = hlavni_problemy_slovnik[-1]
# a mít více hodnocení # a mít více hodnocení
for hodn in list(reseni.hodnoceni_set.all()): for hodn in list(reseni.hodnoceni_set.all()):
@ -1014,11 +1073,26 @@ def vysledkovka_cisla(cislo, context=None):
# vytvoříme jednotlivé sloupce výsledkovky # vytvoříme jednotlivé sloupce výsledkovky
radky_vysledkovky = [] radky_vysledkovky = []
i = 0 i = 0
def ne_clanek_ne_konfera(problem):
return not(isinstance(problem.get_real_instance(), m.Clanek) or isinstance(problem.get_real_instance(), m.Konfera))
temata_a_spol = list(filter(ne_clanek_ne_konfera, hlavni_problemy))
# def not_empty(value):
# return value != ''
#
# je_nejake_ostatni = any(filter(not_empty, hlavni_problemy_slovnik[-1].values())) > 0
je_nejake_ostatni = len(hlavni_problemy) - len(temata_a_spol) > 0
for ar_id in setrizeni_resitele_id: for ar_id in setrizeni_resitele_id:
# získáme seznam bodů za problémy pro daného řešitele # získáme seznam bodů za problémy pro daného řešitele
problemy = [] problemy = []
for hp in hlavni_problemy: for hp in temata_a_spol:
problemy.append(hlavni_problemy_slovnik[hp.id][ar_id]) problemy.append(hlavni_problemy_slovnik[hp.id][ar_id])
if je_nejake_ostatni:
problemy.append(hlavni_problemy_slovnik[-1][ar_id])
# vytáhneme informace pro daného řešitele # vytáhneme informace pro daného řešitele
radek = RadekVysledkovkyCisla( radek = RadekVysledkovkyCisla(
poradi[i], # pořadí poradi[i], # pořadí
@ -1034,7 +1108,8 @@ def vysledkovka_cisla(cislo, context=None):
# vytahané informace předáváme do kontextu # vytahané informace předáváme do kontextu
context['cislo'] = cislo context['cislo'] = cislo
context['radky_vysledkovky'] = radky_vysledkovky context['radky_vysledkovky'] = radky_vysledkovky
context['problemy'] = hlavni_problemy context['problemy'] = temata_a_spol
context['ostatni'] = je_nejake_ostatni
#context['v_cisle_zadane'] = TODO #context['v_cisle_zadane'] = TODO
#context['resene_problemy'] = resene_problemy #context['resene_problemy'] = resene_problemy
return context return context
@ -1063,6 +1138,7 @@ class CisloView(generic.DetailView):
context = super(CisloView, self).get_context_data(**kwargs) context = super(CisloView, self).get_context_data(**kwargs)
cislo = context['cislo'] cislo = context['cislo']
context['prevcislo'] = Cislo.objects.filter((Q(rocnik__lt=self.object.rocnik) | Q(poradi__lt=self.object.poradi))&Q(rocnik__lte=self.object.rocnik)).first()
# vrátíme context (aktuálně obsahuje jen věci ohledně výsledkovky # vrátíme context (aktuálně obsahuje jen věci ohledně výsledkovky
return vysledkovka_cisla(cislo, context) return vysledkovka_cisla(cislo, context)
@ -1079,6 +1155,30 @@ class ArchivTemataView(generic.ListView):
ctx['rocniky'][rocnik] = list(temata) ctx['rocniky'][rocnik] = list(temata)
return ctx return ctx
class OdmenyView(generic.TemplateView):
template_name = 'seminar/archiv/odmeny.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
fromcislo = Cislo.objects.get(rocnik=self.kwargs.get('frocnik'), poradi=self.kwargs.get('fcislo'))
tocislo = Cislo.objects.get(rocnik=self.kwargs.get('trocnik'), poradi=self.kwargs.get('tcislo'))
resitele = aktivniResitele(tocislo)
frombody = body_resitelu(resitele, fromcislo)
tobody = body_resitelu(resitele, tocislo)
outlist = []
for (aid, tbody) in tobody.items():
fbody = frombody.get(aid,0)
resitel = Resitel.objects.get(pk=aid)
ftitul = resitel.get_titul(fbody)
ttitul = resitel.get_titul(tbody)
if ftitul != ttitul:
outlist.append({'jmeno': resitel.osoba.plne_jmeno(), 'ftitul': ftitul, 'ttitul': ttitul})
context['zmeny'] = outlist
return context
### Generovani vysledkovky ### Generovani vysledkovky
class CisloVysledkovkaView(CisloView): class CisloVysledkovkaView(CisloView):
@ -1332,6 +1432,12 @@ class ResitelView(LoginRequiredMixin,generic.DetailView):
### Formulare ### Formulare
# pro přidání políčka do formuláře je potřeba
# - mít v modelu tu položku, kterou chci upravovat
# - přidat do views (prihlaskaView, resitelEditView)
# - přidat do forms
# - includovat do html
class AddSolutionView(LoginRequiredMixin, FormView): class AddSolutionView(LoginRequiredMixin, FormView):
template_name = 'seminar/org/vloz_reseni.html' template_name = 'seminar/org/vloz_reseni.html'
form_class = f.VlozReseniForm form_class = f.VlozReseniForm
@ -1409,7 +1515,7 @@ def prihlaska_log_gdpr_safe(logger, gdpr_logger, msg, form_data):
from django.forms.models import model_to_dict from django.forms.models import model_to_dict
def resitelEditView(request): def resitelEditView(request):
err_logger = logging.getLogger('seminar.prihlaska.problem') err_logger = logging.getLogger('seminar.prihlaska.problem')
## Načtení objektu Osoba a Resitel, patrici k aktuálně přihlášenému uživately ## Načtení objektů Osoba a Resitel patřících k aktuálně přihlášenému uživateli
u = request.user u = request.user
osoba_edit = Osoba.objects.get(user=u) osoba_edit = Osoba.objects.get(user=u)
resitel_edit = osoba_edit.resitel resitel_edit = osoba_edit.resitel
@ -1448,6 +1554,7 @@ def resitelEditView(request):
resitel_edit.skola = fcd['skola'] resitel_edit.skola = fcd['skola']
resitel_edit.rok_maturity = fcd['rok_maturity'] resitel_edit.rok_maturity = fcd['rok_maturity']
resitel_edit.zasilat = fcd['zasilat'] resitel_edit.zasilat = fcd['zasilat']
resitel_edit.zasilat_cislo_emailem = fcd['zasilat_cislo_emailem']
if fcd.get('skola'): if fcd.get('skola'):
resitel_edit.skola = fcd['skola'] resitel_edit.skola = fcd['skola']
else: else:
@ -1511,7 +1618,8 @@ def prihlaskaView(request):
r = Resitel( r = Resitel(
rok_maturity = fcd['rok_maturity'], rok_maturity = fcd['rok_maturity'],
zasilat = fcd['zasilat'] zasilat = fcd['zasilat'],
zasilat_cislo_emailem = fcd['zasilat_cislo_emailem']
) )
r.save() r.save()

165
seminar/views/views_rest.py

@ -6,24 +6,26 @@ from seminar import treelib
DEFAULT_NODE_DEPTH = 2 DEFAULT_NODE_DEPTH = 2
class TextSerializer(serializers.ModelSerializer): class TextSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = m.Text model = m.Text
fields = '__all__' fields = '__all__'
class ProblemSerializer(serializers.ModelSerializer):
class Meta:
model = m.Problem
fields = '__all__'
class UlohaSerializer(serializers.ModelSerializer):
class UlohaVzorakNodeSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = m.UlohaVzorakNode model = m.Uloha
fields = '__all__' fields = '__all__'
depth = DEFAULT_NODE_DEPTH
class UlohaZadaniNodeSerializer(serializers.ModelSerializer): class ReseniSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = m.UlohaZadaniNode model = m.Reseni
fields = '__all__' fields = '__all__'
depth = DEFAULT_NODE_DEPTH
class RocnikNodeSerializer(serializers.ModelSerializer): class RocnikNodeSerializer(serializers.ModelSerializer):
class Meta: class Meta:
@ -138,12 +140,161 @@ class CastNodeCreateSerializer(serializers.ModelSerializer):
fields = ('nadpis','where','refnode') fields = ('nadpis','where','refnode')
depth = DEFAULT_NODE_DEPTH depth = DEFAULT_NODE_DEPTH
class UlohaZadaniNodeSerializer(serializers.ModelSerializer):
class Meta:
model = m.UlohaZadaniNode
fields = '__all__'
depth = DEFAULT_NODE_DEPTH
class UlohaZadaniNodeWriteSerializer(serializers.ModelSerializer):
uloha = UlohaSerializer()
def update(self,node,validated_data):
node.uloha.max_body = validated_data.get('uloha').get('max_body')
node.uloha.kod = validated_data.get('uloha').get('kod')
node.uloha.nazev = validated_data.get('uloha').get('nazev')
node.uloha.save()
return node
class Meta:
model = m.TextNode
fields = ('id','uloha')
depth = DEFAULT_NODE_DEPTH
class UlohaZadaniNodeCreateSerializer(serializers.ModelSerializer):
uloha = UlohaSerializer()
refnode = serializers.IntegerField()
where = serializers.CharField()
def create(self,validated_data):
# text_zadani = validated_data.pop('text_zadani')
temp_uloha = validated_data.pop('uloha')
where = validated_data.pop('where')
refnode_id = validated_data.pop('refnode')
refnode = m.TreeNode.objects.get(pk=refnode_id)
# Z cesty ke koreni stromu zjistime, v jakem jsme tematu a v jakem cisle
cislo = None
tema = None
travelnode = refnode
while travelnode is not None:
if isinstance(travelnode, m.TemaVCisleNode):
tema = travelnode.tema
if isinstance(travelnode, m.CisloNode):
cislo = travelnode.cislo
travelnode = treelib.get_parent(travelnode)
# Vyrobime ulohu
uloha = m.Uloha.objects.create(cislo_zadani=cislo, nadproblem = tema, **temp_uloha)
# A vyrobime UlohaZadaniNode
if where == 'syn':
node = treelib.create_child(refnode,m.UlohaZadaniNode,uloha = uloha)
elif where == 'za':
node = treelib.create_node_after(refnode,m.UlohaZadaniNode,uloha = uloha)
elif where == 'pred':
node = treelib.create_node_before(refnode,m.UlohaZadaniNode,uloha = uloha)
node.where = None
node.refnode = None
node.max_body = None
node.kod = None
return node
class Meta:
model = m.UlohaZadaniNode
fields = ('uloha','where','refnode')
depth = DEFAULT_NODE_DEPTH
class UlohaVzorakNodeSerializer(serializers.ModelSerializer):
class Meta:
model = m.UlohaVzorakNode
fields = '__all__'
depth = DEFAULT_NODE_DEPTH
class UlohaVzorakNodeWriteSerializer(serializers.ModelSerializer):
uloha = serializers.PrimaryKeyRelatedField(queryset=m.Uloha.objects.all(), many=False, read_only=False)
class Meta:
model = m.UlohaVzorakNode
fields = ('id','uloha')
depth = DEFAULT_NODE_DEPTH
class UlohaVzorakNodeCreateSerializer(serializers.ModelSerializer):
uloha_id = serializers.IntegerField()
refnode = serializers.IntegerField()
where = serializers.CharField()
def create(self, validated_data):
uloha_id = validated_data.pop('uloha_id')
uloha = m.Uloha.objects.get(pk=uloha_id)
where = validated_data.pop('where')
refnode_id = validated_data.pop('refnode')
refnode = m.TreeNode.objects.get(pk=refnode_id)
if where == 'syn':
node = treelib.create_child(refnode,m.UlohaVzorakNode,uloha = uloha)
elif where == 'za':
node = treelib.create_node_after(refnode,m.UlohaVzorakNode,uloha = uloha)
elif where == 'pred':
node = treelib.create_node_before(refnode,m.UlohaVzorakNode,uloha = uloha)
node.refnode = None
node.where = None
node.uloha_id = None
return node
class Meta:
model = m.UlohaVzorakNode
fields = ('refnode', 'uloha_id', 'where')
depth = DEFAULT_NODE_DEPTH
class ReseniNodeSerializer(serializers.ModelSerializer): class ReseniNodeSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = m.ReseniNode model = m.ReseniNode
fields = '__all__' fields = '__all__'
depth = DEFAULT_NODE_DEPTH depth = DEFAULT_NODE_DEPTH
class ReseniNodeWriteSerializer(serializers.ModelSerializer):
reseni = serializers.PrimaryKeyRelatedField(queryset=m.Reseni.objects.all(), many=False, read_only=False)
class Meta:
model = m.ReseniNode
fields = ('id','reseni')
depth = DEFAULT_NODE_DEPTH
class ReseniNodeCreateSerializer(serializers.ModelSerializer):
reseni_id = serializers.IntegerField()
refnode = serializers.IntegerField()
where = serializers.CharField()
def create(self,validated_data):
# text_zadani = validated_data.pop('text_zadani')
reseni_id = validated_data.pop('reseni_id')
reseni = m.Reseni.objects.get(pk=reseni_id)
where = validated_data.pop('where')
refnode_id = validated_data.pop('refnode')
refnode = m.TreeNode.objects.get(pk=refnode_id)
# A vyrobime UlohaZadaniNode
if where == 'syn':
node = treelib.create_child(refnode,m.ReseniNode,reseni = reseni)
elif where == 'za':
node = treelib.create_node_after(refnode,m.ReseniNode,reseni = reseni)
elif where == 'pred':
node = treelib.create_node_before(refnode,m.ReseniNode,reseni = reseni)
node.where = None
node.refnode = None
node.reseni_id = None
return node
class Meta:
model = m.ReseniNode
fields = ('reseni_id','where','refnode')
depth = DEFAULT_NODE_DEPTH
class TreeNodeSerializer(PolymorphicSerializer): class TreeNodeSerializer(PolymorphicSerializer):
model_serializer_mapping = { model_serializer_mapping = {

115
seminar/viewsets.py

@ -1,4 +1,7 @@
from rest_framework import viewsets,filters from rest_framework import viewsets,filters
from rest_framework import status
from rest_framework.response import Response
from django.core.exceptions import PermissionDenied
from rest_framework.permissions import BasePermission, AllowAny from rest_framework.permissions import BasePermission, AllowAny
from . import models as m from . import models as m
from . import views from . import views
@ -18,31 +21,6 @@ class PermissionMixin(object):
# návštěvník nemusí být zalogován, aby si prohlížel obsah # návštěvník nemusí být zalogován, aby si prohlížel obsah
return [permission() for permission in permission_classes] return [permission() for permission in permission_classes]
def verejne_nad(self, node):
""" Returns output of verejne for closest Rocnik, Cislo or Problem above.
(All of them have method verejne.)"""
parent = get_parent(node)
while True:
rocnik = isinstance(parent, RocnikNode)
cislo = isinstance(parent, CisloNode)
problem = isinstance(parent, ProblemNode)
if (rocnik or cislo or problem):
break
else:
parent = get_parent(parent)
if rocnik:
return parent.rocnik.verejne()
elif cislo:
return parent.cislo.verejne()
elif problem:
return parent.problem.verjne()
def has_object_permission(self, request, view, obj):
# test that obj is Node
assert isinstance(obj, Node)
return verejne_nad(node)
class ReadWriteSerializerMixin(object): class ReadWriteSerializerMixin(object):
""" """
Overrides get_serializer_class to choose the read serializer Overrides get_serializer_class to choose the read serializer
@ -87,10 +65,6 @@ class ReadWriteSerializerMixin(object):
) )
return self.create_serializer_class return self.create_serializer_class
class UlohaVzorakNodeViewSet(PermissionMixin, viewsets.ModelViewSet):
queryset = m.UlohaVzorakNode.objects.all()
serializer_class = views.UlohaVzorakNodeSerializer
class TextViewSet(PermissionMixin, viewsets.ModelViewSet): class TextViewSet(PermissionMixin, viewsets.ModelViewSet):
queryset = m.Text.objects.all() queryset = m.Text.objects.all()
serializer_class = views.TextSerializer serializer_class = views.TextSerializer
@ -107,12 +81,91 @@ class CastNodeViewSet(PermissionMixin, ReadWriteSerializerMixin,viewsets.ModelVi
write_serializer_class = views.CastNodeSerializer write_serializer_class = views.CastNodeSerializer
create_serializer_class = views.CastNodeCreateSerializer create_serializer_class = views.CastNodeCreateSerializer
class UlohaVzorakNodeViewSet(PermissionMixin, viewsets.ModelViewSet): def destroy(self, request, *args, **kwargs):
serializer_class = views.UlohaVzorakNodeSerializer obj = self.get_object()
print(obj)
if obj.first_child is None:
return super().destroy(request,*args,**kwargs)
raise PermissionDenied('Nelze smazat CastNode, který má děti!')
class UlohaVzorakNodeViewSet(PermissionMixin, ReadWriteSerializerMixin, viewsets.ModelViewSet):
read_serializer_class = views.UlohaVzorakNodeSerializer
write_serializer_class = views.UlohaVzorakNodeWriteSerializer
create_serializer_class = views.UlohaVzorakNodeCreateSerializer
def get_queryset(self): def get_queryset(self):
queryset = m.UlohaVzorakNode.objects.all() queryset = m.UlohaVzorakNode.objects.all()
nazev = self.request.query_params.get('nazev',None) nazev = self.request.query_params.get('nazev',None)
if nazev is not None: if nazev is not None:
queryset = queryset.filter(nazev__contains=nazev) queryset = queryset.filter(nazev__contains=nazev)
if self.request.user.has_perm('auth.org'):
return queryset return queryset
else: # pro neorgy jen zveřejněné vzoráky
return queryset.filter(uloha__cislo_reseni__verejne_db=True)
nadproblem = self.request.query_params.get('nadproblem',None)
if nadproblem is not None:
queryset = queryset.filter(nadproblem__pk = nadproblem)
return queryset
class ReseniViewSet(viewsets.ModelViewSet):
serializer_class = views.ReseniSerializer
def get_queryset(self):
queryset = m.Reseni.objects.all()
#FIXME upravit nazvy dle skutecnych polozek reseni
nazev = self.request.query_params.get('nazev',None)
if nazev is not None:
queryset = queryset.filter(nazev__contains=nazev)
nadproblem = self.request.query_params.get('nadproblem',None)
if nadproblem is not None:
queryset = queryset.filter(nadproblem__pk = nadproblem)
return queryset
class UlohaViewSet(viewsets.ModelViewSet):
serializer_class = views.UlohaSerializer
def get_queryset(self):
queryset = m.Uloha.objects.all()
nazev = self.request.query_params.get('nazev',None)
if nazev is not None:
queryset = queryset.filter(nazev__contains=nazev)
nadproblem = self.request.query_params.get('nadproblem',None)
if nadproblem is not None:
queryset = queryset.filter(nadproblem__pk = nadproblem)
return queryset
class UlohaZadaniNodeViewSet(ReadWriteSerializerMixin, viewsets.ModelViewSet):
queryset = m.UlohaZadaniNode.objects.all()
read_serializer_class = views.UlohaZadaniNodeSerializer
write_serializer_class = views.UlohaZadaniNodeWriteSerializer
create_serializer_class = views.UlohaZadaniNodeCreateSerializer
class ReseniNodeViewSet(ReadWriteSerializerMixin, viewsets.ModelViewSet):
queryset = m.ReseniNode.objects.all()
read_serializer_class = views.ReseniNodeSerializer
write_serializer_class = views.ReseniNodeWriteSerializer
create_serializer_class = views.ReseniNodeCreateSerializer
class ProblemViewSet(viewsets.ModelViewSet):
serializer_class = views.ProblemSerializer
def get_queryset(self):
queryset = m.Problem.objects.all()
ucel = self.request.query_params.get('ucel',None)
rocnik = self.request.query_params.get('rocnik',None)
tema = self.request.query_params.get('tema',None)
if rocnik is not None:
queryset = queryset.filter(rocnik=rocnik)
#if tema is not None:
return queryset

5
vue_frontend/src/components/AddNewNode.vue

@ -1,12 +1,12 @@
<template> <template>
<div class="addnewnode"> <div class="addnewnode">
<button v-if="types.includes('castNode')" v-on:click="selected='castNode'" :disabled="selected && selected !== 'castNode'">Část</button> <button v-if="types.includes('castNode')" v-on:click="selected='castNode'" :disabled="selected && selected !== 'castNode'">Nadpis části</button>
<button v-if="types.includes('textNode')" v-on:click="selected='textNode'" :disabled="selected && selected !== 'textNode'">Text</button> <button v-if="types.includes('textNode')" v-on:click="selected='textNode'" :disabled="selected && selected !== 'textNode'">Text</button>
<button v-if="types.includes('reseniNode')" v-on:click="selected='reseniNode'" :disabled="selected && selected !== 'reseniNode'">Řešení</button> <button v-if="types.includes('reseniNode')" v-on:click="selected='reseniNode'" :disabled="selected && selected !== 'reseniNode'">Řešení</button>
<button v-if="types.includes('ulohaZadaniNode')" v-on:click="selected='ulohaZadaniNode'" :disabled="selected && selected !== 'ulohaZadaniNode'">Zadání úlohy</button> <button v-if="types.includes('ulohaZadaniNode')" v-on:click="selected='ulohaZadaniNode'" :disabled="selected && selected !== 'ulohaZadaniNode'">Zadání úlohy</button>
<button v-if="types.includes('ulohaVzorakNode')" v-on:click="selected='ulohaVzorakNode'" :disabled="selected && selected !== 'ulohaVzorakNode'">Vzorák</button> <button v-if="types.includes('ulohaVzorakNode')" v-on:click="selected='ulohaVzorakNode'" :disabled="selected && selected !== 'ulohaVzorakNode'">Vzorák</button>
<div v-if="selected"> <div v-if="selected">
<component :is='selected' :item='null' :where="where" :refnode="refnode" create></component> <component :is='selected' :item='null' :where="where" :refnode="refnode" :tema="tema" create></component>
</div> </div>
</div> </div>
</template> </template>
@ -24,6 +24,7 @@ export default {
types: Array, types: Array,
where: String, where: String,
refnode: Object, refnode: Object,
tema: Object,
}, },
data: () => ({ data: () => ({
selected: null, selected: null,

21
vue_frontend/src/components/CastNode.vue

@ -7,7 +7,9 @@
<button v-on:click="currentText = originalText;editorShow=!editorShow;">Zahodit úpravy</button> <button v-on:click="currentText = originalText;editorShow=!editorShow;">Zahodit úpravy</button>
</div> </div>
<div v-else> <div v-else>
<h4>{{ currentText }} </h4> <button v-if="editorMode" v-on:click="editorShow=!editorShow">Upravit</button> <h4>{{ currentText }} </h4>
<button v-if="editorMode" v-on:click="editorShow = !editorShow">Upravit</button>
<button v-if="editorMode" v-on:click="deleteCast" class="delete">Smazat</button>
</div> </div>
</div> </div>
</template> </template>
@ -25,6 +27,7 @@ export default {
props: { props: {
item: Object, item: Object,
editorShow: Boolean, editorShow: Boolean,
editorMode: Boolean,
create: Boolean, create: Boolean,
where: String, where: String,
refnode: Object refnode: Object
@ -76,6 +79,16 @@ export default {
} }
this.editorShow = false; this.editorShow = false;
},
deleteCast: function() {
console.log("Deleting cast");
axios.delete('/api/castnode/'+this.item.node.id+'/'
).then( () => {
this.loading = false;
this.$root.$emit('updateData',"castNode delete update");
}).catch(e => {
this.errors.push(e);
});
} }
} }
} }
@ -83,4 +96,10 @@ export default {
<!-- Add "scoped" attribute to limit CSS to this component only --> <!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped> <style scoped>
.changed {
background-color: yellow;
}
.delete {
background-color: #ff6666;
}
</style> </style>

18
vue_frontend/src/components/TextNode.vue

@ -23,7 +23,8 @@
<template v-else v-bind:class="changedObject"> <template v-else v-bind:class="changedObject">
<p v-html="currentText"></p> <p v-html="currentText"></p>
<button v-if="editorMode" v-on:click="editorShow=!editorShow">Upravit</button> <button v-if="editorMode" v-on:click="editorShow = !editorShow">Upravit</button>
<button v-if="editorMode" v-on:click="deleteText" class="delete">Smazat</button>
</template> </template>
</div> </div>
</template> </template>
@ -162,6 +163,16 @@ export default {
} }
this.editorShow = false; this.editorShow = false;
}, },
deleteText: function() {
console.log("Deleting text");
axios.delete('/api/textnode/'+this.item.node.id+'/'
).then( () => {
this.loading = false;
this.$root.$emit('updateData',"textNode delete update");
}).catch(e => {
this.errors.push(e);
});
},
} }
} }
</script> </script>
@ -169,6 +180,9 @@ export default {
<!-- Add "scoped" attribute to limit CSS to this component only --> <!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped> <style scoped>
.changed { .changed {
background-color: 'yellow'; background-color: yellow;
}
.delete {
background-color: #ff6666;
} }
</style> </style>

40
vue_frontend/src/components/TreeNode.vue

@ -1,16 +1,19 @@
<template> <template>
<div :class="editorMode ? 'treenode-org' : 'treenode'"> <div :class="editorMode ? 'treenode-org' : 'treenode'">
<!--b v-if="v_tematu">v tematu</b> <!--b v-if="tema">v tematu</b>
<b v-if="visible">visible</b> <b v-if="visible">visible</b>
Force visible: {{String(force_visible)}}--> Force visible: {{String(force_visible)}}-->
<component :is='item.node.polymorphic_ctype.model' :item='item' :key='item.node.id' <component :is='item.node.polymorphic_ctype.model' :item='item' :key='item.node.id'
:tema="temaOut"
:editorMode="editorMode" :editorMode="editorMode"
:debugMode="debugMode"></component> :debugMode="debugMode"></component>
<button v-if="debugMode" v-on:click="debugShow = !debugShow" class="nodebug">Ladící data</button> <!-- bude tu nějaký if na class="nodebug", v debug módu bude tlačítko vidět, jinak ne --> <button v-if="debugMode" v-on:click="debugShow = !debugShow" class="nodebug">Ladící data</button> <!-- bude tu nějaký if na class="nodebug", v debug módu bude tlačítko vidět, jinak ne -->
<div v-if="debugShow"> <div v-if="debugShow">
<pre>Tema: {{ tema }}</pre>
<pre>TemaOut: {{ temaOut }}</pre>
<pre>{{ item.node.polymorphic_ctype.model }}</pre> <pre>{{ item.node.polymorphic_ctype.model }}</pre>
<pre>{{ item }}</pre> <pre>{{ item }}</pre>
</div> </div>
@ -18,31 +21,31 @@
<div v-if="item.children.length === 0"> <div v-if="item.children.length === 0">
<div v-if="item.appendable_children.length > 0 && editorMode"> <div v-if="item.appendable_children.length > 0 && editorMode">
<b>Vložit jako syna: </b> <b>Vložit jako syna: </b>
<addnewnode :types="item.appendable_children" :refnode="item.node" where="syn" /> <addnewnode :types="item.appendable_children" :refnode="item.node" :tema="temaOut" where="syn" />
</div> </div>
</div> </div>
<div v-else :class="editorMode ? 'children-org' : 'children'"> <!-- bude tu nějaký if na class="children" --> <div v-else :class="editorMode ? 'children-org' : 'children'"> <!-- bude tu nějaký if na class="children" -->
<div v-if="item.children.length > 0 && item.children[0].appendable_siblings.length > 0 && editorMode"> <div v-if="item.children.length > 0 && item.children[0].appendable_siblings.length > 0 && editorMode">
<b>Vložit před: </b> <b>Vložit před: </b>
<addnewnode :types="item.children[0].appendable_siblings" :refnode="item.children[0].node" where="pred" /> <addnewnode :types="item.children[0].appendable_siblings" :refnode="item.children[0].node" :tema="temaOut" where="pred" />
</div> </div>
<div v-if="item.node.polymorphic_ctype.model==='temavcislenode'"> <div v-if="item.node.polymorphic_ctype.model==='temavcislenode'">
<!--Children: {{String(showChildren)}}--> <!--Children: {{String(showChildren)}}-->
<div v-for="(chld, index) in item.children" v-bind:key="chld.nazev" > <div v-for="(chld, index) in item.children" v-bind:key="chld.nazev" >
<!--Hide: {{hideNode(chld)}}, v tematu: {{v_tematu}}, force_visible: {{force_visible}}--> <!--Hide: {{hideNode(chld)}}, v tematu: {{tema}}, force_visible: {{force_visible}}-->
<div v-if="!hideNode(chld)"> <div v-if="!hideNode(chld)">
<div v-if="chld.node.polymorphic_ctype.model==='ulohazadaninode'"> <div v-if="chld.node.polymorphic_ctype.model==='ulohazadaninode'">
<button v-if="showChildren" v-on:click="showChildren=!showChildren"> Schovat </button> <button v-if="showChildren" v-on:click="showChildren=!showChildren"> Schovat </button>
<button v-else v-on:click="showChildren=!showChildren"> Rozbalit </button> <button v-else v-on:click="showChildren=!showChildren"> Rozbalit </button>
<TreeNode :item="chld" :v_tematu="true" <TreeNode :item="chld" :tema="temaOut"
:force_visible="showChildren" :force_visible="showChildren"
:editorMode="editorMode" :editorMode="editorMode"
:debugMode="debugMode"> :debugMode="debugMode">
</TreeNode> </TreeNode>
</div> </div>
<div v-else> <div v-else>
<TreeNode :item="chld" :v_tematu="true" <TreeNode :item="chld" :tema="temaOut"
:force_visible="showChildren" :force_visible="showChildren"
:editorMode="editorMode" :editorMode="editorMode"
:debugMode="debugMode"> :debugMode="debugMode">
@ -51,7 +54,7 @@
<div v-if="chld.appendable_siblings.length > 0 && editorMode" > <div v-if="chld.appendable_siblings.length > 0 && editorMode" >
<b v-if="index < (item.children.length - 1)">Vložit mezi: </b> <b v-if="index < (item.children.length - 1)">Vložit mezi: </b>
<b v-else>Vložit za: </b> <b v-else>Vložit za: </b>
<addnewnode :types="chld.appendable_siblings" :refnode="chld.node" where="za" /> <addnewnode :types="chld.appendable_siblings" :refnode="chld.node" :tema="temaOut" where="za" />
</div> </div>
</div> </div>
</div> </div>
@ -60,16 +63,16 @@
</div> </div>
<div v-else> <div v-else>
<div v-for="(chld, index) in item.children" v-bind:key="chld.nazev" > <div v-for="(chld, index) in item.children" v-bind:key="chld.nazev" >
<div v-if="v_tematu && chld.node.polymorphic_ctype.model==='ulohazadaninode'"> <div v-if="tema !== null && chld.node.polymorphic_ctype.model==='ulohazadaninode'">
<div> Tady možná něco je </div> <div> Tady možná něco je </div>
<TreeNode :item="chld" :v_tematu="v_tematu" <TreeNode :item="chld" :tema="temaOut"
:force_visible="force_visible" :force_visible="force_visible"
:editorMode="editorMode" :editorMode="editorMode"
:debugMode="debugMode"> :debugMode="debugMode">
</TreeNode> </TreeNode>
</div> </div>
<div v-else> <div v-else>
<TreeNode :item="chld" :v_tematu="v_tematu" <TreeNode :item="chld" :tema="temaOut"
:force_visible="force_visible" :force_visible="force_visible"
:editorMode="editorMode" :editorMode="editorMode"
:debugMode="debugMode"> :debugMode="debugMode">
@ -78,7 +81,7 @@
<div v-if="chld.appendable_siblings.length > 0 && editorMode" > <div v-if="chld.appendable_siblings.length > 0 && editorMode" >
<b v-if="index < (item.children.length - 1)">Vložit mezi: </b> <b v-if="index < (item.children.length - 1)">Vložit mezi: </b>
<b v-else>Vložit za: </b> <b v-else>Vložit za: </b>
<addnewnode :types="chld.appendable_siblings" :refnode="chld.node" where="za" /> <addnewnode :types="chld.appendable_siblings" :refnode="chld.node" :tema="temaOut" where="za" />
</div> </div>
</div> </div>
</div> </div>
@ -112,14 +115,15 @@ export default {
}, },
data: () => ({ data: () => ({
debugShow: false, debugShow: false,
showChildren: false showChildren: false,
temaOut: null,
}), }),
computed: { computed: {
}, },
props: { props: {
item: Object, item: Object,
force_visible: Boolean, force_visible: Boolean,
v_tematu: Boolean, tema: Object,
editorMode: Boolean, editorMode: Boolean,
debugMode: Boolean, debugMode: Boolean,
}, },
@ -132,7 +136,17 @@ export default {
return false; return false;
} }
return true; return true;
},
},
mounted: function(){
if (this.item.node.polymorphic_ctype.model === 'temavcislenode'){
this.temaOut = this.item.node.tema;
console.log(this.temaOut);
} else {
this.temaOut = this.tema;
} }
} }
} }
</script> </script>

2
vue_frontend/src/components/TreeNodeRoot.vue

@ -3,6 +3,7 @@
<div id="loading" v-if="loading"> <div id="loading" v-if="loading">
Loading... Loading...
</div> </div>
<div v-else>
<!--pre> <!--pre>
{{item}} {{item}}
</pre--> </pre-->
@ -12,6 +13,7 @@
<button v-show="!debugMode" v-on:click="debugMode = true">Zapnout ladicí mód</button> <button v-show="!debugMode" v-on:click="debugMode = true">Zapnout ladicí mód</button>
<TreeNode :item="item" :editorMode="editorMode" :debugMode="debugMode"/> <TreeNode :item="item" :editorMode="editorMode" :debugMode="debugMode"/>
</div> </div>
</div>
</template> </template>
<script> <script>

90
vue_frontend/src/components/UlohaVzorakNode.vue

@ -1,9 +1,8 @@
<template> <template>
<div class="ulohavzoraknode"> <div class="ulohavzoraknode">
<template v-if="editorShow">
<!--pre>UlohaVzorakNode {{item}} {{typeof(item)}}</pre--> <!--pre>UlohaVzorakNode {{item}} {{typeof(item)}}</pre-->
<h5>Řešení {{item.node.uloha.cislo_zadani.poradi}}.{{ item.node.uloha.kod }}: {{ item.node.uloha.nazev }}</h5> <!--h5 v-if="!editorMode">Řešení {{item.node.uloha.cislo_zadani.poradi}}.{{ item.node.uloha.kod }}: {{ item.node.uloha.nazev }}</h5-->
<button v-if="editorMode" v-on:click="showSelect=!showSelect" class="upravit">Upravit</button>
<div v-if="showSelect">
<form class="searchForm" v-on:submit.prevent="submitSearch"> <form class="searchForm" v-on:submit.prevent="submitSearch">
<input type="text" v-model="searchQuery" placeholder="Napište název" @keyup="submitSearch"> <input type="text" v-model="searchQuery" placeholder="Napište název" @keyup="submitSearch">
</form> </form>
@ -12,7 +11,13 @@
<li v-for="res in searchResults" :key="res.id" v-on:click="setSelected(res)">{{res.nazev}}</li> <li v-for="res in searchResults" :key="res.id" v-on:click="setSelected(res)">{{res.nazev}}</li>
</ul> </ul>
</div> </div>
</div> <button v-if="create" v-on:click="createNode">Vytvořit vzorák</button>
<button v-if="!create" v-on:click="saveNode">Uložit</button>
</template>
<template v-else>
<h5>Řešení {{item.node.uloha.cislo_zadani.poradi}}.{{item.node.uloha.kod}}: {{item.node.uloha.nazev}}</h5>
<button v-if="editorMode" v-on:click="editorShow = !editorShow">Upravit</button>
</template>
</div> </div>
</template> </template>
@ -26,31 +31,58 @@ export default {
isResult: false, isResult: false,
searchQuery: '', searchQuery: '',
searchResults: [], searchResults: [],
showSelect: false,
selected: null, selected: null,
selected_id: null selected_id: null,
editorShow: false,
} }
}, },
props: { props: {
item: Object, item: Object,
create: Boolean, create: Boolean,
showSelect: Boolean,
editorMode: Boolean, editorMode: Boolean,
editorShow: Boolean,
tema: Object,
refnode: Object,
where: String,
}, },
mounted: function(){ mounted: function(){
if (this.item.node.uloha === null){ axios.defaults.headers.common['X-CSRFToken'] = this.getCookie('csrftoken');
if (this.create){
this.editorShow = true;
this.searchQuery = "";
this.selected_id = null;
} else {
this.searchQuery = this.item.node.uloha.nazev;
this.selected_id = this.item.node.uloha.id;
this.selected = this.item.node.uloha;
}
if (this.item !== null && this.item.node.uloha === null){
console.log("Uloha je null!"); console.log("Uloha je null!");
console.log(this.item); console.log(this.item);
} }
if (this.create){
this.showSelect = true;
console.log('Creating');
}
}, },
methods: { methods: {
getCookie: function (name){
var cookieValue = null;
if (document.cookie && document.cookie != '') {
var cookies = document.cookie.split(';');
for (var i = 0; i < cookies.length; i++) {
var cookie = cookies[i].trim();
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) == (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
},
submitSearch: function(){ submitSearch: function(){
if (this.searchQuery.length < 3) { return;} if (this.searchQuery.length < 3) { return;}
var reqURL = "/api/ulohavzoraknode/?nazev="+this.searchQuery; let nazev = "nazev="+this.searchQuery+"&";
let nadproblem = "nadproblem="+this.tema.id+"&";
var reqURL = "/api/uloha/?"+nazev+nadproblem;
axios.get(reqURL).then( (response) => { axios.get(reqURL).then( (response) => {
this.searchResults = response.data.results; this.searchResults = response.data.results;
this.isResult = true; this.isResult = true;
@ -63,6 +95,38 @@ export default {
setSelected: function(res){ setSelected: function(res){
this.searchQuery = res.nazev this.searchQuery = res.nazev
this.selected_id = res.id this.selected_id = res.id
this.selected = res
},
createNode: function(){
console.log('Creating UlohaVzorakNode');
this.loading = true
axios.post('/api/ulohavzoraknode/',{
refnode: this.refnode.id,
where: this.where,
uloha_id: this.selected_id,
}).then(response => {
console.log(response.data);
this.loading=false;
this.$root.$emit('updateData',"ulohaZadaniNode create update");
}).catch( e => {
console.log(e);
});
},
saveNode: function(){
console.log('Saving UlohaVzorakNode');
this.loading = true
axios.put('/api/ulohavzoraknode/'+this.item.node.id+'/',{
id: this.item.node.id,
uloha: this.selected_id
}).then(response => {
console.log(response.data);
this.loading=false;
this.item.node = response.data;
this.$root.$emit('updateData',"ulohaZadaniNode save update");
}).catch( e => {
console.log(e);
});
this.editorShow = false;
} }
} }
} }

100
vue_frontend/src/components/UlohaZadaniNode.vue

@ -1,23 +1,113 @@
<template> <template>
<div class="ulohazadaninode"> <div class="ulohazadaninode">
<!--pre>UlohaZadaniNode {{item.node.uloha}} {{typeof(item)}}</pre--> <template v-if="editorShow">
<h5>Zadání {{item.node.uloha.cislo_zadani.poradi}}.{{ item.node.uloha.kod }}: {{ item.node.uloha.nazev }}</h5> Název: <input tpye="text" v-model="nazev"><br>
Počet bodů: <input type="number" min="0" max="20" v-model="max_body"><br>
Kód: <input type="text" v-model="kod"><br>
<!--Autor: FIXME!<br-->
<button v-if="create" v-on:click="createNode">Vytvořit úlohu</button>
<button v-if="!create" v-on:click="saveNode">Uložit</button>
</template>
<template v-else>
<h5>Zadání {{item.node.uloha.cislo_zadani.poradi}}.{{ item.node.uloha.kod }}: {{ item.node.uloha.nazev }} ({{item.node.uloha.max_body}} b)</h5>
<button v-if="editorMode" v-on:click="editorShow = !editorShow">Upravit</button>
<!--button v-if="editorMode" v-on:click="deleteText" class="delete">Smazat</button-->
</template>
</div> </div>
</template> </template>
<script> <script>
import axios from 'axios'
export default { export default {
name: 'UlohaZadaniNode', name: 'UlohaZadaniNode',
data: () => ({
max_body: 0,
kod: "",
editorShow: false,
editorMode: false,
}),
props: { props: {
item: Object, item: Object,
created: Boolean editorShow: Boolean,
, editorMode: Boolean,
create: Boolean,
where: String,
refnode: Object
},
methods: {
getCookie: function (name){
var cookieValue = null;
if (document.cookie && document.cookie != '') {
var cookies = document.cookie.split(';');
for (var i = 0; i < cookies.length; i++) {
var cookie = cookies[i].trim();
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) == (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
},
createNode: function(){
console.log('Creating UlohaZadaniNode');
this.loading = true
axios.post('/api/ulohazadaninode/',{
refnode: this.refnode.id,
where: this.where,
uloha: {
max_body: this.max_body,
kod: this.kod,
nazev: this.nazev,
},
}).then(response => {
console.log(response.data);
this.loading=false;
this.$root.$emit('updateData',"ulohaZadaniNode create update");
}).catch( e => {
console.log(e);
});},
saveNode: function(){
console.log('Saving UlohaZadaniNode');
this.loading = true
axios.put('/api/ulohazadaninode/'+this.item.node.id+'/',{
uloha: {
max_body: this.max_body,
kod: this.kod,
nazev: this.nazev,
}
}).then(response => {
console.log(response.data);
this.loading=false;
this.item.node = response.data;
this.$root.$emit('updateData',"ulohaZadaniNode save update");
}).catch( e => {
console.log(e);
});
this.editorShow = false;
},
},
mounted: function(){ mounted: function(){
axios.defaults.headers.common['X-CSRFToken'] = this.getCookie('csrftoken');
if (this.create){
this.editorShow = true;
this.max_body = 0;
this.nazev = "";
this.kod = "";
} else {
this.max_body = this.item.node.uloha.max_body;
this.nazev = this.item.node.uloha.nazev;
this.kod = this.item.node.uloha.kod;
}
if (this.item.node.uloha === null){ if (this.item.node.uloha === null){
console.log("Uloha je null!"); console.log("Uloha je null!");
console.log(this.item); console.log(this.item);
} }
} }
} }
}
</script> </script>

2
vue_frontend/src/router/index.js

@ -20,7 +20,7 @@ export default new Router({
}, { }, {
path: '/zadani/aktualni', path: '/zadani/aktualni',
name: 'treenode_zadani', name: 'treenode_zadani',
props: {'tnid': 23}, props: {'tnid': 1655},
component: TreeNodeRoot component: TreeNodeRoot
}, { }, {
path: '/cislo/:cislo', path: '/cislo/:cislo',

5
vue_frontend/vue.config.js

@ -15,7 +15,7 @@ const pages = {
module.exports = { module.exports = {
pages: pages, pages: pages,
filenameHashing: false, filenameHashing: false,
productionSourceMap: false, productionSourceMap: true,
publicPath: process.env.NODE_ENV === 'production' publicPath: process.env.NODE_ENV === 'production'
? '/static/seminar/vue/' ? '/static/seminar/vue/'
: 'http://localhost:8080/', : 'http://localhost:8080/',
@ -23,6 +23,7 @@ module.exports = {
chainWebpack: config => { chainWebpack: config => {
config.optimization.minimize(false)
config.optimization config.optimization
.splitChunks({ .splitChunks({
cacheGroups: { cacheGroups: {
@ -33,7 +34,7 @@ module.exports = {
priority: 1 priority: 1
}, },
}, },
}); }).minimize(false);
Object.keys(pages).forEach(page => { Object.keys(pages).forEach(page => {
config.plugins.delete(`html-${page}`); config.plugins.delete(`html-${page}`);

Loading…
Cancel
Save