diff --git a/backend/core/management/commands/create_dev_dataset.py b/backend/core/management/commands/create_dev_dataset.py index cf307304..f38d2097 100644 --- a/backend/core/management/commands/create_dev_dataset.py +++ b/backend/core/management/commands/create_dev_dataset.py @@ -1,7 +1,7 @@ -from typing import List +from typing import List, Optional from django.db import transaction from django.conf import settings -from django.db.models import Model +from django.db.models import Model, QuerySet from django.core.management.base import CommandError, BaseCommand from functools import wraps from faker import Faker @@ -10,10 +10,15 @@ from case_study.models import CaseStudy from event.models import ( EpistolaryEvent, + EpistolaryEventSelfTrigger, + EpistolaryEventTrigger, LetterAction, LetterActionCategory, LetterEventDate, Role, + WorldEvent, + WorldEventSelfTrigger, + WorldEventTrigger, ) from person.models import ( Person, @@ -21,9 +26,11 @@ Occupation, PersonDateOfBirth, PersonDateOfDeath, + PersonName, ) from letter.models import ( Category, + Gift, Letter, LetterAddressees, LetterCategory, @@ -40,6 +47,7 @@ epistolary_event_names, letter_category_names, source_names, + world_event_names, ) @@ -72,6 +80,50 @@ def get_unique_name( raise ValueError("Could not find a unique name") +def get_random_model_object(model: Model, allow_null=False) -> Optional[Model]: + """ + Returns a random object from the given model. + + If `allow_null` is True, None may also be returned. + + If there are no objects of the specified model, a `ValueError` will be raised. + """ + if allow_null and random.choice([True, False]): + return None + + if model.objects.exists(): + return model.objects.order_by("?").first() + raise ValueError( + f"No objects of type {model._meta.verbose_name_plural} found. Please create at least one." + ) + + +def get_random_model_objects( + model: Model, min_amount=0, max_amount=10, exact=False +) -> List[Model]: + """ + Get a list of random model objects from the specified model. + + If `exact` is True, exactly `max_amount` objects will be returned. + + Else, a random number of objects between `min_amount` and `max_amount` will be returned. + + If there are not enough objects of the specified model, a `ValueError` will be raised. + """ + all_random_objects = model.objects.order_by("?") + if all_random_objects.count() < max_amount: + raise ValueError( + f"Not enough objects of type {model._meta.verbose_name_plural} for requested amount. Please create more." + ) + + if min_amount > max_amount: + raise ValueError("min_amount cannot be greater than max_amount") + + if exact is True: + return list(all_random_objects[:max_amount].all()) + return list(all_random_objects[: random.randint(min_amount, max_amount)].all()) + + class Command(BaseCommand): help = "Create a dataset for development purposes" @@ -127,23 +179,32 @@ def handle(self, *args, **options): fake = Faker("en_GB") with transaction.atomic(): - # Create Letter Actions - # Create Sources - self.info("-" * 80) self.info("Creating Lettercraft development dataset") self.info("-" * 80) - # self._create_case_studies(fake, options, total=10, model=CaseStudy) - # self._create_epistolary_events( - # fake, options, total=40, model=EpistolaryEvent - # ) - # self._create_offices(fake, options, total=50, model=Office) - # self._create_persons(fake, options, total=100, model=Person) - # self._create_letter_categories(fake, options, total=10, model=Category) - # self._create_letters(fake, options, total=200, model=Letter) - # self._create_letter_actions(fake, options, total=200, model=LetterAction) - # self._create_sources(fake, options, total=50, model=Source) + # 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_offices(fake, options, total=50, model=Office) + self._create_persons(fake, options, total=100, model=Person) + 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_references(fake, options, total=250, model=Reference) self.info("-" * 80) @@ -160,10 +221,9 @@ def _create_epistolary_events(self, fake, options, total, model): event = EpistolaryEvent.objects.create(name=unique_name, note=fake.text()) - number_of_case_studies = random.randint(0, 3) - if number_of_case_studies > 0: - case_studies = CaseStudy.objects.order_by("?")[:number_of_case_studies] - event.case_studies.set(case_studies) + event.case_studies.set( + get_random_model_objects(CaseStudy, min_amount=0, max_amount=3) + ) @track_progress def _create_offices(self, fake, options, total, model): @@ -173,8 +233,11 @@ def _create_offices(self, fake, options, total, model): def _create_persons(self, fake: Faker, options, total, model): person = Person.objects.create(gender=random.choice(Person.Gender.values)) for _ in range(random.randint(0, 3)): - person.names.create(value=fake.name()) - person.save() + PersonName.objects.create( + person=person, + value=fake.name(), + **self.fake_field_value(fake), + ) if random.choice([True, False]): PersonDateOfBirth.objects.create( @@ -192,7 +255,7 @@ def _create_persons(self, fake: Faker, options, total, model): for _ in range(random.randint(0, 2)): person.occupations.create( - office=Office.objects.order_by("?").first(), + office=get_random_model_object(Office), **self.fake_date_value(fake), **self.fake_field_value(fake), ) @@ -206,8 +269,8 @@ def _create_letter_categories(self, fake: Faker, *args, **kwargs): @track_progress def _create_letters(self, fake: Faker, *args, **kwargs): - senders = Person.objects.order_by("?")[: random.randint(2, 5)] - addressees = Person.objects.order_by("?")[: random.randint(2, 5)] + senders = get_random_model_objects(Person, min_amount=2, max_amount=5) + addressees = get_random_model_objects(Person, min_amount=2, max_amount=5) subject = ", ".join(fake.words(nb=3, unique=True)) letter = Letter.objects.create( @@ -224,7 +287,7 @@ def _create_letters(self, fake: Faker, *args, **kwargs): if random.choice([True, False]): LetterCategory.objects.create( letter=letter, - category=Category.objects.order_by("?").first(), + category=get_random_model_object(Category), **self.fake_field_value(fake), ) @@ -243,10 +306,12 @@ 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(Letter.objects.order_by("?")[: random.randint(1, 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.epistolary_events.set( - EpistolaryEvent.objects.order_by("?")[: random.randint(0, 5)] + get_random_model_objects(EpistolaryEvent, min_amount=0, max_amount=5) ) LetterEventDate.objects.create( @@ -262,20 +327,76 @@ def _create_letter_actions(self, fake: Faker, *args, **kwargs): for i in range(no_of_categories): LetterActionCategory.objects.create( letter_action=action, - value=random_categories[i], + value=random_categories[i][0], **self.fake_field_value(fake), ) for _ in range(random.randint(1, 5)): Role.objects.create( - person=Person.objects.order_by("?").first(), + person=get_random_model_object(Person), letter_action=action, present=random.choice([True, False]), - role=random.choice(Role.RoleOptions.choices), + 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(Person, 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_sources(self, fake, options, total, model): unique_name = get_unique_name(source_names, Source) @@ -291,9 +412,12 @@ def _create_references(self, fake, options, total, model): .first() ) - random_object_id = ( - random_content_type.model_class().objects.order_by("?").first().id - ) + 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() @@ -302,7 +426,7 @@ def _create_references(self, fake, options, total, model): 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)], + terminology=fake.words(nb=3, unique=True), mention=random.choice(["direct", "implied"]), ) diff --git a/backend/core/management/commands/fixtures.py b/backend/core/management/commands/fixtures.py index 4c4d485a..1f1683c6 100644 --- a/backend/core/management/commands/fixtures.py +++ b/backend/core/management/commands/fixtures.py @@ -280,7 +280,7 @@ "Encouragement Letter", "Announcement Letter", "Sponsorship Letter", - "Query Letter (literary or publishing)", + "Query Letter", "Fundraising Letter", "Informal Letter", "Formal Letter", @@ -389,3 +389,106 @@ "Concordia Regum: Harmony Among the Kings", "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", +] \ No newline at end of file diff --git a/backend/letter/models.py b/backend/letter/models.py index 6fb00f34..b117b37e 100644 --- a/backend/letter/models.py +++ b/backend/letter/models.py @@ -9,6 +9,18 @@ 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)" ) @@ -19,18 +31,7 @@ class Gift(models.Model): ) material = models.CharField( - choices=[ - ("precious metal", "precious metal"), - ("textile", "textile"), - ("wood", "wood"), - ("glass", "glass"), - ("ceramic", "ceramic"), - ("animal product", "animal product"), - ("livestock", "livestock"), - ("paper", "paper"), - ("other", "other"), - ("unknown", "unknown"), - ], + choices=Material.choices, help_text="The material the gift consists of", )