diff --git a/Schema_new.dia b/Schema_new.dia index 0df8a901..f09c0589 100644 Binary files a/Schema_new.dia and b/Schema_new.dia differ diff --git a/seminar/forms.py b/seminar/forms.py index 6a0e7911..fb313272 100644 --- a/seminar/forms.py +++ b/seminar/forms.py @@ -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, + + ) diff --git a/seminar/management/commands/testdata.py b/seminar/management/commands/testdata.py index d7c65367..dbbd908d 100644 --- a/seminar/management/commands/testdata.py +++ b/seminar/management/commands/testdata.py @@ -38,7 +38,8 @@ class Command(BaseCommand): if not options['no_migrate']: call_command('migrate', no_input=True) self.stdout.write('Vytvarim uzivatele "admin" (heslo "admin") a pseudo-nahodna data ...') - create_test_data(size=8) + create_test_data(size=5) + # menší počet ročníků, aby se zrychlilo generování dat a bylo dost úloh self.stdout.write('Vytvoreno {} uzivatelu, {} skol, {} resitelu, {} rocniku, {} cisel,' ' {} problemu, {} reseni.'.format(User.objects.count(), Skola.objects.count(), Resitel.objects.count(), Rocnik.objects.count(), Cislo.objects.count(), diff --git a/seminar/migrations/0074_auto_20200228_1401.py b/seminar/migrations/0074_auto_20200228_1401.py new file mode 100644 index 00000000..447d5aab --- /dev/null +++ b/seminar/migrations/0074_auto_20200228_1401.py @@ -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'), + ), + ] diff --git a/seminar/migrations/0075_auto_20200228_2010.py b/seminar/migrations/0075_auto_20200228_2010.py new file mode 100644 index 00000000..2e703e4e --- /dev/null +++ b/seminar/migrations/0075_auto_20200228_2010.py @@ -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'), + ), + ] diff --git a/seminar/migrations/0076_auto_20200228_2013.py b/seminar/migrations/0076_auto_20200228_2013.py new file mode 100644 index 00000000..04aaea1e --- /dev/null +++ b/seminar/migrations/0076_auto_20200228_2013.py @@ -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'), + ), + ] diff --git a/seminar/migrations/0077_auto_20200318_2146.py b/seminar/migrations/0077_auto_20200318_2146.py new file mode 100644 index 00000000..50053d9c --- /dev/null +++ b/seminar/migrations/0077_auto_20200318_2146.py @@ -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'), + ), + ] diff --git a/seminar/migrations/0078_otistenereseninode.py b/seminar/migrations/0078_otistenereseninode.py new file mode 100644 index 00000000..2f426a17 --- /dev/null +++ b/seminar/migrations/0078_otistenereseninode.py @@ -0,0 +1,27 @@ +# Generated by Django 2.2.9 on 2020-03-18 23:59 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('seminar', '0077_auto_20200318_2146'), + ] + + operations = [ + migrations.CreateModel( + name='OtisteneReseniNode', + 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')), + ('reseni', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='seminar.Reseni', verbose_name='reseni')), + ], + options={ + 'verbose_name': 'Otištěné řešení (Node)', + 'verbose_name_plural': 'Otištěná řešení (Node)', + 'db_table': 'seminar_nodes_otistene_reseni', + }, + bases=('seminar.treenode',), + ), + ] diff --git a/seminar/migrations/0079_clanek_resitelsky.py b/seminar/migrations/0079_clanek_resitelsky.py new file mode 100644 index 00000000..f41cdc51 --- /dev/null +++ b/seminar/migrations/0079_clanek_resitelsky.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.9 on 2020-03-25 19:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('seminar', '0078_otistenereseninode'), + ] + + operations = [ + migrations.AddField( + model_name='clanek', + name='resitelsky', + field=models.BooleanField(default=True, verbose_name='Jde o řešitelský článek?'), + ), + ] diff --git a/seminar/models.py b/seminar/models.py index f8c41ed0..2a1c60d6 100644 --- a/seminar/models.py +++ b/seminar/models.py @@ -304,9 +304,10 @@ class Resitel(SeminarModelBase): return sum(h.body for h in list(vsechna_hodnoceni)) - def get_titul(self): + def get_titul(self, celkove_body=None): "Vrati titul" - celkove_body = self.vsechny_body() + if celkove_body is None: + celkove_body = self.vsechny_body() if celkove_body < 10: return '' @@ -753,6 +754,8 @@ class Clanek(Problem): cislo = models.ForeignKey(Cislo, verbose_name='číslo', blank=True, null=True, on_delete=models.PROTECT) + resitelsky = models.BooleanField('Jde o řešitelský článek?', default=True) + # má OneToOneField s: # ClanekNode @@ -898,6 +901,9 @@ class Reseni(SeminarModelBase): # má OneToOneField s: # Konfera + # má ForeignKey s: + # Hodnoceni + def __str__(self): return "{}({}): {}({})".format(self.resitele.first(),len(self.resitele.all()), self.problem.first() ,len(self.problem.all())) # NOTE: Potenciální DB HOG (bez select_related) @@ -920,10 +926,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 +941,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 +993,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 +1236,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 +1252,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 +1273,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 +1341,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 +1472,34 @@ 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') + + def aktualizuj_nazev(self): + self.nazev = "CastNode: "+str(self.nadpis) + + def getOdkazStr(self): + return str(self.nadpis) + +class OtisteneReseniNode(TreeNode): + class Meta: + db_table = 'seminar_nodes_otistene_reseni' + verbose_name = 'Otištěné řešení (Node)' + verbose_name_plural = 'Otištěná řešení (Node)' + reseni = models.ForeignKey(Reseni, + on_delete=models.PROTECT, + verbose_name = 'reseni') + + def aktualizuj_nazev(self): + self.nazev = "OtisteneReseniNode: "+str(self.reseni) + + def getOdkazStr(self): + return str(self.reseni) ## FIXME: Logiku přesunout do views. #class VysledkyBase(SeminarModelBase): diff --git a/seminar/static/seminar/dynamic_formsets.js b/seminar/static/seminar/dynamic_formsets.js new file mode 100644 index 00000000..a0d99d0a --- /dev/null +++ b/seminar/static/seminar/dynamic_formsets.js @@ -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 + {% block content %} +
+

{% block nadpis1a %}{% block nadpis1b %} Číslo {{ cislo }} @@ -45,14 +46,15 @@
  • Obálkování
  • - {% endif %} + {% endif %} {% if cislo.verejna_vysledkovka %} -

    Výsledkovka

    +

    Výsledkovka ({% now "jS F Y H:i" %})

    + {% else %} {% if user.is_staff %}
    -

    Výsledkovka (neveřejná)

    +

    Výsledkovka (neveřejná, {% now "jS F Y H:i:s" %})

    {% endif %} {% endif %} @@ -61,36 +63,37 @@ # Jméno - {# problémy by měly být veřejné, když je veřejná výsledkovka #} {% for p in problemy %} {{ p.kod_v_rocniku }} {% endfor %} Za číslo Za ročník Odjakživa - {% for rv in vysledkovka %} + {% for rv in radky_vysledkovky %} {% autoescape off %}{{ rv.poradi }}{% endautoescape %} - {% if rv.titul %} + {% if rv.titul is not '' %} {{ rv.titul }}MM {% endif %} - {{ rv.resitel.plne_jmeno }} - {% for b in rv.body_ulohy %} + {{ rv.resitel.osoba.plne_jmeno }} + {% for b in rv.body_problemy_sezn %} {{ b }} {% endfor %} {{ rv.body_cislo }} - {{ rv.body_celkem_rocnik }} + {{ rv.body_rocnik }} {{ rv.body_celkem_odjakziva }} {% endfor %} - {% endif %} + {% endif %} {% if not cislo.verejna_vysledkovka and user.is_staff %}
    {% endif %} - -{% endblock content %} + Čas: {% now "jS F Y H:i:s" %} + + +{% endblock content %} diff --git a/seminar/templates/seminar/nahraj_reseni.html b/seminar/templates/seminar/nahraj_reseni.html new file mode 100644 index 00000000..7e3d1e72 --- /dev/null +++ b/seminar/templates/seminar/nahraj_reseni.html @@ -0,0 +1,44 @@ +{% extends "seminar/zadani/base.html" %} +{% load staticfiles %} +{% block script %} + + {{form.media}} + +{% endblock %} + +{% block content %} +

    + {% block nadpis1a %}{% block nadpis1b %} + Vložit řešení + {% endblock %}{% endblock %} +

    +
    + {% csrf_token %} +{{form}} +{{prilohy.management_form}} +
    +{% for form in prilohy.forms %} +
    + {{form.non_field_errors}} + {{form.errors}} + + {{ form }} +
    + +
    +{% endfor %} +
    + + + +
    + +{% endblock %} diff --git a/seminar/templates/seminar/treenode.html b/seminar/templates/seminar/treenode.html new file mode 100644 index 00000000..0fd734ef --- /dev/null +++ b/seminar/templates/seminar/treenode.html @@ -0,0 +1,10 @@ +{% extends "seminar/archiv/base_ulohy.html" %} + +{% load comments %} + +{% block content %} + +{%with obj=tnldata depth=1 template_name="seminar/treenode_recursive.html" %} + {%include template_name%} +{%endwith%} +{% endblock content %} diff --git a/seminar/templates/seminar/treenode_recursive.html b/seminar/templates/seminar/treenode_recursive.html new file mode 100644 index 00000000..0cf37d9a --- /dev/null +++ b/seminar/templates/seminar/treenode_recursive.html @@ -0,0 +1,28 @@ +{% load treenodes %} +{# {{depth}} #} +
    +{% if obj.node|isRocnik %} + Ročník {{obj.node.rocnik}} +{% elif obj.node|isCislo %} + Číslo {{obj.node.cislo}} +{% elif obj.node|isTemaVCisle %} + Téma {{obj.node.tema.nazev}} +{% elif obj.node|isUlohaZadani %} +Úloha {{obj.node.uloha.kod_v_rocniku}} ({{obj.node.uloha.max_body}} b) +{% elif obj.node|isUlohaVzorak %} +Řešení: {{obj.node.uloha.kod_v_rocniku}} +{% elif obj.node|isText %} +{{obj.node.text.na_web}} +{% else %} +Objekt jiného typu {{obj.node}} +{% endif %} + {%if obj.children %} +
    + {%for ch in obj.children %} + {%with obj=ch depth=depth|add:"1" template_name="seminar/treenode_recursive.html" %} + {%include template_name%} + {%endwith%} + {%endfor%} +
    + {%endif%} +
    diff --git a/seminar/templatetags/treenodes.py b/seminar/templatetags/treenodes.py new file mode 100644 index 00000000..0d60765e --- /dev/null +++ b/seminar/templatetags/treenodes.py @@ -0,0 +1,49 @@ +from django import template +import seminar.models as m + +register = template.Library() + +@register.filter +def isRocnik(value): + return isinstance(value, m.RocnikNode) + +@register.filter +def isCislo(value): + return isinstance(value, m.CisloNode) + +@register.filter +def isCast(value): + return isinstance(value, m.CastNode) + +@register.filter +def isText(value): + return isinstance(value, m.TextNode) + +@register.filter +def isTemaVCisle(value): + return isinstance(value, m.TemaVCisleNode) + +@register.filter +def isKonfera(value): + return isinstance(value, m.KonferaNode) + +@register.filter +def isClanek(value): + return isinstance(value, m.ClanekNode) + +@register.filter +def isUlohaVzorak(value): + return isinstance(value, m.UlohaVzorakNode) + +@register.filter +def isUlohaZadani(value): + return isinstance(value, m.UlohaZadaniNode) + +@register.filter +def isPohadka(value): + return isinstance(value, m.PohadkaNode) + +#@register.filter +#def isOtisteneReseniNode(value): +# return isinstance(value, m.OtisteneReseniNode) + diff --git a/seminar/testutils.py b/seminar/testutils.py index b6583f12..9aec9eba 100644 --- a/seminar/testutils.py +++ b/seminar/testutils.py @@ -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 @@ -19,6 +19,13 @@ zlinska = None # tohle bude speciální škola, které později dodáme kontaktn logger = logging.getLogger(__name__) +# testuje unikátnost vygenerovaného jména +def __unikatni_jmeno(osoby, jmeno, prijmeni): + for os in osoby: + if os.jmeno == jmeno and os.prijmeni == prijmeni: + return 0 + else: return 1 + def gen_osoby(rnd, size): logger.info('Generuji osoby (size={})...'.format(size)) @@ -48,6 +55,19 @@ def gen_osoby(rnd, size): pohlavi = rnd.randint(0,1) jmeno = rnd.choice([jmena_m, jmena_f][pohlavi]) prijmeni = rnd.choice([prijmeni_m, prijmeni_f][pohlavi]) + pokusy = 0 + max_pokusy = 120*size + while (not __unikatni_jmeno and pokusy < max_pokusy): + # pokud jméno a příjmení není unikátní, zkoušíme generovat nová + # do daného limitu (abychom se nezacyklili do nekonečna při málo jménech a příjmeních + # ze kterých se generuje) + jmeno = rnd.choice([jmena_m, jmena_f][pohlavi]) + prijmeni = rnd.choice([prijmeni_m, prijmeni_f][pohlavi]) + pokusy = pokusy + 1 + if pokusy >= max_pokusy: + print("Chyba, na danou velikost testovacích dat příliš málo možných" + " jmen a příjmení") + exit prezdivka = rnd.choice(prezdivky) email = "@".join([unidecode.unidecode(jmeno), rnd.choice(domain)]) telefon = "".join([str(rnd.choice([k for k in range(10)])) for i in range(9)]) @@ -180,11 +200,12 @@ def gen_ulohy_do_cisla(rnd, organizatori, resitele, rocnik_cisla, rocniky, size) k = 0 for rocnik in rocniky: k+=1 + print("Generuji {}. číslo.".format(k)) cisla = rocnik_cisla[k-1] for ci in range(3, len(cisla)+1): # pro všechna čísla resitele_size = round(7/8 * 30 * size) # očekáváný celkový počet řešitelů - poc_res = rnd.randint(round(resitele_size/8), round(3*resitele_size/4)) - # dané číslo řeší něco mezi osminou a tříčtvrtinou všech řešitelů + poc_res = rnd.randint(round(resitele_size/8), round(resitele_size/4)) + # dané číslo řeší něco mezi osminou a čtvrtinou všech řešitelů # (náhodná hausnumera, možno změnit) # účelem je, aby se řešení generovala z menší množiny řešitelů a tedy # bylo více řešení od jednoho řešitele daného čísla @@ -243,9 +264,10 @@ def gen_ulohy_do_cisla(rnd, organizatori, resitele, rocnik_cisla, rocniky, size) p.save() # generování řešení - poc_reseni = rnd.randint(size // 2, size * 2) - # generujeme náhodný počet řešení + poc_reseni = rnd.randint(poc_res, poc_res * 4) + # generujeme náhodný počet řešení vzhledem k počtu řešitelů čísla for ri in range(poc_reseni): + #print("Generuji {}-té řešení".format(ri)) if rnd.randint(1, 10) == 6: # cca desetina řešení od více řešitelů res_vyber = rnd.sample(resitele_cisla, rnd.randint(2, 5)) @@ -383,17 +405,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)]), @@ -401,22 +421,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 @@ -441,81 +471,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)): - cislo = cisla[ci-1] - mozna_tema_vcn = cislo.cislonode.first_child - 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 - - if not temata[int(tema.kod)-1][1] >= ci+2: - mozna_tema_vcn = mozna_tema_vcn.succ + tema_node = mozna_tema_node + tema = tema_node.tema + + # 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( - nazev=": ".join([tema.nazev, - "úloha {}.".format(i)]), + + # 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(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)) - - 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( - [rnd.choice(sloveso), - rnd.choice(koho), - rnd.choice(ceho), - rnd.choice(jmeno), + + poc_opravovatelu = rnd.randint(1, 4) + u.opravovatele.set(rnd.sample(organizatori, poc_opravovatelu)) + + # 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) - - p.save() - - mozna_tema_vcn = mozna_tema_vcn.succ + 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) + + u.save() return def gen_novinky(rnd, organizatori): @@ -594,6 +646,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 @@ -601,6 +654,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 diff --git a/seminar/treelib.py b/seminar/treelib.py new file mode 100644 index 00000000..260a8bac --- /dev/null +++ b/seminar/treelib.py @@ -0,0 +1,286 @@ +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) +# TODO: Chceme, aby všechno nějak zvládlo None jako parametr. + +# 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 + +def first_brother(node): + if node is None: + return None + brother = node + while safe_pred(brother) is not None: + brother = safe_pred(brother) + return brother + +# A to samé pro .father_of_first +def safe_father_of_first(node): + first = first_brother(node) + try: + return first.father_of_first + 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 pravých bratrů (konkrétně sebe a následujících potomků) +# Generátor potomků níže spoléhá na to, že se tohle dá volat i s parametrem None. +def me_and_right_brothers(node): + current = node + while current is not None: + yield current + current = current.succ + +def right_brothers(node): + generator = me_and_right_brothers(node.succ) + for item in generator: + yield item + +# Generátor všech sourozenců (vč. sám sebe) +def all_brothers(node): + # Najdeme prvního bratra + fb = first_brother(node) + marb = me_and_right_brothers(fb) + for cur in marb: + yield cur + +def all_proper_brothers(node): + all = all_brothers(node) + for br in all: + if br is node: + continue + yield br + +# 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" +# Bez tohoto vrcholu +def all_following(node): + current = general_next(node) + while current is not None: + yield current + current = general_next(current) + +## 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(node, type): + for current in right_brothers(node): + if isinstance(current, type): + return current + return None + +def get_prev_brother_of_type(node, type): + # Na tohle není rozumný generátor, ani ho asi nechceme, prostě to implementujeme cyklem. + current = node + while safe_pred(current) is not None: + current = safe_pred(current) + if isinstance(current, type): + return current + return None + +# Totéž pro "the-right-order" pořadí +def get_next_node_of_type(node, type): + for cur in all_folowing(node): + if isinstance(cur, type): + return cur + return None + +def get_prev_node_of_type(node, type): + # Na tohle není rozumný generátor, ani ho asi nechceme, prostě to implementujeme cyklem. + current = node + while general_prev(current) is not None: + current = general_prev(current) + if isinstance(current, type): + return current + return None + + + + +# Editace stromu: +def create_node_after(predecessor, type, **kwargs): + new_node = type.objects.create(**kwargs) + new_node.save() + succ = predecessor.succ + predecessor.succ = new_node + predecessor.save() + new_node.succ = succ + new_node.save() + +# Vyrábí prvního syna, ostatní nalepí za (existují-li) +def create_child(parent, type, **kwargs): + new_node = type.objects.create(**kwargs) + new_node.save() + orig_child = parent.first_child + parent.first_child = new_node + parent.save() + if orig_child is not None: + # Přidáme původního prvního syna jako potomka nového vrcholu + new_node.succ = orig_child + new_node.save() + +def create_node_before(successor, type, **kwargs): + if safe_pred(successor) is not None: + # Easy: přidáme za předchůdce + create_node_after(successor.prev, type, **kwargs) + # Nemáme předchůdce, jsme tedy první z bratrů. Máme otce? + if safe_father_of_first(successor) is not None: + # Ano -> Easy: vyrobíme nového potomka + # NOTE: Tohle je možná trošku abuse implementace výše, ale to nevadí moc... + create_child(successor.father_of_first, type, **kwargs) + # Teď už easy: Jsme sirotci, takže se vyrobíme a našeho následníka si přidáme jako succ + new = type.objects.create(**kwargs) + new.succ = successor + new.save() + + +# ValueError, pokud je (aspoň) jeden parametr None +def swap(node, other): + raise NotImplementedError("YAGNI (You aren't gonna need it).") + +# Exception, kterou některé metody při špatném použití mohou házet +# Hlavní důvod je možnost informovat o selhání, aby se příslušný problém dal zobrazit na frontendu, +class TreeLibError(RuntimeError): + pass + +def swap_pred(node): + if node is None: + raise TreeLibError("Nelze přesunout None. Tohle by se nemělo stát.") + pred = safe_pred(node) + if pred is None: + raise TreeLibError("Nelze posunout vlevo, není tam žádný další uzel.") + pre_pred = safe_pred(pred) + succ = node.succ + + if pre_pred is not None: + pre_pred.succ = node + pre_pred.save() + node.succ = pred + node.save() + pred.succ = succ + pred.save() + +def swap_succ(node): + if node is None: + raise TreeLibError("Nelze přesunout None. Tohle by se nemělo stát.") + succ = node.succ + if succ is None: + raise TreeLibError("Nelze posunout vpravo, není tam žádný další uzel") + pred = safe_pred(node) + post_succ = succ.succ + + if pred is not None: + pred.succ = succ + pred.save() + succ.succ = node + succ.save() + node.succ = post_succ + node.save() + +# 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): + if node is None: + raise TreeLibError("Nelze přesunout None. Tohle by se nemělo stát.") + # Pojmenování viz WIKI (as of 2020-03-19 01:33:44 GMT+1) + # FIXME: Velmi naivní, chybí error checky + D = node + C = get_parent(D) + E = C.succ + subtree4_head = D.first_child + subtree4_tail = last_brother(subtree4_head) + subtree3P_head = D.succ + subtree3L_head = C.first_child + subtree3L_tail = safe_pred(D) + + # Prostor pro motlitbu... + pass + + # Amen. + C.succ = D + C.save() + D.succ = E + D.save() + subtree3L_tail.succ = None + subtree3L_tail.save() + subtree4_tail.succ = subtree3P.head + subtree4_tail.save() + + # To by mělo být všechno... + +def lower_node(node): + if node is None: + raise TreeLibError("Nelze přesunout None. Tohle by se nemělo stát.") + # Pojmenování viz WIKI (as of 2020-03-19 01:33:44 GMT+1) + # FIXME: Velmi naivní, chybí error checky + C = node + D = C.succ + B = safe_pred(C) + subtree2_head = B.first_child + subtree2_tail = last_brother(subtree2_head) + + # Prostor pro motlitbu... + pass + + # Amen. + B.succ = D + B.save() + subtree2_tail.succ = C + subtree2_tail.save() + + # To by mělo být všechno... diff --git a/seminar/urls.py b/seminar/urls.py index f9a62261..adf2cea5 100644 --- a/seminar/urls.py +++ b/seminar/urls.py @@ -8,8 +8,8 @@ from django.contrib.auth import views as auth_views staff_member_required = user_passes_test(lambda u: u.is_staff) urlpatterns = [ - path('aktualni/temata/', views.TemataRozcestnikView), - path('/t/', views.TematkoView), +# path('aktualni/temata/', views.TemataRozcestnikView), +# path('/t/', views.TematkoView), # REDIRECTy path('jak-resit/', RedirectView.as_view(url='/co-je-MaM/jak-resit/')), @@ -25,6 +25,7 @@ urlpatterns = [ path('rocnik//', views.RocnikView.as_view(), name='seminar_rocnik'), path('cislo/./', views.CisloView.as_view(), name='seminar_cislo'), # odkomentované jenom kvůli testování archivu path('problem//', views.ProblemView.as_view(), name='seminar_problem'), + path('treenode//', views.TreeNodeView.as_view(), name='seminar_treenode'), #path('problem/(?P\d+)/(?P\d+)/', views.PrispevekView.as_view(), name='seminar_problem_prispevek'), # Soustredeni @@ -59,8 +60,8 @@ urlpatterns = [ ), # Zadani - path('zadani/aktualni/', views.AktualniZadaniView, name='seminar_aktualni_zadani'), - path('zadani/temata/', views.ZadaniTemataView, name='seminar_temata'), + path('zadani/aktualni/', views.AktualniZadaniView.as_view(), name='seminar_aktualni_zadani'), +# path('zadani/temata/', views.ZadaniTemataView, name='seminar_temata'), #path('zadani/vysledkova-listina/', views.ZadaniAktualniVysledkovkaView, name='seminar_vysledky'), path('stare-novinky/', views.StareNovinkyView.as_view(), name='stare_novinky'), @@ -107,8 +108,6 @@ urlpatterns = [ path('auth/login/', views.LoginView.as_view(), name='login'), path('auth/logout/', views.LogoutView.as_view(), name='logout'), path('auth/resitel/', views.ResitelView.as_view(), name='seminar_resitel'), - path('autocomplete/skola/',views.SkolaAutocomplete.as_view(), name='autocomplete_skola'), - path('autocomplete/resitel/',views.ResitelAutocomplete.as_view(), name='autocomplete_resitel'), 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'), @@ -116,8 +115,13 @@ urlpatterns = [ path('auth/reset_password_complete/', views.PasswordResetCompleteView.as_view(), name='reset_password_complete'), path('auth/resitel_edit', views.resitelEditView, name='seminar_resitel_edit'), + # Autocomplete + 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('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'), diff --git a/seminar/utils.py b/seminar/utils.py index d910a5b6..3869ffd4 100644 --- a/seminar/utils.py +++ b/seminar/utils.py @@ -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 - diff --git a/seminar/views/__init__.py b/seminar/views/__init__.py new file mode 100644 index 00000000..976a34fe --- /dev/null +++ b/seminar/views/__init__.py @@ -0,0 +1,2 @@ +from .views_all import * +from .autocomplete import * diff --git a/seminar/views/autocomplete.py b/seminar/views/autocomplete.py new file mode 100644 index 00000000..04ddca83 --- /dev/null +++ b/seminar/views/autocomplete.py @@ -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 diff --git a/seminar/views/helpers.py b/seminar/views/helpers.py new file mode 100644 index 00000000..0b06c0eb --- /dev/null +++ b/seminar/views/helpers.py @@ -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) diff --git a/seminar/unicodecsv.py b/seminar/views/unicodecsv.py similarity index 100% rename from seminar/unicodecsv.py rename to seminar/views/unicodecsv.py diff --git a/seminar/views/utils.py b/seminar/views/utils.py new file mode 100644 index 00000000..3869ffd4 --- /dev/null +++ b/seminar/views/utils.py @@ -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'%s: %s' % (cls.__name__, msg) + if objs: + s += u' [' + for o in objs: + try: + url = o.admin_url() + except: + url = None + if url: + s += u'%s, ' % (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 + + diff --git a/seminar/views.py b/seminar/views/views_all.py similarity index 73% rename from seminar/views.py rename to seminar/views/views_all.py index 2f3e8f32..12c04078 100644 --- a/seminar/views.py +++ b/seminar/views/views_all.py @@ -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,treelib 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 @@ -84,131 +83,106 @@ class ObalkovaniView(generic.ListView): context['cislo'] = self.cislo return context +class TNLData(object): + def __init__(self,anode): + self.node = anode + self.children = [] +def treenode_strom_na_seznamy(node): + out = TNLData(node) + for ch in treelib.all_children(node): + outitem = treenode_strom_na_seznamy(ch) + out.children.append(outitem) + return out -def AktualniZadaniView(request): - nastaveni = get_object_or_404(Nastaveni) - verejne = nastaveni.aktualni_cislo.verejne() - problemy = Problem.objects.filter(cislo_zadani=nastaveni.aktualni_cislo).filter(stav = 'zadany') - ulohy = problemy.filter(typ = 'uloha').order_by('kod') - serialy = problemy.filter(typ = 'serial').order_by('kod') - jednorazove_problemy = [ulohy, serialy] - return render(request, 'seminar/zadani/AktualniZadani.html', - {'nastaveni': nastaveni, - 'jednorazove_problemy': jednorazove_problemy, - 'temata': verejna_temata(nastaveni.aktualni_rocnik), - 'verejne': verejne, - }, - ) +class TreeNodeView(generic.DetailView): + model = s.TreeNode + template_name = 'seminar/treenode.html' -def ZadaniTemataView(request): - nastaveni = get_object_or_404(Nastaveni) - temata = verejna_temata(nastaveni.aktualni_rocnik) - for t in temata: - if request.user.is_staff: - t.prispevky = t.prispevek_set.filter(problem=t) - else: - t.prispevky = t.prispevek_set.filter(problem=t, zverejnit=True) - return render(request, 'seminar/zadani/Temata.html', - { - 'temata': temata, - } - ) + def get_context_data(self,**kwargs): + context = super().get_context_data(**kwargs) + context['tnldata'] = treenode_strom_na_seznamy(self.object) + return context + -# TODO Napsat tuto funkci znovu rekurzivně podle Jethrorad. Potom se podívat, jak lehce se dá modifikovat pro Rozcestník. Pokud lehce, rozšířit ji. Pokud složitě - použít tuhle -def vytahniZLesaSeznam(tematko, koren, pouze_zajimave=False): - returnVal = [] - - stack = [] - stack.append((koren.first_child, 0, False)) #Tuple of node, depth and relevance - - while len(stack) > 0: - wn, wd, wr = stack.pop() - - if wn.succ != None: - stack.append((wn.succ, wd, wr)) - if isinstance(wn, s.TemaVCisleNode): - print("TEMA") - print(wn.tema.id) - print(tematko.id) - if wn.tema.id == tematko.id: - returnVal.append((posledni_cislo, 0)) - print("PRIDANO") - wr = True - wd = 1 - - if wn.srolovatelne: - tagOpen = s.Text(na_web = "Otevírací srolovací tag") - tagOpenNode = s.TextNode(text = tagOpen) - tagClose = s.Text(na_web = "Zavírací srolovací tag") - tagCloseNode = s.TextNode(text = tagClose) - stack.append((tagCloseNode, wd, True)) - - if wn.first_child != None: - stack.append((wn.first_child, wd + 1, wr)) - - if isinstance(wn, s.CisloNode): - posledni_cislo = wn - print(wn) - - if wr: - print("ZAJIMAVE") - if pouze_zajimave: - if not wn.zajimave: - continue - returnVal.append((wn, wd)) - return returnVal - -def TematkoView(request, rocnik, tematko): - nastaveni = s.Nastaveni.objects.first() - rocnik_object = s.Rocnik.objects.filter(rocnik=rocnik) - tematko_object = s.Tema.objects.filter(rocnik=rocnik_object[0], kod=tematko) - seznam = vytahniZLesaSeznam(tematko_object[0], nastaveni.aktualni_rocnik().rocniknode) - for node, depth in seznam: - if node.isinstance(node, s.KonferaNode): - raise Exception("Not implemented yet") - if node.isinstance(node, s.PohadkaNode): # Mohu ignorovat, má pod sebou - pass - - return render(request, 'seminar/tematka/toaletak.html', {}) - - -def TemataRozcestnikView(request): - print("=============================================") - nastaveni = s.Nastaveni.objects.first() - tematka_objects = s.Tema.objects.filter(rocnik=nastaveni.aktualni_rocnik()) - tematka = [] #List tematka obsahuje pro kazde tematko object a list vsech TemaVCisleNodu - implementované pomocí slovníku - for tematko_object in tematka_objects: - print("AKTUALNI TEMATKO") - print(tematko_object.id) - odkazy = vytahniZLesaSeznam(tematko_object, nastaveni.aktualni_rocnik().rocniknode, pouze_zajimave = True) #Odkazy jsou tuply (node, depth) v listu - print(odkazy) - cisla = [] # List tuplů (nazev cisla, list odkazů) - vcisle = [] - cislo = None - for odkaz in odkazy: - if odkaz[1] == 0: - if cislo != None: - cisla.append((cislo, vcisle)) - cislo = (odkaz[0].getOdkazStr(), odkaz[0].getOdkaz()) - vcisle = [] - else: - print(odkaz[0].getOdkaz()) - vcisle.append((odkaz[0].getOdkazStr(), odkaz[0].getOdkaz())) - if cislo != None: - cisla.append((cislo, vcisle)) - - print(cisla) - tematka.append({ - "kod" : tematko_object.kod, - "nazev" : tematko_object.nazev, - "abstrakt" : tematko_object.abstrakt, - "obrazek": tematko_object.obrazek, - "cisla" : cisla - }) - return render(request, 'seminar/tematka/rozcestnik.html', {"tematka": tematka, "rocnik" : nastaveni.aktualni_rocnik().rocnik}) +class AktualniZadaniView(TreeNodeView): + def get_object(self): + nastaveni = get_object_or_404(Nastaveni) + return nastaveni.aktualni_cislo.cislonode + def get_context_data(self,**kwargs): + nastaveni = get_object_or_404(Nastaveni) + context = super().get_context_data(**kwargs) + verejne = nastaveni.aktualni_cislo.verejne() + context['verejne'] = verejne + return context + +#def ZadaniTemataView(request): +# nastaveni = get_object_or_404(Nastaveni) +# temata = verejna_temata(nastaveni.aktualni_rocnik) +# for t in temata: +# if request.user.is_staff: +# t.prispevky = t.prispevek_set.filter(problem=t) +# else: +# t.prispevky = t.prispevek_set.filter(problem=t, zverejnit=True) +# return render(request, 'seminar/zadani/Temata.html', +# { +# 'temata': temata, +# } +# ) +# +# +# +#def TematkoView(request, rocnik, tematko): +# nastaveni = s.Nastaveni.objects.first() +# rocnik_object = s.Rocnik.objects.filter(rocnik=rocnik) +# tematko_object = s.Tema.objects.filter(rocnik=rocnik_object[0], kod=tematko) +# seznam = vytahniZLesaSeznam(tematko_object[0], nastaveni.aktualni_rocnik().rocniknode) +# for node, depth in seznam: +# if node.isinstance(node, s.KonferaNode): +# raise Exception("Not implemented yet") +# if node.isinstance(node, s.PohadkaNode): # Mohu ignorovat, má pod sebou +# pass +# +# return render(request, 'seminar/tematka/toaletak.html', {}) +# +# +#def TemataRozcestnikView(request): +# print("=============================================") +# nastaveni = s.Nastaveni.objects.first() +# tematka_objects = s.Tema.objects.filter(rocnik=nastaveni.aktualni_rocnik()) +# tematka = [] #List tematka obsahuje pro kazde tematko object a list vsech TemaVCisleNodu - implementované pomocí slovníku +# for tematko_object in tematka_objects: +# print("AKTUALNI TEMATKO") +# print(tematko_object.id) +# odkazy = vytahniZLesaSeznam(tematko_object, nastaveni.aktualni_rocnik().rocniknode, pouze_zajimave = True) #Odkazy jsou tuply (node, depth) v listu +# print(odkazy) +# cisla = [] # List tuplů (nazev cisla, list odkazů) +# vcisle = [] +# cislo = None +# for odkaz in odkazy: +# if odkaz[1] == 0: +# if cislo != None: +# cisla.append((cislo, vcisle)) +# cislo = (odkaz[0].getOdkazStr(), odkaz[0].getOdkaz()) +# vcisle = [] +# else: +# print(odkaz[0].getOdkaz()) +# vcisle.append((odkaz[0].getOdkazStr(), odkaz[0].getOdkaz())) +# if cislo != None: +# cisla.append((cislo, vcisle)) +# +# print(cisla) +# tematka.append({ +# "kod" : tematko_object.kod, +# "nazev" : tematko_object.nazev, +# "abstrakt" : tematko_object.abstrakt, +# "obrazek": tematko_object.obrazek, +# "cisla" : cisla +# }) +# return render(request, 'seminar/tematka/rozcestnik.html', {"tematka": tematka, "rocnik" : nastaveni.aktualni_rocnik().rocnik}) +# #def ZadaniAktualniVysledkovkaView(request): # nastaveni = get_object_or_404(Nastaveni) @@ -429,27 +403,34 @@ def sloupec_s_poradim(seznam_s_body): aktualni_poradi = aktualni_poradi + velikost_skupiny return sloupec_s_poradim -# spočítá součet bodů získaných daným řešitelem za zadaný problém a všechny jeho podproblémy -def __soucet_resitele_problemu(problem, resitel, cislo, soucet): - # sečteme body za daný problém přes všechna řešení daného problému - # od daného řešitele - reseni_resitele = problem.hodnoceni_set.filter(reseni__resitele=resitel, - cislo_body=cislo) - # XXX chyba na řádku výše - řešení může mít více řešitelů, asi chceme contains - # nebo in - for r in reseni_resitele: - soucet += r.body - - # a přičteme k tomu hodnocení všech podproblémů - for p in problem.podproblem.all(): - # i přes jméno by to měla být množina jeho podproblémů - soucet += __soucet_resitele_problemu(p, resitel, soucet) - return soucet - -# spočítá součet všech bodů ze všech podproblémů daného problému daného řešitele -def body_resitele_problemu_v_cisle(problem, resitel, cislo): - # probably FIXED: nezohledňuje číslo, do kterého se body počítají - return __soucet_resitele_problemu(problem, resitel, cislo, 0) +## spočítá součet bodů získaných daným řešitelem za zadaný problém a všechny jeho podproblémy +#def __soucet_resitele_problemu(problem, resitel, cislo, soucet): +# # sečteme body za daný problém přes všechna řešení daného problému +# # od daného řešitele +# reseni_resitele = s.Reseni_Resitele.objects.filter(resitele=resitel) +# hodnoceni_resitele = problem.hodnoceni.filter(reseni__in=reseni_resitele, +# cislo_body=cislo) +# # XXX chyba na řádku výše - řešení může mít více řešitelů, asi chceme contains +# # nebo in +# for r in hodnoceni_resitele: +# soucet += r.body +# +# # a přičteme k tomu hodnocení všech podproblémů +# for p in problem.podproblem.all(): +# # i přes jméno by to měla být množina jeho podproblémů +# soucet += __soucet_resitele_problemu(p, resitel, soucet) +# return soucet + +## spočítá součet všech bodů ze všech podproblémů daného problému daného řešitele +#def body_resitele_problemu_v_cisle(problem, resitel, cislo): +# # probably FIXED: nezohledňuje číslo, do kterého se body počítají +# return __soucet_resitele_problemu(problem, resitel, cislo, 0) + +# pro daný problém vrátí jeho nejvyšší nadproblém +def hlavni_problem(problem): + while not(problem.nadproblem == None): + problem = problem.nadproblem + return problem # vrátí list všech problémů s body v daném čísle, které již nemají nadproblém def hlavni_problemy_cisla(cislo): @@ -464,10 +445,8 @@ def hlavni_problemy_cisla(cislo): # (mají vlastní sloupeček ve výsledkovce, nemají nadproblém) hlavni_problemy = [] for p in problemy: - while not(p.nadproblem == None): - p = p.nadproblem - hlavni_problemy.append(p) - + hlavni_problemy.append(hlavni_problem(p)) + # zunikátnění hlavni_problemy_set = set(hlavni_problemy) hlavni_problemy = list(hlavni_problemy_set) @@ -475,38 +454,83 @@ def hlavni_problemy_cisla(cislo): return hlavni_problemy -def body_resitele_odjakziva(resitel): - body = 0 - resitelova_hodnoceni = Hodnoceni.objects.select_related('body').all().filter(reseni_resitele=resitel) - # TODO: v radku nahore chceme _in nebo _contains - for hodnoceni in resitelova_hodnoceni: - body = body + hodnoceni.body - return body +# vrátí slovník řešitel:body obsahující počty bodů zadaných řešitelů za daný ročník +# POZOR! Aktuálně počítá jen za posledních 10 let od zadaného ročníku +def body_resitelu_odjakziva(rocnik, resitele): + body_odjakziva = {} + + for r in resitele: + body_odjakziva[str(r.id)] = 0 +# # Body za posledních 10 let je dobrá aproximace pro naše potřeby (výsledkovka +# # s aktivními řešiteli) +# +# body_pred_roky = [] +# for i in range(0, 10): +# body_pred_roky.append(body_resitelu_za_rocnik(rocnik-i, resitele)) +# +# for r in resitele: +# for i in range(0,10): +# body_odjakziva[str(r.id)] += body_pred_roky[i][str(r.id)] + + +# Nasledující řešení je sice správné, ale moc pomalé: + for res in Reseni.objects.prefetch_related('resitele', 'hodnoceni_set').all(): + for r in res.resitele.all(): + # daný řešitel nemusí být v naší podmnožině + if r not in resitele: continue + + for hodn in res.hodnoceni_set.all(): + pricti_body(body_odjakziva, r, hodn.body) + return body_odjakziva + +# vrátí slovník řešitel:body obsahující počty bodů zadaných řešitelů za daný ročník +def body_resitelu_za_rocnik(rocnik, aktivni_resitele): + body_za_rocnik = {} + # inicializujeme na 0 pro všechny aktivní řešitele + for ar in aktivni_resitele: + body_za_rocnik[str(ar.id)] = 0 + + # spočítáme body řešitelům přes všechna řešení s hodnocením v daném ročníku + reseni = Reseni.objects.prefetch_related('resitele', 'hodnoceni_set').filter(hodnoceni__cislo_body__rocnik=rocnik) + for res in reseni: + for resitel in res.resitele.all(): + for hodn in res.hodnoceni_set.all(): + pricti_body(body_za_rocnik, resitel, hodn.body) + return body_za_rocnik + +#def body_resitele_odjakziva(resitel): +# body = 0 +# resitelova_hodnoceni = Hodnoceni.objects.select_related('body').all().filter(reseni_resitele=resitel) +# # TODO: v radku nahore chceme _in nebo _contains +# for hodnoceni in resitelova_hodnoceni: +# body = body + hodnoceni.body +# return body # spočítá součet všech bodů řešitele za dané číslo -def body_resitele_v_cisle(resitel, cislo): - hlavni_problemy = hlavni_problemy_cisla(cislo) - body_resitele = 0 - for h in hlavni_problemy: - body_resitele = body_resitele + body_resitele_problemu_v_cisle(h, resitel, cislo) - # TODO: je rozdíl mezi odevzdanou úlohou za 0 a tím, když řešitel nic neodevzdal - # řešit přes kontrolu velikosti množiny řešení daného problému do daného čísla? - # Tady to ale nevadí, tady se počítá součet za číslo. - return body_resitele +#def body_resitele_v_cisle(resitel, cislo): +# hlavni_problemy = hlavni_problemy_cisla(cislo) +# body_resitele = 0 +# for h in hlavni_problemy: +# body_resitele = body_resitele + body_resitele_problemu_v_cisle(h, resitel, cislo) +# # TODO: je rozdíl mezi odevzdanou úlohou za 0 a tím, když řešitel nic neodevzdal +# # řešit přes kontrolu velikosti množiny řešení daného problému do daného čísla? +# # Tady to ale nevadí, tady se počítá součet za číslo. +# return body_resitele # spočítá součet všech bodů řešitele za daný rok (nebo jen do daného čísla včetně) -def body_resitele_v_rocniku(resitel, rocnik, do_cisla=None): - # pokud do_cisla=None, tak do posledního čísla v ročníku - # do_cisla je objekt Cislo - cisla = rocnik.cisla.all() # funkce vrátí pole objektů - # Cislo už lexikograficky setřízené, viz models - body = 0 - for cislo in cisla: - if cislo.poradi == do_cisla.poradi: break - # druhá část zaručuje, že máme výsledky do daného čísla včetně - body = body + body_resitele_v_cisle(resitel, cislo) - return body - +#def body_resitele_v_rocniku(resitel, rocnik, do_cisla=None): +# # pokud do_cisla=None, tak do posledního čísla v ročníku +# # do_cisla je objekt Cislo +# cisla = rocnik.cisla.all() # funkce vrátí pole objektů +# # Cislo už lexikograficky setřízené, viz models +# body = 0 +# for cislo in cisla: +# if cislo.poradi == do_cisla.poradi: break +# # druhá část zaručuje, že máme výsledky do daného čísla včetně +# body = body + body_resitele_v_cisle(resitel, cislo) +# return body + +# TODO: předělat na nový model #def vysledkovka_rocniku(rocnik, jen_verejne=True): # """Přebírá ročník (např. context["rocnik"]) a vrací výsledkovou listinu ve # formě vhodné pro šablonu "seminar/vysledkovka_rocniku.html" @@ -588,7 +612,8 @@ class RocnikView(generic.DetailView): #context['vysledkovka'] = vysledkovka_rocniku(context["rocnik"]) #context['vysledkovka_s_neverejnymi'] = vysledkovka_rocniku(context["rocnik"], jen_verejne=False) - context['temata_v_rocniku'] = verejna_temata(context["rocnik"]) + #context['temata_v_rocniku'] = verejna_temata(context["rocnik"]) + # FIXME: opravit vylistování témat v ročníku return context @@ -612,19 +637,31 @@ class ProblemView(generic.DetailView): return context -class VysledkyResitele(object): - """Pro daného řešitele ukládá počet bodů za jednotlivé úlohy a celkový - počet bodů za konkrétní ročník do daného čísla a za dané číslo.""" +class RadekVysledkovky(object): + """Obsahuje věci, které se hodí vědět při konstruování výsledkovky. + Umožňuje snazší práci v templatu (lepší, než seznam).""" - def __init__(self, resitel, cislo, rocnik): + def __init__(self, poradi, resitel, body_problemy_sezn, + body_cislo, body_rocnik, body_odjakziva): self.resitel = resitel - self.cislo = cislo - self.body_cislo = body_resitele_v_cisle(resitel, cislo) - self.body = [] - self.rocnik = rocnik - self.body_rocnik = body_resitele_v_rocniku(resitel, rocnik, cislo) - self.body_celkem_odjakziva = resitel.vsechny_body() - self.poradi = 0 + self.body_cislo = body_cislo + self.body_rocnik = body_rocnik + self.body_celkem_odjakziva = body_odjakziva + self.poradi = poradi + self.body_problemy_sezn = body_problemy_sezn + self.titul = resitel.get_titul(body_odjakziva) + + +# přiřazuje danému řešiteli body do slovníku +def pricti_body(slovnik, resitel, body): + # testujeme na None (""), pokud je to první řešení + # daného řešitele, předěláme na 0 + # (v dalším kroku přičteme reálný počet bodů), + # rozlišujeme tím mezi 0 a neodevzdaným řešením + if slovnik[str(resitel.id)] == "": + slovnik[str(resitel.id)] = 0 + + slovnik[str(resitel.id)] += body class CisloView(generic.DetailView): model = Cislo @@ -648,48 +685,101 @@ class CisloView(generic.DetailView): def get_context_data(self, **kwargs): context = super(CisloView, self).get_context_data(**kwargs) - ## TODO upravit dle nového modelu cislo = context['cislo'] hlavni_problemy = hlavni_problemy_cisla(cislo) + # TODO setřídit hlavní problémy čísla podle id, ať jsou ve stejném pořadí pokaždé + # pro každý hlavní problém zavedeme slovník s body za daný hlavní problém + # pro jednotlivé řešitele (slovník slovníků hlavních problémů) + hlavni_problemy_slovnik = {} + for hp in hlavni_problemy: + hlavni_problemy_slovnik[str(hp.id)] = {} ## TODO dostat pro tyto problémy součet v daném čísle pro daného řešitele ## TODO možná chytřeji vybírat aktivní řešitele - ## chceme letos něco poslal - aktivni_resitele = Resitel.objects.filter( - rok_maturity__gte=cislo.rocnik.druhy_rok()) + # aktivní řešitelé - chceme letos něco poslal, TODO později vyfiltrujeme ty, kdo mají + # u alespoň jedné hodnoty něco jiného než NULL + aktivni_resitele = list(Resitel.objects.filter( + rok_maturity__gte=cislo.rocnik.druhy_rok())) # TODO: zkusit hodnoceni__rocnik... #.filter(hodnoceni_set__rocnik__eq=cislo_rocnik) - radky_vysledkovky = [] + # zakládání prázdných záznamů pro řešitele + cislobody = {} for ar in aktivni_resitele: - # získáme výsledky řešitele - součty přes číslo a ročník - vr = VysledkyResitele(ar, cislo, cislo.rocnik) + # řešitele převedeme na řetězec pomocí unikátního id + cislobody[str(ar.id)] = "" for hp in hlavni_problemy: - vr.body.append( - body_resitele_problemu_v_cisle(hp, ar, cislo)) - radky_vysledkovky.append(vr) - - # setřídíme řádky výsledkovky/objekty VysledkyResitele podle bodů - radky_vysledkovky.sort(key=lambda vr: vr.body_rocnik, reverse=True) - - # generujeme sloupec s pořadím pomocí stejně zvané funkce - pocty_bodu = [rv.body_rocnik for rv in radky_vysledkovky] - sloupec_poradi = sloupec_s_poradim(pocty_bodu) - - # každému řádku výsledkovky přidáme jeho pořadí - i = 0 - for rv in radky_vysledkovky: - rv.poradi = sloupec_poradi[i] - i = i + 1 + slovnik = hlavni_problemy_slovnik[str(hp.id)] + slovnik[str(ar.id)] = "" + + # vezmeme všechna řešení s body do daného čísla + reseni_do_cisla = Reseni.objects.prefetch_related('problem', 'resitele', 'hodnoceni_set').filter(hodnoceni__cislo_body=cislo) + + # projdeme všechna řešení do čísla a přičteme body každému řešiteli do celkových + # bodů i do bodů za problém + for reseni in reseni_do_cisla: + + # řešení může řešit více problémů + for prob in list(reseni.problem.all()): + nadproblem = hlavni_problem(prob) + nadproblem_slovnik = hlavni_problemy_slovnik[str(nadproblem.id)] + + # a více hodnocení + for hodn in list(reseni.hodnoceni_set.all()): + body = hodn.body + + # a více řešitelů + for resitel in list(reseni.resitele.all()): + pricti_body(cislobody, resitel, body) + pricti_body(nadproblem_slovnik, resitel, body) + + # zeptáme se na dvojice (řešitel, body) za ročník a setřídíme sestupně + resitel_rocnikbody_slov = body_resitelu_za_rocnik(cislo.rocnik, aktivni_resitele) + resitel_rocnikbody_sezn = sorted(resitel_rocnikbody_slov.items(), + key = lambda x: x[1], reverse = True) + + # získáme body odjakživa + resitel_odjakzivabody_slov = body_resitelu_odjakziva(cislo.rocnik.druhy_rok(), + aktivni_resitele) + + # řešitelé setřídění podle bodů za číslo sestupně + setrizeni_resitele_id = [dvojice[0] for dvojice in resitel_rocnikbody_sezn] + setrizeni_resitele = [Resitel.objects.get(id=i) for i in setrizeni_resitele_id] + + # vytvoříme jednotlivé sloupce výsledkovky + radky_vysledkovky = [] + odjakziva_body = [] + rocnik_body = [] + cislo_body = [] + hlavni_problemy_body = [] + for ar_id in setrizeni_resitele_id: + # vytáhneme ze slovníků body pro daného řešitele + odjakziva_body.append(resitel_odjakzivabody_slov[ar_id]) + rocnik_body.append(resitel_rocnikbody_slov[ar_id]) + cislo_body.append(cislobody[ar_id]) + problemy = [] + for hp in hlavni_problemy: + problemy.append(hlavni_problemy_slovnik[str(hp.id)][ar_id]) + hlavni_problemy_body.append(problemy) + print("{}: body za problémy - {}, číslobody - {}, ročníkbody - {}, odjakživabody - ".format(ar_id, problemy, cislobody[ar_id], resitel_rocnikbody_slov[ar_id])) + # pořadí určíme pomocí funkce, které dáme celkové body za ročník vzestupně + poradi = sloupec_s_poradim(rocnik_body) + radky_vysledkovky = [] + for i in range(0, len(setrizeni_resitele_id)): + radek = RadekVysledkovky(poradi[i], setrizeni_resitele[i], + hlavni_problemy_body[i], cislo_body[i], rocnik_body[i], + odjakziva_body[i]) + radky_vysledkovky.append(radek) + print("Přikládám {}-tý řádek.".format(i)) + + print("Následuje předávání do kontextu.") # vytahané informace předáváme do kontextu context['cislo'] = cislo context['radky_vysledkovky'] = radky_vysledkovky context['problemy'] = hlavni_problemy # context['v_cisle_zadane'] = TODO # context['resene_problemy'] = resene_problemy - #XXX testovat - #XXX opravit to, že se nezobrazují body za jednotlivé úlohy - + print("Předávám kontext.") return context # problemy = sorted(set(r.problem for r in reseni), key=lambda x:(poradi_typu[x.typ], x.kod_v_rocniku())) @@ -917,7 +1007,7 @@ def soustredeniUcastniciExportView(request,soustredeni): class ClankyResitelView(generic.ListView): model = Problem template_name = 'seminar/clanky/resitelske_clanky.html' - queryset = Clanek.objects.filter(stav=Problem.STAV_ZADANY).select_related('cislo_zadani__rocnik').order_by('-cislo_zadani__rocnik__rocnik', 'kod') + queryset = Clanek.objects.filter(stav=Problem.STAV_ZADANY, resitelsky=True).select_related('cislo__rocnik').order_by('-cislo__rocnik__rocnik', 'kod') # FIXME: pokud chceme orgoclanky, tak nejak zavest do modelu a podle toho odkomentovat a upravit #class ClankyOrganizatorView(generic.ListView): @@ -1160,6 +1250,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 @@ -1321,58 +1447,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):