Skip to content

Commit 1b900da

Browse files
authored
Moody/doc display names (open5e#757)
* add display name field to documents for common names * add display name to endpoint with fallback * update tests with new field
1 parent c260072 commit 1b900da

File tree

33 files changed

+314
-53
lines changed

33 files changed

+314
-53
lines changed

.cursor

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
# Open5e API Project Documentation for AI Assistants
2+
3+
## Project Overview
4+
This is the Open5e API - a Django REST API providing D&D 5th Edition game content data.
5+
- **Main API versions**: v1 (`api/`) and v2 (`api_v2/`)
6+
- **Primary focus**: v2 API is the current development target
7+
- **Database**: SQLite for development, supports PostgreSQL for production
8+
- **Framework**: Django REST Framework with custom serializers
9+
10+
## Project Structure
11+
12+
### Key Directories
13+
- `api/` - Legacy v1 API (mostly maintenance mode)
14+
- `api_v2/` - Current v2 API development
15+
- `models/` - Database models
16+
- `serializers/` - DRF serializers with inheritance patterns
17+
- `views/` - API viewsets
18+
- `tests/` - Approval tests (see Testing section)
19+
- `data/` - JSON fixture files for database seeding
20+
- `v1/` - Legacy data
21+
- `v2/` - Current data format
22+
- `server/` - Django settings and configuration
23+
24+
### Important Files
25+
- `manage.py` - Standard Django management
26+
- `api/management/commands/quicksetup.py` - Database rebuild script
27+
- `Pipfile` - Python dependencies (using pipenv)
28+
29+
## Database Setup & Management
30+
31+
### Quick Setup (CRITICAL for development)
32+
```bash
33+
python manage.py quicksetup --clean --noindex
34+
```
35+
This command:
36+
- Cleans existing database/static files/search indexes
37+
- Runs migrations
38+
- Loads ALL fixture data from `data/v1/` and `data/v2/`
39+
- Collects static files
40+
- **Always run this after schema changes or when fixture data is updated**
41+
42+
### Manual Steps
43+
```bash
44+
python manage.py makemigrations api_v2 # For model changes
45+
python manage.py migrate # Apply migrations
46+
python manage.py collectstatic # Static files
47+
```
48+
49+
### Fixture Data Loading
50+
- Fixtures are automatically loaded by quicksetup
51+
- Data format: JSON files with Django fixture format
52+
- v2 fixtures in `data/v2/publisher/document/` structure
53+
54+
## Testing System (APPROVAL TESTS)
55+
56+
### How Approval Tests Work
57+
The project uses **approval testing** - tests compare actual API responses against pre-approved JSON files.
58+
59+
#### Test Structure
60+
- Tests in: `api_v2/tests/test_objects.py`
61+
- Approved responses: `api_v2/tests/responses/*.approved.json`
62+
- Failed test responses: `api_v2/tests/responses/*.received.json`
63+
64+
#### Running Tests
65+
```bash
66+
# Requires running server for integration tests
67+
python manage.py runserver 8000 &
68+
python -m pytest api_v2/tests/test_objects.py -v
69+
pkill -f "python manage.py runserver" # Stop server
70+
```
71+
72+
#### Updating Test Expectations (IMPORTANT PATTERN)
73+
When API responses change (like adding new fields):
74+
75+
1. **Run tests** - they will fail and generate `.received.json` files
76+
2. **Review changes** in the `.received.json` files
77+
3. **Update all at once** (EFFICIENT METHOD):
78+
```bash
79+
cd api_v2/tests/responses/
80+
for file in *.received.json; do
81+
mv "$file" "${file%.received.json}.approved.json"
82+
done
83+
```
84+
4. **Re-run tests** to verify they pass
85+
86+
**DO NOT** manually edit `.approved.json` files - use the rename pattern above!
87+
88+
## API v2 Serializer Patterns
89+
90+
### Inheritance Hierarchy
91+
- `GameContentSerializer` - Base class for all game content
92+
- `DocumentSerializer` - Full document serialization
93+
- `DocumentSummarySerializer` - Lightweight document refs (for FKs)
94+
95+
### Common Patterns
96+
97+
#### Fallback Properties
98+
When adding optional display fields, use model properties with fallbacks:
99+
```python
100+
# In model
101+
@property
102+
def display_name_or_name(self):
103+
if self.display_name and self.display_name.strip():
104+
return self.display_name
105+
return self.name
106+
107+
# In serializer
108+
display_name = serializers.SerializerMethodField()
109+
110+
def get_display_name(self, obj):
111+
return obj.display_name_or_name
112+
```
113+
114+
#### Document Relationships
115+
- Full documents: Use `DocumentSerializer`
116+
- FK references: Use `DocumentSummarySerializer`
117+
- Check serializer `fields = '__all__'` vs specific field lists
118+
119+
## Development Workflow
120+
121+
### Adding New Fields
122+
1. **Add field to model** in `api_v2/models/`
123+
2. **Create migration**: `python manage.py makemigrations api_v2 --name descriptive_name`
124+
3. **Update serializers** if needed
125+
4. **Run quicksetup** to rebuild DB with fixture data: `python manage.py quicksetup --clean --noindex`
126+
5. **Update tests** using the `.received.json` → `.approved.json` rename pattern
127+
6. **Test the API** with running server
128+
129+
### API Testing
130+
```bash
131+
# Start server
132+
python manage.py runserver 8000 &
133+
134+
# Test endpoints
135+
curl -s "http://localhost:8000/v2/documents/" | python -m json.tool
136+
137+
# Stop server
138+
pkill -f "python manage.py runserver"
139+
```
140+
141+
## Common Gotchas
142+
143+
### Database State
144+
- **Always run quicksetup after model changes** - migrations alone don't reload fixture data
145+
- Fixture data may have different values than what's in migrations
146+
- Database relationships are complex - FK dependencies matter for loading order
147+
148+
### Test Failures
149+
- Tests are integration tests requiring a running server
150+
- Connection refused errors = server not running
151+
- Approval mismatches = API response changed, update `.approved.json` files
152+
- Tests expect exact JSON matches including field order
153+
154+
### Serializer Field Order
155+
- JSON field order matters for approval tests
156+
- SerializerMethodFields appear in declaration order
157+
- `fields = '__all__'` includes all model fields + method fields
158+
159+
## Useful Commands
160+
161+
```bash
162+
# Full development reset
163+
python manage.py quicksetup --clean --noindex
164+
165+
# Run specific test
166+
python -m pytest api_v2/tests/test_objects.py::TestObjects::test_document_example -v
167+
168+
# Find fixture files
169+
find data/v2/ -name "*.json" | grep -i document
170+
171+
# Check API response
172+
curl -s "http://localhost:8000/v2/documents/srd-2024/" | python -m json.tool
173+
174+
# Update all failing tests at once
175+
cd api_v2/tests/responses/ && for file in *.received.json; do mv "$file" "${file%.received.json}.approved.json"; done
176+
```
177+
178+
## Project Conventions
179+
180+
### Model Patterns
181+
- Inherit from `HasName`, `HasDescription` abstracts when appropriate
182+
- Use `key_field()` for primary keys (CharField, not AutoField)
183+
- Foreign keys typically reference document for data lineage
184+
185+
### API Design
186+
- RESTful endpoints: `/v2/modelname/` and `/v2/modelname/key/`
187+
- Consistent field naming across models
188+
- Rich relationship serialization (nested objects, not just IDs)
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 5.2.1 on 2025-06-07 15:31
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('api_v2', '0048_service_name'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='document',
15+
name='display_name',
16+
field=models.CharField(blank=True, help_text='Display name for the document, used for presentation purposes.', max_length=255, null=True),
17+
),
18+
]

api_v2/models/document.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ class Document(HasName, HasDescription):
3030
author = models.TextField(
3131
help_text='Author or authors.')
3232

33+
display_name = models.CharField(
34+
max_length=255,
35+
blank=True,
36+
null=True,
37+
help_text="Display name for the document, used for presentation purposes.")
38+
3339
publication_date = models.DateTimeField(
3440
help_text="Date of publication, or null if unknown."
3541
)
@@ -41,6 +47,13 @@ class Document(HasName, HasDescription):
4147
distance_unit = distance_unit_field()
4248
weight_unit = distance_unit_field()
4349

50+
@property
51+
def display_name_or_name(self):
52+
"""Returns display_name if it exists and is not blank, otherwise returns name."""
53+
if self.display_name and self.display_name.strip():
54+
return self.display_name
55+
return self.name
56+
4457
@property
4558
def stats(self):
4659
stats = []

api_v2/serializers/document.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ class DocumentSerializer(GameContentSerializer):
5858
licenses = LicenseSummarySerializer(read_only=True, many=True)
5959
publisher = PublisherSummarySerializer(read_only=True)
6060
gamesystem = GameSystemSummarySerializer(read_only=True)
61+
display_name = serializers.SerializerMethodField()
62+
63+
def get_display_name(self, obj):
64+
return obj.display_name_or_name
6165

6266
class Meta:
6367
model = models.Document
@@ -70,7 +74,11 @@ class DocumentSummarySerializer(GameContentSerializer):
7074
'''
7175
publisher = PublisherSummarySerializer()
7276
gamesystem = GameSystemSummarySerializer()
77+
display_name = serializers.SerializerMethodField()
78+
79+
def get_display_name(self, obj):
80+
return obj.display_name_or_name
7381

7482
class Meta:
7583
model = models.Document
76-
fields = ['name', 'key', 'publisher', 'gamesystem', 'permalink']
84+
fields = ['name', 'key', 'display_name', 'publisher', 'gamesystem', 'permalink']

api_v2/tests/responses/TestObjects.test_alignment_example.approved.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"document": {
44
"author": "Mike Mearls, Jeremy Crawford, Chris Perkins, Rodney Thompson, Peter Lee, James Wyatt, Robert J. Schwalb, Bruce R. Cordell, Chris Sims, and Steve Townshend, based on original material by E. Gary Gygax and Dave Arneson.",
55
"desc": "The System Reference Document (SRD) contains guidelines for publishing content under the Open-Gaming License (OGL) or Creative Commons. The Dungeon Masters Guild also provides self-publishing opportunities for individuals and groups.",
6+
"display_name": "5e 2014 Rules",
67
"distance_unit": "feet",
78
"gamesystem": {
89
"key": "o5e",

api_v2/tests/responses/TestObjects.test_armor_example.approved.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"ac_display": "17",
66
"category": "heavy",
77
"document": {
8+
"display_name": "5e 2014 Rules",
89
"gamesystem": {
910
"key": "o5e",
1011
"name": "5th Edition",

api_v2/tests/responses/TestObjects.test_background_example.approved.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
],
2929
"desc": "You have spent your life in the service of a temple to a specific god or pantheon of gods. You act as an intermediary between the realm of the holy and the mortal world, performing sacred rites and offering sacrifices in order to conduct worshipers into the presence of the divine. You are not necessarily a cleric performing sacred rites is not the same thing as channeling divine power.\r\n\r\nChoose a god, a pantheon of gods, or some other quasi-divine being from among those listed in \"Fantasy-Historical Pantheons\" or those specified by your GM, and work with your GM to detail the nature of your religious service. Were you a lesser functionary in a temple, raised from childhood to assist the priests in the sacred rites? Or were you a high priest who suddenly experienced a call to serve your god in a different way? Perhaps you were the leader of a small cult outside of any established temple structure, or even an occult group that served a fiendish master that you now deny.",
3030
"document": {
31+
"display_name": "5e 2014 Rules",
3132
"gamesystem": {
3233
"key": "o5e",
3334
"name": "5th Edition",

api_v2/tests/responses/TestObjects.test_class_example.approved.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"caster_type": "NONE",
33
"document": {
4+
"display_name": "5e 2014 Rules",
45
"gamesystem": {
56
"key": "o5e",
67
"name": "5th Edition",

api_v2/tests/responses/TestObjects.test_condition_example.approved.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"desc": "* A stunned creature is incapacitated (see the condition), can\u2019t move, and can speak only falteringly.\r\n* The creature automatically fails Strength and Dexterity saving throws.\r\n* Attack rolls against the creature have advantage.",
33
"document": {
4+
"display_name": "5e 2014 Rules",
45
"gamesystem": {
56
"key": "o5e",
67
"name": "5th Edition",

api_v2/tests/responses/TestObjects.test_creature_ancient_example.approved.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@
182182
"creaturesets": [],
183183
"darkvision_range": 120.0,
184184
"document": {
185+
"display_name": "5e 2014 Rules",
185186
"gamesystem": {
186187
"key": "o5e",
187188
"name": "5th Edition",

0 commit comments

Comments
 (0)