Merge branch 'data_migrations' of gimli.ms.mff.cuni.cz:/akce/mam/git/mamweb into data_migrations

This commit is contained in:
Anet 2020-03-18 21:53:07 +01:00
commit d9558a750a
21 changed files with 1003 additions and 154 deletions

Binary file not shown.

332
flat.json Normal file

File diff suppressed because one or more lines are too long

View file

@ -2,6 +2,7 @@ from django import forms
from dal import autocomplete
from django.core.exceptions import ObjectDoesNotExist
from django.contrib.auth.models import User
from django.forms.models import inlineformset_factory
from .models import Skola, Resitel, Osoba, Problem
import seminar.models as m
@ -252,6 +253,25 @@ class VlozReseniForm(forms.Form):
super().__init__(*args, **kwargs)
#self.fields['favorite_color'] = forms.ChoiceField(choices=[(color.id, color.name) for color in Resitel.objects.all()])
class NahrajReseniForm(forms.ModelForm):
class Meta:
model = m.Reseni
fields = ('problem',)
widgets = {'problem':
autocomplete.ModelSelect2Multiple(
url='autocomplete_problem_odevzdatelny',
attrs = {'data-placeholder--id': '-1',
'data-placeholder--text' : '---',
'data-allow-clear': 'true'},
)
}
ReseniSPrilohamiFormSet = inlineformset_factory(m.Reseni,m.PrilohaReseni,
form = NahrajReseniForm,
fields = ('soubor','res_poznamka'),
widgets = {'res_poznamka':forms.TextInput()},
extra = 1,
)

View file

@ -0,0 +1,24 @@
# Generated by Django 2.2.9 on 2020-02-28 13:01
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('seminar', '0073_copy_osoba_email_to_user_email'),
]
operations = [
migrations.AddField(
model_name='prilohareseni',
name='res_poznamka',
field=models.TextField(blank=True, help_text='Poznámka k příloze řešení, např. co daný soubor obsahuje', verbose_name='poznámka řešitele'),
),
migrations.AlterField(
model_name='hodnoceni',
name='problem',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='hodnoceni', to='seminar.Problem', verbose_name='problém'),
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 2.2.9 on 2020-02-28 19:10
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('seminar', '0074_auto_20200228_1401'),
]
operations = [
migrations.AlterField(
model_name='hodnoceni',
name='body',
field=models.DecimalField(decimal_places=1, max_digits=8, null=True, verbose_name='body'),
),
]

View file

@ -0,0 +1,19 @@
# Generated by Django 2.2.9 on 2020-02-28 19:13
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('seminar', '0075_auto_20200228_2010'),
]
operations = [
migrations.AlterField(
model_name='hodnoceni',
name='cislo_body',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='hodnoceni', to='seminar.Cislo', verbose_name='číslo pro body'),
),
]

View file

@ -0,0 +1,32 @@
# Generated by Django 2.2.9 on 2020-03-18 20:46
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('seminar', '0076_auto_20200228_2013'),
]
operations = [
migrations.CreateModel(
name='CastNode',
fields=[
('treenode_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='seminar.TreeNode')),
('nadpis', models.CharField(help_text='Nadpis podvěšené části obsahu', max_length=100, verbose_name='Nadpis')),
],
options={
'verbose_name': 'Část (Node)',
'verbose_name_plural': 'Části (Node)',
'db_table': 'seminar_nodes_cast',
},
bases=('seminar.treenode',),
),
migrations.AlterField(
model_name='treenode',
name='first_child',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='father_of_first', to='seminar.TreeNode', verbose_name='první potomek'),
),
]

View file

@ -920,10 +920,10 @@ class Hodnoceni(SeminarModelBase):
body = models.DecimalField(max_digits=8, decimal_places=1, verbose_name='body',
blank=False, null=False)
blank=False, null=True)
cislo_body = models.ForeignKey(Cislo, verbose_name='číslo pro body',
related_name='hodnoceni', blank=False, null=False, on_delete=models.PROTECT)
related_name='hodnoceni', blank=False, null=True, on_delete=models.PROTECT)
reseni = models.ForeignKey(Reseni, verbose_name='řešení', on_delete=models.CASCADE)
@ -935,17 +935,16 @@ class Hodnoceni(SeminarModelBase):
## FIXME: Budeme řešit později, pokud to bude potřeba.
#def aux_generate_filename(self, filename):
# """Pomocná funkce generující ošetřený název souboru v adresáři s datem"""
# clean = get_valid_filename(
# unidecode(filename.replace('/', '-').replace('\0', ''))
# )
# datedir = timezone.now().strftime('%Y-%m')
# fname = "%s_%s" % (
# timezone.now().strftime('%Y-%m-%d-%H:%M'),
# clean)
# return os.path.join(datedir, fname)
def aux_generate_filename(self, filename):
"""Pomocná funkce generující ošetřený název souboru v adresáři s datem"""
clean = get_valid_filename(
unidecode(filename.replace('/', '-').replace('\0', ''))
)
datedir = timezone.now().strftime('%Y-%m')
fname = "{}_{}".format(
timezone.now().strftime('%Y-%m-%d-%H:%M'),
clean)
return os.path.join(datedir, fname)
# Django neumí jednoduše serializovat partial nebo třídu s __call__
# (https://docs.djangoproject.com/en/1.8/topics/migrations/),
@ -988,9 +987,12 @@ class PrilohaReseni(SeminarModelBase):
poznamka = models.TextField('neveřejná poznámka', blank=True,
help_text='Neveřejná poznámka k příloze řešení (plain text), např. o původu')
res_poznamka = models.TextField('poznámka řešitele', blank=True,
help_text='Poznámka k příloze řešení, např. co daný soubor obsahuje')
def __str__(self):
return self.soubor
return str(self.soubor)
class Pohadka(SeminarModelBase):
@ -1228,6 +1230,8 @@ class Obrazek(SeminarModelBase):
help_text = 'Černobílá verze obrázku do čísla',
upload_to = 'obrazky/%Y/%m/%d/', blank=True, null=True)
# TODO placement hint - chci ho tady / pred textem / za textem
class TreeNode(PolymorphicModel):
class Meta:
db_table = "seminar_nodes_treenode"
@ -1242,6 +1246,7 @@ class TreeNode(PolymorphicModel):
on_delete = models.SET_NULL, # Vrcholy s null kořenem jsou sirotci bez ročníku
verbose_name="kořen stromu")
first_child = models.ForeignKey('TreeNode',
related_name='father_of_first',
null = True,
blank = True,
on_delete=models.SET_NULL,
@ -1262,15 +1267,6 @@ class TreeNode(PolymorphicModel):
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")
# Slouží k debugování pro rychlé získání představy o podobě podstromu pod tímto TreeNode.
def print_tree(self,indent=0):
# FIXME: Tady se spoléháme na to, že nedeklarovaný primární klíč se jmenuje by default 'id', což není úplně správně
print("{}{} (id: {})".format(" "*indent,self, self.id))
if self.first_child:
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()
@ -1339,6 +1335,7 @@ class MezicisloNode(TreeNode):
verbose_name = 'Mezičíslo (Node)'
verbose_name_plural = 'Mezičísla (Node)'
# TODO: Využít TreeLib
def aktualizuj_nazev(self):
if self.prev:
if (self.prev.get_real_instance_class() != CisloNode and
@ -1469,6 +1466,14 @@ class TextNode(TreeNode):
def getOdkazStr(self):
return str(self.text)
class CastNode(TreeNode):
class Meta:
db_table = 'seminar_nodes_cast'
verbose_name = 'Část (Node)'
verbose_name_plural = 'Části (Node)'
nadpis = models.CharField('Nadpis', max_length=100, help_text = 'Nadpis podvěšené části obsahu')
## FIXME: Logiku přesunout do views.
#class VysledkyBase(SeminarModelBase):

View file

@ -0,0 +1,49 @@
// Credit https://medium.com/all-about-django/adding-forms-dynamically-to-a-django-formset-375f1090c2b0
function updateElementIndex(el, prefix, ndx) {
var id_regex = new RegExp('(' + prefix + '-\\d+)');
var replacement = prefix + '-' + ndx;
if ($(el).attr("for")) {
$(el).attr("for", $(el).attr("for").replace(id_regex, replacement));
}
if (el.id) {
el.id = el.id.replace(id_regex, replacement);
}
if (el.name) {
el.name = el.name.replace(id_regex, replacement);
}
}
// Credit https://medium.com/all-about-django/adding-forms-dynamically-to-a-django-formset-375f1090c2b0
function deleteForm(prefix, btn) {
var total = parseInt($('#id_' + prefix + '-TOTAL_FORMS').val());
if (total >= 1){
btn.closest('div').remove();
var forms = $('.attachment');
$('#id_' + prefix + '-TOTAL_FORMS').val(forms.length);
for (var i=0, formCount=forms.length; i<formCount; i++) {
$(forms.get(i)).find(':input').each(function() {
updateElementIndex(this, prefix, i);
});
}
}
return false;
}
// Credit: https://simpleit.rocks/python/django/dynamic-add-form-with-add-button-in-django-modelformset-template/
$(document).ready(function(){
$('#add_attachment').click(function() {
var form_idx = $('#id_prilohy-TOTAL_FORMS').val();
var new_form = $('#empty_form').html().replace(/__prefix__/g, form_idx);
$('#form_set').append(new_form);
// Newly created form has not the binding between remove button and remove function
// We need to add it manually
$('.remove_attachment').click(function(){
deleteForm("prilohy",this);
});
$('#id_prilohy-TOTAL_FORMS').val(parseInt(form_idx) + 1);
});
$('.remove_attachment').click(function(){
deleteForm("prilohy",this);
});
});

View file

@ -0,0 +1,44 @@
{% extends "seminar/zadani/base.html" %}
{% load staticfiles %}
{% block script %}
<!--script type="text/javascript" src="{% static 'admin/js/vendor/jquery/jquery.js' %}"></script!-->
{{form.media}}
<script src="{% static 'seminar/dynamic_formsets.js' %}"></script>
{% endblock %}
{% block content %}
<h1>
{% block nadpis1a %}{% block nadpis1b %}
Vložit řešení
{% endblock %}{% endblock %}
</h1>
<form enctype="multipart/form-data" action="{% url 'seminar_nahraj_reseni' %}" method="post">
{% csrf_token %}
{{form}}
{{prilohy.management_form}}
<div id="form_set">
{% for form in prilohy.forms %}
<div class="attachment">
{{form.non_field_errors}}
{{form.errors}}
<table class='no_error'>
{{ form }}
</table>
<input type="button" value="Odebrat" class="remove_attachment" id="{{form.prefix}}-jsremove">
</div>
{% endfor %}
</div>
<input type="button" value="Přidat přílohu" id="add_attachment">
<div id="empty_form" style="display:none">
<div class="attachment">
<table class='no_error'>
{{ prilohy.empty_form }}
</table>
<input type="button" value="Odebrat" class="remove_attachment" id="id_prilohy-__prefix__-jsremove">
</div>
</div>
<input type="submit" value="Odevzdat">
</form>
{% endblock %}

View file

@ -9,7 +9,7 @@ from django.db import transaction
import unidecode
import logging
from seminar.models import Skola, Resitel, Rocnik, Cislo, Problem, Reseni, PrilohaReseni, Nastaveni, Soustredeni, Soustredeni_Ucastnici, Soustredeni_Organizatori, Osoba, Organizator, Prijemce, Tema, Uloha, Konfera, KonferaNode, TextNode, UlohaVzorakNode, RocnikNode, CisloNode, TemaVCisleNode, Text, Hodnoceni, UlohaZadaniNode, Novinky
from seminar.models import Skola, Resitel, Rocnik, Cislo, Problem, Reseni, PrilohaReseni, Nastaveni, Soustredeni, Soustredeni_Ucastnici, Soustredeni_Organizatori, Osoba, Organizator, Prijemce, Tema, Uloha, Konfera, KonferaNode, TextNode, UlohaVzorakNode, RocnikNode, CisloNode, TemaVCisleNode, Text, Hodnoceni, UlohaZadaniNode, Novinky, TreeNode
from django.contrib.flatpages.models import FlatPage
from django.contrib.sites.models import Site
@ -390,17 +390,15 @@ def gen_temata(rnd, rocniky, rocnik_cisla, organizatori):
co = ["téma", "záření", "stavení", "jiskření", "jelito",
"drama", "kuře", "moře", "klání", "proudění", "čekání"]
poc_oboru = rnd.randint(1, 2)
poc_op = rnd.randint(1, 3)
rocnik_temata = []
k = 0
for rocnik in rocniky:
k+=1
n = 0
temata = []
cisla = rocnik_cisla[k-1]
for ci in range(1, 3):
n+=1
# Věříme, že rocnik_cisla je pole polí čísel podle ročníků, tak si necháme dát vždycky jeden ročník a k němu příslušná čísla.
for rocnik, cisla in zip(rocniky, rocnik_cisla):
kod = 1
letosni_temata = []
# Do každého ročníku vymyslíme tři (zatím) témata, v každém z prvních čísel jedno
for zacatek_tematu in range(1, 3):
# Vygenerujeme téma
t = Tema.objects.create(
# atributy třídy Problem
nazev=" ".join([rnd.choice(jake), rnd.choice(co)]),
@ -408,22 +406,32 @@ def gen_temata(rnd, rocniky, rocnik_cisla, organizatori):
zamereni=rnd.sample(["M", "F", "I", "O", "B"], poc_oboru),
autor=rnd.choice(organizatori),
garant=rnd.choice(organizatori),
kod=str(n),
kod=str(kod),
# atributy třídy Téma
tema_typ=rnd.choice(Tema.TEMA_CHOICES)[0],
rocnik=rocnik,
abstrakt = "Abstrakt tematka {}".format(n)
abstrakt = "Abstrakt tematka {}".format(kod)
)
konec_tematu = min(rnd.randint(ci, 7), len(cisla))
for i in range(ci, konec_tematu+1):
kod += 1
# Vymyslíme, kdy skončí
konec_tematu = min(rnd.randint(zacatek_tematu, 7), len(cisla))
# Vyrobíme TemaVCisleNody pro obsah
for i in range(zacatek_tematu, konec_tematu+1):
node = TemaVCisleNode.objects.create(tema = t)
# FIXME: Není to off-by-one?
otec = cisla[i-1].cislonode
otec_syn(otec, node)
t.opravovatele.set(rnd.sample(organizatori, poc_op))
# Vymyslíme, kdo to bude opravovat
poc_opravovatelu = rnd.randint(1, 3)
t.opravovatele.set(rnd.sample(organizatori, poc_opravovatelu))
# Uložíme všechno
t.save()
temata.append((ci, konec_tematu, t))
rocnik_temata.append(temata)
letosni_temata.append((zacatek_tematu, konec_tematu, t))
rocnik_temata.append(letosni_temata)
return rocnik_temata
@ -448,84 +456,103 @@ def gen_ulohy_k_tematum(rnd, rocniky, rocnik_cisla, rocnik_temata, organizatori)
"netriviální aplikace diferenciálních rovnic", "zadání je vnitřně"
"sporné", "nepopsatelně jednoduché", "pokud jste na to nepřišli,"
"tak jste fakt hloupí"]
k = 0
for rocnik in rocniky:
k+=1
cisla = rocnik_cisla[k-1]
temata = rocnik_temata[k-1]
for ci in range(len(cisla)):
print("Generuji {}-té číslo".format(ci))
cislo = cisla[ci-1]
mozna_tema_vcn = cislo.cislonode.first_child
# kdybyste nad tím někdo taky přemýšleli, tak vcn == VCisleNode :)
while mozna_tema_vcn != None:
if type(mozna_tema_vcn) != TemaVCisleNode:
mozna_tema_vcn = mozna_tema_vcn.succ
# Ke každému ročníku si vezmeme příslušná čísla a témata
for rocnik, cisla, temata in zip(rocniky, rocnik_cisla, rocnik_temata):
# Do každého čísla nagenerujeme ke každému témátku pár úložek
for cislo in cisla:
print("Generuji úložky do {}-tého čísla".format(cislo.poradi))
# Vzorák bude o dvě čísla dál
cislo_se_vzorakem = Cislo.objects.filter(
rocnik=rocnik,
poradi=str(int(cislo.poradi) + 2),
)
# Pokud není číslo pro vzorák, tak se dá do posledního čísla (i kdyby tam mělo být zadání i řešení...)
# Tohle sice umožňuje vygenerovat vzorák do čísla dávno po konci témátka, ale to nám pro jednoduchost nevadí.
if len(cislo_se_vzorakem) == 0:
cislo_se_vzorakem = cisla[-1]
else:
cislo_se_vzorakem = cislo_se_vzorakem.first()
# FIXME: Tenhle generátor dát asi někam jinam
def potomci(node):
if not isinstance(node, TreeNode):
raise ValueError("Typ {} nemá potomky", type(node))
current_child = node.first_child
while current_child is not None:
yield current_child
current_child = current_child.succ
for mozna_tema_node in potomci(cislo.cislonode):
if not isinstance(mozna_tema_node, TemaVCisleNode):
continue
else:
tema = mozna_tema_vcn.tema
tema_node = mozna_tema_node
tema = tema_node.tema
if not temata[int(tema.kod)-1][1] >= ci+2:
mozna_tema_vcn = mozna_tema_vcn.succ
# Pokud už témátko skončilo, žádné úložky negenerujeme
# FIXME: Bylo by hezčí, kdyby se čísla předávala jako Cislo a ne jako int v té trojici (start, konec, tema)
if not temata[int(tema.kod)-1][1] >= int(cislo_se_vzorakem.poradi):
continue
for i in range(1, rnd.randint(1, 4)):
poc_op = rnd.randint(1, 4)
poc_oboru = rnd.randint(1, 2)
p = Uloha.objects.create(
# Generujeme 1 až 4 úložky k tomuto témátku do tohoto čísla
for kod in range(1, rnd.randint(1, 4)):
u = Uloha.objects.create(
nazev=": ".join([tema.nazev,
"úloha {}.".format(i)]),
"úloha {}.".format(kod)]),
nadproblem=tema,
stav=Problem.STAV_ZADANY,
zamereni=tema.zamereni,
autor=tema.autor,
garant=tema.garant,
kod=str(i),
kod=str(kod),
cislo_zadani=cislo,
cislo_reseni=cisla[ci+2-1],
cislo_deadline=cisla[ci+2-1],
cislo_reseni=cislo_se_vzorakem,
cislo_deadline=cislo_se_vzorakem,
max_body = rnd.randint(1, 8)
)
p.opravovatele.set(rnd.sample(organizatori, poc_op))
poc_opravovatelu = rnd.randint(1, 4)
u.opravovatele.set(rnd.sample(organizatori, poc_opravovatelu))
text_zadani = Text.objects.create(
na_web = " ".join(
[rnd.choice(sloveso),
rnd.choice(koho),
rnd.choice(ceho),
rnd.choice(jmeno),
rnd.choice(kde)]
),
do_cisla = " ".join(
# Samotný obsah následně vzniklého Textu zadání
obsah = " ".join(
[rnd.choice(sloveso),
rnd.choice(koho),
rnd.choice(ceho),
rnd.choice(jmeno),
rnd.choice(kde)]
)
)
text_zadani = Text.objects.create(
na_web = obsah,
do_cisla = obsah,
)
zad = TextNode.objects.create(text = text_zadani)
uloha_zadani = UlohaZadaniNode.objects.create(uloha=p, first_child = zad)
p.ulohazadaninode = uloha_zadani
otec_syn(mozna_tema_vcn, uloha_zadani)
# TODO dělá se podproblém takto??? TODO
uloha_zadani = UlohaZadaniNode.objects.create(uloha=u, first_child = zad)
u.ulohazadaninode = uloha_zadani
# FIXME: Tohle dává zadání vždy jako prvního potomka témátka, spec. se naskládají v opačném pořadí a nemůže mezi nimi vzniknout žádný (orgo-)text
otec_syn(tema_node, uloha_zadani)
# Text vzoráku stejně
obsah = rnd.choice(reseni)
text_vzoraku = Text.objects.create(
na_web = rnd.choice(reseni),
do_cisla = rnd.choice(reseni)
na_web = obsah,
do_cisla = obsah,
)
vzorak = TextNode.objects.create(text = text_vzoraku)
uloha_vzorak = UlohaVzorakNode.objects.create(uloha=p, first_child = vzorak)
p.UlohaVzorakNode = uloha_vzorak
res_tema_vcn = cisla[ci+2-1].cislonode.first_child
while res_tema_vcn.tema != tema:
res_tema_vcn = res_tema_vcn.succ
otec_syn(res_tema_vcn, uloha_vzorak)
uloha_vzorak = UlohaVzorakNode.objects.create(uloha=u, first_child = vzorak)
u.UlohaVzorakNode = uloha_vzorak
# Najdeme správný TemaVCisleNode pro vložení vzoráku
res_tema_node = None;
for node in potomci(cislo_se_vzorakem.cislonode):
if isinstance(node, TemaVCisleNode) and node.tema == tema:
res_tema_node = node
if res_tema_node is None:
raise LookupError("Nenalezen Node pro vložení vzoráku")
# FIXME: Stejný problém jako výše: vzoráky se dají na začátek v opačném pořadí.
otec_syn(res_tema_node, uloha_vzorak)
p.save()
mozna_tema_vcn = mozna_tema_vcn.succ
u.save()
return
def gen_novinky(rnd, organizatori):
@ -604,6 +631,7 @@ def create_test_data(size = 6, rnd = None):
rocniky = gen_rocniky(last_rocnik, size)
# cisla
# rocnik_cisla je pole polí čísel (typ Cislo), vnitřní pole odpovídají jednotlivým ročníkům.
rocnik_cisla = gen_cisla(rnd, rocniky)
# generování obyčejných úloh do čísel
@ -611,6 +639,7 @@ def create_test_data(size = 6, rnd = None):
# generování témat, zatím v prvních třech číslech po jednom
# FIXME: více témat
# rocnik_temata je pole polí trojic (první číslo :int, poslední číslo :int, téma:Tema), přičemž každé vnitřní pole odpovídá ročníku a FIXME: je to takhle fuj a když to někdo vidí poprvé, tak je z toho smutný, protože vůbec neví, co se děje a co má čekat.
rocnik_temata = gen_temata(rnd, rocniky, rocnik_cisla, organizatori)
# generování úloh k tématům ve všech číslech

124
seminar/treelib.py Normal file
View file

@ -0,0 +1,124 @@
from django.core.exceptions import ObjectDoesNotExist
# NOTE: node.prev a node.succ jsou implementovány přímo v models.TreeNode
# TODO: Všechny tyto funkce se naivně spoléhají na to, že jako parametr dostanou nějaký TreeNode (některé možná zvládnou i None)
# Slouží k debugování pro rychlé získání představy o podobě podstromu pod tímto TreeNode.
def print_tree(node,indent=0):
# FIXME: Tady se spoléháme na to, že nedeklarovaný primární klíč se jmenuje by default 'id', což není úplně správně
print("{}{} (id: {})".format(" "*indent,node, node.id))
if node.first_child:
print_tree(node.first_child, indent=indent+2)
if node.succ:
print_tree(node.succ, indent=indent)
# Django je trošku hloupé, takže node.prev nevrací None, ale hází django.core.exceptions.ObjectDoesNotExist
def safe_pred(node):
try:
return node.prev
except ObjectDoesNotExist:
return None
# A to samé pro .father_of_first
def safe_father_of_first(node):
return node.prev
except ObjectDoesNotExist:
return None
## Rodinné vztahy
def get_parent(node):
# Nejdřív získáme prvního potomka...
while safe_pred(node) is not None:
node = safe_pred(node)
# ... a z prvního potomka umíme najít rodiče
return safe_father_of_first(node)
# Obecný next: další Node v "the-right-order" pořadí (já, pak potomci, pak sousedé)
def general_next(node):
# Máme potomka?
if node.first_child is not None:
return node.first_child
# Nemáme potomka.
# Chceme najít následníka sebe, nebo některého (toho nejblíž příbuzného) z našich předků (tatínka, dědečka, ...)
while node.succ is None:
node = get_parent(node)
if node is None:
return None # žádný z předků nemá následníka, takže žádny vrchol nenásleduje.
return node.succ
def last_brother(node):
while node.succ is not None:
node = node.succ
return node
def general_prev(node):
# Předchůdce je buď rekurzivně poslední potomek předchůdce, nebo náš otec.
# Otce vyřešíme nejdřív:
if safe_pred(node) is None:
return safe_father_of_first(node)
pred = safe_pred(node)
while pred.first_child is not None:
pred = last_brother(pred.first_child)
# pred nyní nemá žádné potomky, takže je to poslední rekurzivní potomek původního předchůdce
return pred
# Generátor bratrů
# Generátor potomků níže spoléhá na to, že se tohle dá volat i s parametrem None.
def all_brothers(node):
current = node
while current is not None:
yield current
current = current.succ
# Generátor potomků
def all_children(node):
brothers = all_brothers(node.first_child)
for br in brothers:
yield br
# Generátor následníků v "the-right-order"
## Filtrační hledání
# Najdi dalšího bratra nějakého typu, nebo None.
# hledá i podtřídy, i.e. get_next_brother_of_type(neco, TreeNode) je prostě succ.
def get_next_brother_of_type(current, type):
pass
def get_prev_brother_of_type(current, type):
pass
# Totéž pro "the-right-order" pořadí
def get_next_node_of_type(current, type):
pass
def get_next_node_of_type(current, type):
pass
# Editace stromu:
def create_node_after(predecessor, type, **kwargs):
pass
# Vyrábí prvního syna, ostatní nalepí za (existují-li)
def create_child(parent, type, **kwargs):
pass
def create_node_before(some, arguments, but, i, dont, know, which, yet):
pass
# Tohle bude hell.
# ValueError, pokud je (aspoň) jeden parametr None
def swap(node, other):
pass
def swap_pred(node):
pass
def swap_succ(node):
pass
# Rotace stromu
# Dokumentace viz wiki:
# (lower bude jednoduchá rotace, ne mega, existence jednoduché rotace mi došla až po nakreslení obrázku)
def raise_node(node):
pass
def lower_node(node):
pass

View file

@ -109,6 +109,7 @@ urlpatterns = [
path('auth/resitel/', views.ResitelView.as_view(), name='seminar_resitel'),
path('autocomplete/skola/',views.SkolaAutocomplete.as_view(), name='autocomplete_skola'),
path('autocomplete/resitel/',views.ResitelAutocomplete.as_view(), name='autocomplete_resitel'),
path('autocomplete/problem/odevzdatelny',views.OdevzdatelnyProblemAutocomplete.as_view(), name='autocomplete_problem_odevzdatelny'),
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'),
@ -118,6 +119,7 @@ urlpatterns = [
path('temp/add_solution', views.AddSolutionView.as_view(),name='seminar_vloz_reseni'),
path('temp/submit_solution', views.SubmitSolutionView.as_view(),name='seminar_nahraj_reseni'),
path('', views.TitulniStranaView.as_view(), name='titulni_strana'),

View file

@ -4,6 +4,8 @@ import datetime
from django.contrib.auth.decorators import user_passes_test
from html.parser import HTMLParser
import seminar.models as m
staff_member_required = user_passes_test(lambda u: u.is_staff)
class FirstTagParser(HTMLParser):
@ -43,7 +45,6 @@ def from_roman(rom):
def seznam_problemu():
from .models import Problem, Resitel, Rocnik, Reseni, Cislo
problemy = []
# Pomocna fce k formatovani problemovych hlasek
@ -65,27 +66,26 @@ def seznam_problemu():
# Duplicita jmen
jmena = {}
for r in Resitel.objects.all():
for r in m.Resitel.objects.all():
j = r.plne_jmeno()
if j not in jmena:
jmena[j] = []
jmena[j].append(r)
for j in jmena:
if len(jmena[j]) > 1:
prb(Resitel, u'Duplicitní jméno "%s"' % (j, ), jmena[j])
prb(m.Resitel, u'Duplicitní jméno "%s"' % (j, ), jmena[j])
# Data maturity a narození
for r in Resitel.objects.all():
for r in m.Resitel.objects.all():
if not r.rok_maturity:
prb(Resitel, u'Neznámý rok maturity', [r])
prb(m.Resitel, u'Neznámý rok maturity', [r])
if r.rok_maturity and (r.rok_maturity < 1990 or r.rok_maturity > datetime.date.today().year + 10):
prb(Resitel, u'Podezřelé datum maturity', [r])
prb(m.Resitel, u'Podezřelé datum maturity', [r])
if r.datum_narozeni and (r.datum_narozeni.year < 1970 or r.datum_narozeni.year > datetime.date.today().year - 12):
prb(Resitel, u'Podezřelé datum narození', [r])
prb(m.Resitel, u'Podezřelé datum narození', [r])
# if not r.email:
# prb(Resitel, u'Neznámý email', [r])
return problemy

View file

@ -0,0 +1,2 @@
from .views_all import *
from .autocomplete import *

View file

@ -0,0 +1,63 @@
from dal import autocomplete
from django.shortcuts import get_object_or_404
import seminar.models as m
from .helpers import LoginRequiredAjaxMixin
class SkolaAutocomplete(autocomplete.Select2QuerySetView):
def get_queryset(self):
# Don't forget to filter out results depending on the visitor !
qs = m.Skola.objects.all()
if self.q:
qs = qs.filter(
Q(nazev__istartswith=self.q)|
Q(kratky_nazev__istartswith=self.q)|
Q(ulice__istartswith=self.q)|
Q(mesto__istartswith=self.q))
return qs
class ResitelAutocomplete(LoginRequiredAjaxMixin,autocomplete.Select2QuerySetView):
def get_queryset(self):
qs = m.Resitel.objects.all()
if self.q:
qs = qs.filter(
Q(osoba__jmeno__startswith=self.q)|
Q(osoba__prijmeni__startswith=self.q)|
Q(osoba__prezdivka__startswith=self.q)
)
return qs
class OdevzdatelnyProblemAutocomplete(autocomplete.Select2QuerySetView):
def get_queryset(self):
nastaveni = get_object_or_404(m.Nastaveni)
rocnik = nastaveni.aktualni_rocnik
temata = m.Tema.objects.filter(rocnik=rocnik, stav=m.Problem.STAV_ZADANY)
ulohy = m.Uloha.objects.filter(cislo_deadline__rocnik = rocnik)
ulohy.union(temata)
qs = ulohy
if self.q:
qs = qs.filter(
Q(nazev__startswith=self.q))
return qs
# Ceka na autocomplete v3
# class OrganizatorAutocomplete(autocomplete.Select2QuerySetView):
# def get_queryset(self):
# if not self.request.user.is_authenticated():
# return Organizator.objects.none()
#
# qs = aktivniOrganizatori()
#
# if self.q:
# if self.q[0] == "!":
# qs = Organizator.objects.all()
# query = self.q[1:]
# else:
# query = self.q
# qs = qs.filter(
# Q(prezdivka__isstartswith=query)|
# Q(user__first_name__isstartswith=query)|
# Q(user__last_name__isstartswith=query))
#
# return qs

8
seminar/views/helpers.py Normal file
View file

@ -0,0 +1,8 @@
from dal import autocomplete
class LoginRequiredAjaxMixin(object):
def dispatch(self, request, *args, **kwargs):
#if request.is_ajax() and not request.user.is_authenticated: # Pokud to otevřu jako stránku, tak se omezení neuplatní, takže to asi nechceme
if not request.user.is_authenticated:
return JsonResponse(data={'results': [], 'pagination': {}}, status=401)
return super(LoginRequiredAjaxMixin, self).dispatch(request, *args, **kwargs)

91
seminar/views/utils.py Normal file
View file

@ -0,0 +1,91 @@
# -*- coding: utf-8 -*-
import datetime
from django.contrib.auth.decorators import user_passes_test
from html.parser import HTMLParser
import seminar.models as m
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:
if i not in d:
d[i] = 0
d[i] += 1
return d
roman_numerals = zip((1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1),
('M', 'CM', 'D', 'CD','C', 'XC','L','XL','X','IX','V','IV','I'))
def roman(num):
res = ""
for i, n in roman_numerals:
res += n * (num // i)
num %= i
return res
def from_roman(rom):
if not rom:
return 0
for i, n in roman_numerals:
if rom.upper().startswith(n):
return i + from_roman(rom[len(n):])
raise Exception('Invalid roman numeral: "%s"', rom)
def seznam_problemu():
problemy = []
# Pomocna fce k formatovani problemovych hlasek
def prb(cls, msg, objs=None):
s = u'<b>%s:</b> %s' % (cls.__name__, msg)
if objs:
s += u' ['
for o in objs:
try:
url = o.admin_url()
except:
url = None
if url:
s += u'<a href="%s">%s</a>, ' % (url, o.pk, )
else:
s += u'%s, ' % (o.pk, )
s = s[:-2] + u']'
problemy.append(s)
# Duplicita jmen
jmena = {}
for r in m.Resitel.objects.all():
j = r.plne_jmeno()
if j not in jmena:
jmena[j] = []
jmena[j].append(r)
for j in jmena:
if len(jmena[j]) > 1:
prb(m.Resitel, u'Duplicitní jméno "%s"' % (j, ), jmena[j])
# Data maturity a narození
for r in m.Resitel.objects.all():
if not r.rok_maturity:
prb(m.Resitel, u'Neznámý rok maturity', [r])
if r.rok_maturity and (r.rok_maturity < 1990 or r.rok_maturity > datetime.date.today().year + 10):
prb(m.Resitel, u'Podezřelé datum maturity', [r])
if r.datum_narozeni and (r.datum_narozeni.year < 1970 or r.datum_narozeni.year > datetime.date.today().year - 12):
prb(m.Resitel, u'Podezřelé datum narození', [r])
# if not r.email:
# prb(Resitel, u'Neznámý email', [r])
return problemy

View file

@ -9,20 +9,19 @@ from django.utils.translation import ugettext as _
from django.http import Http404,HttpResponseBadRequest,HttpResponseRedirect
from django.db.models import Q, Sum, Count
from django.views.decorators.csrf import ensure_csrf_cookie
from django.views.generic.edit import FormView
from django.views.generic.edit import FormView, CreateView
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
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 seminar.models import Problem, Cislo, Reseni, Nastaveni, Rocnik, Soustredeni, Organizator, Resitel, Novinky, Soustredeni_Ucastnici, Pohadka, Tema, Clanek, Osoba, Skola # Tohle je stare a chceme se toho zbavit. Pouzivejte s.ToCoChci
#from .models import VysledkyZaCislo, VysledkyKCisluZaRocnik, VysledkyKCisluOdjakziva
from . import utils
from seminar import utils
from .unicodecsv import UnicodeWriter
from .forms import PrihlaskaForm, LoginForm, ProfileEditForm
from seminar.forms import PrihlaskaForm, LoginForm, ProfileEditForm
import seminar.forms as f
from datetime import timedelta, date, datetime
@ -1223,6 +1222,42 @@ class AddSolutionView(LoginRequiredMixin, FormView):
form_class = f.VlozReseniForm
success_url = '/'
class SubmitSolutionView(LoginRequiredMixin, CreateView):
model = s.Reseni
template_name = 'seminar/nahraj_reseni.html'
form_class = f.NahrajReseniForm
success_url = '/'
def get_context_data(self,**kwargs):
data = super().get_context_data(**kwargs)
if self.request.POST:
data['prilohy'] = f.ReseniSPrilohamiFormSet(self.request.POST,self.request.FILES)
else:
data['prilohy'] = f.ReseniSPrilohamiFormSet()
return data
# FIXME prepsat tak, aby form_valid se volalo jen tehdy, kdyz je form i formset validni
# Inspirace: https://stackoverflow.com/questions/41599809/using-a-django-filefield-in-an-inline-formset
def form_valid(self,form):
context = self.get_context_data()
prilohy = context['prilohy']
if not prilohy.is_valid():
return super().form_invalid(form)
with transaction.atomic():
self.object = form.save()
self.object.resitele.add(Resitel.objects.get(osoba__user = self.request.user))
self.object.cas_doruceni = timezone.now()
self.object.forma = s.Reseni.FORMA_UPLOAD
self.object.save()
prilohy.instance = self.object
prilohy.save()
return HttpResponseRedirect(self.get_success_url())
def resetPasswordView(request):
pass
@ -1384,58 +1419,9 @@ def prihlaskaView(request):
return render(request, 'seminar/prihlaska.html', {'form': form})
class SkolaAutocomplete(autocomplete.Select2QuerySetView):
def get_queryset(self):
# Don't forget to filter out results depending on the visitor !
qs = Skola.objects.all()
if self.q:
qs = qs.filter(
Q(nazev__istartswith=self.q)|
Q(kratky_nazev__istartswith=self.q)|
Q(ulice__istartswith=self.q)|
Q(mesto__istartswith=self.q))
return qs
class LoginRequiredAjaxMixin(object):
def dispatch(self, request, *args, **kwargs):
#if request.is_ajax() and not request.user.is_authenticated: # Pokud to otevřu jako stránku, tak se omezení neuplatní, takže to asi nechceme
if not request.user.is_authenticated:
return JsonResponse(data={'results': [], 'pagination': {}}, status=401)
return super(LoginRequiredAjaxMixin, self).dispatch(request, *args, **kwargs)
class ResitelAutocomplete(LoginRequiredAjaxMixin,autocomplete.Select2QuerySetView):
def get_queryset(self):
qs = Resitel.objects.all()
if self.q:
qs = qs.filter(
Q(osoba__jmeno__startswith=self.q)|
Q(osoba__prijmeni__startswith=self.q)|
Q(osoba__prezdivka__startswith=self.q)
)
return qs
# Ceka na autocomplete v3
# class OrganizatorAutocomplete(autocomplete.Select2QuerySetView):
# def get_queryset(self):
# if not self.request.user.is_authenticated():
# return Organizator.objects.none()
#
# qs = aktivniOrganizatori()
#
# if self.q:
# if self.q[0] == "!":
# qs = Organizator.objects.all()
# query = self.q[1:]
# else:
# query = self.q
# qs = qs.filter(
# Q(prezdivka__isstartswith=query)|
# Q(user__first_name__isstartswith=query)|
# Q(user__last_name__isstartswith=query))
#
# return qs
# FIXME: Tohle asi vlastně vůbec nepatří do aplikace 'seminar'
class LoginView(auth_views.LoginView):

1
sitetree_new.json Normal file

File diff suppressed because one or more lines are too long