From 791a642504809e4041e8adf95af1d689fd426d1c Mon Sep 17 00:00:00 2001 From: Xander Vertegaal Date: Sun, 24 Mar 2024 11:28:00 +0100 Subject: [PATCH 01/16] Missing migration --- ...ription_ecclesiastical_regions_and_more.py | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 backend/space/migrations/0002_alter_spacedescription_ecclesiastical_regions_and_more.py diff --git a/backend/space/migrations/0002_alter_spacedescription_ecclesiastical_regions_and_more.py b/backend/space/migrations/0002_alter_spacedescription_ecclesiastical_regions_and_more.py new file mode 100644 index 00000000..2d0ffdcd --- /dev/null +++ b/backend/space/migrations/0002_alter_spacedescription_ecclesiastical_regions_and_more.py @@ -0,0 +1,49 @@ +# Generated by Django 4.2.7 on 2024-03-24 09:08 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("space", "0001_initial"), + ] + + operations = [ + migrations.AlterField( + model_name="spacedescription", + name="ecclesiastical_regions", + field=models.ManyToManyField( + help_text="Ecclesiastical regions referenced in this description", + through="space.EcclesiasticalRegionField", + to="space.ecclesiasticalregion", + ), + ), + migrations.AlterField( + model_name="spacedescription", + name="geographical_regions", + field=models.ManyToManyField( + help_text="Geographical regions referenced in this description", + through="space.GeographicalRegionField", + to="space.geographicalregion", + ), + ), + migrations.AlterField( + model_name="spacedescription", + name="political_regions", + field=models.ManyToManyField( + help_text="Political regions referenced in this description", + through="space.PoliticalRegionField", + to="space.politicalregion", + ), + ), + migrations.AlterField( + model_name="spacedescription", + name="structures", + field=models.ManyToManyField( + help_text="Man-made structures referenced in this description", + through="space.StructureField", + to="space.structure", + ), + ), + ] From a8c1614c77cef142c464428eef58e39a9354052b Mon Sep 17 00:00:00 2001 From: Xander Vertegaal Date: Sun, 24 Mar 2024 11:35:39 +0100 Subject: [PATCH 02/16] Remove reference model --- .../management/commands/create_dev_dataset.py | 31 +--------- backend/event/admin.py | 2 - backend/letter/admin.py | 2 - backend/source/admin.py | 11 ---- .../migrations/0004_delete_reference.py | 16 +++++ backend/source/models.py | 60 ------------------- backend/space/admin.py | 2 - 7 files changed, 17 insertions(+), 107 deletions(-) create mode 100644 backend/source/migrations/0004_delete_reference.py diff --git a/backend/core/management/commands/create_dev_dataset.py b/backend/core/management/commands/create_dev_dataset.py index c1ae496c..b54489d3 100644 --- a/backend/core/management/commands/create_dev_dataset.py +++ b/backend/core/management/commands/create_dev_dataset.py @@ -2,7 +2,7 @@ from django.conf import settings from django.core.management.base import CommandError, BaseCommand from faker import Faker -from source.models import Reference, Source +from source.models import Source from case_study.models import CaseStudy from event.models import ( @@ -137,7 +137,6 @@ def handle(self, *args, **options): fake, options, total=50, model=EpistolaryEventSelfTrigger ) self._create_sources(fake, options, total=50, model=Source) - self._create_references(fake, options, total=250, model=Reference) print("-" * 80) print("Development dataset created successfully.") @@ -352,31 +351,3 @@ def _create_epistolary_event_self_trigger(self, fake, options, total, model): def _create_sources(self, fake, options, total, model): unique_name = get_unique_name(source_names, Source) Source.objects.create(name=unique_name, bibliographical_info=fake.text()) - - @track_progress - def _create_references(self, fake, options, total, model): - random_content_type = ( - ContentType.objects.exclude( - app_label__in=["admin", "auth", "contenttypes", "sessions", "source"] - ) - .order_by("?") - .first() - ) - - random_objects = random_content_type.model_class().objects.all() - - if not random_objects.exists(): - return - - random_object_id = random_objects.order_by("?").first().id - - random_source = Source.objects.order_by("?").first() - - Reference.objects.create( - content_type=random_content_type, - object_id=random_object_id, - source=random_source, - location=f"chapter {random.randint(1, 10)}, page {random.randint(1, 100)}", - terminology=fake.words(nb=3, unique=True), - mention=random.choice(["direct", "implied"]), - ) diff --git a/backend/event/admin.py b/backend/event/admin.py index d403b84a..67b204b9 100644 --- a/backend/event/admin.py +++ b/backend/event/admin.py @@ -1,5 +1,4 @@ from django.contrib import admin -from source.admin import ReferenceInlineAdmin from . import models @@ -55,7 +54,6 @@ class LetterActionAdmin(admin.ModelAdmin): LetterActionGiftsAdmin, EventDateAdmin, RoleAdmin, - ReferenceInlineAdmin, ] exclude = ["letters"] diff --git a/backend/letter/admin.py b/backend/letter/admin.py index 7f1d8e72..25f6844a 100644 --- a/backend/letter/admin.py +++ b/backend/letter/admin.py @@ -1,5 +1,4 @@ from django.contrib import admin -from source.admin import ReferenceInlineAdmin from . import models @@ -38,7 +37,6 @@ class LetterAdmin(admin.ModelAdmin): LetterMaterialAdmin, LetterSenderAdmin, LetterAddresseesAdmin, - ReferenceInlineAdmin, ] diff --git a/backend/source/admin.py b/backend/source/admin.py index 57f97210..5a0e462b 100644 --- a/backend/source/admin.py +++ b/backend/source/admin.py @@ -5,14 +5,3 @@ @admin.register(models.Source) class SourceAdmin(admin.ModelAdmin): fields = ["name", "bibliographical_info"] - - -class ReferenceInlineAdmin(GenericStackedInline): - model = models.Reference - fields = [ - "source", - "location", - "terminology", - "mention", - ] - extra = 0 diff --git a/backend/source/migrations/0004_delete_reference.py b/backend/source/migrations/0004_delete_reference.py new file mode 100644 index 00000000..7e673dc8 --- /dev/null +++ b/backend/source/migrations/0004_delete_reference.py @@ -0,0 +1,16 @@ +# Generated by Django 4.2.7 on 2024-03-24 09:08 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("source", "0003_alter_reference_location_alter_reference_mention_and_more"), + ] + + operations = [ + migrations.DeleteModel( + name="Reference", + ), + ] diff --git a/backend/source/models.py b/backend/source/models.py index 81221f92..c078214e 100644 --- a/backend/source/models.py +++ b/backend/source/models.py @@ -1,7 +1,4 @@ from django.db import models -from django.contrib.postgres.fields import ArrayField -from django.contrib.contenttypes.fields import GenericForeignKey -from django.contrib.contenttypes.models import ContentType class Source(models.Model): @@ -22,60 +19,3 @@ class Source(models.Model): def __str__(self): return self.name - -class Reference(models.Model): - """ - References link information to sources. - - A Reference describes where and how a source refers to the information presented - in the database object. - """ - - # reference to the object - # c.f. https://docs.djangoproject.com/en/4.2/ref/contrib/contenttypes/#generic-relations - - content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) - object_id = models.PositiveIntegerField() - content_object = GenericForeignKey("content_type", "object_id") - - # reference to a source - - source = models.ForeignKey( - to=Source, - on_delete=models.CASCADE, - help_text="The source text in which this references occurs", - ) - - # description of the reference - - location = models.CharField( - max_length=200, - blank=True, - help_text="Specific location of the reference in the source text", - ) - - terminology = ArrayField( - models.CharField( - max_length=200, - ), - default=list, - blank=True, - size=5, - help_text="Terminology used in the source text to describe this entity", - ) - - mention = models.CharField( - max_length=32, - blank=True, - choices=[("direct", "directly mentioned"), ("implied", "implied")], - help_text="How is this information presented in the text?", - ) - - def __str__(self): - object = f"{self.content_object} ({self.content_type.model})" - source = f"{self.source}" - loc = f" ({self.location})" if self.location else "" - return f"reference to {object} in {source}{loc}" - - class Meta: - indexes = [models.Index(fields=["content_type", "object_id"])] diff --git a/backend/space/admin.py b/backend/space/admin.py index 3d854ea2..c62d83ac 100644 --- a/backend/space/admin.py +++ b/backend/space/admin.py @@ -1,5 +1,4 @@ from django.contrib import admin -from source.admin import ReferenceInlineAdmin from . import models @@ -72,5 +71,4 @@ class SpaceDescriptionAdmin(admin.ModelAdmin): GeographicalRegionFieldInlineAdmin, StructureFieldInlineAdmin, LandscapeFeatureInlineAdmin, - ReferenceInlineAdmin, ] From 68dec413389d418f270c166cab4ffe13565f88c2 Mon Sep 17 00:00:00 2001 From: Luka van der Plas Date: Tue, 9 Apr 2024 12:14:32 +0200 Subject: [PATCH 03/16] remove reference to ReferenceInlineAdmin --- backend/person/admin.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/backend/person/admin.py b/backend/person/admin.py index 12813fc7..e945b793 100644 --- a/backend/person/admin.py +++ b/backend/person/admin.py @@ -1,5 +1,4 @@ from django.contrib import admin -from source.admin import ReferenceInlineAdmin from . import models @@ -36,7 +35,6 @@ class AgentAdmin(admin.ModelAdmin): SocialStatusAdmin, AgentDateOfBirthAdmin, AgentDateOfDeathAdmin, - ReferenceInlineAdmin, ] From 30dea4935431c24adb579315ec1e69f65983385a Mon Sep 17 00:00:00 2001 From: Luka van der Plas Date: Tue, 9 Apr 2024 12:35:28 +0200 Subject: [PATCH 04/16] add base classes for historical/description --- backend/core/models.py | 63 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/backend/core/models.py b/backend/core/models.py index 757b9d98..c5628d1f 100644 --- a/backend/core/models.py +++ b/backend/core/models.py @@ -2,6 +2,10 @@ from django.core.validators import MinValueValidator, MaxValueValidator class Field(models.Model): + """ + A piece of information about an entity. + """ + certainty = models.IntegerField( choices=[ (0, "uncertain"), @@ -21,6 +25,7 @@ class Field(models.Model): class Meta: abstract = True + class LettercraftDate(models.Model): MIN_YEAR = 400 MAX_YEAR = 800 @@ -65,4 +70,60 @@ class Meta: def clean(self): if self.year_exact: self.year_lower = self.year_exact - self.year_upper = self.year_exact \ No newline at end of file + self.year_upper = self.year_exact + + +class Named(models.Model): + """ + An object with a name and description + """ + + name = models.CharField( + max_length=200, + blank=False, + help_text="A name to identify this space when entering data", + ) + description = models.TextField( + blank=True, + ) + + class Meta: + abstract = True + + def __str__(self): + return self.name + + +class HistoricalEntity(Named, models.Model): + + identifiable = models.BooleanField( + default=True, + null=False, + help_text="Whether this entity is identifiable (i.e. can be cross-referenced between descriptions), or a generic description", + ) + + class Meta: + abstract = True + + +class EntityDescription(Named, models.Model): + """ + A description of an entity (person, object, location, event) in a narrative source. + + Descriptions may refer to HistoricalEntity targets. + """ + + class Meta: + abstract = True + + +class DescriptionField(Field, models.Model): + """ + A piece of information contained in an EntityDescription. + + An extension of Field that can contain extra information about how and where the + information is presented in the source. + """ + + class Meta: + abstract = True From 4484d81015948b6b2983a3b1eb4be5a0d40dc70f Mon Sep 17 00:00:00 2001 From: Luka van der Plas Date: Tue, 9 Apr 2024 12:35:38 +0200 Subject: [PATCH 05/16] apply blase classes to space app --- ...lesiasticalregion_identifiable_and_more.py | 58 ++++++++++++++++ backend/space/models.py | 68 ++++--------------- 2 files changed, 70 insertions(+), 56 deletions(-) create mode 100644 backend/space/migrations/0003_alter_ecclesiasticalregion_identifiable_and_more.py diff --git a/backend/space/migrations/0003_alter_ecclesiasticalregion_identifiable_and_more.py b/backend/space/migrations/0003_alter_ecclesiasticalregion_identifiable_and_more.py new file mode 100644 index 00000000..c52418a3 --- /dev/null +++ b/backend/space/migrations/0003_alter_ecclesiasticalregion_identifiable_and_more.py @@ -0,0 +1,58 @@ +# Generated by Django 4.2.7 on 2024-04-09 10:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('space', '0002_alter_spacedescription_ecclesiastical_regions_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='ecclesiasticalregion', + name='identifiable', + field=models.BooleanField(default=True, help_text='Whether this entity is identifiable (i.e. can be cross-referenced between descriptions), or a generic description'), + ), + migrations.AlterField( + model_name='ecclesiasticalregion', + name='name', + field=models.CharField(help_text='A name to identify this space when entering data', max_length=200), + ), + migrations.AlterField( + model_name='geographicalregion', + name='identifiable', + field=models.BooleanField(default=True, help_text='Whether this entity is identifiable (i.e. can be cross-referenced between descriptions), or a generic description'), + ), + migrations.AlterField( + model_name='geographicalregion', + name='name', + field=models.CharField(help_text='A name to identify this space when entering data', max_length=200), + ), + migrations.AlterField( + model_name='politicalregion', + name='identifiable', + field=models.BooleanField(default=True, help_text='Whether this entity is identifiable (i.e. can be cross-referenced between descriptions), or a generic description'), + ), + migrations.AlterField( + model_name='politicalregion', + name='name', + field=models.CharField(help_text='A name to identify this space when entering data', max_length=200), + ), + migrations.AlterField( + model_name='spacedescription', + name='description', + field=models.TextField(blank=True), + ), + migrations.AlterField( + model_name='structure', + name='identifiable', + field=models.BooleanField(default=True, help_text='Whether this entity is identifiable (i.e. can be cross-referenced between descriptions), or a generic description'), + ), + migrations.AlterField( + model_name='structure', + name='name', + field=models.CharField(help_text='A name to identify this space when entering data', max_length=200), + ), + ] diff --git a/backend/space/models.py b/backend/space/models.py index 3bb30dc6..e162dc88 100644 --- a/backend/space/models.py +++ b/backend/space/models.py @@ -2,27 +2,17 @@ from django.contrib import admin import itertools -from core.models import Field +from core.models import DescriptionField, HistoricalEntity, EntityDescription from space import validators -class SpaceDescription(models.Model): + +class SpaceDescription(EntityDescription, models.Model): """ The representation of a space within a source text. This model compounds all different aspects of space (geographical, political, etc.). """ - name = models.CharField( - max_length=200, - blank=False, - help_text="A name to identify this space when entering data", - ) - - description = models.TextField( - blank=True, - help_text="Longer description of this place that can be used to identify it", - ) - political_regions = models.ManyToManyField( to="PoliticalRegion", through="PoliticalRegionField", @@ -47,42 +37,8 @@ class SpaceDescription(models.Model): help_text="Man-made structures referenced in this description", ) - def __str__(self): - return self.name - - -class NamedSpace(models.Model): - """ - Abstract class for "Named" regions, i.e. ones that can be - identified as named entities. - """ - - name = models.CharField( - max_length=200, - unique=True, - blank=False, - ) - - description = models.TextField( - blank=True, - ) - - identifiable = models.BooleanField( - default=True, - null=False, - help_text="Whether this place is an identifiable location that can be cross-referenced between descriptions, or a generic description", - ) - - # may be expanded with geo data? - - def __str__(self): - return self.name - - class Meta: - abstract = True - -class PoliticalRegion(NamedSpace, models.Model): +class PoliticalRegion(HistoricalEntity, models.Model): """ A political region, e.g. a kingdom or duchy """ @@ -90,7 +46,7 @@ class PoliticalRegion(NamedSpace, models.Model): pass -class EcclesiasticalRegion(NamedSpace, models.Model): +class EcclesiasticalRegion(HistoricalEntity, models.Model): """ An ecclesiastical region, e.g. a diocese """ @@ -98,7 +54,7 @@ class EcclesiasticalRegion(NamedSpace, models.Model): pass -class GeographicalRegion(NamedSpace, models.Model): +class GeographicalRegion(HistoricalEntity, models.Model): """ A geographical region or location, e.g. "the Pyrenees". """ @@ -106,7 +62,7 @@ class GeographicalRegion(NamedSpace, models.Model): pass -class Structure(NamedSpace, models.Model): +class Structure(HistoricalEntity, models.Model): """ A structure is a man-made site. @@ -163,31 +119,31 @@ def clean(self): ) -class PoliticalRegionField(Field, models.Model): +class PoliticalRegionField(DescriptionField, models.Model): space = models.ForeignKey(to=SpaceDescription, on_delete=models.CASCADE) political_region = models.ForeignKey(to=PoliticalRegion, on_delete=models.CASCADE) -class EcclesiasticalRegionField(Field, models.Model): +class EcclesiasticalRegionField(DescriptionField, models.Model): space = models.ForeignKey(to=SpaceDescription, on_delete=models.CASCADE) ecclesiastical_region = models.ForeignKey( to=EcclesiasticalRegion, on_delete=models.CASCADE ) -class GeographicalRegionField(Field, models.Model): +class GeographicalRegionField(DescriptionField, models.Model): space = models.ForeignKey(to=SpaceDescription, on_delete=models.CASCADE) geographical_region = models.ForeignKey( to=GeographicalRegion, on_delete=models.CASCADE ) -class StructureField(Field, models.Model): +class StructureField(DescriptionField, models.Model): space = models.ForeignKey(to=SpaceDescription, on_delete=models.CASCADE) structure = models.ForeignKey(to=Structure, on_delete=models.CASCADE) -class LandscapeFeature(Field, models.Model): +class LandscapeFeature(DescriptionField, models.Model): """ A landscape feature describes natural or geological aspects of a space, e.g. "a forest", "a hill", "a cave". From a53bbf06981fc81181df2ebe2bac5e311e7d02f0 Mon Sep 17 00:00:00 2001 From: Luka van der Plas Date: Tue, 9 Apr 2024 12:55:03 +0200 Subject: [PATCH 06/16] add source field to EntityDescription --- backend/core/models.py | 8 ++++++++ .../0004_spacedescription_source.py | 20 +++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 backend/space/migrations/0004_spacedescription_source.py diff --git a/backend/core/models.py b/backend/core/models.py index c5628d1f..04ab5d90 100644 --- a/backend/core/models.py +++ b/backend/core/models.py @@ -1,5 +1,6 @@ from django.db import models from django.core.validators import MinValueValidator, MaxValueValidator +from source.models import Source class Field(models.Model): """ @@ -113,6 +114,13 @@ class EntityDescription(Named, models.Model): Descriptions may refer to HistoricalEntity targets. """ + source = models.ForeignKey( + to=Source, + null=True, + on_delete=models.CASCADE, + help_text="Source text containing this description", + ) + class Meta: abstract = True diff --git a/backend/space/migrations/0004_spacedescription_source.py b/backend/space/migrations/0004_spacedescription_source.py new file mode 100644 index 00000000..1f7f5d79 --- /dev/null +++ b/backend/space/migrations/0004_spacedescription_source.py @@ -0,0 +1,20 @@ +# Generated by Django 4.2.7 on 2024-04-09 10:54 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('source', '0004_delete_reference'), + ('space', '0003_alter_ecclesiasticalregion_identifiable_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='spacedescription', + name='source', + field=models.ForeignKey(help_text='Source text containing this description', null=True, on_delete=django.db.models.deletion.CASCADE, to='source.source'), + ), + ] From 959dc40f3282d3e0c773b937e183b39d8ae9336a Mon Sep 17 00:00:00 2001 From: Luka van der Plas Date: Tue, 9 Apr 2024 13:05:56 +0200 Subject: [PATCH 07/16] add placeholder value for source --- .../migrations/0005_placeholder_source.py | 32 +++++++++++++++++ .../0005_fill_in_placeholder_source.py | 36 +++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 backend/source/migrations/0005_placeholder_source.py create mode 100644 backend/space/migrations/0005_fill_in_placeholder_source.py diff --git a/backend/source/migrations/0005_placeholder_source.py b/backend/source/migrations/0005_placeholder_source.py new file mode 100644 index 00000000..4824b1c5 --- /dev/null +++ b/backend/source/migrations/0005_placeholder_source.py @@ -0,0 +1,32 @@ +# Generated by Django 4.2.7 on 2024-04-09 10:55 + +from django.db import migrations + + +def create_placeholder_source(apps, schema_editor): + Source = apps.get_model("source", "Source") + Source.objects.create( + name="MISSING SOURCE", + bibliographical_info="This is a placeholder value for older data that is mising source information", + ) + + +def remove_placeholder_source(apps, schema_editor): + Source = apps.get_model("source", "Source") + placeholder = Source.objects.filter(name="MISSING SOURCE") + if placeholder.exists(): + placeholder.delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ("source", "0004_delete_reference"), + ] + + operations = [ + migrations.RunPython( + create_placeholder_source, + reverse_code=remove_placeholder_source, + ) + ] diff --git a/backend/space/migrations/0005_fill_in_placeholder_source.py b/backend/space/migrations/0005_fill_in_placeholder_source.py new file mode 100644 index 00000000..20674773 --- /dev/null +++ b/backend/space/migrations/0005_fill_in_placeholder_source.py @@ -0,0 +1,36 @@ +# Generated by Django 4.2.7 on 2024-04-09 10:55 + +from django.db import migrations + + +def fill_in_placeholder_source(apps, schema_editor): + SpaceDescription = apps.get_model("space", "SpaceDescription") + Source = apps.get_model("source", "Source") + placeholder = Source.objects.get(name="MISSING SOURCE") + for obj in SpaceDescription.objects.filter(source__isnull=True): + obj.source = placeholder + obj.save() + + +def clear_placeholder_source(apps, schema_editor): + SpaceDescription = apps.get_model("space", "SpaceDescription") + Source = apps.get_model("source", "Source") + placeholder = Source.objects.get(name="MISSING SOURCE") + for obj in SpaceDescription.objects.filter(source=placeholder): + obj.source = None + obj.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ("space", "0004_spacedescription_source"), + ("source", "0005_placeholder_source"), + ] + + operations = [ + migrations.RunPython( + fill_in_placeholder_source, + reverse_code=clear_placeholder_source, + ) + ] From 3a71f883967a011623ba369605be8227fba56e61 Mon Sep 17 00:00:00 2001 From: Luka van der Plas Date: Tue, 9 Apr 2024 13:08:53 +0200 Subject: [PATCH 08/16] make source field non-nullable --- backend/core/models.py | 1 - .../0006_alter_spacedescription_source.py | 20 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 backend/space/migrations/0006_alter_spacedescription_source.py diff --git a/backend/core/models.py b/backend/core/models.py index 04ab5d90..2cd47be7 100644 --- a/backend/core/models.py +++ b/backend/core/models.py @@ -116,7 +116,6 @@ class EntityDescription(Named, models.Model): source = models.ForeignKey( to=Source, - null=True, on_delete=models.CASCADE, help_text="Source text containing this description", ) diff --git a/backend/space/migrations/0006_alter_spacedescription_source.py b/backend/space/migrations/0006_alter_spacedescription_source.py new file mode 100644 index 00000000..40470eb8 --- /dev/null +++ b/backend/space/migrations/0006_alter_spacedescription_source.py @@ -0,0 +1,20 @@ +# Generated by Django 4.2.7 on 2024-04-09 11:07 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('source', '0005_placeholder_source'), + ('space', '0005_fill_in_placeholder_source'), + ] + + operations = [ + migrations.AlterField( + model_name='spacedescription', + name='source', + field=models.ForeignKey(help_text='Source text containing this description', on_delete=django.db.models.deletion.CASCADE, to='source.source'), + ), + ] From 2b2ed06d936daa3ed9f1b9359b3d82a022851fd3 Mon Sep 17 00:00:00 2001 From: Luka van der Plas Date: Tue, 9 Apr 2024 13:15:33 +0200 Subject: [PATCH 09/16] expand description models --- backend/core/models.py | 36 ++++- ...calregionfield_source_location_and_more.py | 149 ++++++++++++++++++ 2 files changed, 184 insertions(+), 1 deletion(-) create mode 100644 backend/space/migrations/0007_ecclesiasticalregionfield_source_location_and_more.py diff --git a/backend/core/models.py b/backend/core/models.py index 2cd47be7..59059ecd 100644 --- a/backend/core/models.py +++ b/backend/core/models.py @@ -1,6 +1,7 @@ from django.db import models from django.core.validators import MinValueValidator, MaxValueValidator from source.models import Source +from django.contrib.postgres.fields import ArrayField class Field(models.Model): """ @@ -82,10 +83,11 @@ class Named(models.Model): name = models.CharField( max_length=200, blank=False, - help_text="A name to identify this space when entering data", + help_text="A name to help identify this object", ) description = models.TextField( blank=True, + help_text="Longer description to help identify this object", ) class Meta: @@ -119,6 +121,17 @@ class EntityDescription(Named, models.Model): on_delete=models.CASCADE, help_text="Source text containing this description", ) + source_mention = models.CharField( + max_length=32, + blank=True, + choices=[("direct", "directly mentioned"), ("implied", "implied")], + help_text="How is this entity presented in the text?", + ) + source_location = models.CharField( + max_length=200, + blank=True, + help_text="Specific location(s) where the entity is mentioned or described in the source text", + ) class Meta: abstract = True @@ -132,5 +145,26 @@ class DescriptionField(Field, models.Model): information is presented in the source. """ + source_mention = models.CharField( + max_length=32, + blank=True, + choices=[("direct", "directly mentioned"), ("implied", "implied")], + help_text="How is this information presented in the text?", + ) + source_location = models.CharField( + max_length=200, + blank=True, + help_text="Specific location of the information in the source text", + ) + source_terminology = ArrayField( + models.CharField( + max_length=200, + ), + default=list, + blank=True, + size=5, + help_text="Relevant terminology used in the source text", + ) + class Meta: abstract = True diff --git a/backend/space/migrations/0007_ecclesiasticalregionfield_source_location_and_more.py b/backend/space/migrations/0007_ecclesiasticalregionfield_source_location_and_more.py new file mode 100644 index 00000000..8fa97585 --- /dev/null +++ b/backend/space/migrations/0007_ecclesiasticalregionfield_source_location_and_more.py @@ -0,0 +1,149 @@ +# Generated by Django 4.2.7 on 2024-04-09 11:14 + +import django.contrib.postgres.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('space', '0006_alter_spacedescription_source'), + ] + + operations = [ + migrations.AddField( + model_name='ecclesiasticalregionfield', + name='source_location', + field=models.CharField(blank=True, help_text='Specific location of the information in the source text', max_length=200), + ), + migrations.AddField( + model_name='ecclesiasticalregionfield', + name='source_mention', + field=models.CharField(blank=True, choices=[('direct', 'directly mentioned'), ('implied', 'implied')], help_text='How is this information presented in the text?', max_length=32), + ), + migrations.AddField( + model_name='ecclesiasticalregionfield', + name='source_terminology', + field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=200), blank=True, default=list, help_text='Relevant terminology used in the source text', size=5), + ), + migrations.AddField( + model_name='geographicalregionfield', + name='source_location', + field=models.CharField(blank=True, help_text='Specific location of the information in the source text', max_length=200), + ), + migrations.AddField( + model_name='geographicalregionfield', + name='source_mention', + field=models.CharField(blank=True, choices=[('direct', 'directly mentioned'), ('implied', 'implied')], help_text='How is this information presented in the text?', max_length=32), + ), + migrations.AddField( + model_name='geographicalregionfield', + name='source_terminology', + field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=200), blank=True, default=list, help_text='Relevant terminology used in the source text', size=5), + ), + migrations.AddField( + model_name='landscapefeature', + name='source_location', + field=models.CharField(blank=True, help_text='Specific location of the information in the source text', max_length=200), + ), + migrations.AddField( + model_name='landscapefeature', + name='source_mention', + field=models.CharField(blank=True, choices=[('direct', 'directly mentioned'), ('implied', 'implied')], help_text='How is this information presented in the text?', max_length=32), + ), + migrations.AddField( + model_name='landscapefeature', + name='source_terminology', + field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=200), blank=True, default=list, help_text='Relevant terminology used in the source text', size=5), + ), + migrations.AddField( + model_name='politicalregionfield', + name='source_location', + field=models.CharField(blank=True, help_text='Specific location of the information in the source text', max_length=200), + ), + migrations.AddField( + model_name='politicalregionfield', + name='source_mention', + field=models.CharField(blank=True, choices=[('direct', 'directly mentioned'), ('implied', 'implied')], help_text='How is this information presented in the text?', max_length=32), + ), + migrations.AddField( + model_name='politicalregionfield', + name='source_terminology', + field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=200), blank=True, default=list, help_text='Relevant terminology used in the source text', size=5), + ), + migrations.AddField( + model_name='spacedescription', + name='source_location', + field=models.CharField(blank=True, help_text='Specific location(s) where the entity is mentioned or described in the source text', max_length=200), + ), + migrations.AddField( + model_name='spacedescription', + name='source_mention', + field=models.CharField(blank=True, choices=[('direct', 'directly mentioned'), ('implied', 'implied')], help_text='How is this entity presented in the text?', max_length=32), + ), + migrations.AddField( + model_name='structurefield', + name='source_location', + field=models.CharField(blank=True, help_text='Specific location of the information in the source text', max_length=200), + ), + migrations.AddField( + model_name='structurefield', + name='source_mention', + field=models.CharField(blank=True, choices=[('direct', 'directly mentioned'), ('implied', 'implied')], help_text='How is this information presented in the text?', max_length=32), + ), + migrations.AddField( + model_name='structurefield', + name='source_terminology', + field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=200), blank=True, default=list, help_text='Relevant terminology used in the source text', size=5), + ), + migrations.AlterField( + model_name='ecclesiasticalregion', + name='description', + field=models.TextField(blank=True, help_text='Longer description to help identify this object'), + ), + migrations.AlterField( + model_name='ecclesiasticalregion', + name='name', + field=models.CharField(help_text='A name to help identify this object', max_length=200), + ), + migrations.AlterField( + model_name='geographicalregion', + name='description', + field=models.TextField(blank=True, help_text='Longer description to help identify this object'), + ), + migrations.AlterField( + model_name='geographicalregion', + name='name', + field=models.CharField(help_text='A name to help identify this object', max_length=200), + ), + migrations.AlterField( + model_name='politicalregion', + name='description', + field=models.TextField(blank=True, help_text='Longer description to help identify this object'), + ), + migrations.AlterField( + model_name='politicalregion', + name='name', + field=models.CharField(help_text='A name to help identify this object', max_length=200), + ), + migrations.AlterField( + model_name='spacedescription', + name='description', + field=models.TextField(blank=True, help_text='Longer description to help identify this object'), + ), + migrations.AlterField( + model_name='spacedescription', + name='name', + field=models.CharField(help_text='A name to help identify this object', max_length=200), + ), + migrations.AlterField( + model_name='structure', + name='description', + field=models.TextField(blank=True, help_text='Longer description to help identify this object'), + ), + migrations.AlterField( + model_name='structure', + name='name', + field=models.CharField(help_text='A name to help identify this object', max_length=200), + ), + ] From 418d9e01bcfd400551fe0f7855f7b23bece87131 Mon Sep 17 00:00:00 2001 From: Luka van der Plas Date: Tue, 9 Apr 2024 13:22:51 +0200 Subject: [PATCH 10/16] admin forms --- backend/core/admin.py | 28 +++++++++++++++++++++++++++- backend/space/admin.py | 21 ++++++++++++++------- 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/backend/core/admin.py b/backend/core/admin.py index 8c38f3f3..a4a0d83f 100644 --- a/backend/core/admin.py +++ b/backend/core/admin.py @@ -1,3 +1,29 @@ from django.contrib import admin -# Register your models here. +named_fieldset = ( + "Name and description", + { + "description": "Basic information to help identify this description", + "fields": ["name", "description"], + }, +) + +description_source_fieldset = ( + "Source information", + { + "description": "Information about the source from which this description is taken.", + "fields": [ + "source", + "source_location", + "source_mention", + ], + }, +) + +field_fields = ["certainty", "note"] + +description_field_fields = [ + "source_mention", + "source_location", + "source_terminology", +] + field_fields diff --git a/backend/space/admin.py b/backend/space/admin.py index c62d83ac..567aa25e 100644 --- a/backend/space/admin.py +++ b/backend/space/admin.py @@ -1,5 +1,7 @@ from django.contrib import admin + from . import models +from core import admin as core_admin @admin.register(models.PoliticalRegion) @@ -30,41 +32,46 @@ class StructureAdmin(admin.ModelAdmin): class PoliticalRegionFieldInlineAdmin(admin.StackedInline): verbose_name = "Political region reference" model = models.PoliticalRegionField - fields = ["space", "political_region", "certainty", "note"] + fields = ["space", "political_region"] + core_admin.description_field_fields extra = 0 class EcclesiasticalRegionFieldInlineAdmin(admin.StackedInline): verbose_name = "Ecclesiastical region reference" model = models.EcclesiasticalRegionField - fields = ["space", "ecclesiastical_region", "certainty", "note"] + fields = ["space", "ecclesiastical_region"] + core_admin.description_field_fields extra = 0 class GeographicalRegionFieldInlineAdmin(admin.StackedInline): verbose_name = "Geographical region reference" model = models.GeographicalRegionField - fields = ["space", "geographical_region", "certainty", "note"] + fields = ["space", "geographical_region"] + core_admin.description_field_fields extra = 0 class StructureFieldInlineAdmin(admin.StackedInline): verbose_name = "Structure reference" model = models.StructureField - fields = ["space", "structure", "certainty", "note"] + fields = ["space", "structure"] + core_admin.description_field_fields extra = 0 class LandscapeFeatureInlineAdmin(admin.StackedInline): model = models.LandscapeFeature - fields = ["landscape", "certainty", "note"] + fields = ["landscape"] + core_admin.description_field_fields extra = 0 @admin.register(models.SpaceDescription) class SpaceDescriptionAdmin(admin.ModelAdmin): - list_display = ["name", "description"] - fields = ["name", "description"] + list_display = ["name", "description", "source"] + list_filter = ["source"] + search_fields = ["name", "description"] + fieldsets = [ + core_admin.named_fieldset, + core_admin.description_source_fieldset, + ] inlines = [ PoliticalRegionFieldInlineAdmin, EcclesiasticalRegionFieldInlineAdmin, From c9f30e3125061a030cd0ba0b863bb972d9482faf Mon Sep 17 00:00:00 2001 From: Luka van der Plas Date: Tue, 9 Apr 2024 13:23:55 +0200 Subject: [PATCH 11/16] protect source deletion --- backend/core/models.py | 2 +- .../0008_alter_spacedescription_source.py | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 backend/space/migrations/0008_alter_spacedescription_source.py diff --git a/backend/core/models.py b/backend/core/models.py index 59059ecd..077a7cda 100644 --- a/backend/core/models.py +++ b/backend/core/models.py @@ -118,7 +118,7 @@ class EntityDescription(Named, models.Model): source = models.ForeignKey( to=Source, - on_delete=models.CASCADE, + on_delete=models.PROTECT, help_text="Source text containing this description", ) source_mention = models.CharField( diff --git a/backend/space/migrations/0008_alter_spacedescription_source.py b/backend/space/migrations/0008_alter_spacedescription_source.py new file mode 100644 index 00000000..29d9749d --- /dev/null +++ b/backend/space/migrations/0008_alter_spacedescription_source.py @@ -0,0 +1,20 @@ +# Generated by Django 4.2.7 on 2024-04-09 11:23 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('source', '0005_placeholder_source'), + ('space', '0007_ecclesiasticalregionfield_source_location_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='spacedescription', + name='source', + field=models.ForeignKey(help_text='Source text containing this description', on_delete=django.db.models.deletion.PROTECT, to='source.source'), + ), + ] From b6eff185e11f441f05071d865abc19aea76ad962 Mon Sep 17 00:00:00 2001 From: Luka van der Plas Date: Tue, 9 Apr 2024 13:35:29 +0200 Subject: [PATCH 12/16] clear agent data --- backend/conftest.py | 48 ++-- .../management/commands/create_dev_dataset.py | 165 ++++++------ backend/event/admin.py | 28 +- ..._remove_letteraction_actors_delete_role.py | 20 ++ backend/event/models.py | 106 ++++---- backend/letter/admin.py | 24 +- ...09_remove_lettersenders_letter_and_more.py | 31 +++ backend/letter/models.py | 100 ++++---- backend/person/admin.py | 58 ++--- ..._remove_agentdateofbirth_agent_and_more.py | 50 ++++ backend/person/models.py | 240 +++++++++--------- backend/person/tests/test_person_models.py | 56 ++-- 12 files changed, 515 insertions(+), 411 deletions(-) create mode 100644 backend/event/migrations/0010_remove_letteraction_actors_delete_role.py create mode 100644 backend/letter/migrations/0009_remove_lettersenders_letter_and_more.py create mode 100644 backend/person/migrations/0012_remove_agentdateofbirth_agent_and_more.py diff --git a/backend/conftest.py b/backend/conftest.py index af0af1bb..20800b45 100644 --- a/backend/conftest.py +++ b/backend/conftest.py @@ -8,7 +8,7 @@ WorldEvent, LetterEventDate, ) -from person.models import Agent +# from person.models import Agent @pytest.fixture() @@ -19,36 +19,36 @@ def letter(db): return letter -@pytest.fixture() -def agent(db): - agent = Agent.objects.create() - agent.name = "Bert" - agent.save() - return agent +# @pytest.fixture() +# def agent(db): +# agent = Agent.objects.create() +# agent.name = "Bert" +# agent.save() +# return agent -@pytest.fixture() -def agent_2(db): - agent = Agent.objects.create() - agent.name = "Ernie" - agent.save() - return agent +# @pytest.fixture() +# def agent_2(db): +# agent = Agent.objects.create() +# agent.name = "Ernie" +# agent.save() +# return agent -@pytest.fixture() -def agent_group(db): - agent_group = Agent.objects.create() - agent_group.name = "The Muppets" - agent_group.is_group = True - agent_group.save() - return agent_group +# @pytest.fixture() +# def agent_group(db): +# agent_group = Agent.objects.create() +# agent_group.name = "The Muppets" +# agent_group.is_group = True +# agent_group.save() +# return agent_group @pytest.fixture() -def letter_action_writing(db, letter, agent): +def letter_action_writing(db, letter): letter_action = LetterAction.objects.create() letter_action.letters.add(letter) - letter_action.actors.add(agent) + # letter_action.actors.add(agent) LetterActionCategory.objects.create( letter_action=letter_action, @@ -63,10 +63,10 @@ def letter_action_writing(db, letter, agent): @pytest.fixture() -def letter_action_reading(db, letter, agent_2): +def letter_action_reading(db, letter): letter_action = LetterAction.objects.create() letter_action.letters.add(letter) - letter_action.actors.add(agent_2) + # letter_action.actors.add(agent_2) LetterActionCategory.objects.create( letter_action=letter_action, diff --git a/backend/core/management/commands/create_dev_dataset.py b/backend/core/management/commands/create_dev_dataset.py index b54489d3..a06aef52 100644 --- a/backend/core/management/commands/create_dev_dataset.py +++ b/backend/core/management/commands/create_dev_dataset.py @@ -12,26 +12,27 @@ LetterAction, LetterActionCategory, LetterEventDate, - Role, + # Role, WorldEvent, WorldEventSelfTrigger, WorldEventTrigger, ) -from person.models import ( - Agent, - AgentDateOfBirth, - AgentDateOfDeath, - Gender, - StatusMarker, -) + +# from person.models import ( +# Agent, +# AgentDateOfBirth, +# AgentDateOfDeath, +# Gender, +# StatusMarker, +# ) from letter.models import ( Category, Gift, Letter, - LetterAddressees, + # LetterAddressees, LetterCategory, LetterMaterial, - LetterSenders, + # LetterSenders, ) import random @@ -117,8 +118,8 @@ def handle(self, *args, **options): self._create_epistolary_events( fake, options, total=40, model=EpistolaryEvent ) - self._create_status_markers(fake, options, total=50, model=StatusMarker) - self._create_agents(fake, options, total=100, model=Agent) + # self._create_status_markers(fake, options, total=50, model=StatusMarker) + # self._create_agents(fake, options, total=100, model=Agent) self._create_letter_categories(fake, options, total=10, model=Category) self._create_letters(fake, options, total=200, model=Letter) self._create_gifts(fake, options, total=50, model=Gift) @@ -158,55 +159,57 @@ def _create_epistolary_events(self, fake, options, total, model): @track_progress def _create_status_markers(self, fake, options, total, model): - StatusMarker.objects.create(name=fake.job(), description=fake.text()) + # StatusMarker.objects.create(name=fake.job(), description=fake.text()) + pass @track_progress def _create_agents(self, fake: Faker, options, total, model): - is_group = random.choice([True, False]) - - if is_group is True: - gender_options = [ - gender for gender in Gender.values if gender != Gender.MIXED - ] - agent_names = random.sample(group_names, k=random.randint(0, 3)) - else: - gender_options = Gender.values - agent_names = [fake.name() for _ in range(random.randint(0, 3))] - - agent = Agent.objects.create( - is_group=is_group, gender=random.choice(gender_options) - ) - - for name in agent_names: - agent.names.create( - value=name, - **self.fake_field_value(fake), - ) - - if is_group is False: - if random.choice([True, False]): - AgentDateOfBirth.objects.create( - agent=agent, - **self.fake_date_value(fake), - **self.fake_field_value(fake), - ) - - if random.choice([True, False]): - AgentDateOfDeath.objects.create( - agent=agent, - **self.fake_date_value(fake), - **self.fake_field_value(fake), - ) - - for _ in range(random.randint(0, 2)): - agent.social_statuses.create( - status_marker=get_random_model_object(StatusMarker), - **self.fake_date_value(fake), - **self.fake_field_value(fake), - ) - - agent.clean() - agent.save() + pass + # is_group = random.choice([True, False]) + + # if is_group is True: + # gender_options = [ + # gender for gender in Gender.values if gender != Gender.MIXED + # ] + # agent_names = random.sample(group_names, k=random.randint(0, 3)) + # else: + # gender_options = Gender.values + # agent_names = [fake.name() for _ in range(random.randint(0, 3))] + + # agent = Agent.objects.create( + # is_group=is_group, gender=random.choice(gender_options) + # ) + + # for name in agent_names: + # agent.names.create( + # value=name, + # **self.fake_field_value(fake), + # ) + + # if is_group is False: + # if random.choice([True, False]): + # AgentDateOfBirth.objects.create( + # agent=agent, + # **self.fake_date_value(fake), + # **self.fake_field_value(fake), + # ) + + # if random.choice([True, False]): + # AgentDateOfDeath.objects.create( + # agent=agent, + # **self.fake_date_value(fake), + # **self.fake_field_value(fake), + # ) + + # for _ in range(random.randint(0, 2)): + # agent.social_statuses.create( + # status_marker=get_random_model_object(StatusMarker), + # **self.fake_date_value(fake), + # **self.fake_field_value(fake), + # ) + + # agent.clean() + # agent.save() @track_progress def _create_letter_categories(self, fake: Faker, *args, **kwargs): @@ -217,8 +220,8 @@ def _create_letter_categories(self, fake: Faker, *args, **kwargs): @track_progress def _create_letters(self, fake: Faker, *args, **kwargs): - senders = get_random_model_objects(Agent, min_amount=2, max_amount=5) - addressees = get_random_model_objects(Agent, min_amount=2, max_amount=5) + # senders = get_random_model_objects(Agent, min_amount=2, max_amount=5) + # addressees = get_random_model_objects(Agent, min_amount=2, max_amount=5) subject = ", ".join(fake.words(nb=3, unique=True)) letter = Letter.objects.create( @@ -239,17 +242,17 @@ def _create_letters(self, fake: Faker, *args, **kwargs): **self.fake_field_value(fake), ) - sender_object = LetterSenders.objects.create( - letter=letter, - **self.fake_field_value(fake), - ) - sender_object.senders.set(senders) + # sender_object = LetterSenders.objects.create( + # letter=letter, + # **self.fake_field_value(fake), + # ) + # sender_object.senders.set(senders) - addressees_object = LetterAddressees.objects.create( - letter=letter, - **self.fake_field_value(fake), - ) - addressees_object.addressees.set(addressees) + # addressees_object = LetterAddressees.objects.create( + # letter=letter, + # **self.fake_field_value(fake), + # ) + # addressees_object.addressees.set(addressees) @track_progress def _create_letter_actions(self, fake: Faker, *args, **kwargs): @@ -279,26 +282,26 @@ def _create_letter_actions(self, fake: Faker, *args, **kwargs): **self.fake_field_value(fake), ) - for _ in range(random.randint(1, 5)): - Role.objects.create( - agent=get_random_model_object(Agent), - letter_action=action, - present=random.choice([True, False]), - role=random.choice(Role.RoleOptions.choices)[0], - description=fake.text(), - **self.fake_field_value(fake), - ) + # for _ in range(random.randint(1, 5)): + # Role.objects.create( + # agent=get_random_model_object(Agent), + # letter_action=action, + # present=random.choice([True, False]), + # role=random.choice(Role.RoleOptions.choices)[0], + # description=fake.text(), + # **self.fake_field_value(fake), + # ) @track_progress def _create_gifts(self, fake, options, total, model): unique_name = get_unique_name(gift_names, Gift) - gifter = get_random_model_object(Agent, allow_null=True) + # gifter = get_random_model_object(Agent, allow_null=True) Gift.objects.create( name=unique_name, material=random.choice(Gift.Material.choices)[0], - gifted_by=gifter, + # gifted_by=gifter, description=fake.text(), ) diff --git a/backend/event/admin.py b/backend/event/admin.py index 67b204b9..3d521a5e 100644 --- a/backend/event/admin.py +++ b/backend/event/admin.py @@ -17,19 +17,19 @@ class EventDateAdmin(admin.StackedInline): verbose_name_plural = "dates" -class RoleAdmin(admin.StackedInline): - model = models.Role - fields = [ - "agent", - "present", - "role", - "description", - "certainty", - "note", - ] - extra = 0 - verbose_name = "agent/role" - verbose_name_plural = "agents/roles involved" +# class RoleAdmin(admin.StackedInline): +# model = models.Role +# fields = [ +# "agent", +# "present", +# "role", +# "description", +# "certainty", +# "note", +# ] +# extra = 0 +# verbose_name = "agent/role" +# verbose_name_plural = "agents/roles involved" class LetterActionLettersAdmin(admin.StackedInline): @@ -53,7 +53,7 @@ class LetterActionAdmin(admin.ModelAdmin): LetterActionCategoryAdmin, LetterActionGiftsAdmin, EventDateAdmin, - RoleAdmin, + # RoleAdmin, ] exclude = ["letters"] diff --git a/backend/event/migrations/0010_remove_letteraction_actors_delete_role.py b/backend/event/migrations/0010_remove_letteraction_actors_delete_role.py new file mode 100644 index 00000000..6b536584 --- /dev/null +++ b/backend/event/migrations/0010_remove_letteraction_actors_delete_role.py @@ -0,0 +1,20 @@ +# Generated by Django 4.2.7 on 2024-04-09 10:49 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('event', '0009_letteraction_space_descriptions'), + ] + + operations = [ + migrations.RemoveField( + model_name='letteraction', + name='actors', + ), + migrations.DeleteModel( + name='Role', + ), + ] diff --git a/backend/event/models.py b/backend/event/models.py index 637652db..dfa16cf1 100644 --- a/backend/event/models.py +++ b/backend/event/models.py @@ -3,7 +3,7 @@ from core.models import Field, LettercraftDate from case_study.models import CaseStudy -from person.models import Agent +# from person.models import Agent from letter.models import Gift, Letter from space.models import SpaceDescription @@ -65,11 +65,11 @@ class LetterAction(models.Model): help_text="letters involved in this event", ) - actors = models.ManyToManyField( - to=Agent, - through="Role", - related_name="events", - ) + # actors = models.ManyToManyField( + # to=Agent, + # through="Role", + # related_name="events", + # ) epistolary_events = models.ManyToManyField( to=EpistolaryEvent, @@ -155,53 +155,53 @@ def __str__(self): return f"{self.letter_action} ({self.display_date})" -class Role(Field, models.Model): - """ - Describes the involvement of an agent in a letter action. - """ - - class RoleOptions(models.TextChoices): - AUTHOR = "author", "Author" - SCRIBE = "scribe", "Scribe" - READER = "reader", "Reader" - WITNESS = "witness", "Witness" - MESSENGER = "messenger", "Messenger" - RECIPIENT = "recipient", "Recipient" - INTENDED_RECIPIENT = "intended_recipient", "Intended recipient" - AUDIENCE = "audience", "Audience" - INTENDED_AUDIENCE = "intended_audience", "Intended audience" - INSTIGATOR = "instigator", "Instigator" - OTHER = "other", "Other" - - agent = models.ForeignKey( - to=Agent, - on_delete=models.CASCADE, - null=False, - ) - letter_action = models.ForeignKey( - to=LetterAction, - on_delete=models.CASCADE, - null=False, - ) - present = models.BooleanField( - null=False, - default=True, - help_text="Whether this agent was physically present", - ) - role = models.CharField( - choices=RoleOptions.choices, - null=False, - blank=False, - help_text="Role of this agent in the event", - ) - description = models.TextField( - null=False, - blank=True, - help_text="Longer description of this agent's involvement", - ) - - def __str__(self): - return f"role of {self.agent} in {self.letter_action}" +# class Role(Field, models.Model): +# """ +# Describes the involvement of an agent in a letter action. +# """ + +# class RoleOptions(models.TextChoices): +# AUTHOR = "author", "Author" +# SCRIBE = "scribe", "Scribe" +# READER = "reader", "Reader" +# WITNESS = "witness", "Witness" +# MESSENGER = "messenger", "Messenger" +# RECIPIENT = "recipient", "Recipient" +# INTENDED_RECIPIENT = "intended_recipient", "Intended recipient" +# AUDIENCE = "audience", "Audience" +# INTENDED_AUDIENCE = "intended_audience", "Intended audience" +# INSTIGATOR = "instigator", "Instigator" +# OTHER = "other", "Other" + +# agent = models.ForeignKey( +# to=Agent, +# on_delete=models.CASCADE, +# null=False, +# ) +# letter_action = models.ForeignKey( +# to=LetterAction, +# on_delete=models.CASCADE, +# null=False, +# ) +# present = models.BooleanField( +# null=False, +# default=True, +# help_text="Whether this agent was physically present", +# ) +# role = models.CharField( +# choices=RoleOptions.choices, +# null=False, +# blank=False, +# help_text="Role of this agent in the event", +# ) +# description = models.TextField( +# null=False, +# blank=True, +# help_text="Longer description of this agent's involvement", +# ) + +# def __str__(self): +# return f"role of {self.agent} in {self.letter_action}" class WorldEvent(LettercraftDate, models.Model): diff --git a/backend/letter/admin.py b/backend/letter/admin.py index 25f6844a..8a2abe51 100644 --- a/backend/letter/admin.py +++ b/backend/letter/admin.py @@ -17,16 +17,16 @@ class LetterCategoryAdmin(admin.StackedInline): fields = ["letter", "category", "certainty", "note"] -class LetterSenderAdmin(admin.StackedInline): - model = models.LetterSenders - fields = ["letter", "senders", "certainty", "note"] - filter_horizontal = ["senders"] +# class LetterSenderAdmin(admin.StackedInline): +# model = models.LetterSenders +# fields = ["letter", "senders", "certainty", "note"] +# filter_horizontal = ["senders"] -class LetterAddresseesAdmin(admin.StackedInline): - model = models.LetterAddressees - fields = ["letter", "addressees", "certainty", "note"] - filter_horizontal = ["addressees"] +# class LetterAddresseesAdmin(admin.StackedInline): +# model = models.LetterAddressees +# fields = ["letter", "addressees", "certainty", "note"] +# filter_horizontal = ["addressees"] @admin.register(models.Letter) @@ -34,9 +34,9 @@ class LetterAdmin(admin.ModelAdmin): readonly_fields = ["date_active", "date_written"] inlines = [ LetterCategoryAdmin, - LetterMaterialAdmin, - LetterSenderAdmin, - LetterAddresseesAdmin, + # LetterMaterialAdmin, + # LetterSenderAdmin, + # LetterAddresseesAdmin, ] @@ -49,5 +49,5 @@ class GiftLetterActionInline(admin.StackedInline): @admin.register(models.Gift) class GiftAdmin(admin.ModelAdmin): - fields = ["name", "description", "material", "gifted_by"] + fields = ["name", "description", "material"] filter_horizontal = ["letter_actions"] diff --git a/backend/letter/migrations/0009_remove_lettersenders_letter_and_more.py b/backend/letter/migrations/0009_remove_lettersenders_letter_and_more.py new file mode 100644 index 00000000..452818bd --- /dev/null +++ b/backend/letter/migrations/0009_remove_lettersenders_letter_and_more.py @@ -0,0 +1,31 @@ +# Generated by Django 4.2.7 on 2024-04-09 10:49 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('letter', '0008_alter_gift_gifted_by_and_more'), + ] + + operations = [ + migrations.RemoveField( + model_name='lettersenders', + name='letter', + ), + migrations.RemoveField( + model_name='lettersenders', + name='senders', + ), + migrations.RemoveField( + model_name='gift', + name='gifted_by', + ), + migrations.DeleteModel( + name='LetterAddressees', + ), + migrations.DeleteModel( + name='LetterSenders', + ), + ] diff --git a/backend/letter/models.py b/backend/letter/models.py index 2096e311..a9ee01bb 100644 --- a/backend/letter/models.py +++ b/backend/letter/models.py @@ -1,7 +1,7 @@ from django.db import models from django.contrib import admin from core.models import Field -from person.models import Agent +# from person.models import Agent class Gift(models.Model): @@ -35,20 +35,20 @@ class Material(models.TextChoices): help_text="The material the gift consists of", ) - gifted_by = models.ForeignKey( - to=Agent, - on_delete=models.CASCADE, - related_name="gifts_given", - help_text="The agent who gave the gift. Leave empty if unknown.", - null=True, - blank=True, - ) + # gifted_by = models.ForeignKey( + # to=Agent, + # on_delete=models.CASCADE, + # related_name="gifts_given", + # help_text="The agent who gave the gift. Leave empty if unknown.", + # null=True, + # blank=True, + # ) - def __str__(self): - gifter_name = ( - self.gifted_by.names.first() if self.gifted_by is not None else "unknown" - ) - return f"{self.name} ({self.material}), gifted by {gifter_name}" + # def __str__(self): + # gifter_name = ( + # self.gifted_by.names.first() if self.gifted_by is not None else "unknown" + # ) + # return f"{self.name} ({self.material}), gifted by {gifter_name}" class Letter(models.Model): @@ -131,39 +131,39 @@ def __str__(self): return f"material #{self.id}" -class LetterSenders(Field, models.Model): - senders = models.ManyToManyField( - to=Agent, - blank=True, - help_text="Agents whom the letter names as the sender", - ) - letter = models.OneToOneField( - to=Letter, - on_delete=models.CASCADE, - null=False, - ) - - def __str__(self): - if self.letter: - return f"senders of {self.letter}" - else: - return f"senders #{self.id}" - - -class LetterAddressees(Field, models.Model): - addressees = models.ManyToManyField( - to=Agent, - blank=True, - help_text="Agents whom the letter names as the addressee", - ) - letter = models.OneToOneField( - to=Letter, - on_delete=models.CASCADE, - null=False, - ) - - def __str__(self): - if self.letter: - return f"addressees of {self.letter}" - else: - return f"addressees #{self.id}" +# class LetterSenders(Field, models.Model): +# senders = models.ManyToManyField( +# to=Agent, +# blank=True, +# help_text="Agents whom the letter names as the sender", +# ) +# letter = models.OneToOneField( +# to=Letter, +# on_delete=models.CASCADE, +# null=False, +# ) + +# def __str__(self): +# if self.letter: +# return f"senders of {self.letter}" +# else: +# return f"senders #{self.id}" + + +# class LetterAddressees(Field, models.Model): +# addressees = models.ManyToManyField( +# to=Agent, +# blank=True, +# help_text="Agents whom the letter names as the addressee", +# ) +# letter = models.OneToOneField( +# to=Letter, +# on_delete=models.CASCADE, +# null=False, +# ) + +# def __str__(self): +# if self.letter: +# return f"addressees of {self.letter}" +# else: +# return f"addressees #{self.id}" diff --git a/backend/person/admin.py b/backend/person/admin.py index e945b793..be4f4415 100644 --- a/backend/person/admin.py +++ b/backend/person/admin.py @@ -2,42 +2,42 @@ from . import models -class AgentNameAdmin(admin.StackedInline): - model = models.AgentName - fields = ["value", "certainty", "note"] - extra = 0 - verbose_name = "(Alternative) agent name" - verbose_name_plural = "(Alternative) agent names" +# class AgentNameAdmin(admin.StackedInline): +# model = models.AgentName +# fields = ["value", "certainty", "note"] +# extra = 0 +# verbose_name = "(Alternative) agent name" +# verbose_name_plural = "(Alternative) agent names" -class SocialStatusAdmin(admin.StackedInline): - model = models.SocialStatus - fields = ["status_marker", "certainty", "note", "year_lower", "year_upper", "year_exact"] - extra = 0 +# class SocialStatusAdmin(admin.StackedInline): +# model = models.SocialStatus +# fields = ["status_marker", "certainty", "note", "year_lower", "year_upper", "year_exact"] +# extra = 0 -class AgentDateOfBirthAdmin(admin.StackedInline): - model = models.AgentDateOfBirth - fields = ["year_lower", "year_upper", "year_exact", "certainty", "note"] - extra = 0 +# class AgentDateOfBirthAdmin(admin.StackedInline): +# model = models.AgentDateOfBirth +# fields = ["year_lower", "year_upper", "year_exact", "certainty", "note"] +# extra = 0 -class AgentDateOfDeathAdmin(admin.StackedInline): - model = models.AgentDateOfDeath - fields = ["year_lower", "year_upper", "year_exact", "certainty", "note"] - extra = 0 +# class AgentDateOfDeathAdmin(admin.StackedInline): +# model = models.AgentDateOfDeath +# fields = ["year_lower", "year_upper", "year_exact", "certainty", "note"] +# extra = 0 -@admin.register(models.Agent) -class AgentAdmin(admin.ModelAdmin): - inlines = [ - AgentNameAdmin, - SocialStatusAdmin, - AgentDateOfBirthAdmin, - AgentDateOfDeathAdmin, - ] +# @admin.register(models.Agent) +# class AgentAdmin(admin.ModelAdmin): +# inlines = [ +# AgentNameAdmin, +# SocialStatusAdmin, +# AgentDateOfBirthAdmin, +# AgentDateOfDeathAdmin, +# ] -@admin.register(models.StatusMarker) -class StatusMarkerAdmin(admin.ModelAdmin): - pass +# @admin.register(models.StatusMarker) +# class StatusMarkerAdmin(admin.ModelAdmin): +# pass diff --git a/backend/person/migrations/0012_remove_agentdateofbirth_agent_and_more.py b/backend/person/migrations/0012_remove_agentdateofbirth_agent_and_more.py new file mode 100644 index 00000000..cf617f4d --- /dev/null +++ b/backend/person/migrations/0012_remove_agentdateofbirth_agent_and_more.py @@ -0,0 +1,50 @@ +# Generated by Django 4.2.7 on 2024-04-09 10:49 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('event', '0010_remove_letteraction_actors_delete_role'), + ('letter', '0009_remove_lettersenders_letter_and_more'), + ('person', '0011_alter_agent_gender_alter_agent_is_group_and_more'), + ] + + operations = [ + migrations.RemoveField( + model_name='agentdateofbirth', + name='agent', + ), + migrations.RemoveField( + model_name='agentdateofdeath', + name='agent', + ), + migrations.RemoveField( + model_name='agentname', + name='agent', + ), + migrations.RemoveField( + model_name='socialstatus', + name='agent', + ), + migrations.RemoveField( + model_name='socialstatus', + name='status_marker', + ), + migrations.DeleteModel( + name='Agent', + ), + migrations.DeleteModel( + name='AgentDateOfBirth', + ), + migrations.DeleteModel( + name='AgentDateOfDeath', + ), + migrations.DeleteModel( + name='AgentName', + ), + migrations.DeleteModel( + name='SocialStatus', + ), + ] diff --git a/backend/person/models.py b/backend/person/models.py index f0c2fdc3..5f80f9d4 100644 --- a/backend/person/models.py +++ b/backend/person/models.py @@ -30,123 +30,123 @@ class Gender(models.TextChoices): OTHER = "OTHER", "Other" -class Agent(models.Model): - gender = models.CharField( - max_length=8, - choices=Gender.choices, - default=Gender.UNKNOWN, - help_text="The gender of this person or group of people. The option Mixed is only used for groups.", - ) - - is_group = models.BooleanField( - default=False, - help_text="Whether this entity is a group of people (e.g. 'the nuns of Poitiers'). If true, the date of birth and date of death fields should be left empty.", - ) - - class Meta: - constraints = [ - CheckConstraint( - check=~Q(gender=Gender.MIXED, is_group=True), - name="gender_group_constraint", - violation_error_message="The 'mixed' gender option is reserved for groups", - ) - ] - - def clean(self): - if self.is_group and getattr(self, "date_of_birth", None) is not None: - raise ValidationError("A group cannot have a date of birth") - - if self.is_group and getattr(self, "date_of_death", None) is not None: - raise ValidationError("A group cannot have a date of death") - - def __str__(self): - if self.names.count() == 1: - return self.names.first().value - elif self.names.count() > 1: - main_name = self.names.first().value - aliases = ", ".join(name.value for name in self.names.all()[1:]) - return f"{main_name} (aka {aliases})" - else: - return f"Unknown {'person' if self.is_group is False else 'group of people'} #{self.id}" - - -class AgentName(Field, models.Model): - value = models.CharField( - max_length=256, - blank=True, - ) - agent = models.ForeignKey(to=Agent, on_delete=models.CASCADE, related_name="names") - - class Meta: - constraints = [ - models.UniqueConstraint("value", "agent", name="unique_names_for_agent") - ] - - def __str__(self): - return self.value - - -class AgentDateOfBirth(LettercraftDate, Field, models.Model): - """ - A relationship between a agent and their date of birth. - """ - - agent = models.OneToOneField( - Agent, - related_name="date_of_birth", - on_delete=models.CASCADE, - limit_choices_to={"is_group": False}, - ) - - def clean(self): - if self.agent.is_group: - raise ValidationError("A group cannot have a date of birth.") - - def __str__(self): - if self.year_exact: - return f"{self.agent} born in {self.year_exact}" - else: - return f"{self.agent} born c. {self.year_lower}–{self.year_upper}" - - -class AgentDateOfDeath(LettercraftDate, Field, models.Model): - """ " - A relationship between a agent and their date of death. - """ - - agent = models.OneToOneField( - Agent, - related_name="date_of_death", - on_delete=models.CASCADE, - limit_choices_to={"is_group": False}, - ) - - def clean(self): - if self.agent.is_group: - raise ValidationError("A group cannot have a date of death.") - - def __str__(self): - if self.year_exact: - return f"{self.agent} died in {self.year_exact}" - else: - return f"{self.agent} died c. {self.year_lower}–{self.year_upper}" - - -class SocialStatus(Field, LettercraftDate, models.Model): - """ - A relationship between a person or group and a social status marker, - indicating that the person or group is of a certain social status. - """ - - agent = models.ForeignKey( - to=Agent, on_delete=models.CASCADE, related_name="social_statuses" - ) - status_marker = models.ForeignKey( - to=StatusMarker, on_delete=models.CASCADE, related_name="social_statuses" - ) - - class Meta: - verbose_name_plural = "Social statuses" - - def __str__(self): - return f"{self.agent} as {self.status_marker}" +# class Agent(models.Model): +# gender = models.CharField( +# max_length=8, +# choices=Gender.choices, +# default=Gender.UNKNOWN, +# help_text="The gender of this person or group of people. The option Mixed is only used for groups.", +# ) + +# is_group = models.BooleanField( +# default=False, +# help_text="Whether this entity is a group of people (e.g. 'the nuns of Poitiers'). If true, the date of birth and date of death fields should be left empty.", +# ) + +# class Meta: +# constraints = [ +# CheckConstraint( +# check=~Q(gender=Gender.MIXED, is_group=True), +# name="gender_group_constraint", +# violation_error_message="The 'mixed' gender option is reserved for groups", +# ) +# ] + +# def clean(self): +# if self.is_group and getattr(self, "date_of_birth", None) is not None: +# raise ValidationError("A group cannot have a date of birth") + +# if self.is_group and getattr(self, "date_of_death", None) is not None: +# raise ValidationError("A group cannot have a date of death") + +# def __str__(self): +# if self.names.count() == 1: +# return self.names.first().value +# elif self.names.count() > 1: +# main_name = self.names.first().value +# aliases = ", ".join(name.value for name in self.names.all()[1:]) +# return f"{main_name} (aka {aliases})" +# else: +# return f"Unknown {'person' if self.is_group is False else 'group of people'} #{self.id}" + + +# class AgentName(Field, models.Model): +# value = models.CharField( +# max_length=256, +# blank=True, +# ) +# agent = models.ForeignKey(to=Agent, on_delete=models.CASCADE, related_name="names") + +# class Meta: +# constraints = [ +# models.UniqueConstraint("value", "agent", name="unique_names_for_agent") +# ] + +# def __str__(self): +# return self.value + + +# class AgentDateOfBirth(LettercraftDate, Field, models.Model): +# """ +# A relationship between a agent and their date of birth. +# """ + +# agent = models.OneToOneField( +# Agent, +# related_name="date_of_birth", +# on_delete=models.CASCADE, +# limit_choices_to={"is_group": False}, +# ) + +# def clean(self): +# if self.agent.is_group: +# raise ValidationError("A group cannot have a date of birth.") + +# def __str__(self): +# if self.year_exact: +# return f"{self.agent} born in {self.year_exact}" +# else: +# return f"{self.agent} born c. {self.year_lower}–{self.year_upper}" + + +# class AgentDateOfDeath(LettercraftDate, Field, models.Model): +# """ " +# A relationship between a agent and their date of death. +# """ + +# agent = models.OneToOneField( +# Agent, +# related_name="date_of_death", +# on_delete=models.CASCADE, +# limit_choices_to={"is_group": False}, +# ) + +# def clean(self): +# if self.agent.is_group: +# raise ValidationError("A group cannot have a date of death.") + +# def __str__(self): +# if self.year_exact: +# return f"{self.agent} died in {self.year_exact}" +# else: +# return f"{self.agent} died c. {self.year_lower}–{self.year_upper}" + + +# class SocialStatus(Field, LettercraftDate, models.Model): +# """ +# A relationship between a person or group and a social status marker, +# indicating that the person or group is of a certain social status. +# """ + +# agent = models.ForeignKey( +# to=Agent, on_delete=models.CASCADE, related_name="social_statuses" +# ) +# status_marker = models.ForeignKey( +# to=StatusMarker, on_delete=models.CASCADE, related_name="social_statuses" +# ) + +# class Meta: +# verbose_name_plural = "Social statuses" + +# def __str__(self): +# return f"{self.agent} as {self.status_marker}" diff --git a/backend/person/tests/test_person_models.py b/backend/person/tests/test_person_models.py index c683053c..09c1440c 100644 --- a/backend/person/tests/test_person_models.py +++ b/backend/person/tests/test_person_models.py @@ -1,47 +1,47 @@ from django.db import IntegrityError from django.forms import ValidationError import pytest -from person.models import AgentDateOfBirth, AgentName, Gender +# from person.models import AgentDateOfBirth, AgentName, Gender -def test_agent_name_for_unnamed_agent(agent): - assert agent.__str__().startswith("Unknown person #") +# def test_agent_name_for_unnamed_agent(agent): +# assert agent.__str__().startswith("Unknown person #") -def test_agent_name_for_unnamed_agent_group(agent_group): - agent_group.name = "" - assert agent_group.__str__().startswith("Unknown group of people #") +# def test_agent_name_for_unnamed_agent_group(agent_group): +# agent_group.name = "" +# assert agent_group.__str__().startswith("Unknown group of people #") -def test_agent_name_for_agent_with_single_name(agent): - AgentName.objects.create(agent=agent, value="Bert") - assert agent.__str__() == "Bert" +# def test_agent_name_for_agent_with_single_name(agent): +# AgentName.objects.create(agent=agent, value="Bert") +# assert agent.__str__() == "Bert" -def test_agent_name_for_agent_with_multiple_names(agent): - AgentName.objects.create(agent=agent, value="Bert") - AgentName.objects.create(agent=agent, value="Ernie") - AgentName.objects.create(agent=agent, value="Oscar") - assert agent.__str__() == "Bert (aka Ernie, Oscar)" +# def test_agent_name_for_agent_with_multiple_names(agent): +# AgentName.objects.create(agent=agent, value="Bert") +# AgentName.objects.create(agent=agent, value="Ernie") +# AgentName.objects.create(agent=agent, value="Oscar") +# assert agent.__str__() == "Bert (aka Ernie, Oscar)" -def test_agent_with_exact_date_of_birth(agent): - AgentDateOfBirth.objects.create(agent=agent, year_exact=512) - assert agent.date_of_birth.__str__().endswith("born in 512") +# def test_agent_with_exact_date_of_birth(agent): +# AgentDateOfBirth.objects.create(agent=agent, year_exact=512) +# assert agent.date_of_birth.__str__().endswith("born in 512") -def test_agent_with_approx_date_of_birth(agent): - AgentDateOfBirth.objects.create(agent=agent, year_lower=500, year_upper=525) - assert agent.date_of_birth.__str__().endswith("born c. 500–525") +# def test_agent_with_approx_date_of_birth(agent): +# AgentDateOfBirth.objects.create(agent=agent, year_lower=500, year_upper=525) +# assert agent.date_of_birth.__str__().endswith("born c. 500–525") -def test_agent_group_date_of_birth_constraint(agent_group): - with pytest.raises(ValidationError): - AgentDateOfBirth.objects.create(agent=agent_group, year_exact=512) - agent_group.clean() +# def test_agent_group_date_of_birth_constraint(agent_group): +# with pytest.raises(ValidationError): +# AgentDateOfBirth.objects.create(agent=agent_group, year_exact=512) +# agent_group.clean() -def test_agent_group_mixed_gender_constraint(agent_group): - with pytest.raises(IntegrityError): - agent_group.gender = Gender.MIXED - agent_group.save() +# def test_agent_group_mixed_gender_constraint(agent_group): +# with pytest.raises(IntegrityError): +# agent_group.gender = Gender.MIXED +# agent_group.save() From d043fbacfd870cf7b464032072ae1d157704596b Mon Sep 17 00:00:00 2001 From: Luka van der Plas Date: Tue, 9 Apr 2024 14:09:13 +0200 Subject: [PATCH 13/16] draft updated agent models --- ...storicalperson_personreference_and_more.py | 166 ++++++++++ backend/person/models.py | 308 +++++++++++------- 2 files changed, 352 insertions(+), 122 deletions(-) create mode 100644 backend/person/migrations/0013_agentdescription_historicalperson_personreference_and_more.py diff --git a/backend/person/migrations/0013_agentdescription_historicalperson_personreference_and_more.py b/backend/person/migrations/0013_agentdescription_historicalperson_personreference_and_more.py new file mode 100644 index 00000000..a72b07a7 --- /dev/null +++ b/backend/person/migrations/0013_agentdescription_historicalperson_personreference_and_more.py @@ -0,0 +1,166 @@ +# Generated by Django 4.2.7 on 2024-04-09 12:08 + +import django.contrib.postgres.fields +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('source', '0005_placeholder_source'), + ('space', '0008_alter_spacedescription_source'), + ('person', '0012_remove_agentdateofbirth_agent_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='AgentDescription', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(help_text='A name to help identify this object', max_length=200)), + ('description', models.TextField(blank=True, help_text='Longer description to help identify this object')), + ('source_mention', models.CharField(blank=True, choices=[('direct', 'directly mentioned'), ('implied', 'implied')], help_text='How is this entity presented in the text?', max_length=32)), + ('source_location', models.CharField(blank=True, help_text='Specific location(s) where the entity is mentioned or described in the source text', max_length=200)), + ('is_group', models.BooleanField(default=False, help_text="Whether this agent is a group of people (e.g. 'the nuns of Poitiers').")), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='HistoricalPerson', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(help_text='A name to help identify this object', max_length=200)), + ('description', models.TextField(blank=True, help_text='Longer description to help identify this object')), + ('identifiable', models.BooleanField(default=True, help_text='Whether this entity is identifiable (i.e. can be cross-referenced between descriptions), or a generic description')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='PersonReference', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('certainty', models.IntegerField(choices=[(0, 'uncertain'), (1, 'somewhat certain'), (2, 'certain')], default=2, help_text='How certain are you of this value?')), + ('note', models.TextField(blank=True, help_text='Additional notes')), + ('description', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='person.agentdescription')), + ('person', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='person.historicalperson')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='PersonDateOfDeath', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('certainty', models.IntegerField(choices=[(0, 'uncertain'), (1, 'somewhat certain'), (2, 'certain')], default=2, help_text='How certain are you of this value?')), + ('note', models.TextField(blank=True, help_text='Additional notes')), + ('year_lower', models.IntegerField(default=400, help_text='The earliest possible year for this value', validators=[django.core.validators.MinValueValidator(400), django.core.validators.MaxValueValidator(800)])), + ('year_upper', models.IntegerField(default=800, help_text='The latest possible year for this value', validators=[django.core.validators.MinValueValidator(400), django.core.validators.MaxValueValidator(800)])), + ('year_exact', models.IntegerField(blank=True, help_text='The exact year of the value (if known). This will override the values in the lower and upper bounds fields.', null=True, validators=[django.core.validators.MinValueValidator(400), django.core.validators.MaxValueValidator(800)])), + ('person', models.ForeignKey(help_text='date on which this person died', on_delete=django.db.models.deletion.CASCADE, to='person.historicalperson')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='PersonDateOfBirth', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('certainty', models.IntegerField(choices=[(0, 'uncertain'), (1, 'somewhat certain'), (2, 'certain')], default=2, help_text='How certain are you of this value?')), + ('note', models.TextField(blank=True, help_text='Additional notes')), + ('year_lower', models.IntegerField(default=400, help_text='The earliest possible year for this value', validators=[django.core.validators.MinValueValidator(400), django.core.validators.MaxValueValidator(800)])), + ('year_upper', models.IntegerField(default=800, help_text='The latest possible year for this value', validators=[django.core.validators.MinValueValidator(400), django.core.validators.MaxValueValidator(800)])), + ('year_exact', models.IntegerField(blank=True, help_text='The exact year of the value (if known). This will override the values in the lower and upper bounds fields.', null=True, validators=[django.core.validators.MinValueValidator(400), django.core.validators.MaxValueValidator(800)])), + ('person', models.ForeignKey(help_text='date on which this person was born', on_delete=django.db.models.deletion.CASCADE, to='person.historicalperson')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='AgentDescriptionSocialStatus', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('certainty', models.IntegerField(choices=[(0, 'uncertain'), (1, 'somewhat certain'), (2, 'certain')], default=2, help_text='How certain are you of this value?')), + ('note', models.TextField(blank=True, help_text='Additional notes')), + ('source_mention', models.CharField(blank=True, choices=[('direct', 'directly mentioned'), ('implied', 'implied')], help_text='How is this information presented in the text?', max_length=32)), + ('source_location', models.CharField(blank=True, help_text='Specific location of the information in the source text', max_length=200)), + ('source_terminology', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=200), blank=True, default=list, help_text='Relevant terminology used in the source text', size=5)), + ('agent', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='social_statuses', to='person.agentdescription')), + ('status_marker', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='social_statuses', to='person.statusmarker')), + ], + options={ + 'verbose_name': 'social status description', + }, + ), + migrations.CreateModel( + name='AgentDescriptionName', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('certainty', models.IntegerField(choices=[(0, 'uncertain'), (1, 'somewhat certain'), (2, 'certain')], default=2, help_text='How certain are you of this value?')), + ('note', models.TextField(blank=True, help_text='Additional notes')), + ('source_mention', models.CharField(blank=True, choices=[('direct', 'directly mentioned'), ('implied', 'implied')], help_text='How is this information presented in the text?', max_length=32)), + ('source_location', models.CharField(blank=True, help_text='Specific location of the information in the source text', max_length=200)), + ('source_terminology', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=200), blank=True, default=list, help_text='Relevant terminology used in the source text', size=5)), + ('name', models.CharField(max_length=256)), + ('agent', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='names', to='person.agentdescription')), + ], + options={ + 'verbose_name': 'name used in description', + 'verbose_name_plural': 'names used in description', + }, + ), + migrations.CreateModel( + name='AgentDescriptionLocation', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('certainty', models.IntegerField(choices=[(0, 'uncertain'), (1, 'somewhat certain'), (2, 'certain')], default=2, help_text='How certain are you of this value?')), + ('note', models.TextField(blank=True, help_text='Additional notes')), + ('source_mention', models.CharField(blank=True, choices=[('direct', 'directly mentioned'), ('implied', 'implied')], help_text='How is this information presented in the text?', max_length=32)), + ('source_location', models.CharField(blank=True, help_text='Specific location of the information in the source text', max_length=200)), + ('source_terminology', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=200), blank=True, default=list, help_text='Relevant terminology used in the source text', size=5)), + ('agent', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='locations', to='person.agentdescription')), + ('location', models.ForeignKey(help_text='location by which the agent is identified', on_delete=django.db.models.deletion.CASCADE, to='space.spacedescription')), + ], + options={ + 'verbose_name': 'location description', + }, + ), + migrations.CreateModel( + name='AgentDescriptionGender', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('certainty', models.IntegerField(choices=[(0, 'uncertain'), (1, 'somewhat certain'), (2, 'certain')], default=2, help_text='How certain are you of this value?')), + ('note', models.TextField(blank=True, help_text='Additional notes')), + ('source_mention', models.CharField(blank=True, choices=[('direct', 'directly mentioned'), ('implied', 'implied')], help_text='How is this information presented in the text?', max_length=32)), + ('source_location', models.CharField(blank=True, help_text='Specific location of the information in the source text', max_length=200)), + ('source_terminology', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=200), blank=True, default=list, help_text='Relevant terminology used in the source text', size=5)), + ('gender', models.CharField(choices=[('FEMALE', 'Female'), ('MALE', 'Male'), ('UNKNOWN', 'Unknown'), ('MIXED', 'Mixed'), ('OTHER', 'Other')], default='UNKNOWN', help_text='The gender of this agent. The option Mixed is only applicable for groups.', max_length=8)), + ('agent', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='gender', to='person.agentdescription')), + ], + options={ + 'verbose_name': 'gender description', + }, + ), + migrations.AddField( + model_name='agentdescription', + name='describes', + field=models.ManyToManyField(blank=True, help_text='Historical figure(s) referenced by this description. For groups, this can be multiple people.', through='person.PersonReference', to='person.historicalperson'), + ), + migrations.AddField( + model_name='agentdescription', + name='source', + field=models.ForeignKey(help_text='Source text containing this description', on_delete=django.db.models.deletion.PROTECT, to='source.source'), + ), + migrations.AddConstraint( + model_name='agentdescriptionname', + constraint=models.UniqueConstraint(models.F('name'), models.F('agent'), name='unique_names_for_agent'), + ), + ] diff --git a/backend/person/models.py b/backend/person/models.py index 5f80f9d4..269c3958 100644 --- a/backend/person/models.py +++ b/backend/person/models.py @@ -1,7 +1,14 @@ from django.db import models from django.forms import ValidationError -from core.models import Field, LettercraftDate -from django.db.models import Q, CheckConstraint +from core.models import ( + Field, + LettercraftDate, + HistoricalEntity, + EntityDescription, + DescriptionField, +) + +from space.models import SpaceDescription class StatusMarker(models.Model): @@ -22,6 +29,82 @@ def __str__(self): return self.name +class HistoricalPerson(HistoricalEntity, models.Model): + """ + A historical figure, which may be referenced in narrative sources or preserved letters + """ + + pass + + +class PersonDateOfBirth(Field, LettercraftDate, models.Model): + person = models.ForeignKey( + to=HistoricalPerson, + on_delete=models.CASCADE, + help_text="date on which this person was born", + ) + + def __str__(self): + if self.year_exact: + return f"{self.person} born in {self.year_exact}" + else: + return f"{self.person} born c. {self.year_lower}-{self.year_upper}" + + +class PersonDateOfDeath(Field, LettercraftDate, models.Model): + person = models.ForeignKey( + to=HistoricalPerson, + on_delete=models.CASCADE, + help_text="date on which this person died", + ) + + def __str__(self): + if self.year_exact: + return f"{self.person} died in {self.year_exact}" + else: + return f"{self.person} died c. {self.year_lower}-{self.year_upper}" + + +class PersonReference(Field, models.Model): + """ + Link between a historical person and a description in a source text. + """ + + person = models.ForeignKey( + to=HistoricalPerson, + on_delete=models.CASCADE, + ) + description = models.ForeignKey( + to="AgentDescription", + on_delete=models.CASCADE, + ) + + +class AgentDescription(EntityDescription, models.Model): + """ + A description of an agent in a source text; can be a single person or a group + """ + + describes = models.ManyToManyField( + to=HistoricalPerson, + through=PersonReference, + blank=True, + help_text="Historical figure(s) referenced by this description. For groups, this can be multiple people.", + ) + + is_group = models.BooleanField( + default=False, + help_text="Whether this agent is a group of people (e.g. 'the nuns of Poitiers').", + ) + + def clean(self): + # ID check is needed to evaluate the m2m relationship + if self.id and (not self.is_group) and self.describes.count() > 1: + raise ValidationError( + "Only groups can describe multiple historical figures" + ) + + class Gender(models.TextChoices): FEMALE = "FEMALE", "Female" MALE = "MALE", "Male" @@ -30,123 +113,104 @@ class Gender(models.TextChoices): OTHER = "OTHER", "Other" -# class Agent(models.Model): -# gender = models.CharField( -# max_length=8, -# choices=Gender.choices, -# default=Gender.UNKNOWN, -# help_text="The gender of this person or group of people. The option Mixed is only used for groups.", -# ) - -# is_group = models.BooleanField( -# default=False, -# help_text="Whether this entity is a group of people (e.g. 'the nuns of Poitiers'). If true, the date of birth and date of death fields should be left empty.", -# ) - -# class Meta: -# constraints = [ -# CheckConstraint( -# check=~Q(gender=Gender.MIXED, is_group=True), -# name="gender_group_constraint", -# violation_error_message="The 'mixed' gender option is reserved for groups", -# ) -# ] - -# def clean(self): -# if self.is_group and getattr(self, "date_of_birth", None) is not None: -# raise ValidationError("A group cannot have a date of birth") - -# if self.is_group and getattr(self, "date_of_death", None) is not None: -# raise ValidationError("A group cannot have a date of death") - -# def __str__(self): -# if self.names.count() == 1: -# return self.names.first().value -# elif self.names.count() > 1: -# main_name = self.names.first().value -# aliases = ", ".join(name.value for name in self.names.all()[1:]) -# return f"{main_name} (aka {aliases})" -# else: -# return f"Unknown {'person' if self.is_group is False else 'group of people'} #{self.id}" - - -# class AgentName(Field, models.Model): -# value = models.CharField( -# max_length=256, -# blank=True, -# ) -# agent = models.ForeignKey(to=Agent, on_delete=models.CASCADE, related_name="names") - -# class Meta: -# constraints = [ -# models.UniqueConstraint("value", "agent", name="unique_names_for_agent") -# ] - -# def __str__(self): -# return self.value - - -# class AgentDateOfBirth(LettercraftDate, Field, models.Model): -# """ -# A relationship between a agent and their date of birth. -# """ - -# agent = models.OneToOneField( -# Agent, -# related_name="date_of_birth", -# on_delete=models.CASCADE, -# limit_choices_to={"is_group": False}, -# ) - -# def clean(self): -# if self.agent.is_group: -# raise ValidationError("A group cannot have a date of birth.") - -# def __str__(self): -# if self.year_exact: -# return f"{self.agent} born in {self.year_exact}" -# else: -# return f"{self.agent} born c. {self.year_lower}–{self.year_upper}" - - -# class AgentDateOfDeath(LettercraftDate, Field, models.Model): -# """ " -# A relationship between a agent and their date of death. -# """ - -# agent = models.OneToOneField( -# Agent, -# related_name="date_of_death", -# on_delete=models.CASCADE, -# limit_choices_to={"is_group": False}, -# ) - -# def clean(self): -# if self.agent.is_group: -# raise ValidationError("A group cannot have a date of death.") - -# def __str__(self): -# if self.year_exact: -# return f"{self.agent} died in {self.year_exact}" -# else: -# return f"{self.agent} died c. {self.year_lower}–{self.year_upper}" - - -# class SocialStatus(Field, LettercraftDate, models.Model): -# """ -# A relationship between a person or group and a social status marker, -# indicating that the person or group is of a certain social status. -# """ - -# agent = models.ForeignKey( -# to=Agent, on_delete=models.CASCADE, related_name="social_statuses" -# ) -# status_marker = models.ForeignKey( -# to=StatusMarker, on_delete=models.CASCADE, related_name="social_statuses" -# ) - -# class Meta: -# verbose_name_plural = "Social statuses" - -# def __str__(self): -# return f"{self.agent} as {self.status_marker}" +class AgentDescriptionGender(DescriptionField, models.Model): + """ + Characterisation of an agent's gender in a source text description + """ + + agent = models.OneToOneField( + to=AgentDescription, + on_delete=models.CASCADE, + related_name="gender", + ) + gender = models.CharField( + max_length=8, + choices=Gender.choices, + default=Gender.UNKNOWN, + help_text="The gender of this agent. The option Mixed is only applicable for groups.", + ) + + class Meta: + verbose_name = "gender description" + + def __str__(self) -> str: + return self.gender + + def clean(self): + if self.gender == Gender.MIXED and not self.agent.is_group: + raise ValidationError("Mixed gender can only be used for groups") + + +class AgentDescriptionName(DescriptionField, models.Model): + """ + A name used for an agent in a source text description + """ + + agent = models.ForeignKey( + to=AgentDescription, + on_delete=models.CASCADE, + related_name="names", + ) + name = models.CharField( + max_length=256, + ) + + class Meta: + verbose_name = "name used in description" + verbose_name_plural = "names used in description" + constraints = [ + models.UniqueConstraint("name", "agent", name="unique_names_for_agent") + ] + + def __str__(self): + return self.name + + +class AgentDescriptionSocialStatus(DescriptionField, models.Model): + """ + A characterisation of an agent's social status in a source text. + """ + + agent = models.ForeignKey( + to=AgentDescription, + on_delete=models.CASCADE, + related_name="social_statuses", + ) + status_marker = models.ForeignKey( + to=StatusMarker, on_delete=models.CASCADE, related_name="social_statuses" + ) + + class Meta: + verbose_name = "social status description" + + def __str__(self): + return str(self.status_marker) + + +class AgentDescriptionLocation(DescriptionField, models.Model): + """ + A characterisation of a location as a fundamental property of an agent. + + May be used for groups ("the nuns of Poitiers"). + """ + + agent = models.ForeignKey( + to=AgentDescription, + on_delete=models.CASCADE, + related_name="locations", + ) + location = models.ForeignKey( + to=SpaceDescription, + on_delete=models.CASCADE, + help_text="location by which the agent is identified", + ) + + class Meta: + verbose_name = "location description" + + def __str__(self): + return str(self.location) + + def clean(self): + if self.location.source != self.agent.source: + raise ValidationError("Can only link descriptions in the same source text") From 59a484550073fd43b2a6e77cf92d78dee7930dfe Mon Sep 17 00:00:00 2001 From: Luka van der Plas Date: Tue, 9 Apr 2024 14:23:32 +0200 Subject: [PATCH 14/16] agent admin forms --- backend/core/admin.py | 2 + backend/core/models.py | 3 ++ backend/person/admin.py | 90 ++++++++++++++++++++++++++++------------- 3 files changed, 66 insertions(+), 29 deletions(-) diff --git a/backend/core/admin.py b/backend/core/admin.py index a4a0d83f..4b7644f3 100644 --- a/backend/core/admin.py +++ b/backend/core/admin.py @@ -20,6 +20,8 @@ }, ) +date_fields = ["year_lower", "year_upper", "year_exact"] + field_fields = ["certainty", "note"] description_field_fields = [ diff --git a/backend/core/models.py b/backend/core/models.py index 077a7cda..e13002e8 100644 --- a/backend/core/models.py +++ b/backend/core/models.py @@ -136,6 +136,9 @@ class EntityDescription(Named, models.Model): class Meta: abstract = True + def __str__(self): + return f"{self.name} ({self.source})" + class DescriptionField(Field, models.Model): """ diff --git a/backend/person/admin.py b/backend/person/admin.py index be4f4415..cdfee255 100644 --- a/backend/person/admin.py +++ b/backend/person/admin.py @@ -1,43 +1,75 @@ from django.contrib import admin + from . import models +from core import admin as core_admin + + +class PersonDateOfBirthAdmin(admin.StackedInline): + model = models.PersonDateOfBirth + fields = core_admin.date_fields + core_admin.field_fields + extra = 0 + + +class PersonDateOfDeathAdmin(admin.StackedInline): + model = models.PersonDateOfDeath + fields = core_admin.date_fields + core_admin.field_fields + extra = 0 + + +@admin.register(models.HistoricalPerson) +class HistoricalPersonAdmin(admin.ModelAdmin): + list_display = ["name", "description"] + search_fields = ["name", "description"] + fieldsets = [ + core_admin.named_fieldset, + ] + inlines = [ + PersonDateOfBirthAdmin, + PersonDateOfDeathAdmin, + ] -# class AgentNameAdmin(admin.StackedInline): -# model = models.AgentName -# fields = ["value", "certainty", "note"] -# extra = 0 -# verbose_name = "(Alternative) agent name" -# verbose_name_plural = "(Alternative) agent names" +class AgentDescriptionNameAdmin(admin.StackedInline): + model = models.AgentDescriptionName + fields = ["name"] + core_admin.description_field_fields + extra = 0 -# class SocialStatusAdmin(admin.StackedInline): -# model = models.SocialStatus -# fields = ["status_marker", "certainty", "note", "year_lower", "year_upper", "year_exact"] -# extra = 0 +class AgentDescriptionGenderAdmin(admin.StackedInline): + model = models.AgentDescriptionGender + fields = ["gender"] + core_admin.description_field_fields + extra = 0 -# class AgentDateOfBirthAdmin(admin.StackedInline): -# model = models.AgentDateOfBirth -# fields = ["year_lower", "year_upper", "year_exact", "certainty", "note"] -# extra = 0 +class AgentDescriptionSocialStatusAdmin(admin.StackedInline): + model = models.AgentDescriptionSocialStatus + fields = ["status_marker"] + core_admin.description_field_fields + extra = 0 -# class AgentDateOfDeathAdmin(admin.StackedInline): -# model = models.AgentDateOfDeath -# fields = ["year_lower", "year_upper", "year_exact", "certainty", "note"] -# extra = 0 +class AgentDescriptionLocationAdmin(admin.StackedInline): + model = models.AgentDescriptionLocation + fields = ["location"] + core_admin.description_field_fields + extra = 0 -# @admin.register(models.Agent) -# class AgentAdmin(admin.ModelAdmin): -# inlines = [ -# AgentNameAdmin, -# SocialStatusAdmin, -# AgentDateOfBirthAdmin, -# AgentDateOfDeathAdmin, -# ] +@admin.register(models.AgentDescription) +class AgentDescriptionAdmin(admin.ModelAdmin): + list_display = ["name", "description", "source"] + list_filter = ["source"] + search_fields = ["name", "description"] + fieldsets = [ + core_admin.named_fieldset, + core_admin.description_source_fieldset, + ] + inlines = [ + AgentDescriptionNameAdmin, + AgentDescriptionGenderAdmin, + AgentDescriptionSocialStatusAdmin, + AgentDescriptionLocationAdmin, + ] -# @admin.register(models.StatusMarker) -# class StatusMarkerAdmin(admin.ModelAdmin): -# pass +@admin.register(models.StatusMarker) +class StatusMarkerAdmin(admin.ModelAdmin): + pass From 27945fbb106afbc20e5a0b78ce01475a15bfe1cb Mon Sep 17 00:00:00 2001 From: Luka van der Plas Date: Tue, 9 Apr 2024 14:43:39 +0200 Subject: [PATCH 15/16] make birth/death dates one-to-one --- ...alter_persondateofbirth_person_and_more.py | 24 +++++++++++++++++++ backend/person/models.py | 6 +++-- 2 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 backend/person/migrations/0014_alter_persondateofbirth_person_and_more.py diff --git a/backend/person/migrations/0014_alter_persondateofbirth_person_and_more.py b/backend/person/migrations/0014_alter_persondateofbirth_person_and_more.py new file mode 100644 index 00000000..4376d6da --- /dev/null +++ b/backend/person/migrations/0014_alter_persondateofbirth_person_and_more.py @@ -0,0 +1,24 @@ +# Generated by Django 4.2.7 on 2024-04-09 12:43 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('person', '0013_agentdescription_historicalperson_personreference_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='persondateofbirth', + name='person', + field=models.OneToOneField(help_text='date on which this person was born', on_delete=django.db.models.deletion.CASCADE, related_name='date_of_birth', to='person.historicalperson'), + ), + migrations.AlterField( + model_name='persondateofdeath', + name='person', + field=models.OneToOneField(help_text='date on which this person died', on_delete=django.db.models.deletion.CASCADE, related_name='date_of_death', to='person.historicalperson'), + ), + ] diff --git a/backend/person/models.py b/backend/person/models.py index 269c3958..cce9a137 100644 --- a/backend/person/models.py +++ b/backend/person/models.py @@ -38,9 +38,10 @@ class HistoricalPerson(HistoricalEntity, models.Model): class PersonDateOfBirth(Field, LettercraftDate, models.Model): - person = models.ForeignKey( + person = models.OneToOneField( to=HistoricalPerson, on_delete=models.CASCADE, + related_name="date_of_birth", help_text="date on which this person was born", ) @@ -52,9 +53,10 @@ def __str__(self): class PersonDateOfDeath(Field, LettercraftDate, models.Model): - person = models.ForeignKey( + person = models.OneToOneField( to=HistoricalPerson, on_delete=models.CASCADE, + related_name="date_of_death", help_text="date on which this person died", ) From ec1ef970255c619f32b6f0750aae8018e98a3e2f Mon Sep 17 00:00:00 2001 From: Luka van der Plas Date: Tue, 9 Apr 2024 14:45:23 +0200 Subject: [PATCH 16/16] unit tests for person models --- backend/conftest.py | 57 ++++++++++++------- backend/person/tests/test_person_models.py | 66 ++++++++++++---------- 2 files changed, 73 insertions(+), 50 deletions(-) diff --git a/backend/conftest.py b/backend/conftest.py index 20800b45..74cf67e1 100644 --- a/backend/conftest.py +++ b/backend/conftest.py @@ -8,7 +8,13 @@ WorldEvent, LetterEventDate, ) -# from person.models import Agent +from person.models import HistoricalPerson, AgentDescription +from source.models import Source + + +@pytest.fixture() +def source(db): + return Source.objects.create(name="Sesame Street") @pytest.fixture() @@ -19,29 +25,40 @@ def letter(db): return letter -# @pytest.fixture() -# def agent(db): -# agent = Agent.objects.create() -# agent.name = "Bert" -# agent.save() -# return agent +@pytest.fixture() +def historical_person(db): + person = HistoricalPerson.objects.create(name="Bert") + return person -# @pytest.fixture() -# def agent_2(db): -# agent = Agent.objects.create() -# agent.name = "Ernie" -# agent.save() -# return agent +@pytest.fixture() +def historical_person_2(db): + person = HistoricalPerson.objects.create(name="Ernie") + return person -# @pytest.fixture() -# def agent_group(db): -# agent_group = Agent.objects.create() -# agent_group.name = "The Muppets" -# agent_group.is_group = True -# agent_group.save() -# return agent_group +@pytest.fixture() +def agent_description(db, historical_person, source): + agent = AgentDescription.objects.create( + name="Bert", + source=source, + ) + agent.describes.add(historical_person) + agent.save() + return agent + + +@pytest.fixture() +def agent_group_description(db, source, historical_person, historical_person_2): + agent = AgentDescription.objects.create( + name="The Muppets", + source=source, + is_group=True, + ) + agent.describes.add(historical_person) + agent.describes.add(historical_person_2) + agent.save() + return agent @pytest.fixture() diff --git a/backend/person/tests/test_person_models.py b/backend/person/tests/test_person_models.py index 09c1440c..a9a61ce2 100644 --- a/backend/person/tests/test_person_models.py +++ b/backend/person/tests/test_person_models.py @@ -1,47 +1,53 @@ -from django.db import IntegrityError from django.forms import ValidationError import pytest -# from person.models import AgentDateOfBirth, AgentName, Gender +from person import models -# def test_agent_name_for_unnamed_agent(agent): -# assert agent.__str__().startswith("Unknown person #") +def test_agent_description_model(agent_description): + assert str(agent_description) == "Bert (Sesame Street)" -# def test_agent_name_for_unnamed_agent_group(agent_group): -# agent_group.name = "" -# assert agent_group.__str__().startswith("Unknown group of people #") +def test_only_groups_can_describe_multiple_people( + db, agent_description, agent_group_description +): + person = models.HistoricalPerson.objects.create(name="Elmo") -# def test_agent_name_for_agent_with_single_name(agent): -# AgentName.objects.create(agent=agent, value="Bert") -# assert agent.__str__() == "Bert" + agent_group_description.describes.add(person) + agent_description.clean() + with pytest.raises(ValidationError): + agent_description.describes.add(person) + agent_description.clean() -# def test_agent_name_for_agent_with_multiple_names(agent): -# AgentName.objects.create(agent=agent, value="Bert") -# AgentName.objects.create(agent=agent, value="Ernie") -# AgentName.objects.create(agent=agent, value="Oscar") -# assert agent.__str__() == "Bert (aka Ernie, Oscar)" +def test_mixed_gender_only_for_groups(db, agent_description, agent_group_description): + gender = models.AgentDescriptionGender( + agent=agent_description, + gender=models.Gender.MALE, + ) + gender.clean() -# def test_agent_with_exact_date_of_birth(agent): -# AgentDateOfBirth.objects.create(agent=agent, year_exact=512) -# assert agent.date_of_birth.__str__().endswith("born in 512") + with pytest.raises(ValidationError): + gender.gender = models.Gender.MIXED + gender.clean() + gender = models.AgentDescriptionGender( + agent=agent_group_description, + gender=models.Gender.MIXED, + ) + gender.clean() -# def test_agent_with_approx_date_of_birth(agent): -# AgentDateOfBirth.objects.create(agent=agent, year_lower=500, year_upper=525) -# assert agent.date_of_birth.__str__().endswith("born c. 500–525") +def test_agent_with_exact_date_of_birth(db, historical_person): + models.PersonDateOfBirth.objects.create(person=historical_person, year_exact=512) + assert historical_person.date_of_birth.__str__().endswith("born in 512") -# def test_agent_group_date_of_birth_constraint(agent_group): -# with pytest.raises(ValidationError): -# AgentDateOfBirth.objects.create(agent=agent_group, year_exact=512) -# agent_group.clean() - -# def test_agent_group_mixed_gender_constraint(agent_group): -# with pytest.raises(IntegrityError): -# agent_group.gender = Gender.MIXED -# agent_group.save() +def test_agent_with_approx_date_of_birth(db, historical_person): + models.PersonDateOfBirth.objects.create( + person=historical_person, + year_lower=500, + year_upper=525, + ) + assert historical_person.date_of_birth.__str__().endswith("born c. 500-525")