Skip to content

Commit d1d27ec

Browse files
committed
merged from staging
2 parents 3f45b1d + 4a7dcd6 commit d1d27ec

File tree

277 files changed

+814444
-796373
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

277 files changed

+814444
-796373
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,4 @@ api/tests/approved_files/*.recieved.*
3232
# Generated files
3333
openapi-schema.yml
3434

35+
server/vector_index.pkl

api_v2/admin.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,13 @@ class LanguageAdmin(admin.ModelAdmin):
100100

101101
admin.site.register(Condition)
102102

103+
class ConditionConceptAdmin(admin.ModelAdmin):
104+
list_display = ['key', 'name']
105+
filter_horizontal = ['conditions']
106+
search_fields = ['key', 'name', 'desc']
107+
108+
admin.site.register(ConditionConcept, ConditionConceptAdmin)
109+
103110
admin.site.register(ClassFeatureItem)
104111
admin.site.register(ClassFeature, admin_class=ClassFeatureAdmin)
105112
admin.site.register(CharacterClass)

api_v2/management/commands/export.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -124,10 +124,11 @@ def handle(self, *args, **options) -> None:
124124

125125
for model in app_models:
126126
SKIPPED_MODEL_NAMES = ['Document', 'GameSystem', 'License', 'Publisher','SearchResult']
127+
CONCEPT_MODEL_NAMES = ['ConditionConcept'] # These are synthetic/concept models not tied to documents
127128
CHILD_MODEL_NAMES = ['SpeciesTrait', 'FeatBenefit', 'BackgroundBenefit', 'ClassFeatureItem', 'SpellCastingOption','CreatureAction', 'CreatureTrait']
128129
CHILD_CHILD_MODEL_NAMES = ['CreatureActionAttack']
129130

130-
if model._meta.app_label == 'api_v2' and model.__name__ not in SKIPPED_MODEL_NAMES:
131+
if model._meta.app_label == 'api_v2' and model.__name__ not in SKIPPED_MODEL_NAMES and model.__name__ not in CONCEPT_MODEL_NAMES:
131132
modelq=None
132133
if model.__name__ in CHILD_CHILD_MODEL_NAMES:
133134
modelq = model.objects.filter(parent__parent__document=doc).order_by('pk')
@@ -147,6 +148,23 @@ def handle(self, *args, **options) -> None:
147148
self.stdout.write(self.style.SUCCESS(
148149
'Wrote {} to {}'.format(doc.key, doc_path)))
149150

151+
# Export concept models (synthetic objects that aggregate across systems)
152+
concept_models = ['ConditionConcept'] # Add other concept models here as they're created
153+
for concept_model_name in concept_models:
154+
try:
155+
model = apps.get_model('api_v2', concept_model_name)
156+
concept_queryset = model.objects.all().order_by('pk')
157+
concept_path = get_filepath_by_model(
158+
concept_model_name,
159+
'api_v2',
160+
base_path=options['dir'],
161+
format=options['format'])
162+
write_queryset_data(concept_path, concept_queryset, format=options['format'])
163+
self.stdout.write(self.style.SUCCESS(f'Exported {concept_model_name} concept objects'))
164+
except LookupError:
165+
# Model doesn't exist yet, skip it
166+
self.stdout.write(self.style.WARNING(f'Concept model {concept_model_name} not found, skipping'))
167+
150168
self.stdout.write(self.style.SUCCESS('Data for v2 data complete.'))
151169

152170

@@ -158,7 +176,7 @@ def get_filepath_by_model(model_name, app_label, pub_key=None, doc_key=None, bas
158176

159177
if app_label == "api_v2":
160178
root_folder_name = 'v2'
161-
root_models = ['License', 'GameSystem']
179+
root_models = ['License', 'GameSystem', 'ConditionConcept'] # Concept models are exported at root level
162180
pub_models = ['Publisher']
163181

164182
if model_name in root_models:
@@ -192,7 +210,7 @@ def write_queryset_data(filepath, queryset, format='json'):
192210

193211
with open(output_filepath, 'w', encoding='utf-8') as f:
194212
if format=='json':
195-
serializers.serialize("json", queryset, indent=2, stream=f)
213+
serializers.serialize("json", queryset, indent=2, stream=f, sort_keys=True)
196214
if format=='csv':
197215
# Create headers:
198216
fieldnames = []

api_v2/management/commands/import.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ def add_arguments(self, parser):
1717
"--dir",
1818
type=str,
1919
help="Directory to write files to.")
20+
parser.add_argument("--skip-concepts",
21+
action="store_true",
22+
help="Skip automatic concept population after import.")
2023
parser.add_argument("--debug",
2124
action="store_true",
2225
help="Load files one by one to identify problematic data.")
@@ -139,6 +142,15 @@ def _load_files_individually(self, fixture_filepaths):
139142
))
140143
raise
141144

145+
call_command('loaddata', fixture_filepaths)
146+
147+
# After loading data, populate concept objects to aggregate equivalent content across systems
148+
if not options.get('skip_concepts', False):
149+
self.stdout.write(self.style.SUCCESS('Data import complete. Now populating concept aggregations...'))
150+
call_command('populate_concepts')
151+
else:
152+
self.stdout.write('Skipping concept population (--skip-concepts flag provided)')
153+
142154
def _analyze_problematic_file(self, filepath):
143155
"""Analyze the problematic file to identify specific objects causing issues."""
144156
try:
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
"""
2+
Management command to populate concept objects by grouping equivalent content across game systems.
3+
4+
This command analyzes existing content objects and groups them by name to create
5+
concept objects that represent the same conceptual item across different game systems.
6+
7+
Currently supports:
8+
- ConditionConcept (for Condition objects)
9+
10+
This can be extended to support other content types like SpellConcept, ItemConcept, etc.
11+
"""
12+
13+
from django.core.management.base import BaseCommand
14+
from django.db import transaction
15+
from django.utils.text import slugify
16+
from collections import defaultdict
17+
18+
from api_v2.models import Condition, ConditionConcept
19+
20+
21+
class Command(BaseCommand):
22+
help = 'Populate concept objects by grouping equivalent content across game systems'
23+
24+
def handle(self, *args, **options):
25+
self.populate_condition_concepts()
26+
27+
# Future content types can be added here:
28+
# self.populate_damage_type_concepts()
29+
# self.populate_environment_concepts()
30+
31+
def populate_condition_concepts(self):
32+
"""Populate ConditionConcept objects by grouping equivalent conditions."""
33+
self.stdout.write(self.style.HTTP_INFO('Processing conditions...'))
34+
35+
# Always clear existing concepts and reconstruct
36+
self.stdout.write('Clearing existing ConditionConcept objects...')
37+
ConditionConcept.objects.all().delete()
38+
39+
# Group conditions by exact name (no normalization needed)
40+
condition_groups = defaultdict(list)
41+
42+
for condition in Condition.objects.all():
43+
condition_groups[condition.name].append(condition)
44+
45+
self.stdout.write(f'Found {len(condition_groups)} condition concepts to create:')
46+
47+
created_count = 0
48+
49+
with transaction.atomic():
50+
for concept_name, conditions in condition_groups.items():
51+
concept_key = slugify(concept_name)
52+
53+
# Create description based on number of systems
54+
systems = list(set([c.document.gamesystem.name for c in conditions]))
55+
if len(systems) == 1:
56+
concept_desc = f"The {concept_name.lower()} condition from {systems[0]}."
57+
else:
58+
concept_desc = f"The {concept_name.lower()} condition as it appears across different game systems: {', '.join(systems)}."
59+
60+
self.stdout.write(f' - {concept_name} ({concept_key}): {len(conditions)} conditions across {len(systems)} systems')
61+
62+
# Create the ConditionConcept
63+
concept = ConditionConcept.objects.create(
64+
key=concept_key,
65+
name=concept_name,
66+
desc=concept_desc
67+
)
68+
69+
# Add all conditions to this concept
70+
concept.conditions.set(conditions)
71+
created_count += 1
72+
73+
self.stdout.write(self.style.SUCCESS(f'Successfully created {created_count} ConditionConcept objects'))
74+
75+
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Generated by Django 5.2.1 on 2025-06-07 17:20
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('api_v2', '0041_merge_20250509_0736'),
10+
]
11+
12+
operations = [
13+
migrations.CreateModel(
14+
name='ConditionConcept',
15+
fields=[
16+
('name', models.CharField(help_text='Name of the item.', max_length=100)),
17+
('desc', models.TextField(help_text='Description of the game content item. Markdown.')),
18+
('key', models.CharField(help_text="Unique key for the condition concept (e.g., 'invisible').", max_length=100, primary_key=True, serialize=False)),
19+
('conditions', models.ManyToManyField(help_text='All condition implementations that are equivalent to this concept.', related_name='concepts', to='api_v2.condition')),
20+
],
21+
options={
22+
'verbose_name': 'condition concept',
23+
'verbose_name_plural': 'condition concepts',
24+
'ordering': ['name'],
25+
},
26+
),
27+
]
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Generated by Django 5.2.1 on 2025-06-08 02:35
2+
3+
from django.db import migrations
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('api_v2', '0042_add_condition_concept'),
10+
('api_v2', '0049_add_document_display_name'),
11+
]
12+
13+
operations = [
14+
]
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Generated by Django 5.2.1 on 2025-06-15 20:56
2+
3+
from django.db import migrations
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('api_v2', '0050_merge_20250608_0235'),
10+
('api_v2', '0050_spellcastingoption_desc'),
11+
]
12+
13+
operations = [
14+
]

api_v2/models/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
from .alignment import Alignment
4343

4444
from .condition import Condition
45+
from .condition import ConditionConcept
4546

4647
from .spell import Spell
4748
from .spell import SpellCastingOption

api_v2/models/condition.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""The model for a condition."""
22
from django.db import models
3+
from django.db.models import Q
34

45
from .abstracts import HasName, HasDescription
56
from .document import FromDocument
@@ -14,3 +15,52 @@ class Meta:
1415
"""To assist with the UI layer."""
1516

1617
verbose_name_plural = "conditions"
18+
19+
20+
class ConditionConcept(HasName, HasDescription):
21+
"""
22+
This model represents a synthetic condition concept that aggregates
23+
equivalent conditions across different game systems.
24+
25+
For example, "Invisible" may exist in multiple game systems (SRD 2014, A5e, etc.)
26+
but conceptually they represent the same condition. This model provides a
27+
unified view of that concept with links to all its implementations.
28+
"""
29+
30+
key = models.CharField(
31+
primary_key=True,
32+
max_length=100,
33+
help_text="Unique key for the condition concept (e.g., 'invisible')."
34+
)
35+
36+
conditions = models.ManyToManyField(
37+
Condition,
38+
related_name='concepts',
39+
help_text="All condition implementations that are equivalent to this concept."
40+
)
41+
42+
@property
43+
def gamesystems(self):
44+
"""Returns a list of all game systems that have this condition concept."""
45+
return list(set([condition.document.gamesystem for condition in self.conditions.all()]))
46+
47+
@property
48+
def documents(self):
49+
"""Returns a list of all documents that have this condition concept."""
50+
return list(set([condition.document for condition in self.conditions.all()]))
51+
52+
def get_condition_for_gamesystem(self, gamesystem_key):
53+
"""
54+
Returns the condition for a specific game system.
55+
If multiple conditions exist for the same game system, returns the first one.
56+
"""
57+
return self.conditions.filter(document__gamesystem__key=gamesystem_key).first()
58+
59+
def get_condition_for_document(self, document_key):
60+
"""Returns the condition for a specific document."""
61+
return self.conditions.filter(document__key=document_key).first()
62+
63+
class Meta:
64+
verbose_name = "condition concept"
65+
verbose_name_plural = "condition concepts"
66+
ordering = ['name']

0 commit comments

Comments
 (0)