From 2bc076f8ef49eda398546a1a3322a5f3396c3155 Mon Sep 17 00:00:00 2001 From: Xander Vertegaal Date: Fri, 15 Mar 2024 15:12:51 +0100 Subject: [PATCH 01/34] Add collaborator data to CITATION --- CITATION.cff | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/CITATION.cff b/CITATION.cff index e1c01229..03091904 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -5,7 +5,27 @@ message: >- metadata from this file. type: software authors: - - name: 'Research Software Lab, Centre for Digital Humanities, Utrecht University' + - given-names: Robert + family-names: Flierman + email: r.flierman@uu.nl + affiliation: >- + Departement Talen, Literatuur en Communicatie Keltisch + en Klassieken, Utrecht University + orcid: 'https://orcid.org/0000-0002-6480-2462' + - given-names: Hope + family-names: Williard + email: h.d.williard@uu.nl + affiliation: >- + Departement Talen, Literatuur en Communicatie Keltisch + en Klassieken, Utrecht University + orcid: 'https://orcid.org/0000-0001-6249-7591' + - given-names: Anne + family-names: Sieberichs + email: a.p.sieberichs@uu.nl + affiliation: 'Departement Talen, Literatuur en Communicatie' + - name: >- + Research Software Lab, Centre for Digital Humanities, + Utrecht University website: 'https://cdh.uu.nl/rsl/' repository-code: 'https://github.com/CentreForDigitalHumanities/lettercraft' license: BSD-3-Clause From 791a642504809e4041e8adf95af1d689fd426d1c Mon Sep 17 00:00:00 2001 From: Xander Vertegaal Date: Sun, 24 Mar 2024 11:28:00 +0100 Subject: [PATCH 02/34] 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 03/34] 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 04/34] 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 05/34] 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 06/34] 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 07/34] 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 08/34] 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 09/34] 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 10/34] 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 11/34] 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 12/34] 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 13/34] 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 14/34] 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 15/34] 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 16/34] 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 17/34] 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") From de69c15c1fb6b38b06fa879d7855206ef7156702 Mon Sep 17 00:00:00 2001 From: Luka van der Plas Date: Wed, 10 Apr 2024 14:53:15 +0200 Subject: [PATCH 18/34] clear letter data --- backend/event/admin.py | 26 +-- ...0011_remove_letteraction_gifts_and_more.py | 21 ++ backend/event/models.py | 24 +- backend/letter/admin.py | 48 ++-- ...remove_lettercategory_category_and_more.py | 38 ++++ backend/letter/models.py | 211 +++++++++--------- 6 files changed, 214 insertions(+), 154 deletions(-) create mode 100644 backend/event/migrations/0011_remove_letteraction_gifts_and_more.py create mode 100644 backend/letter/migrations/0010_delete_gift_remove_lettercategory_category_and_more.py diff --git a/backend/event/admin.py b/backend/event/admin.py index 3d521a5e..4c72a098 100644 --- a/backend/event/admin.py +++ b/backend/event/admin.py @@ -32,26 +32,26 @@ class EventDateAdmin(admin.StackedInline): # verbose_name_plural = "agents/roles involved" -class LetterActionLettersAdmin(admin.StackedInline): - model = models.LetterAction.letters.through - extra = 0 - verbose_name = "letter" - verbose_name_plural = "letters" +# class LetterActionLettersAdmin(admin.StackedInline): +# model = models.LetterAction.letters.through +# extra = 0 +# verbose_name = "letter" +# verbose_name_plural = "letters" -class LetterActionGiftsAdmin(admin.StackedInline): - model = models.LetterAction.gifts.through - extra = 0 - verbose_name = "gift" - verbose_name_plural = "gifts" +# class LetterActionGiftsAdmin(admin.StackedInline): +# model = models.LetterAction.gifts.through +# extra = 0 +# verbose_name = "gift" +# verbose_name_plural = "gifts" @admin.register(models.LetterAction) class LetterActionAdmin(admin.ModelAdmin): - filter_horizontal = ["epistolary_events", "gifts", "space_descriptions"] + filter_horizontal = ["epistolary_events", "space_descriptions"] list_display=["description", "display_date"] inlines = [ - LetterActionLettersAdmin, + # LetterActionLettersAdmin, LetterActionCategoryAdmin, - LetterActionGiftsAdmin, + # LetterActionGiftsAdmin, EventDateAdmin, # RoleAdmin, ] diff --git a/backend/event/migrations/0011_remove_letteraction_gifts_and_more.py b/backend/event/migrations/0011_remove_letteraction_gifts_and_more.py new file mode 100644 index 00000000..518a227a --- /dev/null +++ b/backend/event/migrations/0011_remove_letteraction_gifts_and_more.py @@ -0,0 +1,21 @@ +# Generated by Django 4.2.7 on 2024-04-10 12:52 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('event', '0010_remove_letteraction_actors_delete_role'), + ] + + operations = [ + migrations.RemoveField( + model_name='letteraction', + name='gifts', + ), + migrations.RemoveField( + model_name='letteraction', + name='letters', + ), + ] diff --git a/backend/event/models.py b/backend/event/models.py index dfa16cf1..9535b118 100644 --- a/backend/event/models.py +++ b/backend/event/models.py @@ -4,7 +4,7 @@ from core.models import Field, LettercraftDate from case_study.models import CaseStudy # from person.models import Agent -from letter.models import Gift, Letter +# from letter.models import Gift, Letter from space.models import SpaceDescription class EpistolaryEvent(models.Model): @@ -59,11 +59,11 @@ class LetterAction(models.Model): These can be grouped into epistolary events. """ - letters = models.ManyToManyField( - to=Letter, - related_name="events", - help_text="letters involved in this event", - ) + # letters = models.ManyToManyField( + # to=Letter, + # related_name="events", + # help_text="letters involved in this event", + # ) # actors = models.ManyToManyField( # to=Agent, @@ -77,12 +77,12 @@ class LetterAction(models.Model): help_text="epistolary events this letter action belongs to", ) - gifts = models.ManyToManyField( - to=Gift, - related_name="letter_actions", - help_text="Gifts associated to this letter action", - blank=True, - ) + # gifts = models.ManyToManyField( + # to=Gift, + # related_name="letter_actions", + # help_text="Gifts associated to this letter action", + # blank=True, + # ) space_descriptions = models.ManyToManyField( to=SpaceDescription, diff --git a/backend/letter/admin.py b/backend/letter/admin.py index 8a2abe51..30912e6b 100644 --- a/backend/letter/admin.py +++ b/backend/letter/admin.py @@ -7,14 +7,14 @@ class CategoryAdmin(admin.ModelAdmin): fields = ["label", "description"] -class LetterMaterialAdmin(admin.StackedInline): - model = models.LetterMaterial - fields = ["surface", "certainty", "note"] +# class LetterMaterialAdmin(admin.StackedInline): +# model = models.LetterMaterial +# fields = ["surface", "certainty", "note"] -class LetterCategoryAdmin(admin.StackedInline): - model = models.LetterCategory - fields = ["letter", "category", "certainty", "note"] +# class LetterCategoryAdmin(admin.StackedInline): +# model = models.LetterCategory +# fields = ["letter", "category", "certainty", "note"] # class LetterSenderAdmin(admin.StackedInline): @@ -29,25 +29,25 @@ class LetterCategoryAdmin(admin.StackedInline): # filter_horizontal = ["addressees"] -@admin.register(models.Letter) -class LetterAdmin(admin.ModelAdmin): - readonly_fields = ["date_active", "date_written"] - inlines = [ - LetterCategoryAdmin, - # LetterMaterialAdmin, - # LetterSenderAdmin, - # LetterAddresseesAdmin, - ] +# @admin.register(models.Letter) +# class LetterAdmin(admin.ModelAdmin): +# readonly_fields = ["date_active", "date_written"] +# inlines = [ +# LetterCategoryAdmin, +# # LetterMaterialAdmin, +# # LetterSenderAdmin, +# # LetterAddresseesAdmin, +# ] -class GiftLetterActionInline(admin.StackedInline): - model = models.Gift.letter_actions.through - extra = 0 - verbose_name_plural = "letter actions" - verbose_name = "relationship between a gift and an associated letter action" +# class GiftLetterActionInline(admin.StackedInline): +# model = models.Gift.letter_actions.through +# extra = 0 +# verbose_name_plural = "letter actions" +# verbose_name = "relationship between a gift and an associated letter action" -@admin.register(models.Gift) -class GiftAdmin(admin.ModelAdmin): - fields = ["name", "description", "material"] - filter_horizontal = ["letter_actions"] +# @admin.register(models.Gift) +# class GiftAdmin(admin.ModelAdmin): +# fields = ["name", "description", "material"] +# filter_horizontal = ["letter_actions"] diff --git a/backend/letter/migrations/0010_delete_gift_remove_lettercategory_category_and_more.py b/backend/letter/migrations/0010_delete_gift_remove_lettercategory_category_and_more.py new file mode 100644 index 00000000..4e0ec70c --- /dev/null +++ b/backend/letter/migrations/0010_delete_gift_remove_lettercategory_category_and_more.py @@ -0,0 +1,38 @@ +# Generated by Django 4.2.7 on 2024-04-10 12:52 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('event', '0011_remove_letteraction_gifts_and_more'), + ('letter', '0009_remove_lettersenders_letter_and_more'), + ] + + operations = [ + migrations.DeleteModel( + name='Gift', + ), + migrations.RemoveField( + model_name='lettercategory', + name='category', + ), + migrations.RemoveField( + model_name='lettercategory', + name='letter', + ), + migrations.RemoveField( + model_name='lettermaterial', + name='letter', + ), + migrations.DeleteModel( + name='Letter', + ), + migrations.DeleteModel( + name='LetterCategory', + ), + migrations.DeleteModel( + name='LetterMaterial', + ), + ] diff --git a/backend/letter/models.py b/backend/letter/models.py index a9ee01bb..e3e8b6d7 100644 --- a/backend/letter/models.py +++ b/backend/letter/models.py @@ -1,85 +1,86 @@ from django.db import models from django.contrib import admin from core.models import Field + # from person.models import Agent -class Gift(models.Model): - """ - A gift presented alongside a letter. - """ - - class Material(models.TextChoices): - PRECIOUS_METAL = "precious metal", "precious metal" - WRITE = "textile", "textile" - WOOD = "wood", "wood" - GLASS = "glass", "glass" - CERAMIC = "ceramic", "ceramic" - ANIMAL_PRODUCT = "animal product", "animal product" - LIVESTOCK = "livestock", "livestock" - PAPER = "paper", "paper" - OTHER = "other", "other" - UNKNOWN = "unknown", "unknown" - - name = models.CharField( - max_length=256, help_text="A short name for the gift (for identification)" - ) - - description = models.TextField( - blank=True, - help_text="A longer description of the gift", - ) - - material = models.CharField( - choices=Material.choices, - 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, - # ) - - # 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): - name = models.CharField( - max_length=200, - blank=False, - unique=True, - help_text="a unique name to identify this letter in the database", - ) +# class Gift(models.Model): +# """ +# A gift presented alongside a letter. +# """ + +# class Material(models.TextChoices): +# PRECIOUS_METAL = "precious metal", "precious metal" +# WRITE = "textile", "textile" +# WOOD = "wood", "wood" +# GLASS = "glass", "glass" +# CERAMIC = "ceramic", "ceramic" +# ANIMAL_PRODUCT = "animal product", "animal product" +# LIVESTOCK = "livestock", "livestock" +# PAPER = "paper", "paper" +# OTHER = "other", "other" +# UNKNOWN = "unknown", "unknown" + +# name = models.CharField( +# max_length=256, help_text="A short name for the gift (for identification)" +# ) - def __str__(self): - return self.name +# description = models.TextField( +# blank=True, +# help_text="A longer description of the gift", +# ) - @admin.display( - description="Date range of actions involving this letter", - ) - def date_active(self): - return self._aggregate_dates(self.events.all()) +# material = models.CharField( +# choices=Material.choices, +# help_text="The material the gift consists of", +# ) - @admin.display( - description="Date range in which this letter was written", - ) - def date_written(self): - return self._aggregate_dates(self.events.filter(categories__value="write")) +# 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 _aggregate_dates(self, actions): - """Calculate a date range based on the dates of related actions""" - dates = [action.date for action in actions] - lower = min(date.year_lower for date in dates) - upper = max(data.year_upper for data in dates) - return lower, upper + +# class Letter(models.Model): +# name = models.CharField( +# max_length=200, +# blank=False, +# unique=True, +# help_text="a unique name to identify this letter in the database", +# ) + +# def __str__(self): +# return self.name + +# @admin.display( +# description="Date range of actions involving this letter", +# ) +# def date_active(self): +# return self._aggregate_dates(self.events.all()) + +# @admin.display( +# description="Date range in which this letter was written", +# ) +# def date_written(self): +# return self._aggregate_dates(self.events.filter(categories__value="write")) + +# def _aggregate_dates(self, actions): +# """Calculate a date range based on the dates of related actions""" +# dates = [action.date for action in actions] +# lower = min(date.year_lower for date in dates) +# upper = max(data.year_upper for data in dates) +# return lower, upper class Category(models.Model): @@ -94,41 +95,41 @@ def __str__(self): return self.label -class LetterCategory(Field, models.Model): - category = models.ForeignKey(to=Category, null=True, on_delete=models.SET_NULL) - letter = models.OneToOneField( - to=Letter, - on_delete=models.CASCADE, - null=False, - ) +# class LetterCategory(Field, models.Model): +# category = models.ForeignKey(to=Category, null=True, on_delete=models.SET_NULL) +# letter = models.OneToOneField( +# to=Letter, +# on_delete=models.CASCADE, +# null=False, +# ) - def __str__(self): - return f"category of {self.letter}" - - -class LetterMaterial(Field, models.Model): - class Surface(models.TextChoices): - PARCHMENT = "parchment", "parchment" - PAPYRUS = "papyrus", "papyrus" - OTHER = "other", "other" - UNKNOWN = "unknown", "unknown" - - surface = models.CharField( - choices=Surface.choices, - null=False, - blank=False, - ) - letter = models.OneToOneField( - to=Letter, - on_delete=models.CASCADE, - null=False, - ) +# def __str__(self): +# return f"category of {self.letter}" - def __str__(self): - if self.letter: - return f"material of {self.letter}" - else: - return f"material #{self.id}" + +# class LetterMaterial(Field, models.Model): +# class Surface(models.TextChoices): +# PARCHMENT = "parchment", "parchment" +# PAPYRUS = "papyrus", "papyrus" +# OTHER = "other", "other" +# UNKNOWN = "unknown", "unknown" + +# surface = models.CharField( +# choices=Surface.choices, +# null=False, +# blank=False, +# ) +# letter = models.OneToOneField( +# to=Letter, +# on_delete=models.CASCADE, +# null=False, +# ) + +# def __str__(self): +# if self.letter: +# return f"material of {self.letter}" +# else: +# return f"material #{self.id}" # class LetterSenders(Field, models.Model): From fbc466d3f83fdfd34d7cf9177066af3fbf0a63a4 Mon Sep 17 00:00:00 2001 From: Luka van der Plas Date: Thu, 11 Apr 2024 13:23:48 +0200 Subject: [PATCH 19/34] update letter + gift models --- backend/conftest.py | 22 +- .../management/commands/create_dev_dataset.py | 4 +- backend/event/models.py | 4 +- backend/event/tests/test_event_models.py | 20 +- backend/letter/admin.py | 110 +++--- ...tdescription_letterdescription_and_more.py | 190 +++++++++++ backend/letter/models.py | 314 +++++++++--------- backend/letter/tests/test_letter_models.py | 7 +- 8 files changed, 450 insertions(+), 221 deletions(-) create mode 100644 backend/letter/migrations/0011_giftcategory_giftdescription_letterdescription_and_more.py diff --git a/backend/conftest.py b/backend/conftest.py index 74cf67e1..32ab1d5d 100644 --- a/backend/conftest.py +++ b/backend/conftest.py @@ -1,6 +1,6 @@ import pytest from case_study.models import CaseStudy -from letter.models import Letter +from letter.models import LetterDescription from event.models import ( EpistolaryEvent, LetterAction, @@ -18,10 +18,12 @@ def source(db): @pytest.fixture() -def letter(db): - letter = Letter.objects.create() - letter.name = "letter for testing" - letter.save() +def letter_description(db, source, agent_description): + letter = LetterDescription.objects.create( + name="Bert's letter", + source=source, + ) + letter.senders.add(agent_description) return letter @@ -62,9 +64,9 @@ def agent_group_description(db, source, historical_person, historical_person_2): @pytest.fixture() -def letter_action_writing(db, letter): +def letter_action_writing(db, letter_description): letter_action = LetterAction.objects.create() - letter_action.letters.add(letter) + # letter_action.letters.add(letter_description) # letter_action.actors.add(agent) LetterActionCategory.objects.create( @@ -80,9 +82,9 @@ def letter_action_writing(db, letter): @pytest.fixture() -def letter_action_reading(db, letter): +def letter_action_reading(db, letter_description): letter_action = LetterAction.objects.create() - letter_action.letters.add(letter) + # letter_action.letters.add(letter_description) # letter_action.actors.add(agent_2) LetterActionCategory.objects.create( @@ -104,7 +106,7 @@ def case_study(db): @pytest.fixture() -def epistolary_event(db, letter, case_study): +def epistolary_event(db, letter_description, case_study): epistolary_event = EpistolaryEvent.objects.create( name="Test Epistolary event", note="Test note" ) diff --git a/backend/core/management/commands/create_dev_dataset.py b/backend/core/management/commands/create_dev_dataset.py index a06aef52..f1d2cc39 100644 --- a/backend/core/management/commands/create_dev_dataset.py +++ b/backend/core/management/commands/create_dev_dataset.py @@ -257,9 +257,9 @@ def _create_letters(self, fake: Faker, *args, **kwargs): @track_progress def _create_letter_actions(self, fake: Faker, *args, **kwargs): action = LetterAction.objects.create() - action.letters.set(get_random_model_objects(Letter, min_amount=1, max_amount=5)) + # action.letters.set(get_random_model_objects(Letter, min_amount=1, max_amount=5)) - action.gifts.set(get_random_model_objects(Gift, min_amount=0, max_amount=5)) + # action.gifts.set(get_random_model_objects(Gift, min_amount=0, max_amount=5)) action.epistolary_events.set( get_random_model_objects(EpistolaryEvent, min_amount=0, max_amount=5) diff --git a/backend/event/models.py b/backend/event/models.py index 9535b118..506989f3 100644 --- a/backend/event/models.py +++ b/backend/event/models.py @@ -101,8 +101,8 @@ def description(self): categories = self.categories.all() category_names = [category.get_value_display() for category in categories] category_desc = ", ".join(category_names) - letters = ", ".join(letter.__str__() for letter in self.letters.all()) - return f"{category_desc} of {letters}" + # letters = ", ".join(letter.__str__() for letter in self.letters.all()) + return f"{category_desc}" def __str__(self): return f"{self.description} ({self.display_date})" diff --git a/backend/event/tests/test_event_models.py b/backend/event/tests/test_event_models.py index 0e72e23d..9a64b5e8 100644 --- a/backend/event/tests/test_event_models.py +++ b/backend/event/tests/test_event_models.py @@ -6,21 +6,21 @@ ) -def test_letter_action_name(letter, letter_action_writing): +def test_letter_action_name(letter_description, letter_action_writing): letter_action_writing.date.year_exact = 500 letter_action_writing.save() action_str = str(letter_action_writing) - assert str(action_str) == f"writing of {str(letter)} (500)" + # assert str(action_str) == f"writing of {str(letter_description)} (500)" def test_letter_event_date_with_exact_date(letter_action_reading): letter_action_reading.date.year_exact = 500 letter_action_reading.save() - assert ( - str(letter_action_reading) - == f"reading of {str(letter_action_reading.letters.first())} (500)" - ) + # assert ( + # str(letter_action_reading) + # == f"reading of {str(letter_action_reading.letters.first())} (500)" + # ) def test_letter_event_date_with_date_range(letter_action_reading): @@ -28,10 +28,10 @@ def test_letter_event_date_with_date_range(letter_action_reading): letter_action_reading.date.year_upper = 600 letter_action_reading.save() - assert ( - str(letter_action_reading) - == f"reading of {str(letter_action_reading.letters.first())} (c. 500–600)" - ) + # assert ( + # str(letter_action_reading) + # == f"reading of {str(letter_action_reading.letters.first())} (c. 500–600)" + # ) def test_world_event(world_event): diff --git a/backend/letter/admin.py b/backend/letter/admin.py index 30912e6b..4259c42d 100644 --- a/backend/letter/admin.py +++ b/backend/letter/admin.py @@ -1,53 +1,89 @@ from django.contrib import admin from . import models +from core import admin as core_admin -@admin.register(models.Category) -class CategoryAdmin(admin.ModelAdmin): - fields = ["label", "description"] - +@admin.register(models.GiftCategory) +class GiftCategoryAdmin(admin.ModelAdmin): + pass -# class LetterMaterialAdmin(admin.StackedInline): -# model = models.LetterMaterial -# fields = ["surface", "certainty", "note"] +class GiftDescriptionCategoryAdmin(admin.StackedInline): + model = models.GiftDescriptionCategory + fields = ["category"] + core_admin.description_field_fields + extra = 0 + verbose_name = "category" + verbose_name_plural = "categories" -# class LetterCategoryAdmin(admin.StackedInline): -# model = models.LetterCategory -# fields = ["letter", "category", "certainty", "note"] +class GiftDescriptionSenderAdmin(admin.StackedInline): + model = models.GiftDescriptionSender + fields = ["agent"] + core_admin.description_field_fields + extra = 0 + verbose_name = "sender" -# class LetterSenderAdmin(admin.StackedInline): -# model = models.LetterSenders -# fields = ["letter", "senders", "certainty", "note"] -# filter_horizontal = ["senders"] +class GiftDescriptionAddresseeAdmin(admin.StackedInline): + model = models.GiftDescriptionAddressee + fields = ["agent"] + core_admin.description_field_fields + extra = 0 + verbose_name = "addressee" -# class LetterAddresseesAdmin(admin.StackedInline): -# model = models.LetterAddressees -# fields = ["letter", "addressees", "certainty", "note"] -# filter_horizontal = ["addressees"] +@admin.register(models.GiftDescription) +class GiftDescriptionAdmin(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 = [ + GiftDescriptionCategoryAdmin, + GiftDescriptionSenderAdmin, + GiftDescriptionAddresseeAdmin, + ] -# @admin.register(models.Letter) -# class LetterAdmin(admin.ModelAdmin): -# readonly_fields = ["date_active", "date_written"] -# inlines = [ -# LetterCategoryAdmin, -# # LetterMaterialAdmin, -# # LetterSenderAdmin, -# # LetterAddresseesAdmin, -# ] - -# class GiftLetterActionInline(admin.StackedInline): -# model = models.Gift.letter_actions.through -# extra = 0 -# verbose_name_plural = "letter actions" -# verbose_name = "relationship between a gift and an associated letter action" +@admin.register(models.Category) +class CategoryAdmin(admin.ModelAdmin): + fields = ["label", "description"] -# @admin.register(models.Gift) -# class GiftAdmin(admin.ModelAdmin): -# fields = ["name", "description", "material"] -# filter_horizontal = ["letter_actions"] +class LetterDescriptionCategoryAdmin(admin.StackedInline): + model = models.LetterDescriptionCategory + fields = ["category"] + core_admin.description_field_fields + extra = 0 + verbose_name = "category" + verbose_name_plural = "categories" + + +class LetterDescriptionSenderAdmin(admin.StackedInline): + model = models.LetterDescriptionSender + fields = ["agent"] + core_admin.description_field_fields + extra = 0 + verbose_name = "sender" + + +class LetterDescriptionAddresseeAdmin(admin.StackedInline): + model = models.LetterDescriptionAddressee + fields = ["agent"] + core_admin.description_field_fields + extra = 0 + verbose_name = "addressee" + + +@admin.register(models.LetterDescription) +class LetterDescriptionAdmin(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 = [ + LetterDescriptionCategoryAdmin, + LetterDescriptionSenderAdmin, + LetterDescriptionAddresseeAdmin, + ] diff --git a/backend/letter/migrations/0011_giftcategory_giftdescription_letterdescription_and_more.py b/backend/letter/migrations/0011_giftcategory_giftdescription_letterdescription_and_more.py new file mode 100644 index 00000000..f52380ca --- /dev/null +++ b/backend/letter/migrations/0011_giftcategory_giftdescription_letterdescription_and_more.py @@ -0,0 +1,190 @@ +# Generated by Django 4.2.7 on 2024-04-11 11:13 + +import django.contrib.postgres.fields +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('person', '0014_alter_persondateofbirth_person_and_more'), + ('source', '0005_placeholder_source'), + ('letter', '0010_delete_gift_remove_lettercategory_category_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='GiftCategory', + 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')), + ], + options={ + 'verbose_name_plural': 'gift categories', + }, + ), + migrations.CreateModel( + name='GiftDescription', + 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)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='LetterDescription', + 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)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='LetterDescriptionSender', + 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, to='person.agentdescription')), + ('letter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='letter.letterdescription')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='LetterDescriptionCategory', + 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)), + ('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='letter.category')), + ('letter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='letter.letterdescription')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='LetterDescriptionAddressee', + 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, to='person.agentdescription')), + ('letter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='letter.letterdescription')), + ], + options={ + 'abstract': False, + }, + ), + migrations.AddField( + model_name='letterdescription', + name='addressees', + field=models.ManyToManyField(related_name='letters_addressed', through='letter.LetterDescriptionAddressee', to='person.agentdescription'), + ), + migrations.AddField( + model_name='letterdescription', + name='categories', + field=models.ManyToManyField(through='letter.LetterDescriptionCategory', to='letter.category'), + ), + migrations.AddField( + model_name='letterdescription', + name='senders', + field=models.ManyToManyField(related_name='letters_sent', through='letter.LetterDescriptionSender', to='person.agentdescription'), + ), + migrations.AddField( + model_name='letterdescription', + name='source', + field=models.ForeignKey(help_text='Source text containing this description', on_delete=django.db.models.deletion.PROTECT, to='source.source'), + ), + migrations.CreateModel( + name='GiftDescriptionSender', + 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, to='person.agentdescription')), + ('gift', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='letter.giftdescription')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='GiftDescriptionCategory', + 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)), + ('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='letter.giftcategory')), + ('gift', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='letter.giftdescription')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='GiftDescriptionAddressee', + 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, to='person.agentdescription')), + ('gift', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='letter.giftdescription')), + ], + options={ + 'abstract': False, + }, + ), + migrations.AddField( + model_name='giftdescription', + name='addressees', + field=models.ManyToManyField(related_name='gifts_addressed', through='letter.GiftDescriptionAddressee', to='person.agentdescription'), + ), + migrations.AddField( + model_name='giftdescription', + name='categories', + field=models.ManyToManyField(through='letter.GiftDescriptionCategory', to='letter.giftcategory'), + ), + migrations.AddField( + model_name='giftdescription', + name='senders', + field=models.ManyToManyField(related_name='gifts_sent', through='letter.GiftDescriptionSender', to='person.agentdescription'), + ), + migrations.AddField( + model_name='giftdescription', + name='source', + field=models.ForeignKey(help_text='Source text containing this description', on_delete=django.db.models.deletion.PROTECT, to='source.source'), + ), + ] diff --git a/backend/letter/models.py b/backend/letter/models.py index e3e8b6d7..a900e9a6 100644 --- a/backend/letter/models.py +++ b/backend/letter/models.py @@ -1,86 +1,112 @@ from django.db import models -from django.contrib import admin -from core.models import Field - -# from person.models import Agent - - -# class Gift(models.Model): -# """ -# A gift presented alongside a letter. -# """ - -# class Material(models.TextChoices): -# PRECIOUS_METAL = "precious metal", "precious metal" -# WRITE = "textile", "textile" -# WOOD = "wood", "wood" -# GLASS = "glass", "glass" -# CERAMIC = "ceramic", "ceramic" -# ANIMAL_PRODUCT = "animal product", "animal product" -# LIVESTOCK = "livestock", "livestock" -# PAPER = "paper", "paper" -# OTHER = "other", "other" -# UNKNOWN = "unknown", "unknown" - -# name = models.CharField( -# max_length=256, help_text="A short name for the gift (for identification)" -# ) - -# description = models.TextField( -# blank=True, -# help_text="A longer description of the gift", -# ) - -# material = models.CharField( -# choices=Material.choices, -# 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, -# ) - -# 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): -# name = models.CharField( -# max_length=200, -# blank=False, -# unique=True, -# help_text="a unique name to identify this letter in the database", -# ) - -# def __str__(self): -# return self.name - -# @admin.display( -# description="Date range of actions involving this letter", -# ) -# def date_active(self): -# return self._aggregate_dates(self.events.all()) - -# @admin.display( -# description="Date range in which this letter was written", -# ) -# def date_written(self): -# return self._aggregate_dates(self.events.filter(categories__value="write")) - -# def _aggregate_dates(self, actions): -# """Calculate a date range based on the dates of related actions""" -# dates = [action.date for action in actions] -# lower = min(date.year_lower for date in dates) -# upper = max(data.year_upper for data in dates) -# return lower, upper +from django.core.exceptions import ValidationError + +from core.models import DescriptionField, EntityDescription, Named +from person.models import AgentDescription + + +class GiftDescription(EntityDescription, models.Model): + """ + A gift described in an narrative source text + """ + + categories = models.ManyToManyField( + to="GiftCategory", + through="GiftDescriptionCategory", + ) + senders = models.ManyToManyField( + to=AgentDescription, + through="GiftDescriptionSender", + related_name="gifts_sent", + ) + addressees = models.ManyToManyField( + to=AgentDescription, + through="GiftDescriptionAddressee", + related_name="gifts_addressed", + ) + + +class GiftCategory(Named, models.Model): + """ + A type of gift (e.g. "silver cup", "hairshirt") + """ + + class Meta: + verbose_name_plural = "gift categories" + + +class GiftDescriptionCategory(DescriptionField, models.Model): + """ + Categorisation of a gift in an a narrative source. + """ + + gift = models.ForeignKey( + to=GiftDescription, + on_delete=models.CASCADE, + ) + category = models.ForeignKey( + to=GiftCategory, + on_delete=models.CASCADE, + ) + + +class GiftDescriptionSender(DescriptionField, models.Model): + """ + Description of a person as the sender of a gift + """ + + gift = models.ForeignKey( + to=GiftDescription, + on_delete=models.CASCADE, + ) + agent = models.ForeignKey( + to=AgentDescription, + on_delete=models.CASCADE, + ) + + def clean(self): + if self.gift.source != self.agent.source: + raise ValidationError("Can only link descriptions in the same source text") + + +class GiftDescriptionAddressee(DescriptionField, models.Model): + """ + Description of a person as the addressee of a gift + """ + + gift = models.ForeignKey( + to=GiftDescription, + on_delete=models.CASCADE, + ) + agent = models.ForeignKey( + to=AgentDescription, + on_delete=models.CASCADE, + ) + + def clean(self): + if self.gift.source != self.agent.source: + raise ValidationError("Can only link descriptions in the same source text") + + +class LetterDescription(EntityDescription, models.Model): + """ + A letter described in a narrative source text + """ + + categories = models.ManyToManyField( + to="Category", + through="LetterDescriptionCategory", + ) + senders = models.ManyToManyField( + to=AgentDescription, + through="LetterDescriptionSender", + related_name="letters_sent", + ) + addressees = models.ManyToManyField( + to=AgentDescription, + through="LetterDescriptionAddressee", + related_name="letters_addressed", + ) class Category(models.Model): @@ -95,76 +121,54 @@ def __str__(self): return self.label -# class LetterCategory(Field, models.Model): -# category = models.ForeignKey(to=Category, null=True, on_delete=models.SET_NULL) -# letter = models.OneToOneField( -# to=Letter, -# on_delete=models.CASCADE, -# null=False, -# ) - -# def __str__(self): -# return f"category of {self.letter}" - - -# class LetterMaterial(Field, models.Model): -# class Surface(models.TextChoices): -# PARCHMENT = "parchment", "parchment" -# PAPYRUS = "papyrus", "papyrus" -# OTHER = "other", "other" -# UNKNOWN = "unknown", "unknown" - -# surface = models.CharField( -# choices=Surface.choices, -# null=False, -# blank=False, -# ) -# letter = models.OneToOneField( -# to=Letter, -# on_delete=models.CASCADE, -# null=False, -# ) - -# def __str__(self): -# if self.letter: -# return f"material of {self.letter}" -# else: -# 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 LetterDescriptionCategory(DescriptionField, models.Model): + """ + Categorisation of a letter in an a narrative source. + """ + + letter = models.ForeignKey( + to=LetterDescription, + on_delete=models.CASCADE, + ) + category = models.ForeignKey( + to=Category, + on_delete=models.CASCADE, + ) + + +class LetterDescriptionSender(DescriptionField, models.Model): + """ + Description of a person as the sender of a letter + """ + + letter = models.ForeignKey( + to=LetterDescription, + on_delete=models.CASCADE, + ) + agent = models.ForeignKey( + to=AgentDescription, + on_delete=models.CASCADE, + ) + + def clean(self): + if self.letter.source != self.agent.source: + raise ValidationError("Can only link descriptions in the same source text") + + +class LetterDescriptionAddressee(DescriptionField, models.Model): + """ + Description of a person as the addressee of a letter + """ + + letter = models.ForeignKey( + to=LetterDescription, + on_delete=models.CASCADE, + ) + agent = models.ForeignKey( + to=AgentDescription, + on_delete=models.CASCADE, + ) + + def clean(self): + if self.letter.source != self.agent.source: + raise ValidationError("Can only link descriptions in the same source text") diff --git a/backend/letter/tests/test_letter_models.py b/backend/letter/tests/test_letter_models.py index 7a16e9ff..5303638f 100644 --- a/backend/letter/tests/test_letter_models.py +++ b/backend/letter/tests/test_letter_models.py @@ -1,5 +1,2 @@ -def test_letter_property_inference( - letter, letter_action_writing, letter_action_reading -): - assert letter.date_written() == (500, 500) - assert letter.date_active() == (500, 510) +def test_letter_description_model(letter_description): + assert letter_description.senders.count() == 1 From 739cab7a181e133c637dd8c41828b8a202edccf0 Mon Sep 17 00:00:00 2001 From: Luka van der Plas Date: Thu, 11 Apr 2024 13:26:53 +0200 Subject: [PATCH 20/34] add base EntityDescriptionAdmin --- backend/core/admin.py | 10 ++++++++++ backend/letter/admin.py | 18 ++---------------- backend/person/admin.py | 9 +-------- 3 files changed, 13 insertions(+), 24 deletions(-) diff --git a/backend/core/admin.py b/backend/core/admin.py index 4b7644f3..ca3a83f6 100644 --- a/backend/core/admin.py +++ b/backend/core/admin.py @@ -29,3 +29,13 @@ "source_location", "source_terminology", ] + field_fields + + +class EntityDescriptionAdmin(admin.ModelAdmin): + list_display = ["name", "description", "source"] + list_filter = ["source"] + search_fields = ["name", "description"] + fieldsets = [ + named_fieldset, + description_source_fieldset, + ] diff --git a/backend/letter/admin.py b/backend/letter/admin.py index 4259c42d..2072e9d3 100644 --- a/backend/letter/admin.py +++ b/backend/letter/admin.py @@ -31,14 +31,7 @@ class GiftDescriptionAddresseeAdmin(admin.StackedInline): @admin.register(models.GiftDescription) -class GiftDescriptionAdmin(admin.ModelAdmin): - list_display = ["name", "description", "source"] - list_filter = ["source"] - search_fields = ["name", "description"] - fieldsets = [ - core_admin.named_fieldset, - core_admin.description_source_fieldset, - ] +class GiftDescriptionAdmin(core_admin.EntityDescriptionAdmin, admin.ModelAdmin): inlines = [ GiftDescriptionCategoryAdmin, GiftDescriptionSenderAdmin, @@ -74,14 +67,7 @@ class LetterDescriptionAddresseeAdmin(admin.StackedInline): @admin.register(models.LetterDescription) -class LetterDescriptionAdmin(admin.ModelAdmin): - list_display = ["name", "description", "source"] - list_filter = ["source"] - search_fields = ["name", "description"] - fieldsets = [ - core_admin.named_fieldset, - core_admin.description_source_fieldset, - ] +class LetterDescriptionAdmin(core_admin.EntityDescriptionAdmin, admin.ModelAdmin): inlines = [ LetterDescriptionCategoryAdmin, LetterDescriptionSenderAdmin, diff --git a/backend/person/admin.py b/backend/person/admin.py index cdfee255..39935d05 100644 --- a/backend/person/admin.py +++ b/backend/person/admin.py @@ -54,14 +54,7 @@ class AgentDescriptionLocationAdmin(admin.StackedInline): @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, - ] +class AgentDescriptionAdmin(core_admin.EntityDescriptionAdmin, admin.ModelAdmin): inlines = [ AgentDescriptionNameAdmin, AgentDescriptionGenderAdmin, From 1889d7f77461d82a00fe8d6dd9d98b0a68c1feee Mon Sep 17 00:00:00 2001 From: Luka van der Plas Date: Thu, 11 Apr 2024 13:36:49 +0200 Subject: [PATCH 21/34] small model updates --- ...tdescription_letterdescription_and_more.py | 14 ++++++------ backend/letter/models.py | 22 +++++++++++++++++-- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/backend/letter/migrations/0011_giftcategory_giftdescription_letterdescription_and_more.py b/backend/letter/migrations/0011_giftcategory_giftdescription_letterdescription_and_more.py index f52380ca..bddc0de5 100644 --- a/backend/letter/migrations/0011_giftcategory_giftdescription_letterdescription_and_more.py +++ b/backend/letter/migrations/0011_giftcategory_giftdescription_letterdescription_and_more.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.7 on 2024-04-11 11:13 +# Generated by Django 4.2.7 on 2024-04-11 11:36 import django.contrib.postgres.fields from django.db import migrations, models @@ -102,17 +102,17 @@ class Migration(migrations.Migration): migrations.AddField( model_name='letterdescription', name='addressees', - field=models.ManyToManyField(related_name='letters_addressed', through='letter.LetterDescriptionAddressee', to='person.agentdescription'), + field=models.ManyToManyField(blank=True, help_text='agents described as the addressee of the letter', related_name='letters_addressed', through='letter.LetterDescriptionAddressee', to='person.agentdescription'), ), migrations.AddField( model_name='letterdescription', name='categories', - field=models.ManyToManyField(through='letter.LetterDescriptionCategory', to='letter.category'), + field=models.ManyToManyField(blank=True, help_text='categories assigned to the letter', through='letter.LetterDescriptionCategory', to='letter.category'), ), migrations.AddField( model_name='letterdescription', name='senders', - field=models.ManyToManyField(related_name='letters_sent', through='letter.LetterDescriptionSender', to='person.agentdescription'), + field=models.ManyToManyField(blank=True, help_text='agents described as the sender of the letter', related_name='letters_sent', through='letter.LetterDescriptionSender', to='person.agentdescription'), ), migrations.AddField( model_name='letterdescription', @@ -170,17 +170,17 @@ class Migration(migrations.Migration): migrations.AddField( model_name='giftdescription', name='addressees', - field=models.ManyToManyField(related_name='gifts_addressed', through='letter.GiftDescriptionAddressee', to='person.agentdescription'), + field=models.ManyToManyField(blank=True, help_text='agents described as the addressee of the gift', related_name='gifts_addressed', through='letter.GiftDescriptionAddressee', to='person.agentdescription'), ), migrations.AddField( model_name='giftdescription', name='categories', - field=models.ManyToManyField(through='letter.GiftDescriptionCategory', to='letter.giftcategory'), + field=models.ManyToManyField(blank=True, help_text='categories assigned to the gift', through='letter.GiftDescriptionCategory', to='letter.giftcategory'), ), migrations.AddField( model_name='giftdescription', name='senders', - field=models.ManyToManyField(related_name='gifts_sent', through='letter.GiftDescriptionSender', to='person.agentdescription'), + field=models.ManyToManyField(blank=True, help_text='agents described as the sender of the gift', related_name='gifts_sent', through='letter.GiftDescriptionSender', to='person.agentdescription'), ), migrations.AddField( model_name='giftdescription', diff --git a/backend/letter/models.py b/backend/letter/models.py index a900e9a6..93894abc 100644 --- a/backend/letter/models.py +++ b/backend/letter/models.py @@ -1,8 +1,14 @@ from django.db import models from django.core.exceptions import ValidationError -from core.models import DescriptionField, EntityDescription, Named -from person.models import AgentDescription +from core.models import ( + DescriptionField, + EntityDescription, + Named, + HistoricalEntity, + Field, +) +from person.models import AgentDescription, HistoricalPerson class GiftDescription(EntityDescription, models.Model): @@ -13,16 +19,22 @@ class GiftDescription(EntityDescription, models.Model): categories = models.ManyToManyField( to="GiftCategory", through="GiftDescriptionCategory", + blank=True, + help_text="categories assigned to the gift", ) senders = models.ManyToManyField( to=AgentDescription, through="GiftDescriptionSender", related_name="gifts_sent", + blank=True, + help_text="agents described as the sender of the gift", ) addressees = models.ManyToManyField( to=AgentDescription, through="GiftDescriptionAddressee", related_name="gifts_addressed", + blank=True, + help_text="agents described as the addressee of the gift", ) @@ -96,16 +108,22 @@ class LetterDescription(EntityDescription, models.Model): categories = models.ManyToManyField( to="Category", through="LetterDescriptionCategory", + blank=True, + help_text="categories assigned to the letter", ) senders = models.ManyToManyField( to=AgentDescription, through="LetterDescriptionSender", related_name="letters_sent", + blank=True, + help_text="agents described as the sender of the letter", ) addressees = models.ManyToManyField( to=AgentDescription, through="LetterDescriptionAddressee", related_name="letters_addressed", + blank=True, + help_text="agents described as the addressee of the letter", ) From 096d6e3b3262e709d6a442d872b835864073dfb9 Mon Sep 17 00:00:00 2001 From: Luka van der Plas Date: Thu, 11 Apr 2024 13:44:11 +0200 Subject: [PATCH 22/34] add preserved letter database models --- backend/letter/admin.py | 17 +++++++ ...rvedletter_preservedletterrole_and_more.py | 45 +++++++++++++++++++ backend/letter/models.py | 31 +++++++++++++ 3 files changed, 93 insertions(+) create mode 100644 backend/letter/migrations/0012_preservedletter_preservedletterrole_and_more.py diff --git a/backend/letter/admin.py b/backend/letter/admin.py index 2072e9d3..de147653 100644 --- a/backend/letter/admin.py +++ b/backend/letter/admin.py @@ -73,3 +73,20 @@ class LetterDescriptionAdmin(core_admin.EntityDescriptionAdmin, admin.ModelAdmin LetterDescriptionSenderAdmin, LetterDescriptionAddresseeAdmin, ] + + +class PreservedLetterRoleAdmin(admin.StackedInline): + model = models.PreservedLetterRole + fields = ["letter", "person"] + core_admin.field_fields + extra = 0 + verbose_name = "involved historical person" + verbose_name_plural = "involved historical persons" + + +@admin.register(models.PreservedLetter) +class PreservedLetterAdmin(admin.ModelAdmin): + list_display = ["name", "description"] + fields = ["name", "description"] + inlines = [ + PreservedLetterRoleAdmin, + ] diff --git a/backend/letter/migrations/0012_preservedletter_preservedletterrole_and_more.py b/backend/letter/migrations/0012_preservedletter_preservedletterrole_and_more.py new file mode 100644 index 00000000..523b933e --- /dev/null +++ b/backend/letter/migrations/0012_preservedletter_preservedletterrole_and_more.py @@ -0,0 +1,45 @@ +# Generated by Django 4.2.7 on 2024-04-11 11:38 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('person', '0014_alter_persondateofbirth_person_and_more'), + ('letter', '0011_giftcategory_giftdescription_letterdescription_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='PreservedLetter', + 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='PreservedLetterRole', + 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')), + ('letter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='letter.preservedletter')), + ('person', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='person.historicalperson')), + ], + options={ + 'abstract': False, + }, + ), + migrations.AddField( + model_name='preservedletter', + name='persons_involved', + field=models.ManyToManyField(blank=True, help_text='historical figures related to the letter', through='letter.PreservedLetterRole', to='person.historicalperson'), + ), + ] diff --git a/backend/letter/models.py b/backend/letter/models.py index 93894abc..75d33aaf 100644 --- a/backend/letter/models.py +++ b/backend/letter/models.py @@ -190,3 +190,34 @@ class LetterDescriptionAddressee(DescriptionField, models.Model): def clean(self): if self.letter.source != self.agent.source: raise ValidationError("Can only link descriptions in the same source text") + + +class PreservedLetter(HistoricalEntity, models.Model): + """ + A letter that has been preserved + """ + + persons_involved = models.ManyToManyField( + to=HistoricalPerson, + through="PreservedLetterRole", + blank=True, + help_text="historical figures related to the letter", + ) + + +class PreservedLetterRole(Field, models.Model): + """ + Relationship between a preserved letter and a historical figure + """ + + letter = models.ForeignKey( + to=PreservedLetter, + on_delete=models.CASCADE, + ) + person = models.ForeignKey( + to=HistoricalPerson, + on_delete=models.CASCADE, + ) + + def __str__(self) -> str: + return f"role of {self.person} in {self.letter}" From 3f789376d8b851c56f3b9542641bd59a640d53ab Mon Sep 17 00:00:00 2001 From: Luka van der Plas Date: Thu, 11 Apr 2024 13:58:25 +0200 Subject: [PATCH 23/34] string representations --- backend/letter/models.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/backend/letter/models.py b/backend/letter/models.py index 75d33aaf..8335b2db 100644 --- a/backend/letter/models.py +++ b/backend/letter/models.py @@ -61,6 +61,9 @@ class GiftDescriptionCategory(DescriptionField, models.Model): on_delete=models.CASCADE, ) + def __str__(self) -> str: + return f"category {self.category} on {self.gift}" + class GiftDescriptionSender(DescriptionField, models.Model): """ @@ -80,6 +83,9 @@ def clean(self): if self.gift.source != self.agent.source: raise ValidationError("Can only link descriptions in the same source text") + def __str__(self): + return f"{self.agent.name} is sender of {self.gift.name} ({self.gift.source})" + class GiftDescriptionAddressee(DescriptionField, models.Model): """ @@ -99,6 +105,11 @@ def clean(self): if self.gift.source != self.agent.source: raise ValidationError("Can only link descriptions in the same source text") + def __str__(self): + return ( + f"{self.agent.name} is addressee of {self.gift.name} ({self.gift.source})" + ) + class LetterDescription(EntityDescription, models.Model): """ @@ -153,6 +164,9 @@ class LetterDescriptionCategory(DescriptionField, models.Model): on_delete=models.CASCADE, ) + def __str__(self) -> str: + return f"category {self.category} on {self.letter}" + class LetterDescriptionSender(DescriptionField, models.Model): """ @@ -172,6 +186,11 @@ def clean(self): if self.letter.source != self.agent.source: raise ValidationError("Can only link descriptions in the same source text") + def __str__(self): + return ( + f"{self.agent.name} is sender of {self.letter.name} ({self.letter.source})" + ) + class LetterDescriptionAddressee(DescriptionField, models.Model): """ @@ -191,6 +210,9 @@ def clean(self): if self.letter.source != self.agent.source: raise ValidationError("Can only link descriptions in the same source text") + def __str__(self): + return f"{self.agent.name} is addressee of {self.letter.name} ({self.letter.source})" + class PreservedLetter(HistoricalEntity, models.Model): """ From e7e28ed3f506b21c8501e683cf734b49e47a67cc Mon Sep 17 00:00:00 2001 From: Luka van der Plas Date: Thu, 11 Apr 2024 14:28:55 +0200 Subject: [PATCH 24/34] clear event data --- backend/case_study/admin.py | 3 +- backend/conftest.py | 90 +++-- .../management/commands/create_dev_dataset.py | 268 +++++++------- backend/event/admin.py | 129 ------- ...ger_triggered_epistolary_event_and_more.py | 96 +++++ backend/event/models.py | 327 ------------------ backend/event/tests/test_event_models.py | 66 ---- 7 files changed, 273 insertions(+), 706 deletions(-) create mode 100644 backend/event/migrations/0012_remove_epistolaryeventselftrigger_triggered_epistolary_event_and_more.py diff --git a/backend/case_study/admin.py b/backend/case_study/admin.py index bb80522e..c64c0a30 100644 --- a/backend/case_study/admin.py +++ b/backend/case_study/admin.py @@ -1,10 +1,9 @@ from django.contrib import admin from . import models -from event.admin import EpistolaryEventInline @admin.register(models.CaseStudy) class CaseStudyAdmin(admin.ModelAdmin): fields = ["name"] - inlines = [EpistolaryEventInline] + inlines = [] extra = 0 diff --git a/backend/conftest.py b/backend/conftest.py index 32ab1d5d..f9caaa03 100644 --- a/backend/conftest.py +++ b/backend/conftest.py @@ -1,13 +1,7 @@ import pytest from case_study.models import CaseStudy from letter.models import LetterDescription -from event.models import ( - EpistolaryEvent, - LetterAction, - LetterActionCategory, - WorldEvent, - LetterEventDate, -) + from person.models import HistoricalPerson, AgentDescription from source.models import Source @@ -63,40 +57,40 @@ def agent_group_description(db, source, historical_person, historical_person_2): return agent -@pytest.fixture() -def letter_action_writing(db, letter_description): - letter_action = LetterAction.objects.create() - # letter_action.letters.add(letter_description) - # letter_action.actors.add(agent) - - LetterActionCategory.objects.create( - letter_action=letter_action, - value="write", - ) +# @pytest.fixture() +# def letter_action_writing(db, letter_description): +# letter_action = LetterAction.objects.create() +# # letter_action.letters.add(letter_description) +# # letter_action.actors.add(agent) - LetterEventDate.objects.create( - year_lower=500, year_upper=500, year_exact=500, letter_action=letter_action - ) +# LetterActionCategory.objects.create( +# letter_action=letter_action, +# value="write", +# ) - return letter_action +# LetterEventDate.objects.create( +# year_lower=500, year_upper=500, year_exact=500, letter_action=letter_action +# ) +# return letter_action -@pytest.fixture() -def letter_action_reading(db, letter_description): - letter_action = LetterAction.objects.create() - # letter_action.letters.add(letter_description) - # letter_action.actors.add(agent_2) - - LetterActionCategory.objects.create( - letter_action=letter_action, - value="read", - ) - LetterEventDate.objects.create( - year_lower=505, year_upper=510, letter_action=letter_action - ) +# @pytest.fixture() +# def letter_action_reading(db, letter_description): +# letter_action = LetterAction.objects.create() +# # letter_action.letters.add(letter_description) +# # letter_action.actors.add(agent_2) - return letter_action +# LetterActionCategory.objects.create( +# letter_action=letter_action, +# value="read", +# ) + +# LetterEventDate.objects.create( +# year_lower=505, year_upper=510, letter_action=letter_action +# ) + +# return letter_action @pytest.fixture() @@ -105,19 +99,19 @@ def case_study(db): return case_study -@pytest.fixture() -def epistolary_event(db, letter_description, case_study): - epistolary_event = EpistolaryEvent.objects.create( - name="Test Epistolary event", note="Test note" - ) - epistolary_event.case_studies.add(case_study) +# @pytest.fixture() +# def epistolary_event(db, letter_description, case_study): +# epistolary_event = EpistolaryEvent.objects.create( +# name="Test Epistolary event", note="Test note" +# ) +# epistolary_event.case_studies.add(case_study) - return epistolary_event +# return epistolary_event -@pytest.fixture() -def world_event(db): - world_event = WorldEvent.objects.create( - name="Test World Event", note="Test World Event note", year_exact=612 - ) - return world_event +# @pytest.fixture() +# def world_event(db): +# world_event = WorldEvent.objects.create( +# name="Test World Event", note="Test World Event note", year_exact=612 +# ) +# return world_event diff --git a/backend/core/management/commands/create_dev_dataset.py b/backend/core/management/commands/create_dev_dataset.py index f1d2cc39..a83cea92 100644 --- a/backend/core/management/commands/create_dev_dataset.py +++ b/backend/core/management/commands/create_dev_dataset.py @@ -5,18 +5,18 @@ from source.models import Source from case_study.models import CaseStudy -from event.models import ( - EpistolaryEvent, - EpistolaryEventSelfTrigger, - EpistolaryEventTrigger, - LetterAction, - LetterActionCategory, - LetterEventDate, - # Role, - WorldEvent, - WorldEventSelfTrigger, - WorldEventTrigger, -) +# from event.models import ( +# EpistolaryEvent, +# EpistolaryEventSelfTrigger, +# EpistolaryEventTrigger, +# LetterAction, +# LetterActionCategory, +# LetterEventDate, +# Role, +# WorldEvent, +# WorldEventSelfTrigger, +# WorldEventTrigger, +# ) # from person.models import ( # Agent, @@ -27,11 +27,11 @@ # ) from letter.models import ( Category, - Gift, - Letter, + # Gift, + # Letter, # LetterAddressees, - LetterCategory, - LetterMaterial, + # LetterCategory, + # LetterMaterial, # LetterSenders, ) import random @@ -115,28 +115,28 @@ def handle(self, *args, **options): # Adjust the `total` parameter to create more or less data. # NB: the order of these function calls is important. self._create_case_studies(fake, options, total=10, model=CaseStudy) - self._create_epistolary_events( - fake, options, total=40, model=EpistolaryEvent - ) + # 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_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) - self._create_letter_actions(fake, options, total=200, model=LetterAction) - self._create_world_events(fake, options, total=50, model=WorldEvent) - self._create_world_event_triggers( - fake, options, total=50, model=WorldEventTrigger - ) - self._create_epistolary_event_triggers( - fake, options, total=50, model=EpistolaryEventTrigger - ) - self._create_world_event_self_triggers( - fake, options, total=50, model=WorldEventSelfTrigger - ) - self._create_epistolary_event_self_trigger( - fake, options, total=50, model=EpistolaryEventSelfTrigger - ) + # self._create_letters(fake, options, total=200, model=Letter) + # self._create_gifts(fake, options, total=50, model=Gift) + # self._create_letter_actions(fake, options, total=200, model=LetterAction) + # self._create_world_events(fake, options, total=50, model=WorldEvent) + # self._create_world_event_triggers( + # fake, options, total=50, model=WorldEventTrigger + # ) + # self._create_epistolary_event_triggers( + # fake, options, total=50, model=EpistolaryEventTrigger + # ) + # self._create_world_event_self_triggers( + # fake, options, total=50, model=WorldEventSelfTrigger + # ) + # self._create_epistolary_event_self_trigger( + # fake, options, total=50, model=EpistolaryEventSelfTrigger + # ) self._create_sources(fake, options, total=50, model=Source) print("-" * 80) @@ -147,15 +147,15 @@ def _create_case_studies(self, fake: Faker, options, total, model): unique_name = get_unique_name(case_study_names, CaseStudy) CaseStudy.objects.create(name=unique_name) - @track_progress - def _create_epistolary_events(self, fake, options, total, model): - unique_name = get_unique_name(epistolary_event_names, EpistolaryEvent) + # @track_progress + # def _create_epistolary_events(self, fake, options, total, model): + # unique_name = get_unique_name(epistolary_event_names, EpistolaryEvent) - event = EpistolaryEvent.objects.create(name=unique_name, note=fake.text()) + # event = EpistolaryEvent.objects.create(name=unique_name, note=fake.text()) - event.case_studies.set( - get_random_model_objects(CaseStudy, min_amount=0, max_amount=3) - ) + # event.case_studies.set( + # get_random_model_objects(CaseStudy, min_amount=0, max_amount=3) + # ) @track_progress def _create_status_markers(self, fake, options, total, model): @@ -218,29 +218,29 @@ def _create_letter_categories(self, fake: Faker, *args, **kwargs): ) Category.objects.create(label=unique_label, description=fake.text()) - @track_progress - def _create_letters(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) - subject = ", ".join(fake.words(nb=3, unique=True)) - letter = Letter.objects.create( - name=f"Letter about {subject}", - ) + # subject = ", ".join(fake.words(nb=3, unique=True)) + # letter = Letter.objects.create( + # name=f"Letter about {subject}", + # ) - if random.choice([True, False]): - LetterMaterial.objects.create( - letter=letter, - surface=random.choice(LetterMaterial.Surface.values), - **self.fake_field_value(fake), - ) + # if random.choice([True, False]): + # LetterMaterial.objects.create( + # letter=letter, + # surface=random.choice(LetterMaterial.Surface.values), + # **self.fake_field_value(fake), + # ) - if random.choice([True, False]): - LetterCategory.objects.create( - letter=letter, - category=get_random_model_object(Category), - **self.fake_field_value(fake), - ) + # if random.choice([True, False]): + # LetterCategory.objects.create( + # letter=letter, + # category=get_random_model_object(Category), + # **self.fake_field_value(fake), + # ) # sender_object = LetterSenders.objects.create( # letter=letter, @@ -254,33 +254,33 @@ def _create_letters(self, fake: Faker, *args, **kwargs): # ) # addressees_object.addressees.set(addressees) - @track_progress - def _create_letter_actions(self, fake: Faker, *args, **kwargs): - action = LetterAction.objects.create() + # @track_progress + # def _create_letter_actions(self, fake: Faker, *args, **kwargs): + # action = LetterAction.objects.create() # action.letters.set(get_random_model_objects(Letter, min_amount=1, max_amount=5)) # action.gifts.set(get_random_model_objects(Gift, min_amount=0, max_amount=5)) - action.epistolary_events.set( - get_random_model_objects(EpistolaryEvent, min_amount=0, max_amount=5) - ) + # action.epistolary_events.set( + # get_random_model_objects(EpistolaryEvent, min_amount=0, max_amount=5) + # ) - LetterEventDate.objects.create( - letter_action=action, - **self.fake_date_value(fake), - **self.fake_field_value(fake), - ) + # LetterEventDate.objects.create( + # letter_action=action, + # **self.fake_date_value(fake), + # **self.fake_field_value(fake), + # ) - no_of_categories = random.randint(1, 5) - random_categories = random.sample( - LetterActionCategory.CategoryOptions.choices, no_of_categories - ) - for i in range(no_of_categories): - LetterActionCategory.objects.create( - letter_action=action, - value=random_categories[i][0], - **self.fake_field_value(fake), - ) + # no_of_categories = random.randint(1, 5) + # random_categories = random.sample( + # LetterActionCategory.CategoryOptions.choices, no_of_categories + # ) + # for i in range(no_of_categories): + # LetterActionCategory.objects.create( + # letter_action=action, + # value=random_categories[i][0], + # **self.fake_field_value(fake), + # ) # for _ in range(random.randint(1, 5)): # Role.objects.create( @@ -292,63 +292,63 @@ def _create_letter_actions(self, fake: Faker, *args, **kwargs): # **self.fake_field_value(fake), # ) - @track_progress - def _create_gifts(self, fake, options, total, model): - unique_name = get_unique_name(gift_names, Gift) + # @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) - Gift.objects.create( - name=unique_name, - material=random.choice(Gift.Material.choices)[0], - # gifted_by=gifter, - description=fake.text(), - ) - - @track_progress - def _create_world_events(self, fake, options, total, model): - unique_name = get_unique_name(world_event_names, WorldEvent) - WorldEvent.objects.create( - name=unique_name, note=fake.text(), **self.fake_date_value(fake) - ) - - @track_progress - def _create_world_event_triggers(self, fake, options, total, model): - WorldEventTrigger.objects.create( - world_event=get_random_model_object(WorldEvent), - epistolary_event=get_random_model_object(EpistolaryEvent), - **self.fake_field_value(fake), - ) - - @track_progress - def _create_epistolary_event_triggers(self, fake, options, total, model): - EpistolaryEventTrigger.objects.create( - epistolary_event=get_random_model_object(EpistolaryEvent), - world_event=get_random_model_object(WorldEvent), - **self.fake_field_value(fake), - ) - - @track_progress - def _create_world_event_self_triggers(self, fake, options, total, model): - [triggering, triggered] = get_random_model_objects( - WorldEvent, max_amount=2, exact=True - ) - WorldEventSelfTrigger.objects.create( - triggered_world_event=triggering, - triggering_world_event=triggered, - **self.fake_field_value(fake), - ) + # Gift.objects.create( + # name=unique_name, + # material=random.choice(Gift.Material.choices)[0], + # # gifted_by=gifter, + # description=fake.text(), + # ) - @track_progress - def _create_epistolary_event_self_trigger(self, fake, options, total, model): - [triggering, triggered] = get_random_model_objects( - EpistolaryEvent, max_amount=2, exact=True - ) - EpistolaryEventSelfTrigger.objects.create( - triggering_epistolary_event=triggering, - triggered_epistolary_event=triggered, - **self.fake_field_value(fake), - ) + # @track_progress + # def _create_world_events(self, fake, options, total, model): + # unique_name = get_unique_name(world_event_names, WorldEvent) + # WorldEvent.objects.create( + # name=unique_name, note=fake.text(), **self.fake_date_value(fake) + # ) + + # @track_progress + # def _create_world_event_triggers(self, fake, options, total, model): + # WorldEventTrigger.objects.create( + # world_event=get_random_model_object(WorldEvent), + # epistolary_event=get_random_model_object(EpistolaryEvent), + # **self.fake_field_value(fake), + # ) + + # @track_progress + # def _create_epistolary_event_triggers(self, fake, options, total, model): + # EpistolaryEventTrigger.objects.create( + # epistolary_event=get_random_model_object(EpistolaryEvent), + # world_event=get_random_model_object(WorldEvent), + # **self.fake_field_value(fake), + # ) + + # @track_progress + # def _create_world_event_self_triggers(self, fake, options, total, model): + # [triggering, triggered] = get_random_model_objects( + # WorldEvent, max_amount=2, exact=True + # ) + # WorldEventSelfTrigger.objects.create( + # triggered_world_event=triggering, + # triggering_world_event=triggered, + # **self.fake_field_value(fake), + # ) + + # @track_progress + # def _create_epistolary_event_self_trigger(self, fake, options, total, model): + # [triggering, triggered] = get_random_model_objects( + # EpistolaryEvent, max_amount=2, exact=True + # ) + # EpistolaryEventSelfTrigger.objects.create( + # triggering_epistolary_event=triggering, + # triggered_epistolary_event=triggered, + # **self.fake_field_value(fake), + # ) @track_progress def _create_sources(self, fake, options, total, model): diff --git a/backend/event/admin.py b/backend/event/admin.py index 4c72a098..34cdcb99 100644 --- a/backend/event/admin.py +++ b/backend/event/admin.py @@ -1,132 +1,3 @@ from django.contrib import admin from . import models - -class LetterActionCategoryAdmin(admin.StackedInline): - model = models.LetterActionCategory - fields = ["value", "certainty", "note"] - extra = 0 - verbose_name = "category" - verbose_name_plural = "categories" - - -class EventDateAdmin(admin.StackedInline): - model = models.LetterEventDate - fields = ["year_exact", "year_lower", "year_upper", "certainty", "note"] - verbose_name = "date" - 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 LetterActionLettersAdmin(admin.StackedInline): -# model = models.LetterAction.letters.through -# extra = 0 -# verbose_name = "letter" -# verbose_name_plural = "letters" - -# class LetterActionGiftsAdmin(admin.StackedInline): -# model = models.LetterAction.gifts.through -# extra = 0 -# verbose_name = "gift" -# verbose_name_plural = "gifts" - -@admin.register(models.LetterAction) -class LetterActionAdmin(admin.ModelAdmin): - filter_horizontal = ["epistolary_events", "space_descriptions"] - list_display=["description", "display_date"] - inlines = [ - # LetterActionLettersAdmin, - LetterActionCategoryAdmin, - # LetterActionGiftsAdmin, - EventDateAdmin, - # RoleAdmin, - ] - exclude = ["letters"] - - -# For use in Case Study form -class EpistolaryEventInline(admin.StackedInline): - model = models.CaseStudy.epistolary_events.through - extra = 0 - verbose_name = "epistolary event" - verbose_name_plural = "epistolary events" - - -class EpistolaryEventCaseStudyInline(admin.StackedInline): - model = models.EpistolaryEvent.case_studies.through - extra = 0 - verbose_name_plural = "case studies" - verbose_name = "relationship between a case study and an epistolary event" - - -class EpistolaryEventLetterActionInline(admin.StackedInline): - model = models.EpistolaryEvent.letter_actions.through - extra = 0 - verbose_name_plural = "letter actions" - verbose_name = "relationship between a epistolary event and a letter action" - - -class EpistolaryEventsTriggeredWorldEventsInline(admin.StackedInline): - model = models.EpistolaryEvent.triggered_world_events.through - fields = ["world_event", "certainty", "note"] - extra = 0 - verbose_name = "World event triggered by this epistolary event" - verbose_name_plural = "World events triggered by this epistolary event" - -class EpistolaryEventsTriggeredEpistolaryEventsInline(admin.StackedInline): - model = models.EpistolaryEvent.triggered_epistolary_events.through - fk_name = "triggering_epistolary_event" - fields = ["triggered_epistolary_event", "certainty", "note"] - extra = 0 - verbose_name = "Epistolary event triggered by this epistolary event" - verbose_name_plural = "Epistolary events triggered by this epistolary event" - - -@admin.register(models.EpistolaryEvent) -class EpistolaryEventAdmin(admin.ModelAdmin): - fields = ["name", "note"] - inlines = [ - EpistolaryEventCaseStudyInline, - EpistolaryEventLetterActionInline, - EpistolaryEventsTriggeredWorldEventsInline, - EpistolaryEventsTriggeredEpistolaryEventsInline - ] - -class WorldEventsTriggeredEpistolaryEventsInline(admin.StackedInline): - model = models.WorldEvent.triggered_epistolary_events.through - fields = ["epistolary_event", "certainty", "note"] - extra = 0 - verbose_name = "Epistolary event triggered by this world event" - verbose_name_plural = "Epistolary events triggered by this world event" - - -class WorldEventsTriggeredWorldEventsInline(admin.StackedInline): - model = models.WorldEvent.triggered_world_events.through - fk_name = "triggering_world_event" - fields = ["triggered_world_event", "certainty", "note"] - extra = 0 - verbose_name = "World event triggered by this world event" - verbose_name_plural = "World events triggered by this world event" - - -@admin.register(models.WorldEvent) -class WorldEventAdmin(admin.ModelAdmin): - fields = ["name", "note", "year_exact", "year_lower", "year_upper"] - inlines = [ - WorldEventsTriggeredEpistolaryEventsInline, - WorldEventsTriggeredWorldEventsInline - ] diff --git a/backend/event/migrations/0012_remove_epistolaryeventselftrigger_triggered_epistolary_event_and_more.py b/backend/event/migrations/0012_remove_epistolaryeventselftrigger_triggered_epistolary_event_and_more.py new file mode 100644 index 00000000..25c10baa --- /dev/null +++ b/backend/event/migrations/0012_remove_epistolaryeventselftrigger_triggered_epistolary_event_and_more.py @@ -0,0 +1,96 @@ +# Generated by Django 4.2.7 on 2024-04-11 12:17 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('event', '0011_remove_letteraction_gifts_and_more'), + ] + + operations = [ + migrations.RemoveField( + model_name='epistolaryeventselftrigger', + name='triggered_epistolary_event', + ), + migrations.RemoveField( + model_name='epistolaryeventselftrigger', + name='triggering_epistolary_event', + ), + migrations.RemoveField( + model_name='epistolaryeventtrigger', + name='epistolary_event', + ), + migrations.RemoveField( + model_name='epistolaryeventtrigger', + name='world_event', + ), + migrations.RemoveField( + model_name='letteraction', + name='epistolary_events', + ), + migrations.RemoveField( + model_name='letteraction', + name='space_descriptions', + ), + migrations.RemoveField( + model_name='letteractioncategory', + name='letter_action', + ), + migrations.RemoveField( + model_name='lettereventdate', + name='letter_action', + ), + migrations.RemoveField( + model_name='worldevent', + name='triggered_epistolary_events', + ), + migrations.RemoveField( + model_name='worldevent', + name='triggered_world_events', + ), + migrations.RemoveField( + model_name='worldeventselftrigger', + name='triggered_world_event', + ), + migrations.RemoveField( + model_name='worldeventselftrigger', + name='triggering_world_event', + ), + migrations.RemoveField( + model_name='worldeventtrigger', + name='epistolary_event', + ), + migrations.RemoveField( + model_name='worldeventtrigger', + name='world_event', + ), + migrations.DeleteModel( + name='EpistolaryEvent', + ), + migrations.DeleteModel( + name='EpistolaryEventSelfTrigger', + ), + migrations.DeleteModel( + name='EpistolaryEventTrigger', + ), + migrations.DeleteModel( + name='LetterAction', + ), + migrations.DeleteModel( + name='LetterActionCategory', + ), + migrations.DeleteModel( + name='LetterEventDate', + ), + migrations.DeleteModel( + name='WorldEvent', + ), + migrations.DeleteModel( + name='WorldEventSelfTrigger', + ), + migrations.DeleteModel( + name='WorldEventTrigger', + ), + ] diff --git a/backend/event/models.py b/backend/event/models.py index 506989f3..e1d88d48 100644 --- a/backend/event/models.py +++ b/backend/event/models.py @@ -6,330 +6,3 @@ # from person.models import Agent # from letter.models import Gift, Letter from space.models import SpaceDescription - -class EpistolaryEvent(models.Model): - """ - Epistolary events are groups of related letter actions that are connected in some way. - - For instance, a political campaign (epistolary event) may consist of the writing, transporting and reading of individual letters (letter actions). - """ - - name = models.CharField( - max_length=256, - null=False, - blank=False, - ) - - case_studies = models.ManyToManyField( - to=CaseStudy, - related_name="epistolary_events", - help_text="case studies this event belongs to", - ) - - note = models.TextField( - null=False, - blank=True, - help_text="Additional notes that describe the event and what connects the letter actions it comprises.", - ) - - triggered_world_events = models.ManyToManyField( - to="WorldEvent", - through="EpistolaryEventTrigger", - symmetrical=False, - related_name="world_event_triggers", - help_text="World events triggered by this epistolary event", - ) - - triggered_epistolary_events = models.ManyToManyField( - to="self", - through="EpistolaryEventSelfTrigger", - through_fields=("triggering_epistolary_event", "triggered_epistolary_event"), - symmetrical=False, - help_text="Other epistolary events triggered by this epistolary event", - ) - - def __str__(self): - return f"{self.name}" - - -class LetterAction(models.Model): - """ - A letter action is an atomic action performed on a letter, e.g. writing, delivering, reading. - - These can be grouped into epistolary events. - """ - - # letters = models.ManyToManyField( - # to=Letter, - # related_name="events", - # help_text="letters involved in this event", - # ) - - # actors = models.ManyToManyField( - # to=Agent, - # through="Role", - # related_name="events", - # ) - - epistolary_events = models.ManyToManyField( - to=EpistolaryEvent, - related_name="letter_actions", - help_text="epistolary events this letter action belongs to", - ) - - # gifts = models.ManyToManyField( - # to=Gift, - # related_name="letter_actions", - # help_text="Gifts associated to this letter action", - # blank=True, - # ) - - space_descriptions = models.ManyToManyField( - to=SpaceDescription, - help_text="Descriptions of the space in which this action took place", - blank=True, - ) - - @property - @admin.display(description="Date") - def display_date(self): - return self.date.display_date if hasattr(self, "date") else "unknown date" - - @property - @admin.display(description="Description") - def description(self): - categories = self.categories.all() - category_names = [category.get_value_display() for category in categories] - category_desc = ", ".join(category_names) - # letters = ", ".join(letter.__str__() for letter in self.letters.all()) - return f"{category_desc}" - - def __str__(self): - return f"{self.description} ({self.display_date})" - - -class LetterActionCategory(Field, models.Model): - - class CategoryOptions(models.TextChoices): - WRITE = "write", "writing" - TRANSPORT = "transport", "transporting" - DELIVER = "deliver", "delivering" - READ = "read", "reading" - SIGN = "sign", "signing" - EAT = "eat", "eating" - - value = models.CharField( - choices=CategoryOptions.choices, - null=False, - blank=False, - help_text="The type of event", - ) - - letter_action = models.ForeignKey( - to=LetterAction, - on_delete=models.CASCADE, - related_name="categories", - null=False, - blank=False, - ) - - class Meta: - constraints = [ - models.UniqueConstraint( - "value", "letter_action", name="unique_categories_for_letter_action" - ) - ] - verbose_name_plural = "letter action categories" - - def __str__(self): - return f"{self.letter_action}: {self.get_value_display()}" - - -class LetterEventDate(Field, LettercraftDate, models.Model): - - letter_action = models.OneToOneField( - to=LetterAction, on_delete=models.CASCADE, related_name="date" - ) - - 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 WorldEvent(LettercraftDate, models.Model): - """ - World events are events that are not directly related to a specific letter - or letter action, but are relevant to the context of the letters. - """ - - name = models.CharField( - max_length=256, - null=False, - blank=False, - help_text="The name of the event, e.g. 'The Great Fire of London' or 'The Battle of Hastings'.", - ) - - note = models.TextField( - null=False, - blank=True, - help_text="Additional notes that describe the event and its relevance to the letters.", - ) - - triggered_epistolary_events = models.ManyToManyField( - to=EpistolaryEvent, - through="WorldEventTrigger", - symmetrical=False, - related_name="epistolary_event_triggers", - help_text="Epistolary events triggered by this world event", - ) - - triggered_world_events = models.ManyToManyField( - to="self", - through="WorldEventSelfTrigger", - through_fields=("triggering_world_event", "triggered_world_event"), - symmetrical=False, - help_text="Other world events triggered by this world event", - ) - - def __str__(self): - return f"{self.name} ({self.display_date})" - - -class WorldEventTrigger(Field, models.Model): - """ - A relationship between an epistolary event and a world event where a world event triggers an epistolary event. - """ - - epistolary_event = models.ForeignKey( - to=EpistolaryEvent, - on_delete=models.CASCADE, - related_name="triggers_by_world_events", - help_text="The epistolary event that was triggered by a world event", - ) - - world_event = models.ForeignKey( - to=WorldEvent, - on_delete=models.CASCADE, - related_name="triggers_for_epistolary_events", - help_text="The world event that triggered an epistolary event", - ) - - def __str__(self): - return f"{self.world_event} triggered {self.epistolary_event}" - - -class EpistolaryEventTrigger(Field, models.Model): - """ - A relationship between an epistolary event and a world event where an epistolary event triggers a world event. - """ - - epistolary_event = models.ForeignKey( - to=EpistolaryEvent, - on_delete=models.CASCADE, - related_name="triggers_by_epistolary_events", - help_text="The epistolary event that triggered a world event", - ) - - world_event = models.ForeignKey( - to=WorldEvent, - on_delete=models.CASCADE, - related_name="triggers_for_world_events", - help_text="The world event that was triggered by an epistolary event", - ) - - def __str__(self): - return f"{self.epistolary_event} triggered {self.world_event}" - - -class WorldEventSelfTrigger(Field, models.Model): - """ - A relationship between a world event and another world event where one world event triggers another. - """ - - triggered_world_event = models.ForeignKey( - to=WorldEvent, - on_delete=models.CASCADE, - related_name="self_triggers_for_world_events", - help_text="The world event that was triggered by another world event", - ) - - triggering_world_event = models.ForeignKey( - to=WorldEvent, - on_delete=models.CASCADE, - related_name="self_triggered_by_world_events", - help_text="The world event that triggered another world event", - ) - - def __str__(self): - return f"{self.triggering_world_event} triggered {self.triggered_world_event}" - - -class EpistolaryEventSelfTrigger(Field, models.Model): - """ - A relationship between an epistolary event and another epistolary event where one epistolary event triggers another. - """ - - triggered_epistolary_event = models.ForeignKey( - to=EpistolaryEvent, - on_delete=models.CASCADE, - related_name="self_triggers_for_epistolary_events", - help_text="The epistolary event that was triggered by another epistolary event", - ) - - triggering_epistolary_event = models.ForeignKey( - to=EpistolaryEvent, - on_delete=models.CASCADE, - related_name="self_triggered_by_epistolary_events", - help_text="The epistolary event that triggered another epistolary event", - ) - - def __str__(self): - return f"{self.triggering_epistolary_event} triggered {self.triggered_epistolary_event}" diff --git a/backend/event/tests/test_event_models.py b/backend/event/tests/test_event_models.py index 9a64b5e8..e69de29b 100644 --- a/backend/event/tests/test_event_models.py +++ b/backend/event/tests/test_event_models.py @@ -1,66 +0,0 @@ -from event.models import ( - EpistolaryEvent, - WorldEvent, - WorldEventSelfTrigger, - WorldEventTrigger, -) - - -def test_letter_action_name(letter_description, letter_action_writing): - letter_action_writing.date.year_exact = 500 - letter_action_writing.save() - action_str = str(letter_action_writing) - - # assert str(action_str) == f"writing of {str(letter_description)} (500)" - - -def test_letter_event_date_with_exact_date(letter_action_reading): - letter_action_reading.date.year_exact = 500 - letter_action_reading.save() - # assert ( - # str(letter_action_reading) - # == f"reading of {str(letter_action_reading.letters.first())} (500)" - # ) - - -def test_letter_event_date_with_date_range(letter_action_reading): - letter_action_reading.date.year_lower = 500 - letter_action_reading.date.year_upper = 600 - letter_action_reading.save() - - # assert ( - # str(letter_action_reading) - # == f"reading of {str(letter_action_reading.letters.first())} (c. 500–600)" - # ) - - -def test_world_event(world_event): - world_event.year_exact = 500 - world_event.save() - assert str(world_event) == "Test World Event (500)" - - -def test_world_event_triggers_epistolary_event(world_event, epistolary_event): - world_event.triggered_epistolary_events.add(epistolary_event) - world_event.save() - trigger = world_event.triggered_epistolary_events.through.objects.first() - - assert str(trigger) == "Test World Event (612) triggered Test Epistolary event" - - -def test_world_event_triggers_world_event(world_event): - world_event_2 = WorldEvent.objects.create(name="Test World Event 2", year_exact=700) - world_event_2.save() - world_event.triggered_world_events.add(world_event_2) - trigger = world_event.triggered_world_events.through.objects.first() - - assert str(trigger) == "Test World Event (612) triggered Test World Event 2 (700)" - - -def test_epistolary_event_triggers_epistolary_event(epistolary_event): - epistolary_event_2 = EpistolaryEvent.objects.create(name="Test Epistolary event 2") - epistolary_event_2.save() - epistolary_event.triggered_epistolary_events.add(epistolary_event_2) - trigger = epistolary_event.triggered_epistolary_events.through.objects.first() - - assert str(trigger) == "Test Epistolary event triggered Test Epistolary event 2" From 53b9bb684378133d51b9384126cb27c458ea2766 Mon Sep 17 00:00:00 2001 From: Luka van der Plas Date: Thu, 11 Apr 2024 15:06:05 +0200 Subject: [PATCH 25/34] event description model --- backend/event/admin.py | 38 +++++++ backend/event/migrations/0013_initial.py | 123 ++++++++++++++++++++++ backend/event/models.py | 128 ++++++++++++++++++++++- 3 files changed, 284 insertions(+), 5 deletions(-) create mode 100644 backend/event/migrations/0013_initial.py diff --git a/backend/event/admin.py b/backend/event/admin.py index 34cdcb99..b36f1ada 100644 --- a/backend/event/admin.py +++ b/backend/event/admin.py @@ -1,3 +1,41 @@ from django.contrib import admin from . import models +from core import admin as core_admin + +class EventDescriptionAgentAdmin(admin.StackedInline): + model = models.EventDescriptionAgent + fields = ["agent"] + core_admin.description_field_fields + extra = 0 + verbose_name = "involved agent" + + +class EventDescriptionGiftAdmin(admin.StackedInline): + model = models.EventDescriptionGift + fields = ["gift"] + core_admin.description_field_fields + extra = 0 + verbose_name = "involved gift" + + +class EventDescriptionLetterAdmin(admin.StackedInline): + model = models.EventDescriptionLetter + fields = ["letter"] + core_admin.description_field_fields + extra = 0 + verbose_name = "involved letter" + + +class EventDescriptionSpaceAdmin(admin.StackedInline): + model = models.EventDescriptionSpace + fields = ["space"] + core_admin.description_field_fields + extra = 0 + verbose_name = "involved space" + + +@admin.register(models.EventDescription) +class EventDescriptionAdmin(core_admin.EntityDescriptionAdmin, admin.ModelAdmin): + inlines = [ + EventDescriptionAgentAdmin, + EventDescriptionGiftAdmin, + EventDescriptionLetterAdmin, + EventDescriptionSpaceAdmin, + ] diff --git a/backend/event/migrations/0013_initial.py b/backend/event/migrations/0013_initial.py new file mode 100644 index 00000000..0dae425f --- /dev/null +++ b/backend/event/migrations/0013_initial.py @@ -0,0 +1,123 @@ +# Generated by Django 4.2.7 on 2024-04-11 12:59 + +import django.contrib.postgres.fields +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('space', '0008_alter_spacedescription_source'), + ('source', '0005_placeholder_source'), + ('person', '0014_alter_persondateofbirth_person_and_more'), + ('letter', '0012_preservedletter_preservedletterrole_and_more'), + ('event', '0012_remove_epistolaryeventselftrigger_triggered_epistolary_event_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='EventDescription', + 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)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='EventDescriptionSpace', + 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)), + ('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='event.eventdescription')), + ('space', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='space.spacedescription')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='EventDescriptionLetter', + 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)), + ('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='event.eventdescription')), + ('letter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='letter.letterdescription')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='EventDescriptionGift', + 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)), + ('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='event.eventdescription')), + ('gift', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='letter.giftdescription')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='EventDescriptionAgent', + 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, to='person.agentdescription')), + ('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='event.eventdescription')), + ], + options={ + 'abstract': False, + }, + ), + migrations.AddField( + model_name='eventdescription', + name='agents', + field=models.ManyToManyField(blank=True, help_text='agents involved in this event', through='event.EventDescriptionAgent', to='person.agentdescription'), + ), + migrations.AddField( + model_name='eventdescription', + name='gifts', + field=models.ManyToManyField(blank=True, help_text='gifts involved in this event', through='event.EventDescriptionGift', to='letter.giftdescription'), + ), + migrations.AddField( + model_name='eventdescription', + name='letters', + field=models.ManyToManyField(blank=True, help_text='letters involved in this event', through='event.EventDescriptionLetter', to='letter.letterdescription'), + ), + migrations.AddField( + model_name='eventdescription', + name='source', + field=models.ForeignKey(help_text='Source text containing this description', on_delete=django.db.models.deletion.PROTECT, to='source.source'), + ), + migrations.AddField( + model_name='eventdescription', + name='spaces', + field=models.ManyToManyField(blank=True, help_text='locations involved in this event', through='event.EventDescriptionSpace', to='space.spacedescription'), + ), + ] diff --git a/backend/event/models.py b/backend/event/models.py index e1d88d48..2505abdd 100644 --- a/backend/event/models.py +++ b/backend/event/models.py @@ -1,8 +1,126 @@ from django.db import models -from django.contrib import admin +from django.core.exceptions import ValidationError -from core.models import Field, LettercraftDate -from case_study.models import CaseStudy -# from person.models import Agent -# from letter.models import Gift, Letter +from core.models import EntityDescription, DescriptionField +from person.models import AgentDescription +from letter.models import GiftDescription, LetterDescription from space.models import SpaceDescription + + +class EventDescription(EntityDescription, models.Model): + """ + An epistolary event described in as source text + """ + + agents = models.ManyToManyField( + to=AgentDescription, + through="EventDescriptionAgent", + blank=True, + help_text="agents involved in this event", + ) + gifts = models.ManyToManyField( + to=GiftDescription, + through="EventDescriptionGift", + blank=True, + help_text="gifts involved in this event", + ) + letters = models.ManyToManyField( + to=LetterDescription, + through="EventDescriptionLetter", + blank=True, + help_text="letters involved in this event", + ) + spaces = models.ManyToManyField( + to=SpaceDescription, + through="EventDescriptionSpace", + blank=True, + help_text="locations involved in this event", + ) + + +class EventDescriptionAgent(DescriptionField, models.Model): + """ + Relationship between an agent and an event described in a source text + """ + + event = models.ForeignKey( + to=EventDescription, + on_delete=models.CASCADE, + ) + agent = models.ForeignKey( + to=AgentDescription, + on_delete=models.CASCADE, + ) + + def clean(self): + if self.event.source != self.agent.source: + raise ValidationError("Can only link descriptions in the same source text") + + def __str__(self): + return f"{self.agent.name} / {self.event}" + + +class EventDescriptionGift(DescriptionField, models.Model): + """ + Relationship between an agent and an gift described in a source text + """ + + event = models.ForeignKey( + to=EventDescription, + on_delete=models.CASCADE, + ) + gift = models.ForeignKey( + to=GiftDescription, + on_delete=models.CASCADE, + ) + + def clean(self): + if self.event.source != self.gift.source: + raise ValidationError("Can only link descriptions in the same source text") + + def __str__(self): + return f"{self.gift.name} / {self.event}" + + +class EventDescriptionLetter(DescriptionField, models.Model): + """ + Relationship between an agent and a letter described in a source text + """ + + event = models.ForeignKey( + to=EventDescription, + on_delete=models.CASCADE, + ) + letter = models.ForeignKey( + to=LetterDescription, + on_delete=models.CASCADE, + ) + + def clean(self): + if self.event.source != self.letter.source: + raise ValidationError("Can only link descriptions in the same source text") + + def __str__(self): + return f"{self.letter.name} / {self.event}" + + +class EventDescriptionSpace(DescriptionField, models.Model): + """ + Relationship between an agent and a space described in a source text + """ + + event = models.ForeignKey( + to=EventDescription, + on_delete=models.CASCADE, + ) + space = models.ForeignKey( + to=SpaceDescription, + on_delete=models.CASCADE, + ) + + def clean(self): + if self.event.source != self.space.source: + raise ValidationError("Can only link descriptions in the same source text") + + def __str__(self): + return f"{self.space.name} / {self.event}" From de6fbdb3c1f1b2cbdbdbb8ee4d66fa3f90f17111 Mon Sep 17 00:00:00 2001 From: Luka van der Plas Date: Thu, 11 Apr 2024 15:09:50 +0200 Subject: [PATCH 26/34] update tests --- backend/conftest.py | 73 +++++++----------------- backend/event/tests/test_event_models.py | 2 + 2 files changed, 23 insertions(+), 52 deletions(-) diff --git a/backend/conftest.py b/backend/conftest.py index f9caaa03..13f1dd5b 100644 --- a/backend/conftest.py +++ b/backend/conftest.py @@ -4,6 +4,7 @@ from person.models import HistoricalPerson, AgentDescription from source.models import Source +from event.models import EventDescription @pytest.fixture() @@ -44,6 +45,17 @@ def agent_description(db, historical_person, source): return agent +@pytest.fixture() +def agent_description_2(db, historical_person, source): + agent = AgentDescription.objects.create( + name="Ernie", + 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( @@ -57,61 +69,18 @@ def agent_group_description(db, source, historical_person, historical_person_2): return agent -# @pytest.fixture() -# def letter_action_writing(db, letter_description): -# letter_action = LetterAction.objects.create() -# # letter_action.letters.add(letter_description) -# # letter_action.actors.add(agent) - -# LetterActionCategory.objects.create( -# letter_action=letter_action, -# value="write", -# ) - -# LetterEventDate.objects.create( -# year_lower=500, year_upper=500, year_exact=500, letter_action=letter_action -# ) - -# return letter_action - - -# @pytest.fixture() -# def letter_action_reading(db, letter_description): -# letter_action = LetterAction.objects.create() -# # letter_action.letters.add(letter_description) -# # letter_action.actors.add(agent_2) - -# LetterActionCategory.objects.create( -# letter_action=letter_action, -# value="read", -# ) - -# LetterEventDate.objects.create( -# year_lower=505, year_upper=510, letter_action=letter_action -# ) - -# return letter_action +@pytest.fixture() +def event_description(db, source, agent_description, agent_description_2): + event = EventDescription.objects.create( + name="Bert writes a letter", + source=source, + ) + event.agents.add(agent_description) + event.agents.add(agent_description_2) + return event @pytest.fixture() def case_study(db): case_study = CaseStudy.objects.create(name="Test Case Study") return case_study - - -# @pytest.fixture() -# def epistolary_event(db, letter_description, case_study): -# epistolary_event = EpistolaryEvent.objects.create( -# name="Test Epistolary event", note="Test note" -# ) -# epistolary_event.case_studies.add(case_study) - -# return epistolary_event - - -# @pytest.fixture() -# def world_event(db): -# world_event = WorldEvent.objects.create( -# name="Test World Event", note="Test World Event note", year_exact=612 -# ) -# return world_event diff --git a/backend/event/tests/test_event_models.py b/backend/event/tests/test_event_models.py index e69de29b..a5c1b85b 100644 --- a/backend/event/tests/test_event_models.py +++ b/backend/event/tests/test_event_models.py @@ -0,0 +1,2 @@ +def test_event_models(event_description, agent_description): + assert agent_description in event_description.agents.all() From 4f1b9d92040ac5fa8a6f03e68fafdb0976f57396 Mon Sep 17 00:00:00 2001 From: Luka van der Plas Date: Thu, 11 Apr 2024 15:19:08 +0200 Subject: [PATCH 27/34] update case study / episode models --- backend/case_study/admin.py | 11 +++-- ...n_alter_casestudy_name_episode_and_more.py | 41 +++++++++++++++++++ backend/case_study/models.py | 24 ++++++++--- 3 files changed, 68 insertions(+), 8 deletions(-) create mode 100644 backend/case_study/migrations/0002_casestudy_description_alter_casestudy_name_episode_and_more.py diff --git a/backend/case_study/admin.py b/backend/case_study/admin.py index c64c0a30..4b97fc1a 100644 --- a/backend/case_study/admin.py +++ b/backend/case_study/admin.py @@ -4,6 +4,11 @@ @admin.register(models.CaseStudy) class CaseStudyAdmin(admin.ModelAdmin): - fields = ["name"] - inlines = [] - extra = 0 + list_display = ["name", "description"] + fields = ["name", "description", "episodes"] + filter_horizontal = ["episodes"] + + +@admin.register(models.Episode) +class EpisodeAdmin(admin.ModelAdmin): + filter_horizontal = ["events"] diff --git a/backend/case_study/migrations/0002_casestudy_description_alter_casestudy_name_episode_and_more.py b/backend/case_study/migrations/0002_casestudy_description_alter_casestudy_name_episode_and_more.py new file mode 100644 index 00000000..41f225ff --- /dev/null +++ b/backend/case_study/migrations/0002_casestudy_description_alter_casestudy_name_episode_and_more.py @@ -0,0 +1,41 @@ +# Generated by Django 4.2.7 on 2024-04-11 13:14 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('event', '0013_initial'), + ('case_study', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='casestudy', + name='description', + field=models.TextField(blank=True, help_text='Longer description to help identify this object'), + ), + migrations.AlterField( + model_name='casestudy', + name='name', + field=models.CharField(help_text='A name to help identify this object', max_length=200), + ), + migrations.CreateModel( + name='Episode', + 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')), + ('events', models.ManyToManyField(blank=True, help_text='Events that make up this episode', to='event.eventdescription')), + ], + options={ + 'abstract': False, + }, + ), + migrations.AddField( + model_name='casestudy', + name='episodes', + field=models.ManyToManyField(blank=True, help_text='Episodes involved in this case study', to='case_study.episode'), + ), + ] diff --git a/backend/case_study/models.py b/backend/case_study/models.py index 97b51fb1..b7beb42e 100644 --- a/backend/case_study/models.py +++ b/backend/case_study/models.py @@ -1,7 +1,9 @@ from django.db import models +from core.models import Named +from event.models import EventDescription -class CaseStudy(models.Model): +class CaseStudy(Named, models.Model): """ A case study is an overarching collection of epistolary events, bound together by a common theme, e.g. `The Saga of St. Boniface` or `The Nun Rebellion of Poitiers`. """ @@ -10,11 +12,23 @@ class Meta: verbose_name = "case study" verbose_name_plural = "case studies" - name = models.CharField( - max_length=256, - null=False, - blank=False, + episodes = models.ManyToManyField( + to="Episode", + blank=True, + help_text="Episodes involved in this case study", ) def __str__(self): return self.name + + +class Episode(Named, models.Model): + """ + A higher abstraction of events into connected "episodes" + """ + + events = models.ManyToManyField( + to=EventDescription, + blank=True, + help_text="Events that make up this episode", + ) From c50f016547ceeba4d75b1f7be6c2e5752973daf0 Mon Sep 17 00:00:00 2001 From: Luka van der Plas Date: Mon, 15 Apr 2024 14:33:30 +0200 Subject: [PATCH 28/34] add key persons + key sites to case study --- backend/case_study/admin.py | 4 +-- ...sestudy_key_persons_casestudy_key_sites.py | 25 +++++++++++++++++++ backend/case_study/models.py | 15 ++++++++++- 3 files changed, 41 insertions(+), 3 deletions(-) create mode 100644 backend/case_study/migrations/0003_casestudy_key_persons_casestudy_key_sites.py diff --git a/backend/case_study/admin.py b/backend/case_study/admin.py index 4b97fc1a..415c4530 100644 --- a/backend/case_study/admin.py +++ b/backend/case_study/admin.py @@ -5,8 +5,8 @@ @admin.register(models.CaseStudy) class CaseStudyAdmin(admin.ModelAdmin): list_display = ["name", "description"] - fields = ["name", "description", "episodes"] - filter_horizontal = ["episodes"] + fields = ["name", "description", "episodes", "key_persons", "key_sites"] + filter_horizontal = ["episodes", "key_persons", "key_sites"] @admin.register(models.Episode) diff --git a/backend/case_study/migrations/0003_casestudy_key_persons_casestudy_key_sites.py b/backend/case_study/migrations/0003_casestudy_key_persons_casestudy_key_sites.py new file mode 100644 index 00000000..a4af114c --- /dev/null +++ b/backend/case_study/migrations/0003_casestudy_key_persons_casestudy_key_sites.py @@ -0,0 +1,25 @@ +# Generated by Django 4.2.7 on 2024-04-15 12:31 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('person', '0014_alter_persondateofbirth_person_and_more'), + ('space', '0008_alter_spacedescription_source'), + ('case_study', '0002_casestudy_description_alter_casestudy_name_episode_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='casestudy', + name='key_persons', + field=models.ManyToManyField(blank=True, help_text='Key historical figures involved in this case study', to='person.historicalperson'), + ), + migrations.AddField( + model_name='casestudy', + name='key_sites', + field=models.ManyToManyField(blank=True, help_text='Key historical sites involved in this case study', to='space.structure'), + ), + ] diff --git a/backend/case_study/models.py b/backend/case_study/models.py index b7beb42e..3f928eeb 100644 --- a/backend/case_study/models.py +++ b/backend/case_study/models.py @@ -1,7 +1,8 @@ from django.db import models from core.models import Named from event.models import EventDescription - +from person.models import HistoricalPerson +from space.models import Structure class CaseStudy(Named, models.Model): """ @@ -18,6 +19,18 @@ class Meta: help_text="Episodes involved in this case study", ) + key_persons = models.ManyToManyField( + to=HistoricalPerson, + blank=True, + help_text="Key historical figures involved in this case study", + ) + + key_sites = models.ManyToManyField( + to=Structure, + blank=True, + help_text="Key historical sites involved in this case study", + ) + def __str__(self): return self.name From 7c7ac7120198a4069512d268d619a247a8ed4dda Mon Sep 17 00:00:00 2001 From: Luka van der Plas Date: Mon, 15 Apr 2024 15:13:20 +0200 Subject: [PATCH 29/34] update dev dataset script --- .../management/commands/create_dev_dataset.py | 346 ++++++------------ backend/core/management/commands/fixtures.py | 103 ------ 2 files changed, 105 insertions(+), 344 deletions(-) diff --git a/backend/core/management/commands/create_dev_dataset.py b/backend/core/management/commands/create_dev_dataset.py index a83cea92..2d4e61ba 100644 --- a/backend/core/management/commands/create_dev_dataset.py +++ b/backend/core/management/commands/create_dev_dataset.py @@ -4,35 +4,18 @@ from faker import Faker from source.models import Source -from case_study.models import CaseStudy -# from event.models import ( -# EpistolaryEvent, -# EpistolaryEventSelfTrigger, -# EpistolaryEventTrigger, -# LetterAction, -# LetterActionCategory, -# LetterEventDate, -# Role, -# WorldEvent, -# WorldEventSelfTrigger, -# WorldEventTrigger, -# ) - -# from person.models import ( -# Agent, -# AgentDateOfBirth, -# AgentDateOfDeath, -# Gender, -# StatusMarker, -# ) +from case_study.models import CaseStudy, Episode +from event.models import EventDescription +from person.models import ( + HistoricalPerson, + AgentDescription, + HistoricalPerson, +) from letter.models import ( Category, - # Gift, - # Letter, - # LetterAddressees, - # LetterCategory, - # LetterMaterial, - # LetterSenders, + GiftDescription, + LetterDescription, + LetterDescriptionCategory, ) import random @@ -44,7 +27,6 @@ epistolary_event_names, letter_category_names, source_names, - world_event_names, group_names, ) from .create_dev_dataset_utils import ( @@ -114,102 +96,88 @@ def handle(self, *args, **options): # Comment out the function calls below as needed. # Adjust the `total` parameter to create more or less data. # NB: the order of these function calls is important. - self._create_case_studies(fake, options, total=10, model=CaseStudy) - # 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_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) - # self._create_letter_actions(fake, options, total=200, model=LetterAction) - # self._create_world_events(fake, options, total=50, model=WorldEvent) - # self._create_world_event_triggers( - # fake, options, total=50, model=WorldEventTrigger - # ) - # self._create_epistolary_event_triggers( - # fake, options, total=50, model=EpistolaryEventTrigger - # ) - # self._create_world_event_self_triggers( - # fake, options, total=50, model=WorldEventSelfTrigger - # ) - # self._create_epistolary_event_self_trigger( - # fake, options, total=50, model=EpistolaryEventSelfTrigger - # ) self._create_sources(fake, options, total=50, model=Source) + self._create_historical_persons( + fake, options, total=50, model=HistoricalPerson + ) + self._create_agent_descriptions( + fake, options, total=100, model=AgentDescription + ) + self._create_letter_categories(fake, options, total=10, model=Category) + self._create_letter_descriptions( + fake, options, total=200, model=LetterDescription + ) + self._create_gift_descriptions( + fake, options, total=50, model=GiftDescription + ) + self._create_event_descriptions( + fake, options, total=50, model=EventDescription + ) + self._create_episodes(fake, options, total=20, model=EventDescription) + self._create_case_studies(fake, options, total=10, model=CaseStudy) + print("-" * 80) print("Development dataset created successfully.") + @track_progress + def _create_episodes(self, fake: Faker, options, total, model): + unique_name = get_unique_name(epistolary_event_names, Episode) + events = get_random_model_objects(EventDescription, min_amount=1, max_amount=5) + Episode.objects.create( + name=unique_name, + description=fake.text(), + events=events, + ) + @track_progress def _create_case_studies(self, fake: Faker, options, total, model): unique_name = get_unique_name(case_study_names, CaseStudy) - CaseStudy.objects.create(name=unique_name) - - # @track_progress - # def _create_epistolary_events(self, fake, options, total, model): - # unique_name = get_unique_name(epistolary_event_names, EpistolaryEvent) + episodes = get_random_model_objects(Episode, min_amount=1, max_amount=5) + CaseStudy.objects.create( + name=unique_name, + episodes=episodes, + ) - # event = EpistolaryEvent.objects.create(name=unique_name, note=fake.text()) + @track_progress + def _create_event_descriptions(self, fake: Faker, options, total, model): + unique_name = get_unique_name(epistolary_event_names, EventDescription) + source = get_random_model_object(Source) - # event.case_studies.set( - # get_random_model_objects(CaseStudy, min_amount=0, max_amount=3) - # ) + event = EventDescription.objects.create( + source=source, name=unique_name, description=fake.text() + ) @track_progress - def _create_status_markers(self, fake, options, total, model): - # StatusMarker.objects.create(name=fake.job(), description=fake.text()) - pass + def _create_historical_person(self, fake: Faker, options, total, model): + HistoricalPerson.objects.create(name=fake.name()) @track_progress - def _create_agents(self, fake: Faker, options, total, model): - 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() + def _create_agent_descriptions(self, fake: Faker, options, total, model): + source = get_random_model_object(Source) + + is_group = random.choice([True, False]) + + if is_group is True: + describes = get_random_model_objects( + HistoricalPerson, min_amount=0, max_amount=5 + ) + name = random.choice(group_names) + else: + referent = get_random_model_object(HistoricalPerson, allow_null=True) + if referent: + name = referent.name + describes = [referent] + else: + name = fake.name() + describes = [] + + AgentDescription.objects.create( + name=name, + source=source, + is_group=is_group, + describes=describes, + ) @track_progress def _create_letter_categories(self, fake: Faker, *args, **kwargs): @@ -218,137 +186,33 @@ def _create_letter_categories(self, fake: Faker, *args, **kwargs): ) Category.objects.create(label=unique_label, description=fake.text()) - # @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) - - # subject = ", ".join(fake.words(nb=3, unique=True)) - # letter = Letter.objects.create( - # name=f"Letter about {subject}", - # ) - - # if random.choice([True, False]): - # LetterMaterial.objects.create( - # letter=letter, - # surface=random.choice(LetterMaterial.Surface.values), - # **self.fake_field_value(fake), - # ) - - # if random.choice([True, False]): - # LetterCategory.objects.create( - # letter=letter, - # category=get_random_model_object(Category), - # **self.fake_field_value(fake), - # ) - - # 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) - - # @track_progress - # def _create_letter_actions(self, fake: Faker, *args, **kwargs): - # action = LetterAction.objects.create() - # action.letters.set(get_random_model_objects(Letter, min_amount=1, max_amount=5)) - - # action.gifts.set(get_random_model_objects(Gift, min_amount=0, max_amount=5)) - - # action.epistolary_events.set( - # get_random_model_objects(EpistolaryEvent, min_amount=0, max_amount=5) - # ) - - # LetterEventDate.objects.create( - # letter_action=action, - # **self.fake_date_value(fake), - # **self.fake_field_value(fake), - # ) - - # no_of_categories = random.randint(1, 5) - # random_categories = random.sample( - # LetterActionCategory.CategoryOptions.choices, no_of_categories - # ) - # for i in range(no_of_categories): - # LetterActionCategory.objects.create( - # letter_action=action, - # value=random_categories[i][0], - # **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) - - # Gift.objects.create( - # name=unique_name, - # material=random.choice(Gift.Material.choices)[0], - # # gifted_by=gifter, - # description=fake.text(), - # ) - - # @track_progress - # def _create_world_events(self, fake, options, total, model): - # unique_name = get_unique_name(world_event_names, WorldEvent) - # WorldEvent.objects.create( - # name=unique_name, note=fake.text(), **self.fake_date_value(fake) - # ) - - # @track_progress - # def _create_world_event_triggers(self, fake, options, total, model): - # WorldEventTrigger.objects.create( - # world_event=get_random_model_object(WorldEvent), - # epistolary_event=get_random_model_object(EpistolaryEvent), - # **self.fake_field_value(fake), - # ) - - # @track_progress - # def _create_epistolary_event_triggers(self, fake, options, total, model): - # EpistolaryEventTrigger.objects.create( - # epistolary_event=get_random_model_object(EpistolaryEvent), - # world_event=get_random_model_object(WorldEvent), - # **self.fake_field_value(fake), - # ) - - # @track_progress - # def _create_world_event_self_triggers(self, fake, options, total, model): - # [triggering, triggered] = get_random_model_objects( - # WorldEvent, max_amount=2, exact=True - # ) - # WorldEventSelfTrigger.objects.create( - # triggered_world_event=triggering, - # triggering_world_event=triggered, - # **self.fake_field_value(fake), - # ) - - # @track_progress - # def _create_epistolary_event_self_trigger(self, fake, options, total, model): - # [triggering, triggered] = get_random_model_objects( - # EpistolaryEvent, max_amount=2, exact=True - # ) - # EpistolaryEventSelfTrigger.objects.create( - # triggering_epistolary_event=triggering, - # triggered_epistolary_event=triggered, - # **self.fake_field_value(fake), - # ) + @track_progress + def _create_letter_descriptions(self, fake: Faker, *args, **kwargs): + source = get_random_model_object(Source) + + subject = ", ".join(fake.words(nb=3, unique=True)) + letter = LetterDescription.objects.create( + source=source, + name=f"Letter about {subject}", + ) + + if random.choice([True, False]): + LetterDescriptionCategory.objects.create( + letter=letter, + category=get_random_model_object(Category), + **self.fake_field_value(fake), + ) + + @track_progress + def _create_gift_descriptions(self, fake, options, total, model): + source = get_random_model_object(Source) + unique_name = get_unique_name(gift_names, GiftDescription) + + GiftDescription.objects.create( + source=source, + name=unique_name, + description=fake.text(), + ) @track_progress def _create_sources(self, fake, options, total, model): diff --git a/backend/core/management/commands/fixtures.py b/backend/core/management/commands/fixtures.py index ce5727bd..5999aad1 100644 --- a/backend/core/management/commands/fixtures.py +++ b/backend/core/management/commands/fixtures.py @@ -390,109 +390,6 @@ "Imperium Justorum: Empire of the Just Rulers", ] -world_event_names = [ - "The Sack of Rome by the Visigoths", - "The Vandalic War", - "Council of Ephesus", - "Attila the Hun invades the Eastern Roman Empire", - "The Fall of the Western Roman Empire", - "Clovis I converts to Christianity", - "The Battle of Mons Badonicus", - "The Byzantine Emperor Justinian I's reconquests", - "The Plague of Justinian", - "The Battle of Camlann", - "The Visigothic War in Spain", - "The First Council of Constantinople", - "Irish monasticism flourishes", - "The Byzantine-Sassanid War", - "The Life of Saint Benedict and the founding of the Benedictine Order", - "The Lombard invasion of Italy", - "The Third Council of Toledo", - "The arrival of St. Augustine of Canterbury in England", - "The Synod of Whitby", - "Battle of Dunnichen", - "The Islamic Conquest of Persia", - "The Battle of Badr", - "The Rashidun Caliphate", - "The Battle of Yarmouk", - "The Establishment of the Umayyad Caliphate", - "The Siege of Constantinople by the Arabs", - "The Tang Dynasty in China", - "The Battle of Talas", - "The Iconoclastic Controversy", - "The Papal States established", - "Charlemagne becomes King of the Franks", - "The Abbasid Caliphate", - "Charlemagne crowned Holy Roman Emperor", - "The Viking raids on Lindisfarne", - "The Battle of Pliska", - "The Avars invade the Eastern Roman Empire", - "The Tang Dynasty's Golden Age", - "The Battle of Lechfeld", - "The Great Heathen Army invades England", - "The Treaty of Verdun", - "The Battle of Talas River", - "The Slavic migration and settlement in Eastern Europe", - "The Battle of Winwaed", - "The construction of the Hagia Sophia in Constantinople", - "The Battle of Maldon", - "The Umayyad invasion of Gaul", - "The Synod of Rome", - "The Battle of Vouillé", - "The life of Saint Patrick in Ireland", - "The Battle of Taginae", - "The Council of Chalcedon", - "The Siege of Paris by the Vikings", - "The Battle of Placentia", - "The Anglo-Saxon mission to the Germanic tribes", - "The Battle of Poitiers", - "The Battle of Dara", - "The Battle of Tertry", - "The Synod of Whitby", - "The Battle of Adda", - "The Second Council of Constantinople", - "The Battle of Jutland", - "The Battle of Carhampton", - "The Battle of Aylesford", - "The Battle of Wogastisburg", - "The Synod of Hertford", - "The Battle of Tricamarum", - "The First Bulgarian Empire", - "The Battle of Compiègne", - "The Siege of Drant’s Dyke", - "The Battle of Lafresnaye", - "The Kingdom of Axum falls", - "The Battle of Tolbiac", - "The Synod of Pavia", - "The Battle of Woden’s Burg", - "The Battle of Toulouse", - "The Council of Aachen", - "The Battle of Vézeronce", - "The Battle of Wenden", - "The Council of Constantinople", - "The Battle of Ceccano", - "The Synod of Chelsea", - "The Battle of Marton", - "The Sogdian Rock Inscription", - "The Battle of Sarno", - "The Battle of Agri", - "The Siege of Canossa", - "The Battle of Gartan", - "The Battle of Bolia", - "The Synod of Auxerre", - "The Battle of Avarayr", - "The Battle of Cillium", - "The Synod of Chalon-sur-Saône", - "The Battle of Bedford", - "The Battle of Novidunum", - "The Synod of Rome", - "The Battle of Dommoc", - "The Battle of Adrianople", - "The Synod of Arles", - "The Battle of Tolbiac", - "The Synod of Paris", -] - group_names = [ "Nobility of Camelot", "Clergy of Byzantium", From a296c06bb140c434ca4289a3aa366a996b0c8ec2 Mon Sep 17 00:00:00 2001 From: Luka van der Plas Date: Thu, 18 Apr 2024 13:51:43 +0200 Subject: [PATCH 30/34] add summary and categories to event description --- backend/event/admin.py | 18 ++++++++++ ...egory_eventdescription_summary_and_more.py | 34 +++++++++++++++++++ backend/event/models.py | 16 ++++++++- 3 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 backend/event/migrations/0014_eventcategory_eventdescription_summary_and_more.py diff --git a/backend/event/admin.py b/backend/event/admin.py index b36f1ada..b4e89704 100644 --- a/backend/event/admin.py +++ b/backend/event/admin.py @@ -3,6 +3,12 @@ from core import admin as core_admin +@admin.register(models.EventCategory) +class EventCategoryAdmin(admin.ModelAdmin): + list_display = ["name", "description"] + search_fields = ["name", "description"] + + class EventDescriptionAgentAdmin(admin.StackedInline): model = models.EventDescriptionAgent fields = ["agent"] + core_admin.description_field_fields @@ -33,6 +39,18 @@ class EventDescriptionSpaceAdmin(admin.StackedInline): @admin.register(models.EventDescription) class EventDescriptionAdmin(core_admin.EntityDescriptionAdmin, admin.ModelAdmin): + filter_horizontal = ["categories"] + list_filter = ["source", "categories"] + fieldsets = [ + core_admin.named_fieldset, + core_admin.description_source_fieldset, + ( + "Contents", + { + "fields": ["summary", "categories"], + }, + ), + ] inlines = [ EventDescriptionAgentAdmin, EventDescriptionGiftAdmin, diff --git a/backend/event/migrations/0014_eventcategory_eventdescription_summary_and_more.py b/backend/event/migrations/0014_eventcategory_eventdescription_summary_and_more.py new file mode 100644 index 00000000..36266df3 --- /dev/null +++ b/backend/event/migrations/0014_eventcategory_eventdescription_summary_and_more.py @@ -0,0 +1,34 @@ +# Generated by Django 4.2.7 on 2024-04-18 11:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('event', '0013_initial'), + ] + + operations = [ + migrations.CreateModel( + name='EventCategory', + 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')), + ], + options={ + 'abstract': False, + }, + ), + migrations.AddField( + model_name='eventdescription', + name='summary', + field=models.TextField(blank=True, help_text='full description of the events in the passage'), + ), + migrations.AddField( + model_name='eventdescription', + name='categories', + field=models.ManyToManyField(help_text='labels assigned to this event', related_name='event_descriptions', to='event.eventcategory'), + ), + ] diff --git a/backend/event/models.py b/backend/event/models.py index 2505abdd..fc069c7a 100644 --- a/backend/event/models.py +++ b/backend/event/models.py @@ -1,17 +1,31 @@ from django.db import models from django.core.exceptions import ValidationError -from core.models import EntityDescription, DescriptionField +from core.models import EntityDescription, DescriptionField, Named from person.models import AgentDescription from letter.models import GiftDescription, LetterDescription from space.models import SpaceDescription +class EventCategory(Named): + class Meta: + verbose_name_plural = "event categories" + + class EventDescription(EntityDescription, models.Model): """ An epistolary event described in as source text """ + summary = models.TextField( + blank=True, + help_text="full description of the events in the passage", + ) + categories = models.ManyToManyField( + to=EventCategory, + related_name="event_descriptions", + help_text="labels assigned to this event", + ) agents = models.ManyToManyField( to=AgentDescription, through="EventDescriptionAgent", From 394b0c6a17aa8292005b8143b30d782a64aa5c72 Mon Sep 17 00:00:00 2001 From: Luka van der Plas Date: Thu, 18 Apr 2024 13:56:08 +0200 Subject: [PATCH 31/34] start user app --- backend/user/__init__.py | 0 backend/user/admin.py | 3 +++ backend/user/apps.py | 6 ++++++ backend/user/migrations/__init__.py | 0 backend/user/models.py | 3 +++ backend/user/views.py | 3 +++ 6 files changed, 15 insertions(+) create mode 100644 backend/user/__init__.py create mode 100644 backend/user/admin.py create mode 100644 backend/user/apps.py create mode 100644 backend/user/migrations/__init__.py create mode 100644 backend/user/models.py create mode 100644 backend/user/views.py diff --git a/backend/user/__init__.py b/backend/user/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/user/admin.py b/backend/user/admin.py new file mode 100644 index 00000000..8c38f3f3 --- /dev/null +++ b/backend/user/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/backend/user/apps.py b/backend/user/apps.py new file mode 100644 index 00000000..36cce4c8 --- /dev/null +++ b/backend/user/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class UserConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'user' diff --git a/backend/user/migrations/__init__.py b/backend/user/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/user/models.py b/backend/user/models.py new file mode 100644 index 00000000..71a83623 --- /dev/null +++ b/backend/user/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/backend/user/views.py b/backend/user/views.py new file mode 100644 index 00000000..91ea44a2 --- /dev/null +++ b/backend/user/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. From 9a079a31e1198b79cb604b5f9d02f1835264ddff Mon Sep 17 00:00:00 2001 From: Luka van der Plas Date: Thu, 18 Apr 2024 14:16:30 +0200 Subject: [PATCH 32/34] add custom User model --- backend/lettercraft/settings.py | 4 + backend/user/admin.py | 7 +- backend/user/migrations/0001_initial.py | 132 ++++++++++++++++++++++++ backend/user/models.py | 10 +- 4 files changed, 151 insertions(+), 2 deletions(-) create mode 100644 backend/user/migrations/0001_initial.py diff --git a/backend/lettercraft/settings.py b/backend/lettercraft/settings.py index e9af4b1b..c5c61b8b 100644 --- a/backend/lettercraft/settings.py +++ b/backend/lettercraft/settings.py @@ -45,6 +45,7 @@ "person", "source", "space", + "user", ] MIDDLEWARE = [ @@ -112,6 +113,9 @@ }, ] +# Authentication + +AUTH_USER_MODEL = "user.User" # Internationalization # https://docs.djangoproject.com/en/3.0/topics/i18n/ diff --git a/backend/user/admin.py b/backend/user/admin.py index 8c38f3f3..704eef0b 100644 --- a/backend/user/admin.py +++ b/backend/user/admin.py @@ -1,3 +1,8 @@ from django.contrib import admin +from django.contrib.auth import admin as auth_admin +from . import models -# Register your models here. + +@admin.register(models.User) +class UserAdmin(auth_admin.UserAdmin): + pass diff --git a/backend/user/migrations/0001_initial.py b/backend/user/migrations/0001_initial.py new file mode 100644 index 00000000..9e0b315e --- /dev/null +++ b/backend/user/migrations/0001_initial.py @@ -0,0 +1,132 @@ +# Generated by Django 4.2.7 on 2024-04-18 12:25 + +from django.conf import settings +import django.contrib.auth.models +import django.contrib.auth.validators +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ] + + operations = [ + migrations.CreateModel( + name="User", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("password", models.CharField(max_length=128, verbose_name="password")), + ( + "last_login", + models.DateTimeField( + blank=True, null=True, verbose_name="last login" + ), + ), + ( + "is_superuser", + models.BooleanField( + default=False, + help_text="Designates that this user has all permissions without explicitly assigning them.", + verbose_name="superuser status", + ), + ), + ( + "username", + models.CharField( + error_messages={ + "unique": "A user with that username already exists." + }, + help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.", + max_length=150, + unique=True, + validators=[ + django.contrib.auth.validators.UnicodeUsernameValidator() + ], + verbose_name="username", + ), + ), + ( + "first_name", + models.CharField( + blank=True, max_length=150, verbose_name="first name" + ), + ), + ( + "last_name", + models.CharField( + blank=True, max_length=150, verbose_name="last name" + ), + ), + ( + "email", + models.EmailField( + blank=True, max_length=254, verbose_name="email address" + ), + ), + ( + "is_staff", + models.BooleanField( + default=False, + help_text="Designates whether the user can log into this admin site.", + verbose_name="staff status", + ), + ), + ( + "is_active", + models.BooleanField( + default=True, + help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.", + verbose_name="active", + ), + ), + ( + "date_joined", + models.DateTimeField( + default=django.utils.timezone.now, verbose_name="date joined" + ), + ), + ( + "groups", + models.ManyToManyField( + blank=True, + help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", + related_name="user_set", + related_query_name="user", + to="auth.group", + verbose_name="groups", + ), + ), + ( + "user_permissions", + models.ManyToManyField( + blank=True, + help_text="Specific permissions for this user.", + related_name="user_set", + related_query_name="user", + to="auth.permission", + verbose_name="user permissions", + ), + ), + ], + options={ + "db_table": "auth_user", + }, + managers=[ + ("objects", django.contrib.auth.models.UserManager()), + ], + ) + ] diff --git a/backend/user/models.py b/backend/user/models.py index 71a83623..74d6e040 100644 --- a/backend/user/models.py +++ b/backend/user/models.py @@ -1,3 +1,11 @@ +import django.contrib.auth.models as django_auth_models from django.db import models -# Create your models here. + +class User(django_auth_models.AbstractUser): + """ + Core user model that is used for authentication. + """ + + class Meta: + db_table = "auth_user" From 79a3e8774f08e3fb1d65fd310c098bcf5bf5ba67 Mon Sep 17 00:00:00 2001 From: Luka van der Plas Date: Wed, 24 Apr 2024 15:32:09 +0200 Subject: [PATCH 33/34] move episodes to event app --- backend/case_study/admin.py | 5 --- ...n_alter_casestudy_name_episode_and_more.py | 42 +++++++++---------- backend/case_study/models.py | 16 +------ .../management/commands/create_dev_dataset.py | 4 +- backend/event/admin.py | 5 +++ ...015_alter_eventcategory_options_episode.py | 29 +++++++++++++ backend/event/models.py | 12 ++++++ 7 files changed, 69 insertions(+), 44 deletions(-) create mode 100644 backend/event/migrations/0015_alter_eventcategory_options_episode.py diff --git a/backend/case_study/admin.py b/backend/case_study/admin.py index 415c4530..869136f5 100644 --- a/backend/case_study/admin.py +++ b/backend/case_study/admin.py @@ -7,8 +7,3 @@ class CaseStudyAdmin(admin.ModelAdmin): list_display = ["name", "description"] fields = ["name", "description", "episodes", "key_persons", "key_sites"] filter_horizontal = ["episodes", "key_persons", "key_sites"] - - -@admin.register(models.Episode) -class EpisodeAdmin(admin.ModelAdmin): - filter_horizontal = ["events"] diff --git a/backend/case_study/migrations/0002_casestudy_description_alter_casestudy_name_episode_and_more.py b/backend/case_study/migrations/0002_casestudy_description_alter_casestudy_name_episode_and_more.py index 41f225ff..53d1fc07 100644 --- a/backend/case_study/migrations/0002_casestudy_description_alter_casestudy_name_episode_and_more.py +++ b/backend/case_study/migrations/0002_casestudy_description_alter_casestudy_name_episode_and_more.py @@ -6,36 +6,32 @@ class Migration(migrations.Migration): dependencies = [ - ('event', '0013_initial'), - ('case_study', '0001_initial'), + ("event", "0015_alter_eventcategory_options_episode"), + ("case_study", "0001_initial"), ] operations = [ migrations.AddField( - model_name='casestudy', - name='description', - field=models.TextField(blank=True, help_text='Longer description to help identify this object'), + model_name="casestudy", + name="description", + field=models.TextField( + blank=True, help_text="Longer description to help identify this object" + ), ), migrations.AlterField( - model_name='casestudy', - name='name', - field=models.CharField(help_text='A name to help identify this object', max_length=200), - ), - migrations.CreateModel( - name='Episode', - 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')), - ('events', models.ManyToManyField(blank=True, help_text='Events that make up this episode', to='event.eventdescription')), - ], - options={ - 'abstract': False, - }, + model_name="casestudy", + name="name", + field=models.CharField( + help_text="A name to help identify this object", max_length=200 + ), ), migrations.AddField( - model_name='casestudy', - name='episodes', - field=models.ManyToManyField(blank=True, help_text='Episodes involved in this case study', to='case_study.episode'), + model_name="casestudy", + name="episodes", + field=models.ManyToManyField( + blank=True, + help_text="Episodes involved in this case study", + to="event.episode", + ), ), ] diff --git a/backend/case_study/models.py b/backend/case_study/models.py index 3f928eeb..d6ac10c0 100644 --- a/backend/case_study/models.py +++ b/backend/case_study/models.py @@ -1,6 +1,6 @@ from django.db import models from core.models import Named -from event.models import EventDescription +from event.models import Episode from person.models import HistoricalPerson from space.models import Structure @@ -14,7 +14,7 @@ class Meta: verbose_name_plural = "case studies" episodes = models.ManyToManyField( - to="Episode", + to=Episode, blank=True, help_text="Episodes involved in this case study", ) @@ -33,15 +33,3 @@ class Meta: def __str__(self): return self.name - - -class Episode(Named, models.Model): - """ - A higher abstraction of events into connected "episodes" - """ - - events = models.ManyToManyField( - to=EventDescription, - blank=True, - help_text="Events that make up this episode", - ) diff --git a/backend/core/management/commands/create_dev_dataset.py b/backend/core/management/commands/create_dev_dataset.py index 2d4e61ba..b0dec337 100644 --- a/backend/core/management/commands/create_dev_dataset.py +++ b/backend/core/management/commands/create_dev_dataset.py @@ -4,8 +4,8 @@ from faker import Faker from source.models import Source -from case_study.models import CaseStudy, Episode -from event.models import EventDescription +from case_study.models import CaseStudy +from event.models import EventDescription, Episode from person.models import ( HistoricalPerson, AgentDescription, diff --git a/backend/event/admin.py b/backend/event/admin.py index b4e89704..35a84324 100644 --- a/backend/event/admin.py +++ b/backend/event/admin.py @@ -57,3 +57,8 @@ class EventDescriptionAdmin(core_admin.EntityDescriptionAdmin, admin.ModelAdmin) EventDescriptionLetterAdmin, EventDescriptionSpaceAdmin, ] + + +@admin.register(models.Episode) +class EpisodeAdmin(admin.ModelAdmin): + filter_horizontal = ["events"] diff --git a/backend/event/migrations/0015_alter_eventcategory_options_episode.py b/backend/event/migrations/0015_alter_eventcategory_options_episode.py new file mode 100644 index 00000000..7c1e75ba --- /dev/null +++ b/backend/event/migrations/0015_alter_eventcategory_options_episode.py @@ -0,0 +1,29 @@ +# Generated by Django 4.2.7 on 2024-04-24 13:29 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('event', '0014_eventcategory_eventdescription_summary_and_more'), + ] + + operations = [ + migrations.AlterModelOptions( + name='eventcategory', + options={'verbose_name_plural': 'event categories'}, + ), + migrations.CreateModel( + name='Episode', + 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')), + ('events', models.ManyToManyField(blank=True, help_text='Events that make up this episode', to='event.eventdescription')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/backend/event/models.py b/backend/event/models.py index fc069c7a..c28b2449 100644 --- a/backend/event/models.py +++ b/backend/event/models.py @@ -138,3 +138,15 @@ def clean(self): def __str__(self): return f"{self.space.name} / {self.event}" + + +class Episode(Named, models.Model): + """ + A higher abstraction of events into connected "episodes" + """ + + events = models.ManyToManyField( + to=EventDescription, + blank=True, + help_text="Events that make up this episode", + ) From f102f67fe0fb45f49e116bf19fd55266c79b3669 Mon Sep 17 00:00:00 2001 From: Luka van der Plas Date: Wed, 24 Apr 2024 15:49:23 +0200 Subject: [PATCH 34/34] update version number --- CITATION.cff | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CITATION.cff b/CITATION.cff index 03091904..1c6b708c 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -29,5 +29,5 @@ authors: website: 'https://cdh.uu.nl/rsl/' repository-code: 'https://github.com/CentreForDigitalHumanities/lettercraft' license: BSD-3-Clause -version: 0.2.0 -date-released: '2024-03-13' +version: 0.3.0 +date-released: '2024-04-24' diff --git a/package.json b/package.json index 1028d3cc..db2f262d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lettercraft", - "version": "0.2.0", + "version": "0.3.0", "description": "Lettercraft & Epistolary Performance in Medieval Europe", "author": "UU Centre for Digital Humanities - Research Software Lab", "license": "BSD-3-Clause",