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/models.py b/odevzdavatko/models.py new file mode 100644 index 00000000..65ab9a37 --- /dev/null +++ b/odevzdavatko/models.py @@ -0,0 +1,243 @@ +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 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'] + managed = False + + # 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í' + managed = False + + # 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'] + managed = False + + # 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'] + managed = False + + # 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/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/odevzdavatko.py b/seminar/models/odevzdavatko.py index bebc74f8..efc88e74 100644 --- a/seminar/models/odevzdavatko.py +++ b/seminar/models/odevzdavatko.py @@ -1,250 +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'] - managed = False - - # 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', - related_name='problem_old', - through='Hodnoceni') - - resitele = models.ManyToManyField(Resitel, verbose_name='autoři řešení', - related_name='resitele_old', - 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_old", - 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í' - managed = False - - # 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_old', 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_old', blank=True, null=True, on_delete=models.PROTECT) - - reseni = models.ForeignKey(Reseni, verbose_name='řešení', on_delete=models.CASCADE, related_name='hodnoceni_old') - - problem = models.ForeignKey(am.Problem, verbose_name='problém', - related_name='hodnoceni_old', 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'] - managed = False - - # Interní ID - id = models.AutoField(primary_key = True) - - reseni = models.ForeignKey(Reseni, verbose_name='řešení', related_name='prilohy_old', - 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'] - managed = False - - # Interní ID - id = models.AutoField(primary_key = True) - - resitele = models.ForeignKey(Resitel, verbose_name='řešitel', on_delete=models.PROTECT, related_name='rere_old') - - reseni = models.ForeignKey(Reseni, verbose_name='řešení', on_delete=models.CASCADE, related_name='rere_old') - - # 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/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