From 912f20b6ca16b3c23e093b0a20fed526347de6d1 Mon Sep 17 00:00:00 2001 From: calumbell Date: Sun, 7 Jun 2026 14:47:26 +0100 Subject: [PATCH 1/6] added CrossReference test data --- .../srd-2024/CrossReference.json | 462 ++++++++++++++++++ 1 file changed, 462 insertions(+) diff --git a/data/v2/wizards-of-the-coast/srd-2024/CrossReference.json b/data/v2/wizards-of-the-coast/srd-2024/CrossReference.json index 60fcccf9..83ded003 100644 --- a/data/v2/wizards-of-the-coast/srd-2024/CrossReference.json +++ b/data/v2/wizards-of-the-coast/srd-2024/CrossReference.json @@ -34,5 +34,467 @@ }, "model": "api_v2.crossreference", "pk": "srd-2024_nick-mastery_light_srd-2024_light-wp" + }, + { + "fields": { + "anchor": "Fireball", + "document": "srd-2024", + "reference_object_key": "srd-2024_fireball", + "reference_content_type": [ + "api_v2", + "spell" + ], + "source_content_type": [ + "api_v2", + "magicitem" + ], + "source_object_key": "srd-2024_necklace-of-fireballs" + }, + "model": "api_v2.crossreference", + "pk": "srd-2024_necklace-of-fireballs_srd-2024_fireball" + }, + { + "fields": { + "anchor": "Fireball", + "document": "srd-2024", + "reference_object_key": "srd-2024_fireball", + "reference_content_type": ["api_v2", "spell"], + "source_content_type": ["api_v2", "creatureaction"], + "source_object_key": "srd-2024_lich_spellcasting" + }, + "model": "api_v2.crossreference", + "pk": "srd-2024_lich_srd-2024_fireball" + }, + { + "fields": { + "anchor": "Detect Magic", + "document": "srd-2024", + "reference_object_key": "srd-2024_detect-magic", + "reference_content_type": ["api_v2", "spell"], + "source_content_type": ["api_v2", "creatureaction"], + "source_object_key": "srd-2024_lich_spellcasting" + }, + "model": "api_v2.crossreference", + "pk": "srd-2024_lich_srd-2024_detect-magic" + }, + { + "fields": { + "anchor": "Detect Thoughts", + "document": "srd-2024", + "reference_object_key": "srd-2024_detect-thoughts", + "reference_content_type": ["api_v2", "spell"], + "source_content_type": ["api_v2", "creatureaction"], + "source_object_key": "srd-2024_lich_spellcasting" + }, + "model": "api_v2.crossreference", + "pk": "srd-2024_lich_srd-2024_detect-thoughts" + }, + { + "fields": { + "anchor": "Dispel Magic", + "document": "srd-2024", + "reference_object_key": "srd-2024_dispel-magic", + "reference_content_type": ["api_v2", "spell"], + "source_content_type": ["api_v2", "creatureaction"], + "source_object_key": "srd-2024_lich_spellcasting" + }, + "model": "api_v2.crossreference", + "pk": "srd-2024_lich_srd-2024_dispel-magic" + }, + { + "fields": { + "anchor": "Invisibility", + "document": "srd-2024", + "reference_object_key": "srd-2024_invisibility", + "reference_content_type": ["api_v2", "spell"], + "source_content_type": ["api_v2", "creatureaction"], + "source_object_key": "srd-2024_lich_spellcasting" + }, + "model": "api_v2.crossreference", + "pk": "srd-2024_lich_srd-2024_invisibility" + }, + { + "fields": { + "anchor": "Lightning Bolt", + "document": "srd-2024", + "reference_object_key": "srd-2024_lightning-bolt", + "reference_content_type": ["api_v2", "spell"], + "source_content_type": ["api_v2", "creatureaction"], + "source_object_key": "srd-2024_lich_spellcasting" + }, + "model": "api_v2.crossreference", + "pk": "srd-2024_lich_srd-2024_lightning-bolt" + }, + { + "fields": { + "anchor": "Mage Hand", + "document": "srd-2024", + "reference_object_key": "srd-2024_mage-hand", + "reference_content_type": ["api_v2", "spell"], + "source_content_type": ["api_v2", "creatureaction"], + "source_object_key": "srd-2024_lich_spellcasting" + }, + "model": "api_v2.crossreference", + "pk": "srd-2024_lich_srd-2024_mage-hand" + }, + { + "fields": { + "anchor": "Prestidigitation", + "document": "srd-2024", + "reference_object_key": "srd-2024_prestidigitation", + "reference_content_type": ["api_v2", "spell"], + "source_content_type": ["api_v2", "creatureaction"], + "source_object_key": "srd-2024_lich_spellcasting" + }, + "model": "api_v2.crossreference", + "pk": "srd-2024_lich_srd-2024_prestidigitation" + }, + { + "fields": { + "anchor": "Animate Dead", + "document": "srd-2024", + "reference_object_key": "srd-2024_animate-dead", + "reference_content_type": ["api_v2", "spell"], + "source_content_type": ["api_v2", "creatureaction"], + "source_object_key": "srd-2024_lich" + }, + "model": "api_v2.crossreference", + "pk": "srd-2024_lich_srd-2024_animate-dead_srd-2024_lich_spellcasting" + }, + { + "fields": { + "anchor": "Dimension Door", + "document": "srd-2024", + "reference_object_key": "srd-2024_dimension-door", + "reference_content_type": ["api_v2", "spell"], + "source_content_type": ["api_v2", "creatureaction"], + "source_object_key": "srd-2024_lich_spellcasting" + }, + "model": "api_v2.crossreference", + "pk": "srd-2024_lich_srd-2024_dimension-door" + }, + { + "fields": { + "anchor": "Plane Shift", + "document": "srd-2024", + "reference_object_key": "srd-2024_plane-shift", + "reference_content_type": ["api_v2", "spell"], + "source_content_type": ["api_v2", "creatureaction"], + "source_object_key": "srd-2024_lich_spellcasting" + }, + "model": "api_v2.crossreference", + "pk": "srd-2024_lich_srd-2024_plane-shift" + }, + { + "fields": { + "anchor": "Chain Lightning", + "document": "srd-2024", + "reference_object_key": "srd-2024_chain-lightning", + "reference_content_type": ["api_v2", "spell"], + "source_content_type": ["api_v2", "creatureaction"], + "source_object_key": "ssrd-2024_lich_spellcasting" + }, + "model": "api_v2.crossreference", + "pk": "srd-2024_lich_srd-2024_chain-lightning" + }, + { + "fields": { + "anchor": "Finger of Death", + "document": "srd-2024", + "reference_object_key": "srd-2024_finger-of-death", + "reference_content_type": ["api_v2", "spell"], + "source_content_type": ["api_v2", "creatureaction"], + "source_object_key": "srd-2024_lich_spellcasting" + }, + "model": "api_v2.crossreference", + "pk": "srd-2024_lich_srd-2024_finger-of-death" + }, + { + "fields": { + "anchor": "Power Word Kill", + "document": "srd-2024", + "reference_object_key": "srd-2024_power-word-kill", + "reference_content_type": ["api_v2", "spell"], + "source_content_type": ["api_v2", "creatureaction"], + "source_object_key": "srd-2024_lich_spellcasting" + }, + "model": "api_v2.crossreference", + "pk": "srd-2024_lich_srd-2024_power-word-kill" + }, + { + "fields": { + "anchor": "Scrying", + "document": "srd-2024", + "reference_object_key": "srd-2024_scrying", + "reference_content_type": ["api_v2", "spell"], + "source_content_type": ["api_v2", "creatureaction"], + "source_object_key": "srd-2024_lich_spellcasting" + }, + "model": "api_v2.crossreference", + "pk": "srd-2024_lich_srd-2024_scrying" + }, + { + "fields": { + "anchor": "Zombie", + "document": "srd-2024", + "reference_object_key": "srd-2024_zombie", + "reference_content_type": ["api_v2", "creature"], + "source_content_type": ["api_v2", "spell"], + "source_object_key": "srd-2024_finger-of-death" + }, + "model": "api_v2.crossreference", + "pk": "srd-2024_finger-of-death_srd-2024_zombie" + }, + { + "fields": { + "anchor": "Giant Goat", + "document": "srd-2024", + "reference_object_key": "srd-2024_giant-goat", + "reference_content_type": ["api_v2", "creature"], + "source_content_type": ["api_v2", "magicitem"], + "source_object_key": "srd-2024_figurine-of-wondrous-power-ivory-goats" + }, + "model": "api_v2.crossreference", + "pk": "srd-2024_figurine-of-wondrous-power-ivory-goats_srd-2024_giant-goat" + }, + { + "fields": { + "anchor": "Riding Horse", + "document": "srd-2024", + "reference_object_key": "srd-2024_riding-horse", + "reference_content_type": ["api_v2", "creature"], + "source_content_type": ["api_v2", "magicitem"], + "source_object_key": "srd-2024_figurine-of-wondrous-power-ivory-goats" + }, + "model": "api_v2.crossreference", + "pk": "srd-2024_figurine-of-wondrous-power-ivory-goats_srd-2024_riding-horse" + }, + { + "fields": { + "anchor": "Poison Spray", + "document": "srd-2024", + "reference_object_key": "srd-2024_poison-spray", + "reference_content_type": ["api_v2", "spell"], + "source_content_type": ["api_v2", "speciestrait"], + "source_object_key": "srd-2024_tiefling_fiendish-legacy" + }, + "model": "api_v2.crossreference", + "pk": "srd-2024_tiefling_fiendish-legacy_srd-2024_poison-spray" + }, + { + "fields": { + "anchor": "Chill Touch", + "document": "srd-2024", + "reference_object_key": "srd-2024_chill-touch", + "reference_content_type": ["api_v2", "spell"], + "source_content_type": ["api_v2", "speciestrait"], + "source_object_key": "srd-2024_tiefling_fiendish-legacy" + }, + "model": "api_v2.crossreference", + "pk": "srd-2024_tiefling_fiendish-legacy_srd-2024_chill-touch" + }, + { + "fields": { + "anchor": "Fire Bolt", + "document": "srd-2024", + "reference_object_key": "srd-2024_fire-bolt", + "reference_content_type": ["api_v2", "spell"], + "source_content_type": ["api_v2", "speciestrait"], + "source_object_key": "srd-2024_tiefling_fiendish-legacy" + }, + "model": "api_v2.crossreference", + "pk": "srd-2024_tiefling_fiendish-legacy_srd-2024_fire-bolt" + }, + { + "fields": { + "anchor": "Ray of Sickness", + "document": "srd-2024", + "reference_object_key": "srd-2024_ray-of-sickness", + "reference_content_type": ["api_v2", "spell"], + "source_content_type": ["api_v2", "speciestrait"], + "source_object_key": "srd-2024_tiefling_fiendish-legacy" + }, + "model": "api_v2.crossreference", + "pk": "srd-2024_tiefling_fiendish-legacy_srd-2024_ray-of-sickness" + }, + { + "fields": { + "anchor": "False Life", + "document": "srd-2024", + "reference_object_key": "srd-2024_false-life", + "reference_content_type": ["api_v2", "spell"], + "source_content_type": ["api_v2", "speciestrait"], + "source_object_key": "srd-2024_tiefling_fiendish-legacy" + }, + "model": "api_v2.crossreference", + "pk": "srd-2024_tiefling_fiendish-legacy_srd-2024_false-life" + }, + { + "fields": { + "anchor": "Hellish Rebuke", + "document": "srd-2024", + "reference_object_key": "srd-2024_hellish-rebuke", + "reference_content_type": ["api_v2", "spell"], + "source_content_type": ["api_v2", "speciestrait"], + "source_object_key": "srd-2024_tiefling_fiendish-legacy" + }, + "model": "api_v2.crossreference", + "pk": "srd-2024_tiefling_fiendish-legacy_srd-2024_hellish-rebuke" + }, + { + "fields": { + "anchor": "Hold Person", + "document": "srd-2024", + "reference_object_key": "srd-2024_hold-person", + "reference_content_type": ["api_v2", "spell"], + "source_content_type": ["api_v2", "speciestrait"], + "source_object_key": "srd-2024_tiefling_fiendish-legacy" + }, + "model": "api_v2.crossreference", + "pk": "srd-2024_tiefling_fiendish-legacy_srd-2024_hold-person" + }, + { + "fields": { + "anchor": "Ray of Enfeeblement", + "document": "srd-2024", + "reference_object_key": "srd-2024_ray-of-enfeeblement", + "reference_content_type": ["api_v2", "spell"], + "source_content_type": ["api_v2", "speciestrait"], + "source_object_key": "srd-2024_tiefling_fiendish-legacy" + }, + "model": "api_v2.crossreference", + "pk": "srd-2024_tiefling_fiendish-legacy_srd-2024_ray-of-enfeeblement" + }, + { + "fields": { + "anchor": "Darkness", + "document": "srd-2024", + "reference_object_key": "srd-2024_darkness", + "reference_content_type": ["api_v2", "spell"], + "source_content_type": ["api_v2", "speciestrait"], + "source_object_key": "srd-2024_tiefling_fiendish-legacy" + }, + "model": "api_v2.crossreference", + "pk": "srd-2024_tiefling_fiendish-legacy_srd-2024_darkness" + }, + { + "fields": { + "anchor": "Zombie", + "document": "srd-2024", + "reference_object_key": "srd-2024_zombie", + "reference_content_type": ["api_v2", "creature"], + "source_content_type": ["api_v2", "spell"], + "source_object_key": "srd-2024_animate-dead" + }, + "model": "api_v2.crossreference", + "pk": "srd-2024_animate-dead_srd-2024_zombie" + }, + { + "fields": { + "anchor": "Skeleton", + "document": "srd-2024", + "reference_object_key": "srd-2024_skeleton", + "reference_content_type": ["api_v2", "creature"], + "source_content_type": ["api_v2", "spell"], + "source_object_key": "srd-2024_animate-dead" + }, + "model": "api_v2.crossreference", + "pk": "srd-2024_animate-dead_srd-2024_skeleton" + }, + { + "fields": { + "anchor": "Ghoul", + "document": "srd-2024", + "reference_object_key": "srd-2024_ghoul", + "reference_content_type": ["api_v2", "creature"], + "source_content_type": ["api_v2", "spell"], + "source_object_key": "srd-2024_create-undead" + }, + "model": "api_v2.crossreference", + "pk": "srd-2024_create-undead_srd-2024_ghoul" + }, + { + "fields": { + "anchor": "Wights", + "document": "srd-2024", + "reference_object_key": "srd-2024_wight", + "reference_content_type": ["api_v2", "creature"], + "source_content_type": ["api_v2", "spell"], + "source_object_key": "srd-2024_create-undead" + }, + "model": "api_v2.crossreference", + "pk": "srd-2024_create-undead_srd-2024_wight" + }, + { + "fields": { + "anchor": "Mummies", + "document": "srd-2024", + "reference_object_key": "srd-2024_mummy", + "reference_content_type": ["api_v2", "creature"], + "source_content_type": ["api_v2", "spell"], + "source_object_key": "srd-2024_create-undead" + }, + "model": "api_v2.crossreference", + "pk": "srd-2024_create-undead_srd-2024_mummy" + }, + { + "fields": { + "anchor": "Boon of Combat Prowess", + "document": "srd-2024", + "reference_object_key": "srd-2024_boon-of-combat-prowess", + "reference_content_type": ["api_v2", "feat"], + "source_content_type": ["api_v2", "classfeature"], + "source_object_key": "srd-2024_fighter_epic-boon" + }, + "model": "api_v2.crossreference", + "pk": "srd-2024_fighter_epic-boon_srd-2024_boon-of-combat-prowess" + }, + { + "fields": { + "anchor": "Defense", + "document": "srd-2024", + "reference_object_key": "srd-2024_defense", + "reference_content_type": ["api_v2", "feat"], + "source_content_type": ["api_v2", "classfeature"], + "source_object_key": "srd-2024_fighter_fighting-style" + }, + "model": "api_v2.crossreference", + "pk": "srd-2024_fighter_fighting-style_srd-2024_defense" + }, + { + "fields": { + "anchor": "Magic Initiate", + "document": "srd-2024", + "reference_object_key": "srd-2024_magic-initiate", + "reference_content_type": ["api_v2", "feat"], + "source_content_type": ["api_v2", "backgroundbenefit"], + "source_object_key": "srd-2024_acolyte_feat" + }, + "model": "api_v2.crossreference", + "pk": "srd-2024_acolyte_feat_srd-2024_magic-initiate" + }, + { + "fields": { + "anchor": "Frightened", + "document": "srd-2024", + "reference_object_key": "srd-2024_frightened", + "reference_content_type": ["api_v2", "conditiondescription"], + "source_content_type": ["api_v2", "spell"], + "source_object_key": "srd-2024_antipathysympathy" + }, + "model": "api_v2.crossreference", + "pk": "srd-2024_antipathysympathy_srd-2024_frightened" + }, + { + "fields": { + "anchor": "Charmed", + "document": "srd-2024", + "reference_object_key": "srd-2024_charmed", + "reference_content_type": ["api_v2", "conditiondescription"], + "source_content_type": ["api_v2", "spell"], + "source_object_key": "srd-2024_antipathysympathy" + }, + "model": "api_v2.crossreference", + "pk": "srd-2024_antipathysympathy_srd-2024_charmed" } ] \ No newline at end of file From 294e40b989bb551a35a661f646a1b83b3e5c0af9 Mon Sep 17 00:00:00 2001 From: calumbell Date: Sun, 7 Jun 2026 14:47:57 +0100 Subject: [PATCH 2/6] updated nested serializers to return crossreferences --- api_v2/serializers/background.py | 14 ++++++++++++-- api_v2/serializers/characterclass.py | 6 ++++++ api_v2/serializers/creature.py | 6 ++++++ api_v2/serializers/species.py | 8 ++++++-- api_v2/serializers/spell.py | 2 +- 5 files changed, 31 insertions(+), 5 deletions(-) diff --git a/api_v2/serializers/background.py b/api_v2/serializers/background.py index 2905f781..3e97ccee 100644 --- a/api_v2/serializers/background.py +++ b/api_v2/serializers/background.py @@ -8,16 +8,26 @@ from .document import DocumentSummarySerializer class BackgroundBenefitSerializer(GameContentSerializer): + # crossreferences are serialized GameContentSerializer. This delegates to parent implementation + crossreferences = serializers.SerializerMethodField(method_name='get_crossreferences_data') + def get_crossreferences_data(self, obj): + return self.get_crossreferences(obj) + class Meta: model = models.BackgroundBenefit - fields = ['name','desc','type'] + fields = [ + 'name', + 'desc', + 'type', + 'crossreferences' + ] class BackgroundSerializer(GameContentSerializer): key = serializers.ReadOnlyField() benefits = BackgroundBenefitSerializer(many=True) document = DocumentSummarySerializer() - + class Meta: model = models.Background fields = '__all__' diff --git a/api_v2/serializers/characterclass.py b/api_v2/serializers/characterclass.py index 7bef40ab..b34e9de6 100644 --- a/api_v2/serializers/characterclass.py +++ b/api_v2/serializers/characterclass.py @@ -26,6 +26,11 @@ class Meta: class ClassFeatureSerializer(GameContentSerializer): key = serializers.ReadOnlyField() feature_items = ClassFeaturePrefetchSerializer(many=True, read_only=True) + + # crossreferences are serialized GameContentSerializer. This delegates to parent implementation + crossreferences = serializers.SerializerMethodField(method_name='get_crossreferences_data') + def get_crossreferences_data(self, obj): + return self.get_crossreferences(obj) def to_representation(self, instance): """ @@ -65,6 +70,7 @@ class Meta: 'key', 'name', 'desc', + 'crossreferences', 'feature_type', 'feature_items' ] diff --git a/api_v2/serializers/creature.py b/api_v2/serializers/creature.py index 7cae5f28..db3d972a 100644 --- a/api_v2/serializers/creature.py +++ b/api_v2/serializers/creature.py @@ -54,11 +54,17 @@ class CreatureActionSerializer(GameContentSerializer): attacks = CreatureActionAttackSerializer(many=True, read_only=True) usage_limits = serializers.SerializerMethodField() + # crossreferences are serialized GameContentSerializer. This delegates to parent implementation + crossreferences = serializers.SerializerMethodField(method_name='get_crossreferences_data') + def get_crossreferences_data(self, obj): + return self.get_crossreferences(obj) + class Meta: model = models.CreatureAction fields = [ 'name', 'desc', + 'crossreferences', 'attacks', 'action_type', 'order_in_statblock', diff --git a/api_v2/serializers/species.py b/api_v2/serializers/species.py index e2d25a71..62b4b04b 100644 --- a/api_v2/serializers/species.py +++ b/api_v2/serializers/species.py @@ -8,10 +8,14 @@ from .document import DocumentSummarySerializer class SpeciesTraitSerializer(GameContentSerializer): - + crossreferences = serializers.SerializerMethodField(method_name='get_crossreferences_data') + def get_crossreferences_data(self, obj): + # delegate to parent implementation in GameContentSerializer + return self.get_crossreferences(obj) + class Meta: model = models.SpeciesTrait - fields = ['name', 'desc', 'type', 'order'] + fields = ['name', 'desc', 'type', 'order', 'crossreferences'] class SpeciesSerializer(GameContentSerializer): diff --git a/api_v2/serializers/spell.py b/api_v2/serializers/spell.py index cd9fb197..b3485337 100644 --- a/api_v2/serializers/spell.py +++ b/api_v2/serializers/spell.py @@ -41,7 +41,7 @@ class SpellSerializer(GameContentSerializer): range_unit = serializers.SerializerMethodField() shape_size_unit = serializers.SerializerMethodField() - + # todo: model typed as any @extend_schema_field(OpenApiTypes.STR) def get_range_unit(self, spell): From 2b59462d2664fa1b2f45226dbf38ef727c045a9d Mon Sep 17 00:00:00 2001 From: calumbell Date: Sun, 7 Jun 2026 14:48:39 +0100 Subject: [PATCH 3/6] updated tests to reflect more serializers now returning crossreferenes --- ...ects.test_background_example.approved.json | 15 +++++ ...stObjects.test_class_example.approved.json | 57 +++++++++++++++++++ ...est_creature_ancient_example.approved.json | 27 +++++++++ ...test_creature_goblin_example.approved.json | 6 ++ ....test_creature_guard_example.approved.json | 3 + ...cts.test_creatureset_example.approved.json | 27 +++++++++ ...Objects.test_species_example.approved.json | 27 +++++++++ ...bjects.test_subclass_example.approved.json | 15 +++++ 8 files changed, 177 insertions(+) diff --git a/api_v2/tests/responses/TestObjects.test_background_example.approved.json b/api_v2/tests/responses/TestObjects.test_background_example.approved.json index 32b1a1f9..b4a4def4 100644 --- a/api_v2/tests/responses/TestObjects.test_background_example.approved.json +++ b/api_v2/tests/responses/TestObjects.test_background_example.approved.json @@ -1,26 +1,41 @@ { "benefits": [ { + "crossreferences": { + "to": [] + }, "desc": "A holy symbol (a gift to you when you entered the priesthood), a prayer book or prayer wheel, 5 sticks of incense, vestments, a set of common clothes, and a pouch containing 15 gp", "name": "Equipment", "type": "equipment" }, { + "crossreferences": { + "to": [] + }, "desc": "Two of your choice", "name": "Languages", "type": "language" }, { + "crossreferences": { + "to": [] + }, "desc": "As an acolyte, you command the respect of those who share your faith, and you can perform the religious ceremonies of your deity. You and your adventuring companions can expect to receive free healing and care at a temple, shrine, or other established presence of your faith, though you must provide any material components needed for spells. Those who share your religion will support you (but only you) at a modest lifestyle.\r\n\r\nYou might also have ties to a specific temple dedicated to your chosen deity or pantheon, and you have a residence there. This could be the temple where you used to serve, if you remain on good terms with it, or a temple where you have found a new home. While near your temple, you can call upon the priests for assistance, provided the assistance you ask for is not hazardous and you remain in good standing with your temple.", "name": "Shelter of the Faithful", "type": "feature" }, { + "crossreferences": { + "to": [] + }, "desc": "Insight, Religion", "name": "Skill Proficiencies", "type": "skill_proficiency" }, { + "crossreferences": { + "to": [] + }, "desc": "Acolytes are shaped by their experience in temples or other religious communities. Their study of the history and tenets of their faith and their relationships to temples, shrines, or hierarchies affect their mannerisms and ideals. Their flaws might be some hidden hypocrisy or heretical idea, or an ideal or bond taken to an extreme.\r\n\r\n| d8 | Personality Trait |\r\n|---|---|\r\n| 1 | I idolize a particular hero of my faith, and constantly refer to that person's deeds and example. |\r\n| 2 | I can find common ground between the fiercest enemies, empathizing with them and always working toward peace. |\r\n| 3 | I see omens in every event and action. The gods try to speak to us, we just need to listen |\r\n| 4 | Nothing can shake my optimistic attitude. |\r\n| 5 | I quote (or misquote) sacred texts and proverbs in almost every situation. |\r\n| 6 | I am tolerant (or intolerant) of other faiths and respect (or condemn) the worship of other gods. |\r\n| 7 | I've enjoyed fine food, drink, and high society among my temple's elite. Rough living grates on me. |\r\n| 8 | I've spent so long in the temple that I have little practical experience dealing with people in the outside world. |\r\n\r\n| d6 | Ideal |\r\n|---|---|\r\n| 1 | Tradition. The ancient traditions of worship and sacrifice must be preserved and upheld. (Lawful) |\r\n| 2 | Charity. I always try to help those in need, no matter what the personal cost. (Good) |\r\n| 3 | Change. We must help bring about the changes the gods are constantly working in the world. (Chaotic) |\r\n| 4 | Power. I hope to one day rise to the top of my faith's religious hierarchy. (Lawful) |\r\n| 5 | Faith. I trust that my deity will guide my actions. I have faith that if I work hard, things will go well. (Lawful) |\r\n| 6 | Aspiration. I seek to prove myself worthy of my god's favor by matching my actions against his or her teachings. (Any) |\r\n\r\n| d6 | Bond |\r\n|---|---|\r\n| 1 | I would die to recover an ancient relic of my faith that was lost long ago. |\r\n| 2 | I will someday get revenge on the corrupt temple hierarchy who branded me a heretic. |\r\n| 3 | I owe my life to the priest who took me in when my parents died. |\r\n| 4 | Everything I do is for the common people. |\r\n| 5 | I will do anything to protect the temple where I served. |\r\n| 6 | I seek to preserve a sacred text that my enemies consider heretical and seek to destroy. |\r\n\r\n| d6 | Flaw |\r\n|---|---|\r\n| 1 | I judge others harshly, and myself even more severely. |\r\n| 2 | I put too much trust in those who wield power within my temple's hierarchy. |\r\n| 3 | My piety sometimes leads me to blindly trust those that profess faith in my god. |\r\n| 4 | I am inflexible in my thinking. |\r\n| 5 | I am suspicious of strangers and expect the worst of them. |\r\n| 6 | Once I pick a goal, I become obsessed with it to the detriment of everything else in my life. |", "name": "Suggested Characteristics", "type": "suggested_characteristics" diff --git a/api_v2/tests/responses/TestObjects.test_class_example.approved.json b/api_v2/tests/responses/TestObjects.test_class_example.approved.json index af6a078b..7229a35a 100644 --- a/api_v2/tests/responses/TestObjects.test_class_example.approved.json +++ b/api_v2/tests/responses/TestObjects.test_class_example.approved.json @@ -21,6 +21,9 @@ }, "features": [ { + "crossreferences": { + "to": [] + }, "data_for_class_table": [], "desc": "When you reach 4th level, and again at 8th, 12th, 16th, and 19th level, you can increase one ability score of your choice by 2, or you can increase two ability scores of your choice by 1. As normal, you can\u2019t increase an ability score above 20 using this feature.", "feature_type": "CLASS_LEVEL_FEATURE", @@ -50,6 +53,9 @@ "name": "Ability Score Improvement" }, { + "crossreferences": { + "to": [] + }, "data_for_class_table": [], "desc": "Beginning at 9th level, you can roll one additional weapon damage die when determining the extra damage for a critical hit with a melee attack.\r\n\r\nThis increases to two additional dice at 13th level and three additional dice at 17th level.", "feature_type": "CLASS_LEVEL_FEATURE", @@ -71,6 +77,9 @@ "name": "Brutal Critical" }, { + "crossreferences": { + "to": [] + }, "data_for_class_table": [], "desc": "At 2nd level, you gain an uncanny sense of when things nearby aren\u2019t as they should be, giving you an edge when you dodge away from danger.\r\n\r\nYou have advantage on Dexterity saving throws against effects that you can see, such as traps and spells. To gain this benefit, you can\u2019t be blinded, deafened, or incapacitated.", "feature_type": "CLASS_LEVEL_FEATURE", @@ -84,6 +93,9 @@ "name": "Danger Sense" }, { + "crossreferences": { + "to": [] + }, "data_for_class_table": [], "desc": "You start with the following equipment, in addition to the equipment granted by your background:\r\n* (*a*) a greataxe or (*b*) any martial melee weapon\r\n* (*a*) two handaxes or (*b*) any simple weapon\r\n* An explorer\u2019s pack and four javelins", "feature_type": "STARTING_EQUIPMENT", @@ -92,6 +104,9 @@ "name": "Equipment" }, { + "crossreferences": { + "to": [] + }, "data_for_class_table": [], "desc": "Beginning at 5th level, you can attack twice, instead of once, whenever you take the Attack action on your turn.", "feature_type": "CLASS_LEVEL_FEATURE", @@ -105,6 +120,9 @@ "name": "Extra Attack" }, { + "crossreferences": { + "to": [] + }, "data_for_class_table": [], "desc": "Starting at 5th level, your speed increases by 10 feet while you aren\u2019t wearing heavy armor.", "feature_type": "CLASS_LEVEL_FEATURE", @@ -118,6 +136,9 @@ "name": "Fast Movement" }, { + "crossreferences": { + "to": [] + }, "data_for_class_table": [], "desc": "By 7th level, your instincts are so honed that you have advantage on initiative rolls.\r\n\r\nAdditionally, if you are surprised at the beginning of combat and aren\u2019t incapacitated, you can act normally on your first turn, but only if you enter your rage before doing anything else on that turn.", "feature_type": "CLASS_LEVEL_FEATURE", @@ -131,6 +152,9 @@ "name": "Feral Instinct" }, { + "crossreferences": { + "to": [] + }, "data_for_class_table": [], "desc": "Beginning at 18th level, if your total for a Strength check is less than your Strength score, you can use that score in place of the total.", "feature_type": "CLASS_LEVEL_FEATURE", @@ -144,6 +168,9 @@ "name": "Indomitable Might" }, { + "crossreferences": { + "to": [] + }, "data_for_class_table": [], "desc": "Beginning at 15th level, your rage is so fierce that it ends early only if you fall unconscious or if you choose to end it.", "feature_type": "CLASS_LEVEL_FEATURE", @@ -157,6 +184,9 @@ "name": "Persistent Rage" }, { + "crossreferences": { + "to": [] + }, "data_for_class_table": [], "desc": "At 20th level, you embody the power of the wilds. Your Strength and Constitution scores increase by 4. Your maximum for those scores is now 24.", "feature_type": "CLASS_LEVEL_FEATURE", @@ -170,6 +200,9 @@ "name": "Primal Champion" }, { + "crossreferences": { + "to": [] + }, "data_for_class_table": [], "desc": "At 3rd level, you choose a path that shapes the nature of your rage. Choose the Path of the Berserker or the Path of the Totem Warrior, both detailed at the end of the class description. Your choice grants you features at 3rd level and again at 6th, 10th, and 14th levels.", "feature_type": "CLASS_LEVEL_FEATURE", @@ -183,6 +216,9 @@ "name": "Primal Path" }, { + "crossreferences": { + "to": [] + }, "data_for_class_table": [], "desc": "**Armor:** Light armor, medium armor, shields\r\n**Weapons:** Simple weapons, martial weapons\r\n**Tools:** None\r\n**Saving Throws:** Strength, Constitution\r\n**Skills:** Choose two from Animal Handling, Athletics, Intimidation, Nature, Perception, and Survival", "feature_type": "PROFICIENCIES", @@ -191,6 +227,9 @@ "name": "Proficiencies" }, { + "crossreferences": { + "to": [] + }, "data_for_class_table": [ { "column_value": "+2", @@ -280,6 +319,9 @@ "name": "Proficiency Bonus" }, { + "crossreferences": { + "to": [] + }, "data_for_class_table": [], "desc": "In battle, you fight with primal ferocity. On your turn, you can enter a rage as a bonus action.\r\n\r\nWhile raging, you gain the following benefits if you aren't wearing heavy armor:\r\n\r\n* You have advantage on Strength checks and Strength saving throws.\r\n* When you make a melee weapon attack using Strength, you gain a bonus to the damage roll that increases as you gain levels as a barbarian, as shown in the Rage Damage column of the Barbarian table.\r\n* You have resistance to bludgeoning, piercing, and slashing damage. \r\n\r\nIf you are able to cast spells, you can't cast them or concentrate on them while raging.\r\n\r\nYour rage lasts for 1 minute. It ends early if you are knocked unconscious or if your turn ends and you haven't attacked a hostile creature since your last turn or taken damage since then. You can also end your rage on your turn as a bonus action.\r\n\r\nOnce you have raged the number of times shown for your barbarian level in the Rages column of the Barbarian table, you must finish a long rest before you can rage again.", "feature_type": "CLASS_LEVEL_FEATURE", @@ -293,6 +335,9 @@ "name": "Rage" }, { + "crossreferences": { + "to": [] + }, "data_for_class_table": [ { "column_value": "+2", @@ -382,6 +427,9 @@ "name": "Rage Damage" }, { + "crossreferences": { + "to": [] + }, "data_for_class_table": [ { "column_value": "2", @@ -471,6 +519,9 @@ "name": "Rages" }, { + "crossreferences": { + "to": [] + }, "data_for_class_table": [], "desc": "Starting at 2nd level, you can throw aside all concern for defense to attack with fierce desperation. When you make your first attack on your turn, you can decide to attack recklessly. Doing so gives you advantage on melee weapon attack rolls using Strength during this turn, but attack rolls against you have advantage until your next turn.", "feature_type": "CLASS_LEVEL_FEATURE", @@ -484,6 +535,9 @@ "name": "Reckless Attack" }, { + "crossreferences": { + "to": [] + }, "data_for_class_table": [], "desc": "Starting at 11th level, your rage can keep you fighting despite grievous wounds. If you drop to 0 hit points while you\u2019re raging and don\u2019t die outright, you can make a DC 10 Constitution saving throw. If you succeed, you drop to 1 hit point instead.\r\n\r\nEach time you use this feature after the first, the DC increases by 5. When you finish a short or long rest, the DC resets to 10.", "feature_type": "CLASS_LEVEL_FEATURE", @@ -497,6 +551,9 @@ "name": "Relentless Rage" }, { + "crossreferences": { + "to": [] + }, "data_for_class_table": [], "desc": "While you are not wearing any armor, your Armor Class equals 10 + your Dexterity modifier + your Constitution modifier. You can use a shield and still gain this benefit.", "feature_type": "CLASS_LEVEL_FEATURE", diff --git a/api_v2/tests/responses/TestObjects.test_creature_ancient_example.approved.json b/api_v2/tests/responses/TestObjects.test_creature_ancient_example.approved.json index 503c3f19..337cbdc0 100644 --- a/api_v2/tests/responses/TestObjects.test_creature_ancient_example.approved.json +++ b/api_v2/tests/responses/TestObjects.test_creature_ancient_example.approved.json @@ -36,6 +36,9 @@ "to_hit_mod": 17 } ], + "crossreferences": { + "to": [] + }, "desc": "Melee Weapon Attack: +17 to hit, reach 15 ft., one target. Hit: 21 (2d10 + 10) piercing damage plus 14 (4d6) fire damage.", "legendary_action_cost": 1, "limited_to_form": null, @@ -68,6 +71,9 @@ "to_hit_mod": 17 } ], + "crossreferences": { + "to": [] + }, "desc": "Melee Weapon Attack: +17 to hit, reach 10 ft., one target. Hit: 17 (2d6 + 10) slashing damage.", "legendary_action_cost": 1, "limited_to_form": null, @@ -78,6 +84,9 @@ { "action_type": "LEGENDARY_ACTION", "attacks": [], + "crossreferences": { + "to": [] + }, "desc": "The dragon makes a Wisdom (Perception) check.", "legendary_action_cost": 1, "limited_to_form": null, @@ -88,6 +97,9 @@ { "action_type": "ACTION", "attacks": [], + "crossreferences": { + "to": [] + }, "desc": "The dragon exhales fire in a 90-foot cone. Each creature in that area must make a DC 24 Dexterity saving throw, taking 91 (26d6) fire damage on a failed save, or half as much damage on a successful one.", "legendary_action_cost": null, "limited_to_form": null, @@ -101,6 +113,9 @@ { "action_type": "ACTION", "attacks": [], + "crossreferences": { + "to": [] + }, "desc": "Each creature of the dragon's choice that is within 120 feet of the dragon and aware of it must succeed on a DC 21 Wisdom saving throw or become frightened for 1 minute. A creature can repeat the saving throw at the end of each of its turns, ending the effect on itself on a success. If a creature's saving throw is successful or the effect ends for it, the creature is immune to the dragon's Frightful Presence for the next 24 hours.", "legendary_action_cost": 1, "limited_to_form": null, @@ -111,6 +126,9 @@ { "action_type": "ACTION", "attacks": [], + "crossreferences": { + "to": [] + }, "desc": "The dragon can use its Frightful Presence. It then makes three attacks: one with its bite and two with its claws.", "legendary_action_cost": 1, "limited_to_form": null, @@ -143,6 +161,9 @@ "to_hit_mod": 17 } ], + "crossreferences": { + "to": [] + }, "desc": "Melee Weapon Attack: +17 to hit, reach 20 ft., one target. Hit: 19 (2d8 + 10) bludgeoning damage.", "legendary_action_cost": 1, "limited_to_form": null, @@ -153,6 +174,9 @@ { "action_type": "LEGENDARY_ACTION", "attacks": [], + "crossreferences": { + "to": [] + }, "desc": "The dragon makes a tail attack.", "legendary_action_cost": 1, "limited_to_form": null, @@ -163,6 +187,9 @@ { "action_type": "LEGENDARY_ACTION", "attacks": [], + "crossreferences": { + "to": [] + }, "desc": "The dragon beats its wings. Each creature within 15 ft. of the dragon must succeed on a DC 25 Dexterity saving throw or take 17 (2d6 + 10) bludgeoning damage and be knocked prone. The dragon can then fly up to half its flying speed.", "legendary_action_cost": 2, "limited_to_form": null, diff --git a/api_v2/tests/responses/TestObjects.test_creature_goblin_example.approved.json b/api_v2/tests/responses/TestObjects.test_creature_goblin_example.approved.json index b662479f..17666dc5 100644 --- a/api_v2/tests/responses/TestObjects.test_creature_goblin_example.approved.json +++ b/api_v2/tests/responses/TestObjects.test_creature_goblin_example.approved.json @@ -33,6 +33,9 @@ "to_hit_mod": 4 } ], + "crossreferences": { + "to": [] + }, "desc": "Melee Weapon Attack: +4 to hit, reach 5 ft., one target. Hit: 5 (1d6 + 2) slashing damage.", "legendary_action_cost": 1, "limited_to_form": null, @@ -65,6 +68,9 @@ "to_hit_mod": 4 } ], + "crossreferences": { + "to": [] + }, "desc": "Ranged Weapon Attack: +4 to hit, range 80/320 ft., one target. Hit: 5 (1d6 + 2) piercing damage.", "legendary_action_cost": 1, "limited_to_form": null, diff --git a/api_v2/tests/responses/TestObjects.test_creature_guard_example.approved.json b/api_v2/tests/responses/TestObjects.test_creature_guard_example.approved.json index 4464cca7..0948aa68 100644 --- a/api_v2/tests/responses/TestObjects.test_creature_guard_example.approved.json +++ b/api_v2/tests/responses/TestObjects.test_creature_guard_example.approved.json @@ -54,6 +54,9 @@ "to_hit_mod": 3 } ], + "crossreferences": { + "to": [] + }, "desc": "Melee or Ranged Weapon Attack: +3 to hit, reach 5 ft. or range 20/60 ft., one target. Hit: 4 (1d6 + 1) piercing damage, or 5 (1d8 + 1) piercing damage if used with two hands to make a melee attack.", "legendary_action_cost": 1, "limited_to_form": null, diff --git a/api_v2/tests/responses/TestObjects.test_creatureset_example.approved.json b/api_v2/tests/responses/TestObjects.test_creatureset_example.approved.json index 2c2d77b8..2ae12fa1 100644 --- a/api_v2/tests/responses/TestObjects.test_creatureset_example.approved.json +++ b/api_v2/tests/responses/TestObjects.test_creatureset_example.approved.json @@ -35,6 +35,9 @@ "to_hit_mod": 5 } ], + "crossreferences": { + "to": [] + }, "desc": "Melee Weapon Attack: +5 to hit, reach 5 ft., one target. Hit: 2 (1d4) bludgeoning damage.", "legendary_action_cost": 1, "limited_to_form": null, @@ -379,6 +382,9 @@ "to_hit_mod": 6 } ], + "crossreferences": { + "to": [] + }, "desc": "Melee Weapon Attack: +6 to hit, reach 5 ft., one target. Hit: 9 (2d4 + 4) bludgeoning damage.", "legendary_action_cost": 1, "limited_to_form": null, @@ -568,6 +574,9 @@ "to_hit_mod": 8 } ], + "crossreferences": { + "to": [] + }, "desc": "Melee Weapon Attack: +8 to hit, reach 5 ft., one target. Hit: 19 (3d8 + 6) piercing damage.", "legendary_action_cost": 1, "limited_to_form": null, @@ -600,6 +609,9 @@ "to_hit_mod": 8 } ], + "crossreferences": { + "to": [] + }, "desc": "Melee Weapon Attack: +8 to hit, reach 5 ft., one prone creature. Hit: 22 (3d10 + 6) bludgeoning damage.", "legendary_action_cost": 1, "limited_to_form": null, @@ -802,6 +814,9 @@ "to_hit_mod": 3 } ], + "crossreferences": { + "to": [] + }, "desc": "Melee Weapon Attack: +3 to hit, reach 5 ft., one target. Hit: 4 (1d6 + 1) piercing damage. If the target is a creature, it must succeed on a DC 11 Strength saving throw or be knocked prone.", "legendary_action_cost": 1, "limited_to_form": null, @@ -980,6 +995,9 @@ "to_hit_mod": 2 } ], + "crossreferences": { + "to": [] + }, "desc": "Melee Weapon Attack: +2 to hit, reach 5 ft., one target. Hit: 4 (1d4 + 2) bludgeoning damage.", "legendary_action_cost": 1, "limited_to_form": null, @@ -1186,6 +1204,9 @@ "to_hit_mod": 4 } ], + "crossreferences": { + "to": [] + }, "desc": "Melee Weapon Attack: +4 to hit, reach 5 ft., one target. Hit: 7 (2d4 + 2) bludgeoning damage.", "legendary_action_cost": 1, "limited_to_form": null, @@ -1379,6 +1400,9 @@ "to_hit_mod": 5 } ], + "crossreferences": { + "to": [] + }, "desc": "Melee Weapon Attack: +5 to hit, reach 5 ft., one target. Hit: 8 (2d4 + 3) bludgeoning damage.", "legendary_action_cost": 1, "limited_to_form": null, @@ -1572,6 +1596,9 @@ "to_hit_mod": 6 } ], + "crossreferences": { + "to": [] + }, "desc": "Melee Weapon Attack: +6 to hit, reach 5 ft., one target. Hit: 11 (2d6 + 4) bludgeoning damage.", "legendary_action_cost": 1, "limited_to_form": null, diff --git a/api_v2/tests/responses/TestObjects.test_species_example.approved.json b/api_v2/tests/responses/TestObjects.test_species_example.approved.json index efb1ff6b..ae335c22 100644 --- a/api_v2/tests/responses/TestObjects.test_species_example.approved.json +++ b/api_v2/tests/responses/TestObjects.test_species_example.approved.json @@ -24,54 +24,81 @@ "subspecies_of": null, "traits": [ { + "crossreferences": { + "to": [] + }, "desc": "Your Dexterity score increases by 2.", "name": "Ability Score Increase", "order": null, "type": null }, { + "crossreferences": { + "to": [] + }, "desc": "Your base walking speed is 25 feet.", "name": "Speed", "order": null, "type": null }, { + "crossreferences": { + "to": [] + }, "desc": "A halfling reaches adulthood at the age of 20 and generally lives into the middle of his or her second century.", "name": "Age", "order": null, "type": null }, { + "crossreferences": { + "to": [] + }, "desc": "Most halflings are lawful good. As a rule, they are good-hearted and kind, hate to see others in pain, and have no tolerance for oppression. They are also very orderly and traditional, leaning heavily on the support of their community and the comfort of their old ways.", "name": "Alignment", "order": null, "type": null }, { + "crossreferences": { + "to": [] + }, "desc": "Halflings average about 3 feet tall and weigh about 40 pounds. Your size is Small.", "name": "Size", "order": null, "type": null }, { + "crossreferences": { + "to": [] + }, "desc": "You can speak, read, and write Common and Halfling. The Halfling language isn't secret, but halflings are loath to share it with others. They write very little, so they don't have a rich body of literature. Their oral tradition, however, is very strong. Almost all halflings speak Common to converse with the people in whose lands they dwell or through which they are traveling.", "name": "Languages", "order": null, "type": null }, { + "crossreferences": { + "to": [] + }, "desc": "When you roll a 1 on the d20 for an attack roll, ability check, or saving throw, you can reroll the die and must use the new roll.", "name": "Lucky", "order": null, "type": null }, { + "crossreferences": { + "to": [] + }, "desc": "You have advantage on saving throws against being frightened.", "name": "Brave", "order": null, "type": null }, { + "crossreferences": { + "to": [] + }, "desc": "You can move through the space of any creature that is of a size larger than yours.", "name": "Halfling Nimbleness", "order": null, diff --git a/api_v2/tests/responses/TestObjects.test_subclass_example.approved.json b/api_v2/tests/responses/TestObjects.test_subclass_example.approved.json index e06c0b60..49cff17c 100644 --- a/api_v2/tests/responses/TestObjects.test_subclass_example.approved.json +++ b/api_v2/tests/responses/TestObjects.test_subclass_example.approved.json @@ -21,6 +21,9 @@ }, "features": [ { + "crossreferences": { + "to": [] + }, "data_for_class_table": [], "desc": "Starting at 3rd level, you can use the bonus action granted by your Cunning Action to make a Dexterity (Sleight of Hand) check, use your thieves' tools to disarm a trap or open a lock, or take the Use an Object action.", "feature_type": "CLASS_LEVEL_FEATURE", @@ -34,6 +37,9 @@ "name": "Fast Hands" }, { + "crossreferences": { + "to": [] + }, "data_for_class_table": [], "desc": "When you choose this archetype at 3rd level, you gain the ability to climb faster than normal; climbing no longer costs you extra movement.\r\n\r\nIn addition, when you make a running jump, the distance you cover increases by a number of feet equal to your Dexterity modifier.", "feature_type": "CLASS_LEVEL_FEATURE", @@ -47,6 +53,9 @@ "name": "Second-Story Work" }, { + "crossreferences": { + "to": [] + }, "data_for_class_table": [], "desc": "Starting at 9th level, you have advantage on a Dexterity (Stealth) check if you move no more than half your speed on the same turn.", "feature_type": "CLASS_LEVEL_FEATURE", @@ -60,6 +69,9 @@ "name": "Supreme Sneak" }, { + "crossreferences": { + "to": [] + }, "data_for_class_table": [], "desc": "When you reach 17th level, you have become adept at laying ambushes and quickly escaping danger. You can take two turns during the first round of any combat. You take your first turn at your normal initiative and your second turn at your initiative minus 10. You can't use this feature when you are surprised.", "feature_type": "CLASS_LEVEL_FEATURE", @@ -73,6 +85,9 @@ "name": "Thief's Reflexes" }, { + "crossreferences": { + "to": [] + }, "data_for_class_table": [], "desc": "By 13th level, you have learned enough about the workings of magic that you can improvise the use of items even when they are not intended for you. You ignore all class, race, and level requirements on the use of magic items.", "feature_type": "CLASS_LEVEL_FEATURE", From 585b507ab60652de2b908b9eaa83d89d2eaa34e5 Mon Sep 17 00:00:00 2001 From: calumbell Date: Sun, 7 Jun 2026 17:21:17 +0100 Subject: [PATCH 4/6] updated view prefetching to fix n+1 issues on CrossReferences --- api_v2/views/background.py | 1 + api_v2/views/characterclass.py | 1 + api_v2/views/feat.py | 7 ++++++- api_v2/views/species.py | 1 + 4 files changed, 9 insertions(+), 1 deletion(-) diff --git a/api_v2/views/background.py b/api_v2/views/background.py index d7ea380e..bfa57339 100644 --- a/api_v2/views/background.py +++ b/api_v2/views/background.py @@ -29,6 +29,7 @@ class BackgroundViewSet(EagerLoadingMixin, ExcludeFieldsMixin, viewsets.ReadOnly prefetch_related_fields = [ 'crossreferences__reference_content_type', 'benefits', + 'benefits__crossreferences', 'document', 'document__publisher', 'document__gamesystem' diff --git a/api_v2/views/characterclass.py b/api_v2/views/characterclass.py index 59f2d2a4..29a03852 100644 --- a/api_v2/views/characterclass.py +++ b/api_v2/views/characterclass.py @@ -34,6 +34,7 @@ class CharacterClassViewSet(ExcludeFieldsMixin, EagerLoadingMixin, viewsets.Read 'crossreferences__reference_content_type', 'document', 'features', + 'features__crossreferences', 'features__feature_items', 'primary_abilities', 'saving_throws', diff --git a/api_v2/views/feat.py b/api_v2/views/feat.py index 531e8e89..22c20a22 100644 --- a/api_v2/views/feat.py +++ b/api_v2/views/feat.py @@ -25,5 +25,10 @@ class FeatViewSet(EagerLoadingMixin, ExcludeFieldsMixin, viewsets.ReadOnlyModelV filterset_class = FeatFilterSet select_related_fields = [] - prefetch_related_fields = ['crossreferences__reference_content_type', 'benefits', 'document'] + prefetch_related_fields = [ + 'crossreferences__reference_content_type', + 'benefits', + 'benefits__crossreferences', + 'document' + ] diff --git a/api_v2/views/species.py b/api_v2/views/species.py index 8970ccbb..e0a1657e 100644 --- a/api_v2/views/species.py +++ b/api_v2/views/species.py @@ -33,5 +33,6 @@ class SpeciesViewSet(EagerLoadingMixin, ExcludeFieldsMixin, viewsets.ReadOnlyMod 'document', 'document__gamesystem', 'traits', + 'traits__crossreferences', 'subspecies_of' ] \ No newline at end of file From faeaca0ff6303b17dc7073f9c58baf409438484b Mon Sep 17 00:00:00 2001 From: calumbell Date: Mon, 8 Jun 2026 12:32:55 +0100 Subject: [PATCH 5/6] added missing srd-2024 lich crossreferences --- .../srd-2024/CrossReference.json | 30 +++++++++++++++++-- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/data/v2/wizards-of-the-coast/srd-2024/CrossReference.json b/data/v2/wizards-of-the-coast/srd-2024/CrossReference.json index 83ded003..ec22844c 100644 --- a/data/v2/wizards-of-the-coast/srd-2024/CrossReference.json +++ b/data/v2/wizards-of-the-coast/srd-2024/CrossReference.json @@ -156,10 +156,10 @@ "reference_object_key": "srd-2024_animate-dead", "reference_content_type": ["api_v2", "spell"], "source_content_type": ["api_v2", "creatureaction"], - "source_object_key": "srd-2024_lich" + "source_object_key": "srd-2024_lich_spellcasting" }, "model": "api_v2.crossreference", - "pk": "srd-2024_lich_srd-2024_animate-dead_srd-2024_lich_spellcasting" + "pk": "srd-2024_lich_spellcasting_srd-2024_animate-dead" }, { "fields": { @@ -192,7 +192,7 @@ "reference_object_key": "srd-2024_chain-lightning", "reference_content_type": ["api_v2", "spell"], "source_content_type": ["api_v2", "creatureaction"], - "source_object_key": "ssrd-2024_lich_spellcasting" + "source_object_key": "srd-2024_lich_spellcasting" }, "model": "api_v2.crossreference", "pk": "srd-2024_lich_srd-2024_chain-lightning" @@ -233,6 +233,30 @@ "model": "api_v2.crossreference", "pk": "srd-2024_lich_srd-2024_scrying" }, + { + "fields": { + "anchor": "Counterspell", + "document": "srd-2024", + "reference_object_key": "srd-2024_counterspell", + "reference_content_type": ["api_v2", "spell"], + "source_content_type": ["api_v2", "creatureaction"], + "source_object_key": "srd-2024_lich_protective-magic" + }, + "model": "api_v2.crossreference", + "pk": "srd-2024_lich_protective-magic_srd-2024_counterspell" + }, + { + "fields": { + "anchor": "Shield", + "document": "srd-2024", + "reference_object_key": "srd-2024_shield", + "reference_content_type": ["api_v2", "spell"], + "source_content_type": ["api_v2", "creatureaction"], + "source_object_key": "srd-2024_lich_protective-magic" + }, + "model": "api_v2.crossreference", + "pk": "srd-2024_lich_protective-magic_srd-2024_shield" + }, { "fields": { "anchor": "Zombie", From 2bb5fe54a120e0cf1a4f6571ed75a097d463f7ee Mon Sep 17 00:00:00 2001 From: calumbell Date: Sun, 14 Jun 2026 09:39:47 +0100 Subject: [PATCH 6/6] improved comments --- api_v2/serializers/background.py | 2 +- api_v2/serializers/characterclass.py | 2 +- api_v2/serializers/creature.py | 2 +- api_v2/serializers/species.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/api_v2/serializers/background.py b/api_v2/serializers/background.py index 3e97ccee..288d99fd 100644 --- a/api_v2/serializers/background.py +++ b/api_v2/serializers/background.py @@ -8,7 +8,7 @@ from .document import DocumentSummarySerializer class BackgroundBenefitSerializer(GameContentSerializer): - # crossreferences are serialized GameContentSerializer. This delegates to parent implementation + # crossreferences are serialized in GameContentSerializer. This delegates to parent implementation crossreferences = serializers.SerializerMethodField(method_name='get_crossreferences_data') def get_crossreferences_data(self, obj): return self.get_crossreferences(obj) diff --git a/api_v2/serializers/characterclass.py b/api_v2/serializers/characterclass.py index b34e9de6..7ca37763 100644 --- a/api_v2/serializers/characterclass.py +++ b/api_v2/serializers/characterclass.py @@ -27,7 +27,7 @@ class ClassFeatureSerializer(GameContentSerializer): key = serializers.ReadOnlyField() feature_items = ClassFeaturePrefetchSerializer(many=True, read_only=True) - # crossreferences are serialized GameContentSerializer. This delegates to parent implementation + # crossreferences are serialized on GameContentSerializer. This delegates to parent implementation crossreferences = serializers.SerializerMethodField(method_name='get_crossreferences_data') def get_crossreferences_data(self, obj): return self.get_crossreferences(obj) diff --git a/api_v2/serializers/creature.py b/api_v2/serializers/creature.py index db3d972a..5789bca7 100644 --- a/api_v2/serializers/creature.py +++ b/api_v2/serializers/creature.py @@ -54,7 +54,7 @@ class CreatureActionSerializer(GameContentSerializer): attacks = CreatureActionAttackSerializer(many=True, read_only=True) usage_limits = serializers.SerializerMethodField() - # crossreferences are serialized GameContentSerializer. This delegates to parent implementation + # crossreferences are serialized on GameContentSerializer. This delegates to parent implementation crossreferences = serializers.SerializerMethodField(method_name='get_crossreferences_data') def get_crossreferences_data(self, obj): return self.get_crossreferences(obj) diff --git a/api_v2/serializers/species.py b/api_v2/serializers/species.py index 62b4b04b..5a6eeb71 100644 --- a/api_v2/serializers/species.py +++ b/api_v2/serializers/species.py @@ -8,9 +8,9 @@ from .document import DocumentSummarySerializer class SpeciesTraitSerializer(GameContentSerializer): + # crossreferences are serialized on GameContentSerializer. This delegates to parent implementation crossreferences = serializers.SerializerMethodField(method_name='get_crossreferences_data') def get_crossreferences_data(self, obj): - # delegate to parent implementation in GameContentSerializer return self.get_crossreferences(obj) class Meta: