diff --git a/odevzdavatko/migrations/0001_create.py b/odevzdavatko/migrations/0001_create.py new file mode 100644 index 00000000..22ee11ed --- /dev/null +++ b/odevzdavatko/migrations/0001_create.py @@ -0,0 +1,99 @@ +# Generated by Django 4.2.13 on 2024-10-22 22:51 + +from django.db import migrations, models +import django.utils.timezone +import odevzdavatko.models + +def nastav_nove_contenttypes(apps, schema_editor): + ContentType = apps.get_model('contenttypes', 'ContentType') + for m in ('reseni', 'hodnoceni', 'reseni_resitele', 'prilohareseni'): + ContentType.objects.filter(app_label='seminar', model=m).update(app_label='odevzdavatko') + +def nastav_stare_contenttypes(apps, schema_editor): + ContentType = apps.get_model('contenttypes', 'ContentType') + for m in ('reseni', 'hodnoceni', 'reseni_resitele', 'prilohareseni'): + ContentType.objects.filter(app_label='odevzdavatko', model=m).update(app_label='seminar') + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('seminar', '0132_unmanage_odevzdavatko'), + ] + + operations = [ + migrations.CreateModel( + name='Hodnoceni', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('body', models.DecimalField(blank=True, decimal_places=1, max_digits=8, null=True, verbose_name='body')), + ('feedback', models.TextField(blank=True, default='', help_text='Zpětná vazba řešiteli (plain text)', verbose_name='zpětná vazba')), + ('cislo_body', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='hodnoceni', to='seminar.cislo', verbose_name='číslo pro body')), + ('deadline_body', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='hodnoceni', to='seminar.deadline', verbose_name='deadline pro body')), + ('problem', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='hodnoceni', to='seminar.problem', verbose_name='problém')), + ('reseni', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='odevzdavatko.reseni', verbose_name='řešení')), + ], + options={ + 'verbose_name': 'Hodnocení', + 'verbose_name_plural': 'Hodnocení', + 'db_table': 'seminar_hodnoceni', + 'managed': False, + }, + ), + migrations.CreateModel( + name='PrilohaReseni', + 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')), + ('soubor', models.FileField(upload_to=odevzdavatko.models.generate_filename, verbose_name='soubor')), + ('poznamka', models.TextField(blank=True, help_text='Neveřejná poznámka k příloze řešení (plain text), např. o původu', verbose_name='neveřejná poznámka')), + ('res_poznamka', models.TextField(blank=True, help_text='Poznámka k příloze řešení, např. co daný soubor obsahuje', verbose_name='poznámka řešitele')), + ('reseni', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='prilohy', to='odevzdavatko.reseni', verbose_name='řešení')), + ], + options={ + 'verbose_name': 'Příloha řešení', + 'verbose_name_plural': 'Přílohy řešení', + 'db_table': 'seminar_priloha_reseni', + 'ordering': ['reseni', 'vytvoreno'], + 'managed': False, + }, + ), + migrations.CreateModel( + name='Reseni', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('cas_doruceni', models.DateTimeField(blank=True, default=django.utils.timezone.now, verbose_name='čas_doručení')), + ('forma', models.CharField(choices=[('papir', 'Papírové řešení'), ('email', 'Emailem'), ('upload', 'Upload přes web')], default='email', max_length=16, verbose_name='forma řešení')), + ('poznamka', models.TextField(blank=True, help_text='Neveřejná poznámka k řešení (plain text)', verbose_name='neveřejná poznámka')), + ('zverejneno', models.BooleanField(default=False, help_text='Udává, zda je řešení zveřejněno', verbose_name='řešení zveřejněno')), + ('problem', models.ManyToManyField(help_text='Problém', through='odevzdavatko.Hodnoceni', to='seminar.problem', verbose_name='problém')), + ('resitele', models.ManyToManyField(help_text='Seznam autorů řešení', through='odevzdavatko.Reseni_Resitele', to='personalni.resitel', verbose_name='autoři řešení')), + ('text_cely', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='reseni_cely_set', to='seminar.reseninode', verbose_name='Plná verze textu řešení')), + ], + options={ + 'verbose_name': 'Řešení', + 'verbose_name_plural': 'Řešení', + 'db_table': 'seminar_reseni', + 'ordering': ['-cas_doruceni'], + 'managed': False, + }, + ), + migrations.CreateModel( + name='Reseni_Resitele', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('reseni', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='odevzdavatko.reseni', verbose_name='řešení')), + ('resitele', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='personalni.resitel', verbose_name='řešitel')), + ], + options={ + 'verbose_name': 'Řešení řešitelů', + 'verbose_name_plural': 'Řešení řešitelů', + 'db_table': 'seminar_reseni_resitele', + 'ordering': ['reseni', 'resitele'], + 'managed': False, + }, + ), + migrations.RunPython(nastav_nove_contenttypes, nastav_stare_contenttypes), + + ] diff --git a/odevzdavatko/migrations/0002_manage.py b/odevzdavatko/migrations/0002_manage.py new file mode 100644 index 00000000..73c35288 --- /dev/null +++ b/odevzdavatko/migrations/0002_manage.py @@ -0,0 +1,30 @@ +# Generated by Django 4.2.13 on 2024-10-23 21:07 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('odevzdavatko', '0001_create'), + ('seminar', '0134_delete_odevzdavatko'), + ] + + operations = [ + migrations.AlterModelOptions( + name='hodnoceni', + options={'verbose_name': 'Hodnocení', 'verbose_name_plural': 'Hodnocení'}, + ), + migrations.AlterModelOptions( + name='prilohareseni', + options={'ordering': ['reseni', 'vytvoreno'], 'verbose_name': 'Příloha řešení', 'verbose_name_plural': 'Přílohy řešení'}, + ), + migrations.AlterModelOptions( + name='reseni', + options={'ordering': ['-cas_doruceni'], 'verbose_name': 'Řešení', 'verbose_name_plural': 'Řešení'}, + ), + migrations.AlterModelOptions( + name='reseni_resitele', + options={'ordering': ['reseni', 'resitele'], 'verbose_name': 'Řešení řešitelů', 'verbose_name_plural': 'Řešení řešitelů'}, + ), + ] diff --git a/odevzdavatko/migrations/0003_odstrel_odevzdavatka_post.py b/odevzdavatko/migrations/0003_odstrel_odevzdavatka_post.py new file mode 100644 index 00000000..77936a26 --- /dev/null +++ b/odevzdavatko/migrations/0003_odstrel_odevzdavatka_post.py @@ -0,0 +1,13 @@ +# Generated by Django 4.2.13 on 2024-10-23 21:10 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('odevzdavatko', '0002_manage'), + ] + + operations = [ + ] diff --git a/odevzdavatko/models.py b/odevzdavatko/models.py new file mode 100644 index 00000000..a5b6a7e1 --- /dev/null +++ b/odevzdavatko/models.py @@ -0,0 +1,239 @@ +import os + +import reversion + +from django.contrib.sites.shortcuts import get_current_site +from django.db import models +from django.db.models import Sum +from django.urls import reverse_lazy +from django.utils import timezone +from django.conf import settings + +import seminar.models as am # tvorba +from seminar.models import base as bm + +from odevzdavatko.utils import vzorecek_na_prepocet, inverze_vzorecku_na_prepocet +from personalni.models import Resitel + +@reversion.register(ignore_duplicates=True) +class Reseni(bm.SeminarModelBase): + + class Meta: + db_table = 'seminar_reseni' + verbose_name = 'Řešení' + verbose_name_plural = 'Řešení' + #ordering = ['-problem', 'resitele'] # FIXME: Takhle to chceme, ale nefunguje to. + ordering = ['-cas_doruceni'] + + # Interní ID + id = models.AutoField(primary_key = True) + + # Ke každé dvojici řešní a problém existuje nanejvýš jedno hodnocení, doplnění vazby. + problem = models.ManyToManyField(am.Problem, verbose_name='problém', help_text='Problém', + through='Hodnoceni') + + resitele = models.ManyToManyField(Resitel, verbose_name='autoři řešení', + help_text='Seznam autorů řešení', through='Reseni_Resitele') + + + cas_doruceni = models.DateTimeField('čas_doručení', default=timezone.now, blank=True) + + FORMA_PAPIR = 'papir' + FORMA_EMAIL = 'email' + FORMA_UPLOAD = 'upload' + FORMA_CHOICES = [ + (FORMA_PAPIR, 'Papírové řešení'), + (FORMA_EMAIL, 'Emailem'), + (FORMA_UPLOAD, 'Upload přes web'), + ] + forma = models.CharField('forma řešení', max_length=16, choices=FORMA_CHOICES, blank=False, + default=FORMA_EMAIL) + + text_cely = models.OneToOneField('seminar.ReseniNode', verbose_name='Plná verze textu řešení', + blank=True, null=True, related_name="reseni_cely_set", + on_delete=models.PROTECT) + + poznamka = models.TextField('neveřejná poznámka', blank=True, + help_text='Neveřejná poznámka k řešení (plain text)') + + zverejneno = models.BooleanField('řešení zveřejněno', default=False, + help_text='Udává, zda je řešení zveřejněno') + + def verejne_url(self): + return str(reverse_lazy('odevzdavatko_detail_reseni', args=[self.id])) + + def absolute_url(self): + return "https://" + str(get_current_site(None)) + self.verejne_url() + + # má OneToOneField s: + # Konfera + + # má ForeignKey s: + # Hodnoceni + + def sum_body(self): + return self.hodnoceni_set.all().aggregate(Sum('body'))["body__sum"] + + def __str__(self): + return "{}({}): {}({})".format(self.resitele.first(),len(self.resitele.all()), self.problem.first() ,len(self.problem.all())) + # NOTE: Potenciální DB HOG (bez select_related) + + def deadline_reseni(self): + return am.Deadline.objects.filter(deadline__gte=self.cas_doruceni).order_by("deadline").first() + +## Pravdepodobne uz nebude potreba: +# def save(self, *args, **kwargs): +# if ((self.cislo_body is None) and (self.problem.cislo_reseni) and +# (self.problem.typ == Problem.TYP_ULOHA)): +# self.cislo_body = self.problem.cislo_reseni +# super(Reseni, self).save(*args, **kwargs) + +class Hodnoceni(bm.SeminarModelBase): + class Meta: + db_table = 'seminar_hodnoceni' + verbose_name = 'Hodnocení' + verbose_name_plural = 'Hodnocení' + + # Interní ID + id = models.AutoField(primary_key = True) + + + body = models.DecimalField(max_digits=8, decimal_places=1, verbose_name='body', + blank=True, null=True) + + cislo_body = models.ForeignKey(am.Cislo, verbose_name='číslo pro body', + related_name='hodnoceni', blank=True, null=True, on_delete=models.PROTECT) + + # V ročníku < 26 nastaveno na deadline vygenerovaný pro původní cislo_body + deadline_body = models.ForeignKey(am.Deadline, verbose_name='deadline pro body', + related_name='hodnoceni', blank=True, null=True, on_delete=models.PROTECT) + + reseni = models.ForeignKey(Reseni, verbose_name='řešení', on_delete=models.CASCADE) + + problem = models.ForeignKey(am.Problem, verbose_name='problém', + related_name='hodnoceni', on_delete=models.PROTECT) + + feedback = models.TextField('zpětná vazba', blank=True, default='', help_text='Zpětná vazba řešiteli (plain text)') + + @property + def body_celkem(self): + # FIXME řeším jen prvního řešitele. + return Hodnoceni.objects.filter(problem=self.problem, reseni__resitele=self.reseni.resitele.first(), body__isnull=False).aggregate(Sum("body"))["body__sum"] + + @body_celkem.setter + def body_celkem(self, value): + if value is None: + self.body = None + else: + if self.body is None: + self.body = 0 + if self.body_celkem is None: + self.body += value + else: + self.body += value - self.body_celkem + + @property + def body_neprepocitane(self): + if self.body is None: + return None + return inverze_vzorecku_na_prepocet(self.body, self.reseni.resitele.count()) + + @body_neprepocitane.setter + def body_neprepocitane(self, value): + if value is None: + self.body = None + else: + self.body = vzorecek_na_prepocet(value, self.reseni.resitele.count()) + + @property + def body_neprepocitane_celkem(self): + if self.body_celkem is None: + return None + return inverze_vzorecku_na_prepocet(self.body_celkem, self.reseni.resitele.count()) + + @body_neprepocitane_celkem.setter + def body_neprepocitane_celkem(self, value): + if value is None: + self.body = None + else: + self.body_celkem = vzorecek_na_prepocet(value, self.reseni.resitele.count()) + + @property + def body_max(self): + if self.body_neprepocitane_max is None: + return None + return vzorecek_na_prepocet(self.body_neprepocitane_max, self.reseni.resitele.count()) + + @property + def body_neprepocitane_max(self): + if not isinstance(self.problem.get_real_instance(), am.Uloha): + return None + return self.problem.uloha.max_body + + def __str__(self): + return "{}, {}, {}".format(self.problem, self.reseni, self.body) + +def generate_filename(self, filename): + return os.path.join( + settings.SEMINAR_RESENI_DIR, + am.aux_generate_filename(self, filename) + ) + + +@reversion.register(ignore_duplicates=True) +class PrilohaReseni(bm.SeminarModelBase): + + class Meta: + db_table = 'seminar_priloha_reseni' + verbose_name = 'Příloha řešení' + verbose_name_plural = 'Přílohy řešení' + ordering = ['reseni', 'vytvoreno'] + + # Interní ID + id = models.AutoField(primary_key = True) + + reseni = models.ForeignKey(Reseni, verbose_name='řešení', related_name='prilohy', + on_delete=models.CASCADE) + + vytvoreno = models.DateTimeField('vytvořeno', default=timezone.now, blank=True, editable=False) + + soubor = models.FileField('soubor', upload_to = generate_filename) + + poznamka = models.TextField('neveřejná poznámka', blank=True, + help_text='Neveřejná poznámka k příloze řešení (plain text), např. o původu') + + res_poznamka = models.TextField('poznámka řešitele', blank=True, + help_text='Poznámka k příloze řešení, např. co daný soubor obsahuje') + + def __str__(self): + return str(self.soubor) + + def split(self): + "Vrátí cestu rozsekanou po složkách. To se hodí v templatech" + # Věřím, že tohle funguje, případně použít os.path nebo pathlib. + return self.soubor.url.split('/') + + +# Vazebna tabulka. Mozna se generuje automaticky. +@reversion.register(ignore_duplicates=True) +class Reseni_Resitele(models.Model): + + class Meta: + db_table = 'seminar_reseni_resitele' + verbose_name = 'Řešení řešitelů' + verbose_name_plural = 'Řešení řešitelů' + ordering = ['reseni', 'resitele'] + + # Interní ID + id = models.AutoField(primary_key = True) + + resitele = models.ForeignKey(Resitel, verbose_name='řešitel', on_delete=models.PROTECT) + + reseni = models.ForeignKey(Reseni, verbose_name='řešení', on_delete=models.CASCADE) + + # podil - jakou merou se ktery resitel podilel na danem reseni + # - pouziti v budoucnu, pokud by resitele nemeli dostat vsichni stejne bodu za spolecne reseni + + def __str__(self): + return '{} od {}'.format(self.reseni, self.resitel) + # NOTE: Poteciální DB HOG bez select_related diff --git a/odevzdavatko/testutils.py b/odevzdavatko/testutils.py index 1f382438..c1240ab4 100644 --- a/odevzdavatko/testutils.py +++ b/odevzdavatko/testutils.py @@ -1,7 +1,7 @@ import datetime import random -from seminar.models.odevzdavatko import Reseni, Hodnoceni +from odevzdavatko.models import Reseni, Hodnoceni def gen_reseni_ulohy(rnd, cisla, uloha, pocet_resitelu, poradi_cisla, resitele_cisla, resitele): diff --git a/personalni/migrations/0012_odstrel_odevzdavatka_pre.py b/personalni/migrations/0012_odstrel_odevzdavatka_pre.py new file mode 100644 index 00000000..c712d421 --- /dev/null +++ b/personalni/migrations/0012_odstrel_odevzdavatka_pre.py @@ -0,0 +1,13 @@ +# Generated by Django 4.2.13 on 2024-10-22 22:17 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('personalni', '0011_osloveni_vsechny_choices'), + ] + + operations = [ + ] diff --git a/personalni/migrations/0013_odstrel_odevzdavatka_post.py b/personalni/migrations/0013_odstrel_odevzdavatka_post.py new file mode 100644 index 00000000..127e1c40 --- /dev/null +++ b/personalni/migrations/0013_odstrel_odevzdavatka_post.py @@ -0,0 +1,14 @@ +# Generated by Django 4.2.13 on 2024-10-23 21:10 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('personalni', '0012_odstrel_odevzdavatka_pre'), + ('odevzdavatko', '0003_odstrel_odevzdavatka_post'), + ] + + operations = [ + ] diff --git a/personalni/models.py b/personalni/models.py index 8f6cd63f..b8bcbdec 100644 --- a/personalni/models.py +++ b/personalni/models.py @@ -296,7 +296,7 @@ class Resitel(SeminarModelBase): def vsechny_body(self): "Spočítá body odjakživa." vsechna_reseni = self.reseni_set.all() - from seminar.models.odevzdavatko import Hodnoceni + from odevzdavatko.models import Hodnoceni vsechna_hodnoceni = Hodnoceni.objects.filter( reseni__in=vsechna_reseni) return sum(h.body for h in list(vsechna_hodnoceni) if h.body is not None) @@ -343,7 +343,7 @@ class Resitel(SeminarModelBase): # - body z 25. ročníku a dříve byly shledány dvakrát hodnotnějšími # - proto se započítávají dvojnásobně a byly posunuté hranice titulů # - staré tituly se ale nemají odebrat, pokud řešitel v t.č. minulém (26.) ročníku měl titul, má ho mít pořád. - from seminar.models.odevzdavatko import Hodnoceni + from odevzdavatko.models import Hodnoceni hodnoceni_do_25_rocniku = Hodnoceni.objects.filter(deadline_body__cislo__rocnik__rocnik__lte=25,reseni__in=self.reseni_set.all()) novejsi_hodnoceni = Hodnoceni.objects.filter(reseni__in=self.reseni_set.all()).difference(hodnoceni_do_25_rocniku) @@ -381,7 +381,7 @@ class Resitel(SeminarModelBase): else: return Titul.akad - from seminar.models.odevzdavatko import Hodnoceni + from odevzdavatko.models import Hodnoceni hodnoceni_do_26_rocniku = Hodnoceni.objects.filter(deadline_body__cislo__rocnik__rocnik__lte=26,reseni__in=self.reseni_set.all()) novejsi_body = body_z_hodnoceni( Hodnoceni.objects.filter(reseni__in=self.reseni_set.all()) diff --git a/seminar/migrations/0131_odstrel_odevzdavatka_pre.py b/seminar/migrations/0131_odstrel_odevzdavatka_pre.py new file mode 100644 index 00000000..6d68cdc8 --- /dev/null +++ b/seminar/migrations/0131_odstrel_odevzdavatka_pre.py @@ -0,0 +1,14 @@ +# Generated by Django 4.2.13 on 2024-10-22 22:17 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('seminar', '0130_clanek_strana'), + ('personalni', '0012_odstrel_odevzdavatka_pre'), + ] + + operations = [ + ] diff --git a/seminar/migrations/0132_unmanage_odevzdavatko.py b/seminar/migrations/0132_unmanage_odevzdavatko.py new file mode 100644 index 00000000..75575b0a --- /dev/null +++ b/seminar/migrations/0132_unmanage_odevzdavatko.py @@ -0,0 +1,29 @@ +# Generated by Django 4.2.13 on 2024-10-22 22:31 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('seminar', '0131_odstrel_odevzdavatka_pre'), + ] + + operations = [ + migrations.AlterModelOptions( + name='hodnoceni', + options={'managed': False, 'verbose_name': 'Hodnocení', 'verbose_name_plural': 'Hodnocení'}, + ), + migrations.AlterModelOptions( + name='prilohareseni', + options={'managed': False, 'ordering': ['reseni', 'vytvoreno'], 'verbose_name': 'Příloha řešení', 'verbose_name_plural': 'Přílohy řešení'}, + ), + migrations.AlterModelOptions( + name='reseni', + options={'managed': False, 'ordering': ['-cas_doruceni'], 'verbose_name': 'Řešení', 'verbose_name_plural': 'Řešení'}, + ), + migrations.AlterModelOptions( + name='reseni_resitele', + options={'managed': False, 'ordering': ['reseni', 'resitele'], 'verbose_name': 'Řešení řešitelů', 'verbose_name_plural': 'Řešení řešitelů'}, + ), + ] diff --git a/seminar/migrations/0133_relink_odevzdavatko.py b/seminar/migrations/0133_relink_odevzdavatko.py new file mode 100644 index 00000000..e0b7d93a --- /dev/null +++ b/seminar/migrations/0133_relink_odevzdavatko.py @@ -0,0 +1,20 @@ +# Generated by Django 4.2.13 on 2024-10-23 19:53 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('odevzdavatko', '0001_create'), + ('seminar', '0132_unmanage_odevzdavatko'), + ] + + operations = [ + migrations.AlterField( + model_name='reseninode', + name='reseni', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='odevzdavatko.reseni', verbose_name='reseni'), + ), + ] diff --git a/seminar/migrations/0134_delete_odevzdavatko.py b/seminar/migrations/0134_delete_odevzdavatko.py new file mode 100644 index 00000000..479b9d6c --- /dev/null +++ b/seminar/migrations/0134_delete_odevzdavatko.py @@ -0,0 +1,50 @@ +# Generated by Django 4.2.13 on 2024-10-23 19:56 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('seminar', '0133_relink_odevzdavatko'), + ('odevzdavatko', '0001_create'), + ] + + operations = [ + migrations.RemoveField( + model_name='prilohareseni', + name='reseni', + ), + migrations.RemoveField( + model_name='reseni', + name='problem', + ), + migrations.RemoveField( + model_name='reseni', + name='resitele', + ), + migrations.RemoveField( + model_name='reseni', + name='text_cely', + ), + migrations.RemoveField( + model_name='reseni_resitele', + name='reseni', + ), + migrations.RemoveField( + model_name='reseni_resitele', + name='resitele', + ), + migrations.DeleteModel( + name='Hodnoceni', + ), + migrations.DeleteModel( + name='PrilohaReseni', + ), + migrations.DeleteModel( + name='Reseni', + ), + migrations.DeleteModel( + name='Reseni_Resitele', + ), + ] diff --git a/seminar/migrations/0135_odstrel_odevzdavatka_post.py b/seminar/migrations/0135_odstrel_odevzdavatka_post.py new file mode 100644 index 00000000..d515b73e --- /dev/null +++ b/seminar/migrations/0135_odstrel_odevzdavatka_post.py @@ -0,0 +1,14 @@ +# Generated by Django 4.2.13 on 2024-10-23 21:10 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('seminar', '0134_delete_odevzdavatko'), + ('odevzdavatko', '0003_odstrel_odevzdavatka_post'), + ] + + operations = [ + ] diff --git a/seminar/models/__init__.py b/seminar/models/__init__.py index e404864a..4bc85266 100644 --- a/seminar/models/__init__.py +++ b/seminar/models/__init__.py @@ -8,6 +8,9 @@ from various.models import Nastaveni 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 # Kvůli migr. 0041 from soustredeni.models import generate_filename_konfera +# migr. 0001 +from odevzdavatko.models import generate_filename diff --git a/seminar/models/base.py b/seminar/models/base.py index 1069f165..6ea34242 100644 --- a/seminar/models/base.py +++ b/seminar/models/base.py @@ -14,8 +14,9 @@ class SeminarModelBase(models.Model): # return "https://" + str(get_current_site(None)) + self.verejne_url() def admin_url(self): - model_name = self.__class__.__name__.lower() - return reverse('admin:seminar_{}_change'.format(model_name), args=(self.id, )) + app_name = self._meta.app_label + model_name = self._meta.model_name + return reverse('admin:{}_{}_change'.format(app_name, model_name), args=(self.id, )) # def verejne_url(self): # return None diff --git a/seminar/models/odevzdavatko.py b/seminar/models/odevzdavatko.py index 0c106df7..efc88e74 100644 --- a/seminar/models/odevzdavatko.py +++ b/seminar/models/odevzdavatko.py @@ -1,244 +1,7 @@ -import os - -import reversion - -from django.contrib.sites.shortcuts import get_current_site from django.db import models -from django.db.models import Sum -from django.urls import reverse_lazy -from django.utils import timezone -from django.conf import settings -from seminar.models import tvorba as am from seminar.models import treenode as tm -from seminar.models import base as bm - -from odevzdavatko.utils import vzorecek_na_prepocet, inverze_vzorecku_na_prepocet -from personalni.models import Resitel - - -@reversion.register(ignore_duplicates=True) -class Reseni(bm.SeminarModelBase): - - class Meta: - db_table = 'seminar_reseni' - verbose_name = 'Řešení' - verbose_name_plural = 'Řešení' - #ordering = ['-problem', 'resitele'] # FIXME: Takhle to chceme, ale nefunguje to. - ordering = ['-cas_doruceni'] - - # Interní ID - id = models.AutoField(primary_key = True) - - # Ke každé dvojici řešní a problém existuje nanejvýš jedno hodnocení, doplnění vazby. - problem = models.ManyToManyField(am.Problem, verbose_name='problém', help_text='Problém', - through='Hodnoceni') - - resitele = models.ManyToManyField(Resitel, verbose_name='autoři řešení', - help_text='Seznam autorů řešení', through='Reseni_Resitele') - - - cas_doruceni = models.DateTimeField('čas_doručení', default=timezone.now, blank=True) - - FORMA_PAPIR = 'papir' - FORMA_EMAIL = 'email' - FORMA_UPLOAD = 'upload' - FORMA_CHOICES = [ - (FORMA_PAPIR, 'Papírové řešení'), - (FORMA_EMAIL, 'Emailem'), - (FORMA_UPLOAD, 'Upload přes web'), - ] - forma = models.CharField('forma řešení', max_length=16, choices=FORMA_CHOICES, blank=False, - default=FORMA_EMAIL) - - text_cely = models.OneToOneField('ReseniNode', verbose_name='Plná verze textu řešení', - blank=True, null=True, related_name="reseni_cely_set", - on_delete=models.PROTECT) - - poznamka = models.TextField('neveřejná poznámka', blank=True, - help_text='Neveřejná poznámka k řešení (plain text)') - - zverejneno = models.BooleanField('řešení zveřejněno', default=False, - help_text='Udává, zda je řešení zveřejněno') - - def verejne_url(self): - return str(reverse_lazy('odevzdavatko_detail_reseni', args=[self.id])) - - def absolute_url(self): - return "https://" + str(get_current_site(None)) + self.verejne_url() - - # má OneToOneField s: - # Konfera - - # má ForeignKey s: - # Hodnoceni - - def sum_body(self): - return self.hodnoceni_set.all().aggregate(Sum('body'))["body__sum"] - - def __str__(self): - return "{}({}): {}({})".format(self.resitele.first(),len(self.resitele.all()), self.problem.first() ,len(self.problem.all())) - # NOTE: Potenciální DB HOG (bez select_related) - - def deadline_reseni(self): - return am.Deadline.objects.filter(deadline__gte=self.cas_doruceni).order_by("deadline").first() - -## Pravdepodobne uz nebude potreba: -# def save(self, *args, **kwargs): -# if ((self.cislo_body is None) and (self.problem.cislo_reseni) and -# (self.problem.typ == Problem.TYP_ULOHA)): -# self.cislo_body = self.problem.cislo_reseni -# super(Reseni, self).save(*args, **kwargs) - -class Hodnoceni(bm.SeminarModelBase): - class Meta: - db_table = 'seminar_hodnoceni' - verbose_name = 'Hodnocení' - verbose_name_plural = 'Hodnocení' - - # Interní ID - id = models.AutoField(primary_key = True) - - - body = models.DecimalField(max_digits=8, decimal_places=1, verbose_name='body', - blank=True, null=True) - - cislo_body = models.ForeignKey(am.Cislo, verbose_name='číslo pro body', - related_name='hodnoceni', blank=True, null=True, on_delete=models.PROTECT) - - # V ročníku < 26 nastaveno na deadline vygenerovaný pro původní cislo_body - deadline_body = models.ForeignKey(am.Deadline, verbose_name='deadline pro body', - related_name='hodnoceni', blank=True, null=True, on_delete=models.PROTECT) - - reseni = models.ForeignKey(Reseni, verbose_name='řešení', on_delete=models.CASCADE) - - problem = models.ForeignKey(am.Problem, verbose_name='problém', - related_name='hodnoceni', on_delete=models.PROTECT) - - feedback = models.TextField('zpětná vazba', blank=True, default='', help_text='Zpětná vazba řešiteli (plain text)') - - @property - def body_celkem(self): - # FIXME řeším jen prvního řešitele. - return Hodnoceni.objects.filter(problem=self.problem, reseni__resitele=self.reseni.resitele.first(), body__isnull=False).aggregate(Sum("body"))["body__sum"] - - @body_celkem.setter - def body_celkem(self, value): - if value is None: - self.body = None - else: - if self.body is None: - self.body = 0 - if self.body_celkem is None: - self.body += value - else: - self.body += value - self.body_celkem - - @property - def body_neprepocitane(self): - if self.body is None: - return None - return inverze_vzorecku_na_prepocet(self.body, self.reseni.resitele.count()) - - @body_neprepocitane.setter - def body_neprepocitane(self, value): - if value is None: - self.body = None - else: - self.body = vzorecek_na_prepocet(value, self.reseni.resitele.count()) - - @property - def body_neprepocitane_celkem(self): - if self.body_celkem is None: - return None - return inverze_vzorecku_na_prepocet(self.body_celkem, self.reseni.resitele.count()) - - @body_neprepocitane_celkem.setter - def body_neprepocitane_celkem(self, value): - if value is None: - self.body = None - else: - self.body_celkem = vzorecek_na_prepocet(value, self.reseni.resitele.count()) - - @property - def body_max(self): - if self.body_neprepocitane_max is None: - return None - return vzorecek_na_prepocet(self.body_neprepocitane_max, self.reseni.resitele.count()) - - @property - def body_neprepocitane_max(self): - if not isinstance(self.problem.get_real_instance(), am.Uloha): - return None - return self.problem.uloha.max_body - - def __str__(self): - return "{}, {}, {}".format(self.problem, self.reseni, self.body) - -def generate_filename(self, filename): - return os.path.join( - settings.SEMINAR_RESENI_DIR, - am.aux_generate_filename(self, filename) - ) - - -@reversion.register(ignore_duplicates=True) -class PrilohaReseni(bm.SeminarModelBase): - - class Meta: - db_table = 'seminar_priloha_reseni' - verbose_name = 'Příloha řešení' - verbose_name_plural = 'Přílohy řešení' - ordering = ['reseni', 'vytvoreno'] - - # Interní ID - id = models.AutoField(primary_key = True) - - reseni = models.ForeignKey(Reseni, verbose_name='řešení', related_name='prilohy', - on_delete=models.CASCADE) - - vytvoreno = models.DateTimeField('vytvořeno', default=timezone.now, blank=True, editable=False) - - soubor = models.FileField('soubor', upload_to = generate_filename) - - poznamka = models.TextField('neveřejná poznámka', blank=True, - help_text='Neveřejná poznámka k příloze řešení (plain text), např. o původu') - - res_poznamka = models.TextField('poznámka řešitele', blank=True, - help_text='Poznámka k příloze řešení, např. co daný soubor obsahuje') - - def __str__(self): - return str(self.soubor) - - def split(self): - "Vrátí cestu rozsekanou po složkách. To se hodí v templatech" - # Věřím, že tohle funguje, případně použít os.path nebo pathlib. - return self.soubor.url.split('/') - - -# Vazebna tabulka. Mozna se generuje automaticky. -@reversion.register(ignore_duplicates=True) -class Reseni_Resitele(models.Model): - - class Meta: - db_table = 'seminar_reseni_resitele' - verbose_name = 'Řešení řešitelů' - verbose_name_plural = 'Řešení řešitelů' - ordering = ['reseni', 'resitele'] - - # Interní ID - id = models.AutoField(primary_key = True) - - resitele = models.ForeignKey(Resitel, verbose_name='řešitel', on_delete=models.PROTECT) - - reseni = models.ForeignKey(Reseni, verbose_name='řešení', on_delete=models.CASCADE) - - # podil - jakou merou se ktery resitel podilel na danem reseni - # - pouziti v budoucnu, pokud by resitele nemeli dostat vsichni stejne bodu za spolecne reseni - - def __str__(self): - return '{} od {}'.format(self.reseni, self.resitel) - # NOTE: Poteciální DB HOG bez select_related +from odevzdavatko.models import Reseni class ReseniNode(tm.TreeNode): class Meta: diff --git a/soustredeni/models.py b/soustredeni/models.py index f71e6736..fb5c5239 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 -from seminar.models import tvorba as am +import seminar.models as am # tvorba logger = logging.getLogger(__name__) @@ -77,10 +77,6 @@ class Soustredeni(SeminarModelBase): #return reverse('seminar_soustredeni', kwargs={'pk': self.id}) return reverse('seminar_seznam_soustredeni') - def admin_url(self): - model_name = self.__class__.__name__.lower() - return reverse('admin:soustredeni_{}_change'.format(model_name), args=(self.id, )) - @reversion.register(ignore_duplicates=True) class Soustredeni_Ucastnici(SeminarModelBase): diff --git a/soustredeni/testutils.py b/soustredeni/testutils.py index 52e81d1c..6591cbb3 100644 --- a/soustredeni/testutils.py +++ b/soustredeni/testutils.py @@ -6,7 +6,7 @@ from typing import Sequence import lorem from .models import Soustredeni, Konfera -import seminar.models.tvorba as am +import seminar.models as am # tvorba import personalni.models as pm logger = logging.getLogger(__name__) diff --git a/split-apps-meta/create.notes b/split-apps-meta/create.notes index a48a17f8..2e9f6228 100644 --- a/split-apps-meta/create.notes +++ b/split-apps-meta/create.notes @@ -5,5 +5,6 @@ makemigrations ! Doplnit ForeignKeys (Vypadá to, že se dá vesměs zkopírovat předpis z models.py, jen místo prvního fieldu dát `to='app.model'. Dokonce asi funguje použít už novou aplikaci pro vazby v rámci aplikace.) To samé s ManyToManyFieldy (through= musí taky být 'app.model') (Zdá se, že jde dobastlit tuhle migraci polozpětně – doplnit co chybělo až podle toho, co vygeneruje migrace po zamanagování nového modelu.) + Alternativa: zagitovat si unmanaged model, upravit ho na `managed = True`, vyrobit migrace, vyrobit je ještě jednou (z nějakého důvodu) a vykrást ty. Pak `models.py` vrátit do unmanaged stavu a soubory s novými migracemi smazat bez náhrady (obdobné vzniknou znovu v případě potřeby). doplnit závislost na unmanage migrate diff --git a/tvorba/admin.py b/tvorba/admin.py index e6c2c64b..01880d5b 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.tvorba import Rocnik, ZmrazenaVysledkovka, Deadline, Uloha, Problem, Tema, Clanek, Cislo +from seminar.models import Rocnik, ZmrazenaVysledkovka, Deadline, Uloha, Problem, Tema, Clanek, Cislo # tvorba admin.site.register(Rocnik) admin.site.register(ZmrazenaVysledkovka)