From 92c05342fb9163be03a0488dcac66af6b0830285 Mon Sep 17 00:00:00 2001 From: Pavel 'LEdoian' Turinsky Date: Wed, 30 Oct 2024 14:36:06 +0100 Subject: [PATCH 1/4] =?UTF-8?q?odst=C5=99el=20tvorby:=20pre=20=E2=80=93=20?= =?UTF-8?q?relink?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- odevzdavatko/migrations/0004_tvorba_pre.py | 13 + odevzdavatko/migrations/0005_tvorba_relink.py | 35 + odevzdavatko/models.py | 2 +- personalni/migrations/0014_tvorba_pre.py | 13 + seminar/migrations/0136_tvorba_pre.py | 21 + seminar/migrations/0137_tvorba_unmanage.py | 59 ++ seminar/models/__init__.py | 1 + seminar/models/tvorba.py | 52 +- soustredeni/migrations/0004_tvorba_pre.py | 13 + soustredeni/migrations/0005_tvorba_relink.py | 25 + soustredeni/models.py | 2 +- split-apps-meta/polymorphic | 3 + tvorba/admin.py | 2 +- tvorba/migrations/0001_tvorba_create.py | 197 +++++ tvorba/models.py | 717 ++++++++++++++++++ various/migrations/0004_tvorba_pre.py | 13 + various/migrations/0005_tvorba_relink.py | 20 + various/models.py | 2 +- 18 files changed, 1172 insertions(+), 18 deletions(-) create mode 100644 odevzdavatko/migrations/0004_tvorba_pre.py create mode 100644 odevzdavatko/migrations/0005_tvorba_relink.py create mode 100644 personalni/migrations/0014_tvorba_pre.py create mode 100644 seminar/migrations/0136_tvorba_pre.py create mode 100644 seminar/migrations/0137_tvorba_unmanage.py create mode 100644 soustredeni/migrations/0004_tvorba_pre.py create mode 100644 soustredeni/migrations/0005_tvorba_relink.py create mode 100644 split-apps-meta/polymorphic create mode 100644 tvorba/migrations/0001_tvorba_create.py create mode 100644 tvorba/models.py create mode 100644 various/migrations/0004_tvorba_pre.py create mode 100644 various/migrations/0005_tvorba_relink.py diff --git a/odevzdavatko/migrations/0004_tvorba_pre.py b/odevzdavatko/migrations/0004_tvorba_pre.py new file mode 100644 index 00000000..a571c07e --- /dev/null +++ b/odevzdavatko/migrations/0004_tvorba_pre.py @@ -0,0 +1,13 @@ +# Generated by Django 4.2.16 on 2024-10-30 01:06 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('odevzdavatko', '0003_odstrel_odevzdavatka_post'), + ] + + operations = [ + ] diff --git a/odevzdavatko/migrations/0005_tvorba_relink.py b/odevzdavatko/migrations/0005_tvorba_relink.py new file mode 100644 index 00000000..2d235410 --- /dev/null +++ b/odevzdavatko/migrations/0005_tvorba_relink.py @@ -0,0 +1,35 @@ +# Generated by Django 4.2.16 on 2024-10-30 13:18 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('tvorba', '0001_tvorba_create'), + ('odevzdavatko', '0004_tvorba_pre'), + ] + + operations = [ + migrations.AlterField( + model_name='hodnoceni', + name='cislo_body', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='hodnoceni', to='tvorba.cislo', verbose_name='číslo pro body'), + ), + migrations.AlterField( + model_name='hodnoceni', + name='deadline_body', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='hodnoceni', to='tvorba.deadline', verbose_name='deadline pro body'), + ), + migrations.AlterField( + model_name='hodnoceni', + name='problem', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='hodnoceni', to='tvorba.problem', verbose_name='problém'), + ), + migrations.AlterField( + model_name='reseni', + name='problem', + field=models.ManyToManyField(help_text='Problém', through='odevzdavatko.Hodnoceni', to='tvorba.problem', verbose_name='problém'), + ), + ] diff --git a/odevzdavatko/models.py b/odevzdavatko/models.py index a5b6a7e1..a52f370f 100644 --- a/odevzdavatko/models.py +++ b/odevzdavatko/models.py @@ -9,7 +9,7 @@ from django.urls import reverse_lazy from django.utils import timezone from django.conf import settings -import seminar.models as am # tvorba +import tvorba.models as am from seminar.models import base as bm from odevzdavatko.utils import vzorecek_na_prepocet, inverze_vzorecku_na_prepocet diff --git a/personalni/migrations/0014_tvorba_pre.py b/personalni/migrations/0014_tvorba_pre.py new file mode 100644 index 00000000..2d976333 --- /dev/null +++ b/personalni/migrations/0014_tvorba_pre.py @@ -0,0 +1,13 @@ +# Generated by Django 4.2.16 on 2024-10-30 01:07 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('personalni', '0013_odstrel_odevzdavatka_post'), + ] + + operations = [ + ] diff --git a/seminar/migrations/0136_tvorba_pre.py b/seminar/migrations/0136_tvorba_pre.py new file mode 100644 index 00000000..07f7d234 --- /dev/null +++ b/seminar/migrations/0136_tvorba_pre.py @@ -0,0 +1,21 @@ +# Generated by Django 4.2.16 on 2024-10-30 01:06 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('seminar', '0135_odstrel_odevzdavatka_post'), + ('odevzdavatko', '0004_tvorba_pre'), + ('various', '0004_tvorba_pre'), + ('soustredeni', '0004_tvorba_pre'), + ('personalni', '0014_tvorba_pre'), + # Polymorphic: + ('contenttypes', '0002_remove_content_type_name'), + # Taggit + ('taggit', '0006_rename_taggeditem_content_type_object_id_taggit_tagg_content_8fc721_idx'), + ] + + operations = [ + ] diff --git a/seminar/migrations/0137_tvorba_unmanage.py b/seminar/migrations/0137_tvorba_unmanage.py new file mode 100644 index 00000000..132f979b --- /dev/null +++ b/seminar/migrations/0137_tvorba_unmanage.py @@ -0,0 +1,59 @@ +# Generated by Django 4.2.16 on 2024-10-30 11:19 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('seminar', '0136_tvorba_pre'), + ] + + operations = [ + migrations.CreateModel( + name='Problemy_Opravovatele', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ], + options={ + 'db_table': 'seminar_problemy_opravovatele', + 'managed': False, + }, + ), + migrations.AlterModelOptions( + name='cislo', + options={'managed': False, 'ordering': ['-rocnik__rocnik', '-poradi'], 'verbose_name': 'Číslo', 'verbose_name_plural': 'Čísla'}, + ), + migrations.AlterModelOptions( + name='clanek', + options={'managed': False, 'verbose_name': 'Článek', 'verbose_name_plural': 'Články'}, + ), + migrations.AlterModelOptions( + name='deadline', + options={'managed': False, 'ordering': ['deadline'], 'verbose_name': 'Deadline', 'verbose_name_plural': 'Deadliny'}, + ), + migrations.AlterModelOptions( + name='pohadka', + options={'managed': False, 'ordering': ['vytvoreno'], 'verbose_name': 'Pohádka', 'verbose_name_plural': 'Pohádky'}, + ), + migrations.AlterModelOptions( + name='problem', + options={'managed': False, 'ordering': ['nazev'], 'verbose_name': 'Problém', 'verbose_name_plural': 'Problémy'}, + ), + migrations.AlterModelOptions( + name='rocnik', + options={'managed': False, 'ordering': ['-rocnik'], 'verbose_name': 'Ročník', 'verbose_name_plural': 'Ročníky'}, + ), + migrations.AlterModelOptions( + name='tema', + options={'managed': False, 'verbose_name': 'Téma', 'verbose_name_plural': 'Témata'}, + ), + migrations.AlterModelOptions( + name='uloha', + options={'managed': False, 'verbose_name': 'Úloha', 'verbose_name_plural': 'Úlohy'}, + ), + migrations.AlterModelOptions( + name='zmrazenavysledkovka', + options={'managed': False, 'verbose_name': 'Zmražená výsledkovka', 'verbose_name_plural': 'Zmražené výsledkovky'}, + ), + ] diff --git a/seminar/models/__init__.py b/seminar/models/__init__.py index 4bc85266..2eabe50f 100644 --- a/seminar/models/__init__.py +++ b/seminar/models/__init__.py @@ -9,6 +9,7 @@ from personalni.models import Organizator, Resitel, Skola, Prijemce, Osoba from soustredeni.models import Soustredeni, Soustredeni_Ucastnici, Soustredeni_Organizatori, Konfera, Konfery_Ucastnici from novinky.models import Novinky from odevzdavatko.models import Reseni, PrilohaReseni, Reseni_Resitele, Hodnoceni +from tvorba.models import ZmrazenaVysledkovka, Deadline, Cislo, Rocnik, Pohadka, Tema, Problem, Problemy_Opravovatele, Uloha, Clanek # Kvůli migr. 0041 from soustredeni.models import generate_filename_konfera diff --git a/seminar/models/tvorba.py b/seminar/models/tvorba.py index c11a3861..a8aea6ae 100644 --- a/seminar/models/tvorba.py +++ b/seminar/models/tvorba.py @@ -54,6 +54,7 @@ class Rocnik(SeminarModelBase): verbose_name = 'Ročník' verbose_name_plural = 'Ročníky' ordering = ['-rocnik'] + managed = False # Interní ID id = models.AutoField(primary_key = True) @@ -144,11 +145,12 @@ class Cislo(SeminarModelBase): verbose_name = 'Číslo' verbose_name_plural = 'Čísla' ordering = ['-rocnik__rocnik', '-poradi'] + managed = False # Interní ID id = models.AutoField(primary_key = True) - rocnik = models.ForeignKey(Rocnik, verbose_name='ročník', related_name='cisla', + rocnik = models.ForeignKey(Rocnik, verbose_name='ročník', related_name='cisla_old', db_index=True,on_delete=models.PROTECT) poradi = models.CharField('název čísla', max_length=32, db_index=True, @@ -338,6 +340,7 @@ class Deadline(SeminarModelBase): verbose_name = 'Deadline' verbose_name_plural = 'Deadliny' ordering = ['deadline'] + managed = False def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -349,7 +352,7 @@ class Deadline(SeminarModelBase): deadline = models.DateTimeField(blank=False, default=timezone.make_aware(datetime.datetime.combine(timezone.now(), datetime.time.max))) cislo = models.ForeignKey(Cislo, verbose_name='deadline v čísle', - related_name='deadline_v_cisle', blank=False, + related_name='deadline_v_cisle_old', blank=False, on_delete=models.CASCADE) TYP_CISLA = 'cisla' @@ -400,16 +403,31 @@ class ZmrazenaVysledkovka(SeminarModelBase): db_table = 'seminar_vysledkovky' verbose_name = 'Zmražená výsledkovka' verbose_name_plural = 'Zmražené výsledkovky' + managed = False deadline = models.OneToOneField( Deadline, on_delete=models.CASCADE, primary_key=True, - related_name="vysledkovka_v_deadlinu" + related_name="vysledkovka_v_deadlinu_old" ) html = models.TextField(null=False, blank=False) +class Problemy_Opravovatele(SeminarModelBase): + """Jen vazebná tabulka pro opravovatele. + + Ona stejně existovala, při přesunu mezi aplikacemi jen potřebujeme zajistit nepřejmenování DB tabulky. + Proto taky nepotřebuje žádná specifika, ze :py:class:SeminarModelBase: dědí ze zvyku než že by to k něčemu kdy měo být. + """ + class Meta: + db_table = 'seminar_problemy_opravovatele' + managed = False + + id = models.AutoField(primary_key = True) + + problem = models.ForeignKey('Problem', on_delete=models.CASCADE, related_name='awawa1_old') + organizator = models.ForeignKey(Organizator, on_delete=models.CASCADE, related_name='awawa2_old') @reversion.register(ignore_duplicates=True) # Pozor na následující řádek. *Nekrmit, asi kouše!* @@ -426,6 +444,7 @@ class Problem(SeminarModelBase,PolymorphicModel): verbose_name = 'Problém' verbose_name_plural = 'Problémy' ordering = ['nazev'] + managed = False # Interní ID id = models.AutoField(primary_key = True) @@ -435,7 +454,7 @@ class Problem(SeminarModelBase,PolymorphicModel): # Problém má podproblémy nadproblem = models.ForeignKey('self', verbose_name='nadřazený problém', - related_name='podproblem', null=True, blank=True, + related_name='podproblem_old', null=True, blank=True, on_delete=models.SET_NULL) STAV_NAVRH = 'navrh' @@ -451,22 +470,22 @@ class Problem(SeminarModelBase,PolymorphicModel): stav = models.CharField('stav problému', max_length=32, choices=STAV_CHOICES, blank=False, default=STAV_NAVRH) # Téma je taky Problém, takže má stavy, "zadané" témátko je aktuálně otevřené a dá se k němu něco poslat (řešení nebo článek) - zamereni = TaggableManager(verbose_name='zaměření', + zamereni = TaggableManager(verbose_name='zaměření', related_name='zamereni_old', help_text='Zaměření M/F/I/O problému, příp. další tagy', blank=True) poznamka = models.TextField('org poznámky (HTML)', blank=True, help_text='Neveřejný návrh úlohy, návrh řešení, text zadání, poznámky ...') autor = models.ForeignKey(Organizator, verbose_name='autor problému', - related_name='autor_problemu_%(class)s', null=True, blank=True, + related_name='autor_problemu_%(class)s_old', null=True, blank=True, on_delete=models.SET_NULL) garant = models.ForeignKey(Organizator, verbose_name='garant zadaného problému', - related_name='garant_problemu_%(class)s', null=True, blank=True, + related_name='garant_problemu_%(class)s_old', null=True, blank=True, on_delete=models.SET_NULL) opravovatele = models.ManyToManyField(Organizator, verbose_name='opravovatelé', - blank=True, related_name='opravovatele_%(class)s') + blank=True, related_name='opravovatele_%(class)s_old', through=Problemy_Opravovatele) kod = models.CharField('lokální kód', max_length=32, blank=True, default='', help_text='Číslo/kód úlohy v čísle nebo kód tématu/článku/seriálu v ročníku') @@ -545,6 +564,7 @@ class Tema(Problem): db_table = 'seminar_temata' verbose_name = 'Téma' verbose_name_plural = 'Témata' + managed = False TEMA_TEMA = 'tema' TEMA_SERIAL = 'serial' @@ -555,7 +575,7 @@ class Tema(Problem): tema_typ = models.CharField('Typ tématu', max_length=16, choices=TEMA_CHOICES, blank=False, default=TEMA_TEMA) - rocnik = models.ForeignKey(Rocnik, verbose_name='ročník',related_name='temata',blank=True, null=True, + rocnik = models.ForeignKey(Rocnik, verbose_name='ročník',related_name='temata_old',blank=True, null=True, on_delete=models.PROTECT) abstrakt = models.TextField('Abstrakt na rozcestník', blank=True) @@ -592,9 +612,10 @@ class Clanek(Problem): db_table = 'seminar_clanky' verbose_name = 'Článek' verbose_name_plural = 'Články' + managed = False cislo = models.ForeignKey(Cislo, blank=True, null=True, on_delete=models.PROTECT, - verbose_name='číslo vydání', related_name='vydane_clanky') + verbose_name='číslo vydání', related_name='vydane_clanky_old') strana = models.PositiveIntegerField(verbose_name="první strana", blank=True, null=True) @@ -617,15 +638,16 @@ class Uloha(Problem): db_table = 'seminar_ulohy' verbose_name = 'Úloha' verbose_name_plural = 'Úlohy' + managed = False cislo_zadani = models.ForeignKey(Cislo, verbose_name='číslo zadání', blank=True, - null=True, related_name='zadane_ulohy', on_delete=models.PROTECT) + null=True, related_name='zadane_ulohy_old', on_delete=models.PROTECT) cislo_deadline = models.ForeignKey(Cislo, verbose_name='číslo deadlinu', blank=True, - null=True, related_name='deadlinove_ulohy', on_delete=models.PROTECT) + null=True, related_name='deadlinove_ulohy_old', on_delete=models.PROTECT) cislo_reseni = models.ForeignKey(Cislo, verbose_name='číslo řešení', blank=True, - null=True, related_name='resene_ulohy', + null=True, related_name='resene_ulohy_old', help_text='Číslo s řešením úlohy, jen pro úlohy', on_delete=models.PROTECT) @@ -683,6 +705,7 @@ class Pohadka(SeminarModelBase): verbose_name = 'Pohádka' verbose_name_plural = 'Pohádky' ordering = ['vytvoreno'] + managed = False # Interní ID id = models.AutoField(primary_key=True) @@ -694,7 +717,8 @@ class Pohadka(SeminarModelBase): # Při nahrávání z TeXu není vyplnění vyžadováno, v adminu je null=True, blank=False, - on_delete=models.SET_NULL + on_delete=models.SET_NULL, + related_name='awawa3_old', ) vytvoreno = models.DateTimeField( diff --git a/soustredeni/migrations/0004_tvorba_pre.py b/soustredeni/migrations/0004_tvorba_pre.py new file mode 100644 index 00000000..76461971 --- /dev/null +++ b/soustredeni/migrations/0004_tvorba_pre.py @@ -0,0 +1,13 @@ +# Generated by Django 4.2.16 on 2024-10-30 01:07 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('soustredeni', '0003_post_split_soustredeni'), + ] + + operations = [ + ] diff --git a/soustredeni/migrations/0005_tvorba_relink.py b/soustredeni/migrations/0005_tvorba_relink.py new file mode 100644 index 00000000..7786449f --- /dev/null +++ b/soustredeni/migrations/0005_tvorba_relink.py @@ -0,0 +1,25 @@ +# Generated by Django 4.2.16 on 2024-10-30 13:18 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('tvorba', '0001_tvorba_create'), + ('soustredeni', '0004_tvorba_pre'), + ] + + operations = [ + migrations.AlterField( + model_name='konfera', + name='problem_ptr', + field=models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tvorba.problem'), + ), + migrations.AlterField( + model_name='soustredeni', + name='rocnik', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='soustredeni', to='tvorba.rocnik', verbose_name='ročník'), + ), + ] diff --git a/soustredeni/models.py b/soustredeni/models.py index fb5c5239..23553c34 100644 --- a/soustredeni/models.py +++ b/soustredeni/models.py @@ -10,7 +10,7 @@ from django.conf import settings from personalni.models import Resitel, Organizator from seminar.models.base import SeminarModelBase -import seminar.models as am # tvorba +import tvorba.models as am logger = logging.getLogger(__name__) diff --git a/split-apps-meta/polymorphic b/split-apps-meta/polymorphic new file mode 100644 index 00000000..9c6ba514 --- /dev/null +++ b/split-apps-meta/polymorphic @@ -0,0 +1,3 @@ +django-polymorphic by *nemělo* být potřeba řešit, protože se odkazuje na id contenttype a tedy když přepisujeme ctype na správném místě rovnou, tak to bude fungovat. IN THEORY. + +Better safe than sorry: přidáme si v seminar.pre vazbu na model contenttypes. (technicky asi měl být všude?) diff --git a/tvorba/admin.py b/tvorba/admin.py index 01880d5b..f090062b 100644 --- a/tvorba/admin.py +++ b/tvorba/admin.py @@ -9,7 +9,7 @@ from django.utils.safestring import mark_safe import soustredeni.models -from seminar.models import Rocnik, ZmrazenaVysledkovka, Deadline, Uloha, Problem, Tema, Clanek, Cislo # tvorba +from tvorba.models import Rocnik, ZmrazenaVysledkovka, Deadline, Uloha, Problem, Tema, Clanek, Cislo # tvorba admin.site.register(Rocnik) admin.site.register(ZmrazenaVysledkovka) diff --git a/tvorba/migrations/0001_tvorba_create.py b/tvorba/migrations/0001_tvorba_create.py new file mode 100644 index 00000000..085cb5b4 --- /dev/null +++ b/tvorba/migrations/0001_tvorba_create.py @@ -0,0 +1,197 @@ +# Generated by Django 4.2.16 on 2024-10-30 11:37 + +import datetime +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +import seminar.models.tvorba +import tvorba.models +import taggit.managers + +def nastav_nove_contenttypes(apps, schema_editor): + ContentType = apps.get_model('contenttypes', 'ContentType') + for m in ('zmrazenavysledkovka', 'deadline', 'cislo', 'rocnik', 'pohadka', 'tema', 'problem', 'problemy_opravovatele', 'uloha', 'clanek'): + ContentType.objects.filter(app_label='seminar', model=m).update(app_label='tvorba') + +def nastav_stare_contenttypes(apps, schema_editor): + ContentType = apps.get_model('contenttypes', 'ContentType') + for m in ('zmrazenavysledkovka', 'deadline', 'cislo', 'rocnik', 'pohadka', 'tema', 'problem', 'problemy_opravovatele', 'uloha', 'clanek'): + ContentType.objects.filter(app_label='tvorba', model=m).update(app_label='seminar') + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('seminar', '0137_tvorba_unmanage'), + ] + + operations = [ + migrations.CreateModel( + name='Cislo', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('poradi', models.CharField(db_index=True, help_text='Většinou jen "1", vyjímečně "7-8", lexikograficky určuje pořadí v ročníku!', max_length=32, verbose_name='název čísla')), + ('datum_vydani', models.DateField(blank=True, help_text='Datum vydání finální verze', null=True, verbose_name='datum vydání')), + ('verejne_db', models.BooleanField(db_column='verejne', default=False, verbose_name='číslo zveřejněno')), + ('poznamka', models.TextField(blank=True, help_text='Neveřejná poznámka k číslu (plain text)', verbose_name='neveřejná poznámka')), + ('pdf', models.FileField(blank=True, help_text='PDF čísla, které si mohou řešitelé stáhnout', null=True, storage=seminar.models.tvorba.OverwriteStorage(), upload_to=tvorba.models.cislo_pdf_filename, verbose_name='pdf')), + ('titulka_nahled', models.ImageField(blank=True, help_text='Obrázek titulní strany, generuje se automaticky', null=True, upload_to=tvorba.models.cislo_png_filename, verbose_name='Obrázek titulní strany')), + ('rocnik', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='cisla', to='tvorba.rocnik', verbose_name='ročník')), + ], + options={ + 'verbose_name': 'Číslo', + 'verbose_name_plural': 'Čísla', + 'db_table': 'seminar_cisla', + 'ordering': ['-rocnik__rocnik', '-poradi'], + 'managed': False, + }, + ), + migrations.CreateModel( + name='Deadline', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('deadline', models.DateTimeField(default=datetime.datetime(2024, 10, 30, 22, 59, 59, 999999, tzinfo=datetime.timezone.utc))), + ('cislo', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='deadline_v_cisle', to='tvorba.cislo', verbose_name='deadline v čísle')), + ('typ', models.CharField(choices=[('cisla', 'Deadline celého čísla'), ('prvni', 'První deadline'), ('prvniasous', 'Sousový a první deadline'), ('sous', 'Sousový deadline')], max_length=32, verbose_name='typ deadlinu')), + ('verejna_vysledkovka', models.BooleanField(db_column='verejna_vysledkovka', default=False, verbose_name='veřejná výsledkovka')), + ], + options={ + 'verbose_name': 'Deadline', + 'verbose_name_plural': 'Deadliny', + 'db_table': 'seminar_deadliny', + 'ordering': ['deadline'], + 'managed': False, + }, + ), + migrations.CreateModel( + name='Pohadka', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('vytvoreno', models.DateTimeField(blank=True, default=django.utils.timezone.now, editable=False, verbose_name='Vytvořeno')), + ('autor', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='personalni.organizator', verbose_name='Autor pohádky')), + ], + options={ + 'verbose_name': 'Pohádka', + 'verbose_name_plural': 'Pohádky', + 'db_table': 'seminar_pohadky', + 'ordering': ['vytvoreno'], + 'managed': False, + }, + ), + migrations.CreateModel( + name='Problem', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('nazev', models.CharField(max_length=256, verbose_name='název')), + ('stav', models.CharField(choices=[('navrh', 'Návrh'), ('zadany', 'Zadaný'), ('vyreseny', 'Vyřešený'), ('smazany', 'Smazaný')], default='navrh', max_length=32, verbose_name='stav problému')), + ('autor', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='autor_problemu_%(class)s', to='personalni.organizator', verbose_name='autor problému')), + ('garant', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='garant_problemu_%(class)s', to='personalni.organizator', verbose_name='garant zadaného problému')), + ('nadproblem', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='podproblem', to='tvorba.problem', verbose_name='nadřazený problém')), + ('opravovatele', models.ManyToManyField(blank=True, related_name='opravovatele_%(class)s', through='tvorba.Problemy_Opravovatele', to='personalni.organizator', verbose_name='opravovatelé')), + ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), + ('zamereni', taggit.managers.TaggableManager(blank=True, help_text='Zaměření M/F/I/O problému, příp. další tagy', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='zaměření')), + ('poznamka', models.TextField(blank=True, help_text='Neveřejný návrh úlohy, návrh řešení, text zadání, poznámky ...', verbose_name='org poznámky (HTML)')), + ('kod', models.CharField(blank=True, default='', help_text='Číslo/kód úlohy v čísle nebo kód tématu/článku/seriálu v ročníku', max_length=32, verbose_name='lokální kód')), + ('vytvoreno', models.DateTimeField(blank=True, default=django.utils.timezone.now, editable=False, verbose_name='vytvořeno')), + ], + options={ + 'verbose_name': 'Problém', + 'verbose_name_plural': 'Problémy', + 'db_table': 'seminar_problemy', + 'ordering': ['nazev'], + 'managed': False, + }, + ), + migrations.CreateModel( + name='Problemy_Opravovatele', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('problem', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tvorba.problem')), + ('organizator', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='personalni.organizator')), + ], + options={ + 'db_table': 'seminar_problemy_opravovatele', + 'managed': False, + }, + ), + migrations.CreateModel( + name='Rocnik', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('prvni_rok', models.IntegerField(db_index=True, unique=True, verbose_name='první rok')), + ('rocnik', models.IntegerField(db_index=True, unique=True, verbose_name='číslo ročníku')), + ('exportovat', models.BooleanField(db_column='exportovat', default=False, help_text='Exportuje se jen podle tohoto flagu (ne veřejnosti), a to jen čísla s veřejnou výsledkovkou', verbose_name='export do AESOPa')), + ], + options={ + 'verbose_name': 'Ročník', + 'verbose_name_plural': 'Ročníky', + 'db_table': 'seminar_rocniky', + 'ordering': ['-rocnik'], + 'managed': False, + }, + ), + migrations.CreateModel( + name='Clanek', + fields=[ + ('problem_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tvorba.problem')), + ('strana', models.PositiveIntegerField(blank=True, null=True, verbose_name='první strana')), + ('cislo', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='vydane_clanky', to='tvorba.cislo', verbose_name='číslo vydání')), + ], + options={ + 'verbose_name': 'Článek', + 'verbose_name_plural': 'Články', + 'db_table': 'seminar_clanky', + 'managed': False, + }, + bases=('tvorba.problem',), + ), + migrations.CreateModel( + name='Tema', + fields=[ + ('problem_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tvorba.problem')), + ('tema_typ', models.CharField(choices=[('tema', 'Téma'), ('serial', 'Seriál')], default='tema', max_length=16, verbose_name='Typ tématu')), + ('rocnik', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='temata', to='tvorba.rocnik', verbose_name='ročník')), + ('abstrakt', models.TextField(blank=True, verbose_name='Abstrakt na rozcestník')), + ('obrazek', models.ImageField(blank=True, null=True, upload_to='', verbose_name='Obrázek na rozcestník')), + ], + options={ + 'verbose_name': 'Téma', + 'verbose_name_plural': 'Témata', + 'db_table': 'seminar_temata', + 'managed': False, + }, + bases=('tvorba.problem',), + ), + migrations.CreateModel( + name='Uloha', + fields=[ + ('problem_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tvorba.problem')), + ('max_body', models.DecimalField(blank=True, decimal_places=1, max_digits=8, null=True, verbose_name='maximum bodů')), + ('cislo_zadani', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='zadane_ulohy', to='tvorba.cislo', verbose_name='číslo zadání')), + ('cislo_reseni', models.ForeignKey(blank=True, help_text='Číslo s řešením úlohy, jen pro úlohy', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='resene_ulohy', to='tvorba.cislo', verbose_name='číslo řešení')), + ('cislo_deadline', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='deadlinove_ulohy', to='tvorba.cislo', verbose_name='číslo deadlinu')), + ], + options={ + 'verbose_name': 'Úloha', + 'verbose_name_plural': 'Úlohy', + 'db_table': 'seminar_ulohy', + 'managed': False, + }, + bases=('tvorba.problem',), + ), + migrations.CreateModel( + name='ZmrazenaVysledkovka', + fields=[ + ('deadline', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, related_name='vysledkovka_v_deadlinu', serialize=False, to='tvorba.deadline')), + ('html', models.TextField()), + ], + options={ + 'verbose_name': 'Zmražená výsledkovka', + 'verbose_name_plural': 'Zmražené výsledkovky', + 'db_table': 'seminar_vysledkovky', + 'managed': False, + }, + ), + migrations.RunPython(nastav_nove_contenttypes, nastav_stare_contenttypes), + ] diff --git a/tvorba/models.py b/tvorba/models.py new file mode 100644 index 00000000..40d46797 --- /dev/null +++ b/tvorba/models.py @@ -0,0 +1,717 @@ +import datetime +import os +import subprocess +import pathlib +import tempfile +import logging + +from django.contrib.sites.shortcuts import get_current_site +from django.db import models +from django.db.models import Q +from django.template.loader import render_to_string +from django.utils import timezone +from django.conf import settings +from django.urls import reverse +from django.core.cache import cache +from django.core.exceptions import ObjectDoesNotExist, ValidationError +from django.utils.text import get_valid_filename +from django.utils.functional import cached_property + +from solo.models import SingletonModel +from taggit.managers import TaggableManager + +from reversion import revisions as reversion + +from tvorba.utils import roman, aktivniResitele +from treenode import treelib + +from unidecode import unidecode # Používám pro získání ID odkazu (ještě je to někde po někom zakomentované) + +from polymorphic.models import PolymorphicModel + +from django.core.mail import EmailMessage + +from seminar.models import SeminarModelBase, OverwriteStorage +from personalni.models import Prijemce, Organizator + +logger = logging.getLogger(__name__) + +@reversion.register(ignore_duplicates=True) +class Rocnik(SeminarModelBase): + + class Meta: + db_table = 'seminar_rocniky' + verbose_name = 'Ročník' + verbose_name_plural = 'Ročníky' + ordering = ['-rocnik'] + managed = False + + # Interní ID + id = models.AutoField(primary_key = True) + + prvni_rok = models.IntegerField('první rok', db_index=True, unique=True) + + rocnik = models.IntegerField('číslo ročníku', db_index=True, unique=True) + + exportovat = models.BooleanField('export do AESOPa', db_column='exportovat', default=False, + help_text='Exportuje se jen podle tohoto flagu (ne veřejnosti),' + ' a to jen čísla s veřejnou výsledkovkou') + + def __str__(self): + return '{} ({}/{})'.format(self.rocnik, self.prvni_rok, self.prvni_rok+1) + + # Ročník v římských číslech + def roman(self): + return roman(int(self.rocnik)) + + def verejne(self): + return len(self.verejna_cisla()) > 0 + verejne.boolean = True + verejne.short_description = 'Veřejný (jen dle čísel)' + + def neverejna_cisla(self): + vc = [c for c in self.cisla.all() if not c.verejne()] + vc.sort(key=lambda c: c.poradi) + return vc + + def verejna_cisla(self): + vc = [c for c in self.cisla.all() if c.verejne()] + vc.sort(key=lambda c: c.poradi) + return vc + + def posledni_verejne_cislo(self): + vc = self.verejna_cisla() + return vc[-1] if vc else None + + def verejne_vysledkovky_cisla(self): + vc = list(self.cisla.filter(deadline_v_cisle__verejna_vysledkovka=True).distinct()) + vc.sort(key=lambda c: c.poradi) + return vc + + def posledni_zverejnena_vysledkovka_cislo(self): + vc = self.verejne_vysledkovky_cisla() + return vc[-1] if vc else None + + def druhy_rok(self): + return self.prvni_rok + 1 + + def verejne_url(self): + return reverse('seminar_rocnik', kwargs={'rocnik': self.rocnik}) + + @classmethod + def cached_rocnik(cls, r_id): + name = 'rocnik_%s' % (r_id, ) + c = cache.get(name) + if c is None: + c = cls.objects.get(id=r_id) + cache.set(name, c, 300) + return c + + def save(self, *args, **kwargs): + super().save(*args, **kwargs) + # *Node.save() aktualizuje název *Nodu. + try: + self.rocniknode.save() + except ObjectDoesNotExist: + # Neexistující *Node nemá smysl aktualizovat. + pass + +def cislo_pdf_filename(self, filename): + rocnik = str(self.rocnik.rocnik) + return pathlib.Path('cislo', 'pdf', rocnik, '{}-{}.pdf'.format(rocnik, self.poradi)) + +def cislo_png_filename(self, filename): + rocnik = str(self.rocnik.rocnik) + return pathlib.Path('cislo', 'png', rocnik, '{}-{}.png'.format(rocnik, self.poradi)) + +@reversion.register(ignore_duplicates=True) +class Cislo(SeminarModelBase): + + class Meta: + db_table = 'seminar_cisla' + verbose_name = 'Číslo' + verbose_name_plural = 'Čísla' + ordering = ['-rocnik__rocnik', '-poradi'] + managed = False + + # Interní ID + id = models.AutoField(primary_key = True) + + rocnik = models.ForeignKey(Rocnik, verbose_name='ročník', related_name='cisla', + db_index=True,on_delete=models.PROTECT) + + poradi = models.CharField('název čísla', max_length=32, db_index=True, + help_text='Většinou jen "1", vyjímečně "7-8", lexikograficky určuje pořadí v ročníku!') + + datum_vydani = models.DateField('datum vydání', blank=True, null=True, + help_text='Datum vydání finální verze') + + verejne_db = models.BooleanField('číslo zveřejněno', + db_column='verejne', default=False) + + poznamka = models.TextField('neveřejná poznámka', blank=True, + help_text='Neveřejná poznámka k číslu (plain text)') + + pdf = models.FileField('pdf', upload_to=cislo_pdf_filename, null=True, blank=True, + help_text='PDF čísla, které si mohou řešitelé stáhnout', storage=OverwriteStorage()) + + titulka_nahled = models.ImageField('Obrázek titulní strany', upload_to=cislo_png_filename, null=True, blank=True, + help_text='Obrázek titulní strany, generuje se automaticky') + + def kod(self): + return '%s.%s' % (self.rocnik.rocnik, self.poradi) + kod.short_description = 'Kód čísla' + + def __str__(self): + # Potenciální DB HOG, pokud by se ročník necachoval + r = Rocnik.cached_rocnik(self.rocnik_id) + return '{}.{}'.format(r.rocnik, self.poradi) + + def verejne(self): + return self.verejne_db + verejne.boolean = True + + def verejne_url(self): + return reverse('seminar_cislo', kwargs={'rocnik': self.rocnik.rocnik, 'cislo': self.poradi}) + + def absolute_url(self): + return "https://" + str(get_current_site(None)) + self.verejne_url() + + def nasledujici(self): + "Vrací None, pokud je toto poslední" + return self.relativni_v_rocniku(1) + + def predchozi(self): + "Vrací None, pokud je toto první" + return self.relativni_v_rocniku(-1) + + def relativni_v_rocniku(self, rel_index): + "Číslo o `index` dále v ročníku. None pokud neexistuje." + cs = self.rocnik.cisla.order_by('poradi').all() + i = list(cs).index(self) + rel_index + if (i < 0) or (i >= len(cs)): + return None + return cs[i] + + def vygeneruj_nahled(self): + VYSKA = 594 + sirka = int(VYSKA*210/297) + if not self.pdf: + return + + + # Pokud obrázek neexistuje nebo není aktuální, vytvoř jej + if not self.titulka_nahled or os.path.getmtime(self.titulka_nahled.path) < os.path.getmtime(self.pdf.path): + png_filename = pathlib.Path(tempfile.mkdtemp(), 'nahled.png') + + subprocess.run([ + "gs", + "-sstdout=%stderr", + "-dSAFER", + "-dNOPAUSE", + "-dBATCH", + "-dNOPROMPT", + "-sDEVICE=png16m", + "-r300x300", + "-dFirstPage=1d", + "-dLastPage=1d", + "-sOutputFile=" + str(png_filename), + "-f%s" % self.pdf.path + ], + check=True, + capture_output=True + ) + + with open(png_filename,'rb') as f: + self.titulka_nahled.save('',f,True) + + png_filename.unlink() + png_filename.parent.rmdir() + + @classmethod + def get(cls, rocnik, cislo): + try: + r = Rocnik.objects.get(rocnik=rocnik) + c = r.cisla.get(poradi=cislo) + except ObjectDoesNotExist: + return None + return c + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.__original_verejne = self.verejne_db + + def posli_cislo_mailem(self): + # parametry e-mailu + odkaz = self.absolute_url() + + poslat_z_mailu = 'zadani@mam.mff.cuni.cz' + predmet = 'Vyšlo číslo {}'.format(self.kod()) + # TODO Možná nechceme všem psát „Ahoj“, např. příjemcům… + text_mailu = 'Ahoj,\n' \ + 'na adrese {} najdete nejnovější číslo.\n' \ + 'Vaše M&M\n'.format(odkaz) + + predmet_prvni = 'Právě vyšlo 1. číslo M&M, pomoz nám ho poslat dál!' + text_mailu_prvni = 'Milý řešiteli,\n'\ + 'právě jsme na našem webu zveřejnili první číslo {}. ročníku, najdeš ho na tomto odkazu: {}.\n\n'\ + 'Doufáme, že tě M&M baví, a byli bychom rádi, kdyby mohlo dělat radost i dalším středoškolákům. Máme na tebe proto jednu prosbu. Sdílej prosím odkaz alespoň s jedním svým kamarádem, který by mohl mít o řešení M&M zájem. Je to pro nás moc důležité a velmi nám tím pomůžeš. Díky!\n\n'\ + 'Organizátoři M&M\n'.format(self.rocnik.rocnik, odkaz) + + predmet_resitel = predmet_prvni if self.poradi == "1" else predmet + text_mailu_resitel = text_mailu_prvni if self.poradi == "1" else text_mailu + + # Prijemci e-mailu + resitele_vsichni = aktivniResitele(self).filter(zasilat_cislo_emailem=True) + + def posli(subject, text, resitele): + emaily = map(lambda resitel: resitel.osoba.email, resitele) + + email = EmailMessage( + subject=subject, + body=text, + from_email=poslat_z_mailu, + bcc=list(emaily) + #bcc = příjemci skryté kopie + ) + + email.send() + + paticka = "---\nK odběru těchto e-mailů jste se přihlásili na stránkách https://mam.matfyz.cz. Z odběru se lze odhlásit na https://mam.matfyz.cz/resitel/osobni-udaje/" + + posli(predmet_resitel, text_mailu_resitel + paticka, resitele_vsichni.filter(zasilat_cislo_papirove=False)) + posli(predmet_resitel, text_mailu_resitel + 'P. S. Brzy budeme též rozesílat papírovou verzi čísla. Připomínáme, že pokud papírovou verzi čísla nevyužijete, můžete v https://mam.mff.cuni.cz/resitel/osobni-udaje/ zaškrtnout, abychom vám ji neposílali. Čísla vždy můžete nalézt v našem archivu a dál vám budou chodit e-mailem. Děkujeme.\n' + paticka, + resitele_vsichni.filter(zasilat_cislo_papirove=True)) + + paticka_prijemce = "---\nPokud tyto e-maily nechcete nadále dostávat, prosíme, ozvěte se nám na mam@matfyz.cz." + posli(predmet, text_mailu + paticka_prijemce, Prijemce.objects.filter(zasilat_cislo_emailem=True)) + + def save(self, *args, **kwargs): + super().save(*args, **kwargs) + self.vygeneruj_nahled() + # Při zveřejnění pošle mail + if self.verejne_db and not self.__original_verejne: + self.posli_cislo_mailem() + # *Node.save() aktualizuje název *Nodu. + try: + self.cislonode.save() + except ObjectDoesNotExist: + # Neexistující *Node nemá smysl aktualizovat, ale je potřeba ho naopak vyrobit + logger.warning(f'Číslo {self} nemělo ČísloNode, vyrábím…') + from seminar.models.treenode import CisloNode + CisloNode.objects.create(cislo=self) + + def zlomovy_deadline_pro_papirove_cislo(self): + prvni_deadline = Deadline.objects.filter(Q(typ=Deadline.TYP_PRVNI) | Q(typ=Deadline.TYP_PRVNI_A_SOUS), cislo=self).first() + if prvni_deadline is None: + posledni_deadline = self.posledni_deadline + if posledni_deadline is None: + # TODO promyslet, co se má stát tady + return Deadline.objects.filter(Q(cislo__poradi__lt=self.poradi, cislo__rocnik=self.rocnik) | Q(cislo__rocnik__rocnik__lt=self.rocnik.rocnik)).order_by("deadline").last() + return posledni_deadline + return prvni_deadline + + @property + def posledni_deadline(self): + return self.deadline_v_cisle.all().order_by("deadline").last() + +class Deadline(SeminarModelBase): + class Meta: + db_table = 'seminar_deadliny' + verbose_name = 'Deadline' + verbose_name_plural = 'Deadliny' + ordering = ['deadline'] + managed = False + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.__original_verejna_vysledkovka = self.verejna_vysledkovka + + id = models.AutoField(primary_key=True) + + # V ročníku < 26 nastaveno na datetime.datetime.combine(datetime.date(1994 + cislo.rocnik.rocnik, 6, int(cislo.poradi[0])), datetime.time.min) + deadline = models.DateTimeField(blank=False, default=timezone.make_aware(datetime.datetime.combine(timezone.now(), datetime.time.max))) + + cislo = models.ForeignKey(Cislo, verbose_name='deadline v čísle', + related_name='deadline_v_cisle', blank=False, + on_delete=models.CASCADE) + + TYP_CISLA = 'cisla' + TYP_PRVNI_A_SOUS = 'prvniasous' + TYP_PRVNI = 'prvni' + TYP_SOUS = 'sous' + TYP_CHOICES = [ + (TYP_CISLA, 'Deadline celého čísla'), + (TYP_PRVNI, 'První deadline'), + (TYP_PRVNI_A_SOUS, 'Sousový a první deadline'), + (TYP_SOUS, 'Sousový deadline'), + ] + CHOICES_MAP = dict(TYP_CHOICES) + typ = models.CharField('typ deadlinu', max_length=32, + choices=TYP_CHOICES, blank=False) + + verejna_vysledkovka = models.BooleanField('veřejná výsledkovka', + db_column='verejna_vysledkovka', + default=False) + + def __str__(self): + return self.CHOICES_MAP[self.typ] + " " + str(self.cislo) + + def save(self, *args, **kwargs): + super().save(*args, **kwargs) + if self.verejna_vysledkovka and not self.__original_verejna_vysledkovka: + self.vygeneruj_vysledkovku() + if not self.verejna_vysledkovka and hasattr(self, "vysledkovka_v_deadlinu"): + self.vysledkovka_v_deadlinu.delete() + + def vygeneruj_vysledkovku(self): + from vysledkovky.utils import VysledkovkaCisla + if hasattr(self, "vysledkovka_v_deadlinu"): + self.vysledkovka_v_deadlinu.delete() + vysledkovka = VysledkovkaCisla(self.cislo, jen_verejne=True, do_deadlinu=self) + if len(vysledkovka.radky_vysledkovky) != 0: + ZmrazenaVysledkovka.objects.create( + deadline=self, + html=render_to_string( + "vysledkovky/vysledkovka_cisla.html", + context={"vysledkovka": vysledkovka, "oznaceni_vysledkovky": self.id} + ) + ) + + +class ZmrazenaVysledkovka(SeminarModelBase): + class Meta: + db_table = 'seminar_vysledkovky' + verbose_name = 'Zmražená výsledkovka' + verbose_name_plural = 'Zmražené výsledkovky' + managed = False + + deadline = models.OneToOneField( + Deadline, + on_delete=models.CASCADE, + primary_key=True, + related_name="vysledkovka_v_deadlinu" + ) + + html = models.TextField(null=False, blank=False) + +class Problemy_Opravovatele(SeminarModelBase): + """Jen vazebná tabulka pro opravovatele. + + Ona stejně existovala, při přesunu mezi aplikacemi jen potřebujeme zajistit nepřejmenování DB tabulky. + Proto taky nepotřebuje žádná specifika, ze :py:class:SeminarModelBase: dědí ze zvyku než že by to k něčemu kdy měo být. + """ + class Meta: + db_table = 'seminar_problemy_opravovatele' + managed = False + + id = models.AutoField(primary_key = True) + + problem = models.ForeignKey('Problem', on_delete=models.CASCADE) + organizator = models.ForeignKey(Organizator, on_delete=models.CASCADE) + +@reversion.register(ignore_duplicates=True) +# Pozor na následující řádek. *Nekrmit, asi kouše!* +class Problem(SeminarModelBase,PolymorphicModel): + + class Meta: + # Není abstraktní, protože se na něj jinak nedají dělat ForeignKeys. + # TODO: Udělat to polymorfní (pomocí django-polymorphic), abychom dostali + # po těch vazbách přímo tu úlohu/témátko vč. fieldů, které nejsou součástí + # modelu Problem? + + #abstract = True + db_table = 'seminar_problemy' + verbose_name = 'Problém' + verbose_name_plural = 'Problémy' + ordering = ['nazev'] + managed = False + + # Interní ID + id = models.AutoField(primary_key = True) + + # Název + nazev = models.CharField('název', max_length=256) # Zveřejnitelný na stránky + + # Problém má podproblémy + nadproblem = models.ForeignKey('self', verbose_name='nadřazený problém', + related_name='podproblem', null=True, blank=True, + on_delete=models.SET_NULL) + + STAV_NAVRH = 'navrh' + STAV_ZADANY = 'zadany' + STAV_VYRESENY = 'vyreseny' + STAV_SMAZANY = 'smazany' + STAV_CHOICES = [ + (STAV_NAVRH, 'Návrh'), + (STAV_ZADANY, 'Zadaný'), + (STAV_VYRESENY, 'Vyřešený'), + (STAV_SMAZANY, 'Smazaný'), + ] + stav = models.CharField('stav problému', max_length=32, choices=STAV_CHOICES, blank=False, default=STAV_NAVRH) + # Téma je taky Problém, takže má stavy, "zadané" témátko je aktuálně otevřené a dá se k němu něco poslat (řešení nebo článek) + + zamereni = TaggableManager(verbose_name='zaměření', + help_text='Zaměření M/F/I/O problému, příp. další tagy', blank=True) + + poznamka = models.TextField('org poznámky (HTML)', blank=True, + help_text='Neveřejný návrh úlohy, návrh řešení, text zadání, poznámky ...') + + autor = models.ForeignKey(Organizator, verbose_name='autor problému', + related_name='autor_problemu_%(class)s', null=True, blank=True, + on_delete=models.SET_NULL) + + garant = models.ForeignKey(Organizator, verbose_name='garant zadaného problému', + related_name='garant_problemu_%(class)s', null=True, blank=True, + on_delete=models.SET_NULL) + + opravovatele = models.ManyToManyField(Organizator, verbose_name='opravovatelé', + blank=True, related_name='opravovatele_%(class)s', through=Problemy_Opravovatele) + + kod = models.CharField('lokální kód', max_length=32, blank=True, default='', + help_text='Číslo/kód úlohy v čísle nebo kód tématu/článku/seriálu v ročníku') + + vytvoreno = models.DateTimeField('vytvořeno', default=timezone.now, blank=True, editable=False) + + def __str__(self): + return self.nazev + + # Implicitini implementace, jednotlivé dědící třídy si přepíšou + @cached_property + def kod_v_rocniku(self): + if self.stav == Problem.STAV_ZADANY or self.stav == Problem.STAV_VYRESENY: + if self.nadproblem: + return self.nadproblem.kod_v_rocniku+".{}".format(self.kod) + return str(self.kod) + logger.warning(f"K problému {self} byl vyžadován kód v ročníku, i když není zadaný ani vyřešený.") + return f'' + +# def verejne(self): +# # aktuálně podle stavu problému +# # FIXME pro některé problémy možná chceme override +# # FIXME vrací veřejnost čistě problému, nezávisle na čísle, ve kterém je. +# # Je to tak správně? Podle aktuální představy ano. +# stav_verejny = False +# if self.stav == 'zadany' or self.stav == 'vyreseny': +# stav_verejny = True +# print("stav_verejny: {}".format(stav_verejny)) +# +# cislo_verejne = False +# cislonode = self.cislo_node() +# if cislonode is None: +# # problém nemá vlastní node, veřejnost posuzujeme jen podle stavu +# print("empty node") +# return stav_verejny +# else: +# cislo_zadani = cislonode.cislo +# if (cislo_zadani and cislo_zadani.verejne()): +# print("cislo: {}".format(cislo_zadani)) +# cislo_verejne = True +# print("stav_verejny: {}".format(stav_verejny)) +# print("cislo_verejne: {}".format(cislo_verejne)) +# return (stav_verejny and cislo_verejne) +# verejne.boolean = True + + def verejne_url(self): + return reverse('seminar_problem', kwargs={'pk': self.id}) + + def admin_url(self): + return reverse('admin:seminar_problem_change', args=(self.id, )) + + @cached_property + def hlavni_problem(self): + """ Pro daný problém vrátí jeho nejvyšší nadproblém.""" + problem = self + while not (problem.nadproblem is None): + problem = problem.nadproblem + return problem + +# FIXME - k úloze + def body_v_zavorce(self): + """Vrať string s body v závorce jsou-li u problému vyplněné, jinak '' + + Je-li desetinná část nulová, nezobrazuj ji. + """ + pocet_bodu = None + if self.body: + b = self.body + pocet_bodu = int(b) if int(b) == b else b + return "({}\u2009b)".format(pocet_bodu) if self.body else "" + +class Tema(Problem): + class Meta: + db_table = 'seminar_temata' + verbose_name = 'Téma' + verbose_name_plural = 'Témata' + managed = False + + TEMA_TEMA = 'tema' + TEMA_SERIAL = 'serial' + TEMA_CHOICES = [ + (TEMA_TEMA, 'Téma'), + (TEMA_SERIAL, 'Seriál'), + ] + tema_typ = models.CharField('Typ tématu', max_length=16, choices=TEMA_CHOICES, + blank=False, default=TEMA_TEMA) + + rocnik = models.ForeignKey(Rocnik, verbose_name='ročník',related_name='temata',blank=True, null=True, + on_delete=models.PROTECT) + + abstrakt = models.TextField('Abstrakt na rozcestník', blank=True) + obrazek = models.ImageField('Obrázek na rozcestník', null=True, blank=True) + + @cached_property + def kod_v_rocniku(self): + if self.stav == Problem.STAV_ZADANY or self.stav == Problem.STAV_VYRESENY: + if self.nadproblem: + return self.nadproblem.kod_v_rocniku+".t{}".format(self.kod) + return 't'+self.kod + logger.warning(f"K problému {self} byl vyžadován kód v ročníku, i když není zadaný ani vyřešený.") + return f'' + + def save(self, *args, **kwargs): + super().save(*args, **kwargs) + # *Node.save() aktualizuje název *Nodu. + for tvcn in self.temavcislenode_set.all(): + tvcn.save() + + def cislo_node(self): + tema_node_set = self.temavcislenode_set.all() + tema_cisla_vyskyt = [] + from seminar.models.treenode import CisloNode + for tn in tema_node_set: + tema_cisla_vyskyt.append( + treelib.get_upper_node_of_type(tn, CisloNode).cislo) + tema_cisla_vyskyt.sort(key=lambda x:x.datum_vydani) + prvni_zadani = tema_cisla_vyskyt[0] + return prvni_zadani.cislonode + +class Clanek(Problem): + class Meta: + db_table = 'seminar_clanky' + verbose_name = 'Článek' + verbose_name_plural = 'Články' + managed = False + + cislo = models.ForeignKey(Cislo, blank=True, null=True, on_delete=models.PROTECT, + verbose_name='číslo vydání', related_name='vydane_clanky') + + strana = models.PositiveIntegerField(verbose_name="první strana", blank=True, null=True) + + @cached_property + def kod_v_rocniku(self): + if self.stav == Problem.STAV_ZADANY or self.stav == Problem.STAV_VYRESENY: +# Nemělo by být potřeba +# if self.nadproblem: +# return self.nadproblem.kod_v_rocniku+".c{}".format(self.kod) + return "c" + self.kod + logger.warning(f"K problému {self} byl vyžadován kód v ročníku, i když není zadaný ani vyřešený.") + return f'' + + def node(self): + return None + + +class Uloha(Problem): + class Meta: + db_table = 'seminar_ulohy' + verbose_name = 'Úloha' + verbose_name_plural = 'Úlohy' + managed = False + + cislo_zadani = models.ForeignKey(Cislo, verbose_name='číslo zadání', blank=True, + null=True, related_name='zadane_ulohy', on_delete=models.PROTECT) + + cislo_deadline = models.ForeignKey(Cislo, verbose_name='číslo deadlinu', blank=True, + null=True, related_name='deadlinove_ulohy', on_delete=models.PROTECT) + + cislo_reseni = models.ForeignKey(Cislo, verbose_name='číslo řešení', blank=True, + null=True, related_name='resene_ulohy', + help_text='Číslo s řešením úlohy, jen pro úlohy', + on_delete=models.PROTECT) + + max_body = models.DecimalField(max_digits=8, decimal_places=1, verbose_name='maximum bodů', + blank=True, null=True) + + @cached_property + def kod_v_rocniku(self): + if self.stav == Problem.STAV_ZADANY or self.stav == Problem.STAV_VYRESENY: + return f"{self.cislo_zadani.poradi}.{self.kod}" + logger.warning(f"K problému {self} byl vyžadován kód v ročníku, i když není zadaný ani vyřešený.") + return f'' + + def save(self, *args, **kwargs): + super().save(*args, **kwargs) + # *Node.save() aktualizuje název *Nodu. + try: + self.ulohazadaninode.save() + except ObjectDoesNotExist: + # Neexistující *Node nemá smysl aktualizovat. + pass + try: + self.ulohavzoraknode.save() + except ObjectDoesNotExist: + # Neexistující *Node nemá smysl aktualizovat. + pass + + def cislo_node(self): + zadani_node = self.ulohazadaninode + from seminar.models.treenode import CisloNode + return treelib.get_upper_node_of_type(zadani_node, CisloNode) + + +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) + + +class Pohadka(SeminarModelBase): + """Kus pohádky před/za úlohou v čísle""" + + class Meta: + db_table = 'seminar_pohadky' + verbose_name = 'Pohádka' + verbose_name_plural = 'Pohádky' + ordering = ['vytvoreno'] + managed = False + + # Interní ID + id = models.AutoField(primary_key=True) + + autor = models.ForeignKey( + Organizator, + verbose_name="Autor pohádky", + + # Při nahrávání z TeXu není vyplnění vyžadováno, v adminu je + null=True, + blank=False, + on_delete=models.SET_NULL, + ) + + vytvoreno = models.DateTimeField( + 'Vytvořeno', + default=timezone.now, + blank=True, + editable=False + ) + + def __str__(self): + uryvek = self.text if len(self.text) < 50 else self.text[:(50-3)]+"..." + return uryvek + + def save(self, *args, **kwargs): + super().save(*args, **kwargs) + # *Node.save() aktualizuje název *Nodu. + try: + self.pohadkanode.save() + except ObjectDoesNotExist: + # Neexistující *Node nemá smysl aktualizovat. + pass + diff --git a/various/migrations/0004_tvorba_pre.py b/various/migrations/0004_tvorba_pre.py new file mode 100644 index 00000000..20c1a3a0 --- /dev/null +++ b/various/migrations/0004_tvorba_pre.py @@ -0,0 +1,13 @@ +# Generated by Django 4.2.16 on 2024-10-30 01:06 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('various', '0003_fix_permissions'), + ] + + operations = [ + ] diff --git a/various/migrations/0005_tvorba_relink.py b/various/migrations/0005_tvorba_relink.py new file mode 100644 index 00000000..5ec264e8 --- /dev/null +++ b/various/migrations/0005_tvorba_relink.py @@ -0,0 +1,20 @@ +# Generated by Django 4.2.16 on 2024-10-30 13:18 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('tvorba', '0001_tvorba_create'), + ('various', '0004_tvorba_pre'), + ] + + operations = [ + migrations.AlterField( + model_name='nastaveni', + name='aktualni_cislo', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='tvorba.cislo', verbose_name='Aktuální číslo'), + ), + ] diff --git a/various/models.py b/various/models.py index 17632c46..e31b4e72 100644 --- a/various/models.py +++ b/various/models.py @@ -3,7 +3,7 @@ from django.db import models from reversion import revisions as reversion from solo.models import SingletonModel -from seminar.models import Cislo +from tvorba.models import Cislo from django.urls import reverse -- 2.39.5 From 062f70e9471a9c2693452f6b65e975c16aa36fde Mon Sep 17 00:00:00 2001 From: Pavel 'LEdoian' Turinsky Date: Wed, 30 Oct 2024 22:41:11 +0100 Subject: [PATCH 2/4] =?UTF-8?q?odst=C5=99el=20tvorby:=20relink=20=E2=80=93?= =?UTF-8?q?=20post?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy_v2/admin_org_prava.json | 62 +- odevzdavatko/migrations/0006_tvorba_post.py | 14 + personalni/migrations/0015_tvorba_post.py | 14 + seminar/migrations/0138_tvorba_delete.py | 150 ++++ seminar/migrations/0139_tvorba_post.py | 14 + seminar/models/__init__.py | 6 + seminar/models/treenode.py | 2 +- seminar/models/tvorba.py | 731 ------------------ soustredeni/migrations/0005_tvorba_relink.py | 13 +- soustredeni/migrations/0006_tvorba_relink2.py | 17 + soustredeni/migrations/0007_tvorba_relink3.py | 15 + soustredeni/migrations/0008_tvorba_relink4.py | 34 + soustredeni/migrations/0009_tvorba_relink5.py | 17 + soustredeni/migrations/0010_tvorba_post.py | 14 + tvorba/migrations/0002_tvorba_manage.py | 54 ++ tvorba/migrations/0003_tvorba_post.py | 13 + tvorba/models.py | 13 +- various/migrations/0006_tvorba_post.py | 14 + 18 files changed, 418 insertions(+), 779 deletions(-) create mode 100644 odevzdavatko/migrations/0006_tvorba_post.py create mode 100644 personalni/migrations/0015_tvorba_post.py create mode 100644 seminar/migrations/0138_tvorba_delete.py create mode 100644 seminar/migrations/0139_tvorba_post.py create mode 100644 soustredeni/migrations/0006_tvorba_relink2.py create mode 100644 soustredeni/migrations/0007_tvorba_relink3.py create mode 100644 soustredeni/migrations/0008_tvorba_relink4.py create mode 100644 soustredeni/migrations/0009_tvorba_relink5.py create mode 100644 soustredeni/migrations/0010_tvorba_post.py create mode 100644 tvorba/migrations/0002_tvorba_manage.py create mode 100644 tvorba/migrations/0003_tvorba_post.py create mode 100644 various/migrations/0006_tvorba_post.py diff --git a/deploy_v2/admin_org_prava.json b/deploy_v2/admin_org_prava.json index 370aec4f..c7fc8c7d 100644 --- a/deploy_v2/admin_org_prava.json +++ b/deploy_v2/admin_org_prava.json @@ -216,57 +216,57 @@ }, { "codename": "add_cislo", - "ct_app_label": "seminar", + "ct_app_label": "tvorba", "ct_model": "cislo" }, { "codename": "change_cislo", - "ct_app_label": "seminar", + "ct_app_label": "tvorba", "ct_model": "cislo" }, { "codename": "delete_cislo", - "ct_app_label": "seminar", + "ct_app_label": "tvorba", "ct_model": "cislo" }, { "codename": "view_cislo", - "ct_app_label": "seminar", + "ct_app_label": "tvorba", "ct_model": "cislo" }, { "codename": "add_clanek", - "ct_app_label": "seminar", + "ct_app_label": "tvorba", "ct_model": "clanek" }, { "codename": "change_clanek", - "ct_app_label": "seminar", + "ct_app_label": "tvorba", "ct_model": "clanek" }, { "codename": "delete_clanek", - "ct_app_label": "seminar", + "ct_app_label": "tvorba", "ct_model": "clanek" }, { "codename": "view_clanek", - "ct_app_label": "seminar", + "ct_app_label": "tvorba", "ct_model": "clanek" }, { "codename": "add_deadline", - "ct_app_label": "seminar", + "ct_app_label": "tvorba", "ct_model": "deadline" }, { "codename": "change_deadline", - "ct_app_label": "seminar", + "ct_app_label": "tvorba", "ct_model": "deadline" }, { "codename": "view_deadline", - "ct_app_label": "seminar", + "ct_app_label": "tvorba", "ct_model": "deadline" }, { @@ -371,22 +371,22 @@ }, { "codename": "add_pohadka", - "ct_app_label": "seminar", + "ct_app_label": "tvorba", "ct_model": "pohadka" }, { "codename": "change_pohadka", - "ct_app_label": "seminar", + "ct_app_label": "tvorba", "ct_model": "pohadka" }, { "codename": "delete_pohadka", - "ct_app_label": "seminar", + "ct_app_label": "tvorba", "ct_model": "pohadka" }, { "codename": "view_pohadka", - "ct_app_label": "seminar", + "ct_app_label": "tvorba", "ct_model": "pohadka" }, { @@ -411,22 +411,22 @@ }, { "codename": "add_problem", - "ct_app_label": "seminar", + "ct_app_label": "tvorba", "ct_model": "problem" }, { "codename": "change_problem", - "ct_app_label": "seminar", + "ct_app_label": "tvorba", "ct_model": "problem" }, { "codename": "delete_problem", - "ct_app_label": "seminar", + "ct_app_label": "tvorba", "ct_model": "problem" }, { "codename": "view_problem", - "ct_app_label": "seminar", + "ct_app_label": "tvorba", "ct_model": "problem" }, { @@ -441,22 +441,22 @@ }, { "codename": "add_rocnik", - "ct_app_label": "seminar", + "ct_app_label": "tvorba", "ct_model": "rocnik" }, { "codename": "change_rocnik", - "ct_app_label": "seminar", + "ct_app_label": "tvorba", "ct_model": "rocnik" }, { "codename": "delete_rocnik", - "ct_app_label": "seminar", + "ct_app_label": "tvorba", "ct_model": "rocnik" }, { "codename": "view_rocnik", - "ct_app_label": "seminar", + "ct_app_label": "tvorba", "ct_model": "rocnik" }, { @@ -541,42 +541,42 @@ }, { "codename": "add_tema", - "ct_app_label": "seminar", + "ct_app_label": "tvorba", "ct_model": "tema" }, { "codename": "change_tema", - "ct_app_label": "seminar", + "ct_app_label": "tvorba", "ct_model": "tema" }, { "codename": "delete_tema", - "ct_app_label": "seminar", + "ct_app_label": "tvorba", "ct_model": "tema" }, { "codename": "view_tema", - "ct_app_label": "seminar", + "ct_app_label": "tvorba", "ct_model": "tema" }, { "codename": "add_uloha", - "ct_app_label": "seminar", + "ct_app_label": "tvorba", "ct_model": "uloha" }, { "codename": "change_uloha", - "ct_app_label": "seminar", + "ct_app_label": "tvorba", "ct_model": "uloha" }, { "codename": "delete_uloha", - "ct_app_label": "seminar", + "ct_app_label": "tvorba", "ct_model": "uloha" }, { "codename": "view_uloha", - "ct_app_label": "seminar", + "ct_app_label": "tvorba", "ct_model": "uloha" }, { diff --git a/odevzdavatko/migrations/0006_tvorba_post.py b/odevzdavatko/migrations/0006_tvorba_post.py new file mode 100644 index 00000000..13ab895c --- /dev/null +++ b/odevzdavatko/migrations/0006_tvorba_post.py @@ -0,0 +1,14 @@ +# Generated by Django 4.2.16 on 2024-10-30 21:34 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('odevzdavatko', '0005_tvorba_relink'), + ('tvorba', '0003_tvorba_post'), + ] + + operations = [ + ] diff --git a/personalni/migrations/0015_tvorba_post.py b/personalni/migrations/0015_tvorba_post.py new file mode 100644 index 00000000..6580b1e8 --- /dev/null +++ b/personalni/migrations/0015_tvorba_post.py @@ -0,0 +1,14 @@ +# Generated by Django 4.2.16 on 2024-10-30 21:35 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('personalni', '0014_tvorba_pre'), + ('tvorba', '0003_tvorba_post'), + ] + + operations = [ + ] diff --git a/seminar/migrations/0138_tvorba_delete.py b/seminar/migrations/0138_tvorba_delete.py new file mode 100644 index 00000000..0448a160 --- /dev/null +++ b/seminar/migrations/0138_tvorba_delete.py @@ -0,0 +1,150 @@ +# Generated by Django 4.2.16 on 2024-10-30 14:03 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('tvorba', '0001_tvorba_create'), + ('seminar', '0137_tvorba_unmanage'), + ('odevzdavatko', '0005_tvorba_relink'), + ('soustredeni', '0009_tvorba_relink5'), + ('various', '0005_tvorba_relink'), + ] + + operations = [ + migrations.RemoveField( + model_name='cislo', + name='rocnik', + ), + migrations.RemoveField( + model_name='clanek', + name='cislo', + ), + migrations.RemoveField( + model_name='clanek', + name='problem_ptr', + ), + migrations.RemoveField( + model_name='deadline', + name='cislo', + ), + migrations.RemoveField( + model_name='pohadka', + name='autor', + ), + migrations.RemoveField( + model_name='problem', + name='autor', + ), + migrations.RemoveField( + model_name='problem', + name='garant', + ), + migrations.RemoveField( + model_name='problem', + name='nadproblem', + ), + migrations.RemoveField( + model_name='problem', + name='opravovatele', + ), + migrations.RemoveField( + model_name='problem', + name='polymorphic_ctype', + ), + migrations.RemoveField( + model_name='problem', + name='zamereni', + ), + migrations.DeleteModel( + name='Problemy_Opravovatele', + ), + migrations.RemoveField( + model_name='tema', + name='problem_ptr', + ), + migrations.RemoveField( + model_name='tema', + name='rocnik', + ), + migrations.RemoveField( + model_name='uloha', + name='cislo_deadline', + ), + migrations.RemoveField( + model_name='uloha', + name='cislo_reseni', + ), + migrations.RemoveField( + model_name='uloha', + name='cislo_zadani', + ), + migrations.RemoveField( + model_name='uloha', + name='problem_ptr', + ), + migrations.RemoveField( + model_name='zmrazenavysledkovka', + name='deadline', + ), + migrations.AlterField( + model_name='cislonode', + name='cislo', + field=models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, to='tvorba.cislo', verbose_name='číslo'), + ), + migrations.AlterField( + model_name='pohadkanode', + name='pohadka', + field=models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, to='tvorba.pohadka', verbose_name='pohádka'), + ), + migrations.AlterField( + model_name='rocniknode', + name='rocnik', + field=models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, to='tvorba.rocnik', verbose_name='ročník'), + ), + migrations.AlterField( + model_name='temavcislenode', + name='tema', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='tvorba.tema', verbose_name='téma v čísle'), + ), + migrations.AlterField( + model_name='ulohavzoraknode', + name='uloha', + field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.PROTECT, to='tvorba.uloha', verbose_name='úloha'), + ), + migrations.AlterField( + model_name='ulohazadaninode', + name='uloha', + field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.PROTECT, to='tvorba.uloha', verbose_name='úloha'), + ), + migrations.DeleteModel( + name='Cislo', + ), + migrations.DeleteModel( + name='Clanek', + ), + migrations.DeleteModel( + name='Deadline', + ), + migrations.DeleteModel( + name='Pohadka', + ), + migrations.DeleteModel( + name='Problem', + ), + migrations.DeleteModel( + name='Rocnik', + ), + migrations.DeleteModel( + name='Tema', + ), + migrations.DeleteModel( + name='Uloha', + ), + migrations.DeleteModel( + name='ZmrazenaVysledkovka', + ), + ] diff --git a/seminar/migrations/0139_tvorba_post.py b/seminar/migrations/0139_tvorba_post.py new file mode 100644 index 00000000..560ddde4 --- /dev/null +++ b/seminar/migrations/0139_tvorba_post.py @@ -0,0 +1,14 @@ +# Generated by Django 4.2.16 on 2024-10-30 21:35 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('seminar', '0138_tvorba_delete'), + ('tvorba', '0003_tvorba_post'), + ] + + operations = [ + ] diff --git a/seminar/models/__init__.py b/seminar/models/__init__.py index 2eabe50f..95e449ab 100644 --- a/seminar/models/__init__.py +++ b/seminar/models/__init__.py @@ -15,3 +15,9 @@ from tvorba.models import ZmrazenaVysledkovka, Deadline, Cislo, Rocnik, Pohadka, from soustredeni.models import generate_filename_konfera # migr. 0001 from odevzdavatko.models import generate_filename +# migr. 0031, 0032, 0081 +from tvorba.models import cislo_pdf_filename +# migr. 0082 +from tvorba.models import cislo_png_filename +# migr 0100 (hack) +import tvorba.models as tvorba diff --git a/seminar/models/treenode.py b/seminar/models/treenode.py index abc20eab..eee40281 100644 --- a/seminar/models/treenode.py +++ b/seminar/models/treenode.py @@ -14,7 +14,7 @@ from .pomocne import Text logger = logging.getLogger(__name__) -from seminar.models import tvorba as am +import tvorba.models as am class TreeNode(PolymorphicModel): class Meta: diff --git a/seminar/models/tvorba.py b/seminar/models/tvorba.py index a8aea6ae..c1f91196 100644 --- a/seminar/models/tvorba.py +++ b/seminar/models/tvorba.py @@ -1,40 +1,7 @@ -import datetime import os -import subprocess -import pathlib -import tempfile import logging -from django.contrib.sites.shortcuts import get_current_site -from django.db import models -from django.db.models import Q -from django.template.loader import render_to_string -from django.utils import timezone -from django.conf import settings -from django.urls import reverse -from django.core.cache import cache -from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.core.files.storage import FileSystemStorage -from django.utils.text import get_valid_filename -from django.utils.functional import cached_property - -from solo.models import SingletonModel -from taggit.managers import TaggableManager - -from reversion import revisions as reversion - -from tvorba.utils import roman, aktivniResitele -from treenode import treelib - -from unidecode import unidecode # Používám pro získání ID odkazu (ještě je to někde po někom zakomentované) - -from polymorphic.models import PolymorphicModel - -from django.core.mail import EmailMessage - -from personalni.models import Prijemce, Organizator - -from .base import SeminarModelBase logger = logging.getLogger(__name__) @@ -45,701 +12,3 @@ class OverwriteStorage(FileSystemStorage): if self.exists(name): os.remove(os.path.join(self.location,name)) return super().get_available_name(name,max_length) - -@reversion.register(ignore_duplicates=True) -class Rocnik(SeminarModelBase): - - class Meta: - db_table = 'seminar_rocniky' - verbose_name = 'Ročník' - verbose_name_plural = 'Ročníky' - ordering = ['-rocnik'] - managed = False - - # Interní ID - id = models.AutoField(primary_key = True) - - prvni_rok = models.IntegerField('první rok', db_index=True, unique=True) - - rocnik = models.IntegerField('číslo ročníku', db_index=True, unique=True) - - exportovat = models.BooleanField('export do AESOPa', db_column='exportovat', default=False, - help_text='Exportuje se jen podle tohoto flagu (ne veřejnosti),' - ' a to jen čísla s veřejnou výsledkovkou') - - # má OneToOneField s: - # RocnikNode - - def __str__(self): - return '{} ({}/{})'.format(self.rocnik, self.prvni_rok, self.prvni_rok+1) - - # Ročník v římských číslech - def roman(self): - return roman(int(self.rocnik)) - - def verejne(self): - return len(self.verejna_cisla()) > 0 - verejne.boolean = True - verejne.short_description = 'Veřejný (jen dle čísel)' - - def neverejna_cisla(self): - vc = [c for c in self.cisla.all() if not c.verejne()] - vc.sort(key=lambda c: c.poradi) - return vc - - def verejna_cisla(self): - vc = [c for c in self.cisla.all() if c.verejne()] - vc.sort(key=lambda c: c.poradi) - return vc - - def posledni_verejne_cislo(self): - vc = self.verejna_cisla() - return vc[-1] if vc else None - - def verejne_vysledkovky_cisla(self): - vc = list(self.cisla.filter(deadline_v_cisle__verejna_vysledkovka=True).distinct()) - vc.sort(key=lambda c: c.poradi) - return vc - - def posledni_zverejnena_vysledkovka_cislo(self): - vc = self.verejne_vysledkovky_cisla() - return vc[-1] if vc else None - - def druhy_rok(self): - return self.prvni_rok + 1 - - def verejne_url(self): - return reverse('seminar_rocnik', kwargs={'rocnik': self.rocnik}) - - @classmethod - def cached_rocnik(cls, r_id): - name = 'rocnik_%s' % (r_id, ) - c = cache.get(name) - if c is None: - c = cls.objects.get(id=r_id) - cache.set(name, c, 300) - return c - - def save(self, *args, **kwargs): - super().save(*args, **kwargs) - # *Node.save() aktualizuje název *Nodu. - try: - self.rocniknode.save() - except ObjectDoesNotExist: - # Neexistující *Node nemá smysl aktualizovat. - pass - -def cislo_pdf_filename(self, filename): - rocnik = str(self.rocnik.rocnik) - return pathlib.Path('cislo', 'pdf', rocnik, '{}-{}.pdf'.format(rocnik, self.poradi)) - -def cislo_png_filename(self, filename): - rocnik = str(self.rocnik.rocnik) - return pathlib.Path('cislo', 'png', rocnik, '{}-{}.png'.format(rocnik, self.poradi)) - -@reversion.register(ignore_duplicates=True) -class Cislo(SeminarModelBase): - - class Meta: - db_table = 'seminar_cisla' - verbose_name = 'Číslo' - verbose_name_plural = 'Čísla' - ordering = ['-rocnik__rocnik', '-poradi'] - managed = False - - # Interní ID - id = models.AutoField(primary_key = True) - - rocnik = models.ForeignKey(Rocnik, verbose_name='ročník', related_name='cisla_old', - db_index=True,on_delete=models.PROTECT) - - poradi = models.CharField('název čísla', max_length=32, db_index=True, - help_text='Většinou jen "1", vyjímečně "7-8", lexikograficky určuje pořadí v ročníku!') - - datum_vydani = models.DateField('datum vydání', blank=True, null=True, - help_text='Datum vydání finální verze') - - verejne_db = models.BooleanField('číslo zveřejněno', - db_column='verejne', default=False) - - poznamka = models.TextField('neveřejná poznámka', blank=True, - help_text='Neveřejná poznámka k číslu (plain text)') - - pdf = models.FileField('pdf', upload_to=cislo_pdf_filename, null=True, blank=True, - help_text='PDF čísla, které si mohou řešitelé stáhnout', storage=OverwriteStorage()) - - titulka_nahled = models.ImageField('Obrázek titulní strany', upload_to=cislo_png_filename, null=True, blank=True, - help_text='Obrázek titulní strany, generuje se automaticky') - - # má OneToOneField s: - # CisloNode - - def kod(self): - return '%s.%s' % (self.rocnik.rocnik, self.poradi) - kod.short_description = 'Kód čísla' - - def __str__(self): - # Potenciální DB HOG, pokud by se ročník necachoval - r = Rocnik.cached_rocnik(self.rocnik_id) - return '{}.{}'.format(r.rocnik, self.poradi) - - def verejne(self): - return self.verejne_db - verejne.boolean = True - - def verejne_url(self): - return reverse('seminar_cislo', kwargs={'rocnik': self.rocnik.rocnik, 'cislo': self.poradi}) - - def absolute_url(self): - return "https://" + str(get_current_site(None)) + self.verejne_url() - - def nasledujici(self): - "Vrací None, pokud je toto poslední" - return self.relativni_v_rocniku(1) - - def predchozi(self): - "Vrací None, pokud je toto první" - return self.relativni_v_rocniku(-1) - - def relativni_v_rocniku(self, rel_index): - "Číslo o `index` dále v ročníku. None pokud neexistuje." - cs = self.rocnik.cisla.order_by('poradi').all() - i = list(cs).index(self) + rel_index - if (i < 0) or (i >= len(cs)): - return None - return cs[i] - - def vygeneruj_nahled(self): - VYSKA = 594 - sirka = int(VYSKA*210/297) - if not self.pdf: - return - - - # Pokud obrázek neexistuje nebo není aktuální, vytvoř jej - if not self.titulka_nahled or os.path.getmtime(self.titulka_nahled.path) < os.path.getmtime(self.pdf.path): - png_filename = pathlib.Path(tempfile.mkdtemp(), 'nahled.png') - - subprocess.run([ - "gs", - "-sstdout=%stderr", - "-dSAFER", - "-dNOPAUSE", - "-dBATCH", - "-dNOPROMPT", - "-sDEVICE=png16m", - "-r300x300", - "-dFirstPage=1d", - "-dLastPage=1d", - "-sOutputFile=" + str(png_filename), - "-f%s" % self.pdf.path - ], - check=True, - capture_output=True - ) - - with open(png_filename,'rb') as f: - self.titulka_nahled.save('',f,True) - - png_filename.unlink() - png_filename.parent.rmdir() - - - - @classmethod - def get(cls, rocnik, cislo): - try: - r = Rocnik.objects.get(rocnik=rocnik) - c = r.cisla.get(poradi=cislo) - except ObjectDoesNotExist: - return None - return c - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.__original_verejne = self.verejne_db - - def posli_cislo_mailem(self): - # parametry e-mailu - odkaz = self.absolute_url() - - poslat_z_mailu = 'zadani@mam.mff.cuni.cz' - predmet = 'Vyšlo číslo {}'.format(self.kod()) - # TODO Možná nechceme všem psát „Ahoj“, např. příjemcům… - text_mailu = 'Ahoj,\n' \ - 'na adrese {} najdete nejnovější číslo.\n' \ - 'Vaše M&M\n'.format(odkaz) - - predmet_prvni = 'Právě vyšlo 1. číslo M&M, pomoz nám ho poslat dál!' - text_mailu_prvni = 'Milý řešiteli,\n'\ - 'právě jsme na našem webu zveřejnili první číslo {}. ročníku, najdeš ho na tomto odkazu: {}.\n\n'\ - 'Doufáme, že tě M&M baví, a byli bychom rádi, kdyby mohlo dělat radost i dalším středoškolákům. Máme na tebe proto jednu prosbu. Sdílej prosím odkaz alespoň s jedním svým kamarádem, který by mohl mít o řešení M&M zájem. Je to pro nás moc důležité a velmi nám tím pomůžeš. Díky!\n\n'\ - 'Organizátoři M&M\n'.format(self.rocnik.rocnik, odkaz) - - predmet_resitel = predmet_prvni if self.poradi == "1" else predmet - text_mailu_resitel = text_mailu_prvni if self.poradi == "1" else text_mailu - - - # Prijemci e-mailu - resitele_vsichni = aktivniResitele(self).filter(zasilat_cislo_emailem=True) - - def posli(subject, text, resitele): - emaily = map(lambda resitel: resitel.osoba.email, resitele) - - email = EmailMessage( - subject=subject, - body=text, - from_email=poslat_z_mailu, - bcc=list(emaily) - #bcc = příjemci skryté kopie - ) - - email.send() - - paticka = "---\nK odběru těchto e-mailů jste se přihlásili na stránkách https://mam.matfyz.cz. Z odběru se lze odhlásit na https://mam.matfyz.cz/resitel/osobni-udaje/" - - posli(predmet_resitel, text_mailu_resitel + paticka, resitele_vsichni.filter(zasilat_cislo_papirove=False)) - posli(predmet_resitel, text_mailu_resitel + 'P. S. Brzy budeme též rozesílat papírovou verzi čísla. Připomínáme, že pokud papírovou verzi čísla nevyužijete, můžete v https://mam.mff.cuni.cz/resitel/osobni-udaje/ zaškrtnout, abychom vám ji neposílali. Čísla vždy můžete nalézt v našem archivu a dál vám budou chodit e-mailem. Děkujeme.\n' + paticka, - resitele_vsichni.filter(zasilat_cislo_papirove=True)) - - paticka_prijemce = "---\nPokud tyto e-maily nechcete nadále dostávat, prosíme, ozvěte se nám na mam@matfyz.cz." - posli(predmet, text_mailu + paticka_prijemce, Prijemce.objects.filter(zasilat_cislo_emailem=True)) - - def save(self, *args, **kwargs): - super().save(*args, **kwargs) - self.vygeneruj_nahled() - # Při zveřejnění pošle mail - if self.verejne_db and not self.__original_verejne: - self.posli_cislo_mailem() - # *Node.save() aktualizuje název *Nodu. - try: - self.cislonode.save() - except ObjectDoesNotExist: - # Neexistující *Node nemá smysl aktualizovat, ale je potřeba ho naopak vyrobit - logger.warning(f'Číslo {self} nemělo ČísloNode, vyrábím…') - from seminar.models.treenode import CisloNode - CisloNode.objects.create(cislo=self) - - def zlomovy_deadline_pro_papirove_cislo(self): - prvni_deadline = Deadline.objects.filter(Q(typ=Deadline.TYP_PRVNI) | Q(typ=Deadline.TYP_PRVNI_A_SOUS), cislo=self).first() - if prvni_deadline is None: - posledni_deadline = self.posledni_deadline - if posledni_deadline is None: - # TODO promyslet, co se má stát tady - return Deadline.objects.filter(Q(cislo__poradi__lt=self.poradi, cislo__rocnik=self.rocnik) | Q(cislo__rocnik__rocnik__lt=self.rocnik.rocnik)).order_by("deadline").last() - return posledni_deadline - return prvni_deadline - - @property - def posledni_deadline(self): - return self.deadline_v_cisle.all().order_by("deadline").last() - -class Deadline(SeminarModelBase): - class Meta: - db_table = 'seminar_deadliny' - verbose_name = 'Deadline' - verbose_name_plural = 'Deadliny' - ordering = ['deadline'] - managed = False - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.__original_verejna_vysledkovka = self.verejna_vysledkovka - - id = models.AutoField(primary_key=True) - - # V ročníku < 26 nastaveno na datetime.datetime.combine(datetime.date(1994 + cislo.rocnik.rocnik, 6, int(cislo.poradi[0])), datetime.time.min) - deadline = models.DateTimeField(blank=False, default=timezone.make_aware(datetime.datetime.combine(timezone.now(), datetime.time.max))) - - cislo = models.ForeignKey(Cislo, verbose_name='deadline v čísle', - related_name='deadline_v_cisle_old', blank=False, - on_delete=models.CASCADE) - - TYP_CISLA = 'cisla' - TYP_PRVNI_A_SOUS = 'prvniasous' - TYP_PRVNI = 'prvni' - TYP_SOUS = 'sous' - TYP_CHOICES = [ - (TYP_CISLA, 'Deadline celého čísla'), - (TYP_PRVNI, 'První deadline'), - (TYP_PRVNI_A_SOUS, 'Sousový a první deadline'), - (TYP_SOUS, 'Sousový deadline'), - ] - CHOICES_MAP = dict(TYP_CHOICES) - typ = models.CharField('typ deadlinu', max_length=32, - choices=TYP_CHOICES, blank=False) - - verejna_vysledkovka = models.BooleanField('veřejná výsledkovka', - db_column='verejna_vysledkovka', - default=False) - - def __str__(self): - return self.CHOICES_MAP[self.typ] + " " + str(self.cislo) - - def save(self, *args, **kwargs): - super().save(*args, **kwargs) - if self.verejna_vysledkovka and not self.__original_verejna_vysledkovka: - self.vygeneruj_vysledkovku() - if not self.verejna_vysledkovka and hasattr(self, "vysledkovka_v_deadlinu"): - self.vysledkovka_v_deadlinu.delete() - - def vygeneruj_vysledkovku(self): - from vysledkovky.utils import VysledkovkaCisla - if hasattr(self, "vysledkovka_v_deadlinu"): - self.vysledkovka_v_deadlinu.delete() - vysledkovka = VysledkovkaCisla(self.cislo, jen_verejne=True, do_deadlinu=self) - if len(vysledkovka.radky_vysledkovky) != 0: - ZmrazenaVysledkovka.objects.create( - deadline=self, - html=render_to_string( - "vysledkovky/vysledkovka_cisla.html", - context={"vysledkovka": vysledkovka, "oznaceni_vysledkovky": self.id} - ) - ) - - -class ZmrazenaVysledkovka(SeminarModelBase): - class Meta: - db_table = 'seminar_vysledkovky' - verbose_name = 'Zmražená výsledkovka' - verbose_name_plural = 'Zmražené výsledkovky' - managed = False - - deadline = models.OneToOneField( - Deadline, - on_delete=models.CASCADE, - primary_key=True, - related_name="vysledkovka_v_deadlinu_old" - ) - - html = models.TextField(null=False, blank=False) - -class Problemy_Opravovatele(SeminarModelBase): - """Jen vazebná tabulka pro opravovatele. - - Ona stejně existovala, při přesunu mezi aplikacemi jen potřebujeme zajistit nepřejmenování DB tabulky. - Proto taky nepotřebuje žádná specifika, ze :py:class:SeminarModelBase: dědí ze zvyku než že by to k něčemu kdy měo být. - """ - class Meta: - db_table = 'seminar_problemy_opravovatele' - managed = False - - id = models.AutoField(primary_key = True) - - problem = models.ForeignKey('Problem', on_delete=models.CASCADE, related_name='awawa1_old') - organizator = models.ForeignKey(Organizator, on_delete=models.CASCADE, related_name='awawa2_old') - -@reversion.register(ignore_duplicates=True) -# Pozor na následující řádek. *Nekrmit, asi kouše!* -class Problem(SeminarModelBase,PolymorphicModel): - - class Meta: - # Není abstraktní, protože se na něj jinak nedají dělat ForeignKeys. - # TODO: Udělat to polymorfní (pomocí django-polymorphic), abychom dostali - # po těch vazbách přímo tu úlohu/témátko vč. fieldů, které nejsou součástí - # modelu Problem? - - #abstract = True - db_table = 'seminar_problemy' - verbose_name = 'Problém' - verbose_name_plural = 'Problémy' - ordering = ['nazev'] - managed = False - - # Interní ID - id = models.AutoField(primary_key = True) - - # Název - nazev = models.CharField('název', max_length=256) # Zveřejnitelný na stránky - - # Problém má podproblémy - nadproblem = models.ForeignKey('self', verbose_name='nadřazený problém', - related_name='podproblem_old', null=True, blank=True, - on_delete=models.SET_NULL) - - STAV_NAVRH = 'navrh' - STAV_ZADANY = 'zadany' - STAV_VYRESENY = 'vyreseny' - STAV_SMAZANY = 'smazany' - STAV_CHOICES = [ - (STAV_NAVRH, 'Návrh'), - (STAV_ZADANY, 'Zadaný'), - (STAV_VYRESENY, 'Vyřešený'), - (STAV_SMAZANY, 'Smazaný'), - ] - stav = models.CharField('stav problému', max_length=32, choices=STAV_CHOICES, blank=False, default=STAV_NAVRH) - # Téma je taky Problém, takže má stavy, "zadané" témátko je aktuálně otevřené a dá se k němu něco poslat (řešení nebo článek) - - zamereni = TaggableManager(verbose_name='zaměření', related_name='zamereni_old', - help_text='Zaměření M/F/I/O problému, příp. další tagy', blank=True) - - poznamka = models.TextField('org poznámky (HTML)', blank=True, - help_text='Neveřejný návrh úlohy, návrh řešení, text zadání, poznámky ...') - - autor = models.ForeignKey(Organizator, verbose_name='autor problému', - related_name='autor_problemu_%(class)s_old', null=True, blank=True, - on_delete=models.SET_NULL) - - garant = models.ForeignKey(Organizator, verbose_name='garant zadaného problému', - related_name='garant_problemu_%(class)s_old', null=True, blank=True, - on_delete=models.SET_NULL) - - opravovatele = models.ManyToManyField(Organizator, verbose_name='opravovatelé', - blank=True, related_name='opravovatele_%(class)s_old', through=Problemy_Opravovatele) - - kod = models.CharField('lokální kód', max_length=32, blank=True, default='', - help_text='Číslo/kód úlohy v čísle nebo kód tématu/článku/seriálu v ročníku') - - - vytvoreno = models.DateTimeField('vytvořeno', default=timezone.now, blank=True, editable=False) - - - def __str__(self): - return self.nazev - - # Implicitini implementace, jednotlivé dědící třídy si přepíšou - @cached_property - def kod_v_rocniku(self): - if self.stav == Problem.STAV_ZADANY or self.stav == Problem.STAV_VYRESENY: - if self.nadproblem: - return self.nadproblem.kod_v_rocniku+".{}".format(self.kod) - return str(self.kod) - logger.warning(f"K problému {self} byl vyžadován kód v ročníku, i když není zadaný ani vyřešený.") - return f'' - -# def verejne(self): -# # aktuálně podle stavu problému -# # FIXME pro některé problémy možná chceme override -# # FIXME vrací veřejnost čistě problému, nezávisle na čísle, ve kterém je. -# # Je to tak správně? Podle aktuální představy ano. -# stav_verejny = False -# if self.stav == 'zadany' or self.stav == 'vyreseny': -# stav_verejny = True -# print("stav_verejny: {}".format(stav_verejny)) -# -# cislo_verejne = False -# cislonode = self.cislo_node() -# if cislonode is None: -# # problém nemá vlastní node, veřejnost posuzujeme jen podle stavu -# print("empty node") -# return stav_verejny -# else: -# cislo_zadani = cislonode.cislo -# if (cislo_zadani and cislo_zadani.verejne()): -# print("cislo: {}".format(cislo_zadani)) -# cislo_verejne = True -# print("stav_verejny: {}".format(stav_verejny)) -# print("cislo_verejne: {}".format(cislo_verejne)) -# return (stav_verejny and cislo_verejne) -# verejne.boolean = True - - def verejne_url(self): - return reverse('seminar_problem', kwargs={'pk': self.id}) - - def admin_url(self): - return reverse('admin:seminar_problem_change', args=(self.id, )) - - @cached_property - def hlavni_problem(self): - """ Pro daný problém vrátí jeho nejvyšší nadproblém.""" - problem = self - while not (problem.nadproblem is None): - problem = problem.nadproblem - return problem - -# FIXME - k úloze - def body_v_zavorce(self): - """Vrať string s body v závorce jsou-li u problému vyplněné, jinak '' - - Je-li desetinná část nulová, nezobrazuj ji. - """ - pocet_bodu = None - if self.body: - b = self.body - pocet_bodu = int(b) if int(b) == b else b - return "({}\u2009b)".format(pocet_bodu) if self.body else "" - -class Tema(Problem): - class Meta: - db_table = 'seminar_temata' - verbose_name = 'Téma' - verbose_name_plural = 'Témata' - managed = False - - TEMA_TEMA = 'tema' - TEMA_SERIAL = 'serial' - TEMA_CHOICES = [ - (TEMA_TEMA, 'Téma'), - (TEMA_SERIAL, 'Seriál'), - ] - tema_typ = models.CharField('Typ tématu', max_length=16, choices=TEMA_CHOICES, - blank=False, default=TEMA_TEMA) - - rocnik = models.ForeignKey(Rocnik, verbose_name='ročník',related_name='temata_old',blank=True, null=True, - on_delete=models.PROTECT) - - abstrakt = models.TextField('Abstrakt na rozcestník', blank=True) - obrazek = models.ImageField('Obrázek na rozcestník', null=True, blank=True) - - @cached_property - def kod_v_rocniku(self): - if self.stav == Problem.STAV_ZADANY or self.stav == Problem.STAV_VYRESENY: - if self.nadproblem: - return self.nadproblem.kod_v_rocniku+".t{}".format(self.kod) - return 't'+self.kod - logger.warning(f"K problému {self} byl vyžadován kód v ročníku, i když není zadaný ani vyřešený.") - return f'' - - def save(self, *args, **kwargs): - super().save(*args, **kwargs) - # *Node.save() aktualizuje název *Nodu. - for tvcn in self.temavcislenode_set.all(): - tvcn.save() - - def cislo_node(self): - tema_node_set = self.temavcislenode_set.all() - tema_cisla_vyskyt = [] - from seminar.models.treenode import CisloNode - for tn in tema_node_set: - tema_cisla_vyskyt.append( - treelib.get_upper_node_of_type(tn, CisloNode).cislo) - tema_cisla_vyskyt.sort(key=lambda x:x.datum_vydani) - prvni_zadani = tema_cisla_vyskyt[0] - return prvni_zadani.cislonode - -class Clanek(Problem): - class Meta: - db_table = 'seminar_clanky' - verbose_name = 'Článek' - verbose_name_plural = 'Články' - managed = False - - cislo = models.ForeignKey(Cislo, blank=True, null=True, on_delete=models.PROTECT, - verbose_name='číslo vydání', related_name='vydane_clanky_old') - - strana = models.PositiveIntegerField(verbose_name="první strana", blank=True, null=True) - - @cached_property - def kod_v_rocniku(self): - if self.stav == Problem.STAV_ZADANY or self.stav == Problem.STAV_VYRESENY: -# Nemělo by být potřeba -# if self.nadproblem: -# return self.nadproblem.kod_v_rocniku+".c{}".format(self.kod) - return "c" + self.kod - logger.warning(f"K problému {self} byl vyžadován kód v ročníku, i když není zadaný ani vyřešený.") - return f'' - - def node(self): - return None - - -class Uloha(Problem): - class Meta: - db_table = 'seminar_ulohy' - verbose_name = 'Úloha' - verbose_name_plural = 'Úlohy' - managed = False - - cislo_zadani = models.ForeignKey(Cislo, verbose_name='číslo zadání', blank=True, - null=True, related_name='zadane_ulohy_old', on_delete=models.PROTECT) - - cislo_deadline = models.ForeignKey(Cislo, verbose_name='číslo deadlinu', blank=True, - null=True, related_name='deadlinove_ulohy_old', on_delete=models.PROTECT) - - cislo_reseni = models.ForeignKey(Cislo, verbose_name='číslo řešení', blank=True, - null=True, related_name='resene_ulohy_old', - help_text='Číslo s řešením úlohy, jen pro úlohy', - on_delete=models.PROTECT) - - max_body = models.DecimalField(max_digits=8, decimal_places=1, verbose_name='maximum bodů', - blank=True, null=True) - - # má OneToOneField s: - # UlohaZadaniNode - # UlohaVzorakNode - - @cached_property - def kod_v_rocniku(self): - if self.stav == Problem.STAV_ZADANY or self.stav == Problem.STAV_VYRESENY: - return f"{self.cislo_zadani.poradi}.{self.kod}" - logger.warning(f"K problému {self} byl vyžadován kód v ročníku, i když není zadaný ani vyřešený.") - return f'' - - def save(self, *args, **kwargs): - super().save(*args, **kwargs) - # *Node.save() aktualizuje název *Nodu. - try: - self.ulohazadaninode.save() - except ObjectDoesNotExist: - # Neexistující *Node nemá smysl aktualizovat. - pass - try: - self.ulohavzoraknode.save() - except ObjectDoesNotExist: - # Neexistující *Node nemá smysl aktualizovat. - pass - - def cislo_node(self): - zadani_node = self.ulohazadaninode - from seminar.models.treenode import CisloNode - return treelib.get_upper_node_of_type(zadani_node, CisloNode) - - -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) - - -class Pohadka(SeminarModelBase): - """Kus pohádky před/za úlohou v čísle""" - - class Meta: - db_table = 'seminar_pohadky' - verbose_name = 'Pohádka' - verbose_name_plural = 'Pohádky' - ordering = ['vytvoreno'] - managed = False - - # Interní ID - id = models.AutoField(primary_key=True) - - autor = models.ForeignKey( - Organizator, - verbose_name="Autor pohádky", - - # Při nahrávání z TeXu není vyplnění vyžadováno, v adminu je - null=True, - blank=False, - on_delete=models.SET_NULL, - related_name='awawa3_old', - ) - - vytvoreno = models.DateTimeField( - 'Vytvořeno', - default=timezone.now, - blank=True, - editable=False - ) - - # má OneToOneField s: - # PohadkaNode - - def __str__(self): - uryvek = self.text if len(self.text) < 50 else self.text[:(50-3)]+"..." - return uryvek - - def save(self, *args, **kwargs): - super().save(*args, **kwargs) - # *Node.save() aktualizuje název *Nodu. - try: - self.pohadkanode.save() - except ObjectDoesNotExist: - # Neexistující *Node nemá smysl aktualizovat. - pass diff --git a/soustredeni/migrations/0005_tvorba_relink.py b/soustredeni/migrations/0005_tvorba_relink.py index 7786449f..1a3a09bf 100644 --- a/soustredeni/migrations/0005_tvorba_relink.py +++ b/soustredeni/migrations/0005_tvorba_relink.py @@ -12,11 +12,14 @@ class Migration(migrations.Migration): ] operations = [ - migrations.AlterField( - model_name='konfera', - name='problem_ptr', - field=models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tvorba.problem'), - ), + ## Konferu zmigrujeme jinak, kvůli jí nejde přepsat někde ve stavu `bases`. + ## Proto si ji unmanagujeme a vyrobíme celou znovu, to by nemělo vadit (zvlášť když t.č. v DB žádná instance Konfery není). + ## (Šlo by `SeparateStateAndData`, což v principu děláme taky ale ty migrace jsou lehce čitelnější a o poznání konzistentnější.) + #migrations.AlterField( + # model_name='konfera', + # name='problem_ptr', + # field=models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tvorba.problem'), + #), migrations.AlterField( model_name='soustredeni', name='rocnik', diff --git a/soustredeni/migrations/0006_tvorba_relink2.py b/soustredeni/migrations/0006_tvorba_relink2.py new file mode 100644 index 00000000..0fe70b8c --- /dev/null +++ b/soustredeni/migrations/0006_tvorba_relink2.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.16 on 2024-10-30 19:37 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('soustredeni', '0005_tvorba_relink'), + ] + + operations = [ + migrations.AlterModelOptions( + name='konfera', + options={'managed': False, 'verbose_name': 'Konfera', 'verbose_name_plural': 'Konfery'}, + ), + ] diff --git a/soustredeni/migrations/0007_tvorba_relink3.py b/soustredeni/migrations/0007_tvorba_relink3.py new file mode 100644 index 00000000..48a84686 --- /dev/null +++ b/soustredeni/migrations/0007_tvorba_relink3.py @@ -0,0 +1,15 @@ +# Generated by Django 4.2.16 on 2024-10-30 19:38 + +from django.db import migrations + +class Migration(migrations.Migration): + + dependencies = [ + ('soustredeni', '0006_tvorba_relink2'), + ] + + operations = [ + migrations.DeleteModel( + name='Konfera', + ), + ] diff --git a/soustredeni/migrations/0008_tvorba_relink4.py b/soustredeni/migrations/0008_tvorba_relink4.py new file mode 100644 index 00000000..d2792c8e --- /dev/null +++ b/soustredeni/migrations/0008_tvorba_relink4.py @@ -0,0 +1,34 @@ +# Generated by Django 4.2.16 on 2024-10-30 19:45 + +from django.db import migrations,models +import django.db.models.deletion +import soustredeni.models + +class Migration(migrations.Migration): + + dependencies = [ + ('soustredeni', '0007_tvorba_relink3'), + ] + + operations = [ + migrations.CreateModel( + name='Konfera', + fields=[ + ('problem_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tvorba.problem')), + ('anotace', models.TextField(blank=True, help_text='Popis, o čem bude konfera.', verbose_name='anotace')), + ('abstrakt', models.TextField(blank=True, help_text='Abstrakt konfery tak, jak byl uveden ve sborníku', verbose_name='abstrakt')), + ('typ_prezentace', models.CharField(choices=[('veletrh', 'Veletrh (postery)'), ('prezentace', 'Prezentace (přednáška)')], default='veletrh', max_length=16, verbose_name='typ prezentace')), + ('prezentace', models.FileField(blank=True, help_text='Prezentace nebo fotka posteru', upload_to=soustredeni.models.generate_filename_konfera, verbose_name='prezentace')), + ('materialy', models.FileField(blank=True, help_text='Další materiály ke konfeře zabalené do jednoho souboru', upload_to=soustredeni.models.generate_filename_konfera, verbose_name='materialy')), + ('soustredeni', models.ForeignKey(to='soustredeni.soustredeni', verbose_name='soustředění', on_delete=models.SET_NULL, null=True, related_name='konfery')), + ('ucastnici', models.ManyToManyField(help_text='Seznam účastníků konfery', through='soustredeni.Konfery_Ucastnici', to='personalni.resitel', verbose_name='účastníci konfery')), + ], + options={ + 'verbose_name': 'Konfera', + 'verbose_name_plural': 'Konfery', + 'db_table': 'seminar_konfera', + 'managed': False, + }, + bases=('tvorba.problem',), + ), + ] diff --git a/soustredeni/migrations/0009_tvorba_relink5.py b/soustredeni/migrations/0009_tvorba_relink5.py new file mode 100644 index 00000000..cfe5c97b --- /dev/null +++ b/soustredeni/migrations/0009_tvorba_relink5.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.16 on 2024-10-30 20:03 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('soustredeni', '0008_tvorba_relink4'), + ] + + operations = [ + migrations.AlterModelOptions( + name='konfera', + options={'verbose_name': 'Konfera', 'verbose_name_plural': 'Konfery'}, + ), + ] diff --git a/soustredeni/migrations/0010_tvorba_post.py b/soustredeni/migrations/0010_tvorba_post.py new file mode 100644 index 00000000..1a700298 --- /dev/null +++ b/soustredeni/migrations/0010_tvorba_post.py @@ -0,0 +1,14 @@ +# Generated by Django 4.2.16 on 2024-10-30 21:35 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('soustredeni', '0009_tvorba_relink5'), + ('tvorba', '0003_tvorba_post'), + ] + + operations = [ + ] diff --git a/tvorba/migrations/0002_tvorba_manage.py b/tvorba/migrations/0002_tvorba_manage.py new file mode 100644 index 00000000..593d3263 --- /dev/null +++ b/tvorba/migrations/0002_tvorba_manage.py @@ -0,0 +1,54 @@ +# Generated by Django 4.2.16 on 2024-10-30 21:29 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('tvorba', '0001_tvorba_create'), + ('seminar', '0138_tvorba_delete'), + ] + + operations = [ + migrations.AlterModelOptions( + name='cislo', + options={'ordering': ['-rocnik__rocnik', '-poradi'], 'verbose_name': 'Číslo', 'verbose_name_plural': 'Čísla'}, + ), + migrations.AlterModelOptions( + name='clanek', + options={'verbose_name': 'Článek', 'verbose_name_plural': 'Články'}, + ), + migrations.AlterModelOptions( + name='deadline', + options={'ordering': ['deadline'], 'verbose_name': 'Deadline', 'verbose_name_plural': 'Deadliny'}, + ), + migrations.AlterModelOptions( + name='pohadka', + options={'ordering': ['vytvoreno'], 'verbose_name': 'Pohádka', 'verbose_name_plural': 'Pohádky'}, + ), + migrations.AlterModelOptions( + name='problem', + options={'ordering': ['nazev'], 'verbose_name': 'Problém', 'verbose_name_plural': 'Problémy'}, + ), + migrations.AlterModelOptions( + name='problemy_opravovatele', + options={}, + ), + migrations.AlterModelOptions( + name='rocnik', + options={'ordering': ['-rocnik'], 'verbose_name': 'Ročník', 'verbose_name_plural': 'Ročníky'}, + ), + migrations.AlterModelOptions( + name='tema', + options={'verbose_name': 'Téma', 'verbose_name_plural': 'Témata'}, + ), + migrations.AlterModelOptions( + name='uloha', + options={'verbose_name': 'Úloha', 'verbose_name_plural': 'Úlohy'}, + ), + migrations.AlterModelOptions( + name='zmrazenavysledkovka', + options={'verbose_name': 'Zmražená výsledkovka', 'verbose_name_plural': 'Zmražené výsledkovky'}, + ), + ] diff --git a/tvorba/migrations/0003_tvorba_post.py b/tvorba/migrations/0003_tvorba_post.py new file mode 100644 index 00000000..16e6d203 --- /dev/null +++ b/tvorba/migrations/0003_tvorba_post.py @@ -0,0 +1,13 @@ +# Generated by Django 4.2.16 on 2024-10-30 21:34 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('tvorba', '0002_tvorba_manage'), + ] + + operations = [ + ] diff --git a/tvorba/models.py b/tvorba/models.py index 40d46797..5abbe748 100644 --- a/tvorba/models.py +++ b/tvorba/models.py @@ -31,7 +31,8 @@ from polymorphic.models import PolymorphicModel from django.core.mail import EmailMessage -from seminar.models import SeminarModelBase, OverwriteStorage +from seminar.models.base import SeminarModelBase +from seminar.models.tvorba import OverwriteStorage from personalni.models import Prijemce, Organizator logger = logging.getLogger(__name__) @@ -44,7 +45,6 @@ class Rocnik(SeminarModelBase): verbose_name = 'Ročník' verbose_name_plural = 'Ročníky' ordering = ['-rocnik'] - managed = False # Interní ID id = models.AutoField(primary_key = True) @@ -132,7 +132,6 @@ class Cislo(SeminarModelBase): verbose_name = 'Číslo' verbose_name_plural = 'Čísla' ordering = ['-rocnik__rocnik', '-poradi'] - managed = False # Interní ID id = models.AutoField(primary_key = True) @@ -321,7 +320,6 @@ class Deadline(SeminarModelBase): verbose_name = 'Deadline' verbose_name_plural = 'Deadliny' ordering = ['deadline'] - managed = False def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -384,7 +382,6 @@ class ZmrazenaVysledkovka(SeminarModelBase): db_table = 'seminar_vysledkovky' verbose_name = 'Zmražená výsledkovka' verbose_name_plural = 'Zmražené výsledkovky' - managed = False deadline = models.OneToOneField( Deadline, @@ -403,7 +400,6 @@ class Problemy_Opravovatele(SeminarModelBase): """ class Meta: db_table = 'seminar_problemy_opravovatele' - managed = False id = models.AutoField(primary_key = True) @@ -425,7 +421,6 @@ class Problem(SeminarModelBase,PolymorphicModel): verbose_name = 'Problém' verbose_name_plural = 'Problémy' ordering = ['nazev'] - managed = False # Interní ID id = models.AutoField(primary_key = True) @@ -543,7 +538,6 @@ class Tema(Problem): db_table = 'seminar_temata' verbose_name = 'Téma' verbose_name_plural = 'Témata' - managed = False TEMA_TEMA = 'tema' TEMA_SERIAL = 'serial' @@ -591,7 +585,6 @@ class Clanek(Problem): db_table = 'seminar_clanky' verbose_name = 'Článek' verbose_name_plural = 'Články' - managed = False cislo = models.ForeignKey(Cislo, blank=True, null=True, on_delete=models.PROTECT, verbose_name='číslo vydání', related_name='vydane_clanky') @@ -617,7 +610,6 @@ class Uloha(Problem): db_table = 'seminar_ulohy' verbose_name = 'Úloha' verbose_name_plural = 'Úlohy' - managed = False cislo_zadani = models.ForeignKey(Cislo, verbose_name='číslo zadání', blank=True, null=True, related_name='zadane_ulohy', on_delete=models.PROTECT) @@ -680,7 +672,6 @@ class Pohadka(SeminarModelBase): verbose_name = 'Pohádka' verbose_name_plural = 'Pohádky' ordering = ['vytvoreno'] - managed = False # Interní ID id = models.AutoField(primary_key=True) diff --git a/various/migrations/0006_tvorba_post.py b/various/migrations/0006_tvorba_post.py new file mode 100644 index 00000000..4b339a05 --- /dev/null +++ b/various/migrations/0006_tvorba_post.py @@ -0,0 +1,14 @@ +# Generated by Django 4.2.16 on 2024-10-30 21:35 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('various', '0005_tvorba_relink'), + ('tvorba', '0003_tvorba_post'), + ] + + operations = [ + ] -- 2.39.5 From 26d37d96f7c4c755b80810057399445e15c7a918 Mon Sep 17 00:00:00 2001 From: "Pavel \"LEdoian\" Turinsky" Date: Thu, 31 Oct 2024 00:01:07 +0100 Subject: [PATCH 3/4] =?UTF-8?q?Frontendov=C3=A9=20n=C3=A1hrady=20semin?= =?UTF-8?q?=C3=A1=C5=99e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (některé netestované, smůla.) --- personalni/templates/personalni/profil/orgorozcestnik.html | 2 +- tvorba/admin.py | 3 ++- tvorba/models.py | 2 +- various/models.py | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/personalni/templates/personalni/profil/orgorozcestnik.html b/personalni/templates/personalni/profil/orgorozcestnik.html index 90d5867d..9d0bbdba 100644 --- a/personalni/templates/personalni/profil/orgorozcestnik.html +++ b/personalni/templates/personalni/profil/orgorozcestnik.html @@ -20,7 +20,7 @@

Tvorba čísla

    -
  • přidat téma
  • +
  • přidat téma
  • korektury
    • korekturování
    • diff --git a/tvorba/admin.py b/tvorba/admin.py index f090062b..f62d2576 100644 --- a/tvorba/admin.py +++ b/tvorba/admin.py @@ -1,6 +1,7 @@ from django.contrib import admin from django.forms import ModelForm from django.core.exceptions import ValidationError +from django.urls import reverse from polymorphic.admin import PolymorphicParentModelAdmin, PolymorphicChildModelAdmin, PolymorphicChildModelFilter from django.utils.safestring import mark_safe @@ -70,7 +71,7 @@ class CisloForm(ModelForm): ValidationError('Úloha %(uloha)s není zadaná ani vyřešená', params={'uloha': ch})) if errors: errors.append(ValidationError(mark_safe( - 'Pokud chceš učinit všechny problémy, co nejsou zadané ani vyřešené, zadanými a číslo zveřejnit, můžeš to udělat pomocí akce v seznamu čísel'))) + 'Pokud chceš učinit všechny problémy, co nejsou zadané ani vyřešené, zadanými a číslo zveřejnit, můžeš to udělat pomocí akce v seznamu čísel'))) if self.cleaned_data.get('datum_vydani') == None: self.add_error('datum_vydani','Číslo určené ke zveřejnění nemá nastavené datum vydání') diff --git a/tvorba/models.py b/tvorba/models.py index 5abbe748..d7290ae3 100644 --- a/tvorba/models.py +++ b/tvorba/models.py @@ -511,7 +511,7 @@ class Problem(SeminarModelBase,PolymorphicModel): return reverse('seminar_problem', kwargs={'pk': self.id}) def admin_url(self): - return reverse('admin:seminar_problem_change', args=(self.id, )) + return reverse('admin:tvorba_problem_change', args=(self.id, )) @cached_property def hlavni_problem(self): diff --git a/various/models.py b/various/models.py index e31b4e72..85ba4702 100644 --- a/various/models.py +++ b/various/models.py @@ -33,7 +33,7 @@ class Nastaveni(SingletonModel): return 'Nastavení semináře' def admin_url(self): - return reverse('admin:seminar_nastaveni_change', args=(self.id, )) + return reverse('admin:various_nastaveni_change', args=(self.id, )) def verejne(self): return False -- 2.39.5 From ad9a496ceea67c1a3592f69c65078f6652d5d8e0 Mon Sep 17 00:00:00 2001 From: "Pavel \"LEdoian\" Turinsky" Date: Thu, 31 Oct 2024 00:03:00 +0100 Subject: [PATCH 4/4] =?UTF-8?q?Kus=20k=C3=B3du=20nen=C3=AD=20pot=C5=99eba?= =?UTF-8?q?=20(a=20nav=C3=ADc=20obsahuje=20slovo=20semin=C3=A1=C5=99=20:-P?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tvorba/admin.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tvorba/admin.py b/tvorba/admin.py index f62d2576..0e873c29 100644 --- a/tvorba/admin.py +++ b/tvorba/admin.py @@ -60,9 +60,6 @@ class CisloForm(ModelForm): # if problem not in \ # (Problem.STAV_ZADANY, Problem.STAV_VYRESENY): # errors.append(ValidationError('Problém %s není zadaný ani vyřešený', code=problem)) - # if errors: - # errors.append(ValidationError(mark_safe('Pokud chceš učinit všechny problémy, co nejsou zadané ani vyřešené, zadanými a číslo zveřejnit, můžeš to udělat pomocí akce v seznamu čísel'))) - # raise ValidationError(errors) errors = [] for ch in Uloha.objects.filter(cislo_zadani=self.instance): -- 2.39.5