This document provides guidelines for Windsurf AI agents working on the Bible Research API project. Follow these guidelines to ensure consistent, high-quality contributions that align with project standards.
- Project Context
- Code Style & Standards
- Architecture Patterns
- Database Guidelines
- API Design Principles
- Testing Guidelines
- Security Considerations
- Git Workflow
- Common Tasks
- Troubleshooting
- Don'ts - Important Restrictions
This is a Django REST API backend for a Bible research application. It serves as the backend for a separate React frontend.
- Provide Bible verse data (text and audio) via DBT API integration
- Manage user annotations (notes and tags)
- Handle authentication and authorization
- Support multiple Bible translations
- Framework: Django 4.2.6 with Django REST Framework
- Database: PostgreSQL (production), SQLite (development)
- External API: Digital Bible Platform (DBT) API v4
- Authentication: Token-based (DRF TokenAuthentication)
CRITICAL: Python lines must NOT exceed 79 characters (PEP 8)
Good:
response_format = request.query_params.get(
'response_format', 'text'
)
error_message = (
f"Verse not found for '{verse_ref_data['book']} "
f"{verse_ref_data['chapter']}:{verse_ref_data['verse']}'"
)Bad:
response_format = request.query_params.get('response_format', 'text') # Too long!
error_message = f"Verse not found for '{verse_ref_data['book']} {verse_ref_data['chapter']}:{verse_ref_data['verse']}'" # Too long!# Standard library imports
import os
import sys
from typing import Dict, Optional, Any
# Django imports
from django.db import models
from django.contrib.auth import get_user_model
# Third-party imports
from rest_framework import serializers, viewsets
# Local imports
from .models import Tag, Note
from bible.models import VerseUse triple-quoted docstrings for classes and functions:
def get_verses(self, book: str, chapter: str, **kwargs):
"""
Get verses for a specific chapter.
Args:
book: Book ID (e.g., 'JHN')
chapter: Chapter number
**kwargs: Additional query parameters
Returns:
Dictionary with verse data
"""- Classes:
PascalCase(e.g.,BiblePassageView,NoteSerializer) - Functions/Methods:
snake_case(e.g.,get_verses,create_note) - Constants:
UPPER_SNAKE_CASE(e.g.,MAX_LENGTH,DEFAULT_TRANSLATION) - Variables:
snake_case(e.g.,verse_count,user_notes)
The project has two main apps:
Purpose: Bible data retrieval and verse management
Components:
models.py:Versemodel onlyviews.py:BiblePassageViewfor fetching passagesserializers.py:BiblePassageSerializerwith DBT integrationservices/dbt/client.py: DBT API client wrapperutils/bible_books.py: Book name mappings
When to modify: Adding new Bible-related features, translations, or verse retrieval methods
Purpose: User-generated content (notes and tags)
Components:
models.py:Tag,Note,NoteVersemodelsviews.py:TagViewSet,NoteViewSetfor CRUD operationsserializers.py: Serializers with nested relationshipsadmin.py: Django admin configuration
When to modify: Adding features related to user notes, tags, or annotations
Use ViewSet (for models with full CRUD):
class TagViewSet(viewsets.ModelViewSet):
"""Provides list, create, retrieve, update, destroy actions"""
serializer_class = TagSerializer
def get_queryset(self):
# Custom filtering logic
passUse APIView (for custom endpoints):
class BiblePassageView(APIView):
"""Custom endpoint with specific logic"""
def get(self, request, format=None):
# Custom retrieval logic
passclass NoteSerializer(serializers.ModelSerializer):
# Write-only field (accepts IDs)
tag = serializers.PrimaryKeyRelatedField(
queryset=Tag.objects.all(),
write_only=True
)
# Read-only field (returns full object)
tag_detail = TagSerializer(source='tag', read_only=True)def to_representation(self, instance):
"""Override to add computed fields or nested data"""
representation = super().to_representation(instance)
# Add verse text from DBT API
representation['verses'] = self._get_verses_with_text(instance)
return representationAll models use custom ID generation with prefixes:
def generate_tag_id():
return f"TAG{str(uuid.uuid4()).upper().replace('-', '')[:15]}"
class Tag(models.Model):
id = models.CharField(
max_length=18,
default=generate_tag_id,
primary_key=True,
editable=False,
help_text="Unique identifier for the tag."
)ID Prefixes:
VER- VerseTAG- TagNOT- NoteNVE- NoteVerse
CASCADE: Delete related objects when parent is deleted
user = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name='tags'
)PROTECT: Prevent deletion if related objects exist
verse = models.ForeignKey(
Verse,
on_delete=models.PROTECT # Can't delete verses with notes
)Use explicit through model for additional fields:
class Note(models.Model):
verses = models.ManyToManyField(
Verse,
through='NoteVerse',
related_name='notes'
)
class NoteVerse(models.Model):
note = models.ForeignKey(Note, on_delete=models.CASCADE)
verse = models.ForeignKey(Verse, on_delete=models.PROTECT)
created_at = models.DateTimeField(auto_now_add=True)Always create migrations after model changes:
python manage.py makemigrations
python manage.py migrateCheck migration SQL before applying:
python manage.py sqlmigrate annotations 0001RESTful conventions:
# Good - RESTful
urlpatterns = [
path('tags/', TagViewSet.as_view({'get': 'list', 'post': 'create'})),
path('tags/<pk>/', TagViewSet.as_view({
'get': 'retrieve',
'put': 'update',
'delete': 'destroy'
})),
]
# Better - Use routers for ViewSets
router = DefaultRouter()
router.register(r'tags', TagViewSet, basename='tag')
urlpatterns = [
path('', include(router.urls)),
]Use clear, descriptive parameter names:
# Good
GET /api/v1/bible/?passage=John+3&translation=ENGESV&response_format=text
# Bad
GET /api/v1/bible/?p=John+3&t=ENGESV&f=textConsistent structure:
# Success response
{
"id": "NOT123...",
"note_text": "...",
"verses": [...],
"created_at": "2025-12-28T03:51:55Z"
}
# Error response
{
"error": "Passage parameter is required. Example: ?passage=John+3:16"
}Support common filtering patterns:
def get_queryset(self):
queryset = Note.objects.all()
# Filter by tag
tag_id = self.request.query_params.get('tag_id')
if tag_id:
queryset = queryset.filter(tag_id=tag_id)
# Filter by public status
public = self.request.query_params.get('public')
if public == 'true':
queryset = queryset.filter(
Q(user=self.request.user) | Q(public=True)
)
return querysetIMPORTANT: Tests should use the test user accounts
from django.contrib.auth import get_user_model
User = get_user_model()
# In tests
test_user = User.objects.get(username='testuser')
guest_user = User.objects.get(username='guest')DO NOT run full test suite if it takes too long:
# Good - Test specific file
python manage.py test annotations.tests.TestNoteViewSet
# Good - Test specific app
python manage.py test annotations
# Avoid - Full suite (if slow)
# python manage.py testfrom rest_framework.test import APITestCase
from rest_framework import status
class TestNoteViewSet(APITestCase):
def setUp(self):
"""Set up test data"""
self.user = User.objects.create_user(
username='testuser',
password='password123'
)
self.client.force_authenticate(user=self.user)
def test_create_note(self):
"""Test note creation"""
data = {
'note_text': 'Test note',
'verse_references': [
{'book': 'John', 'chapter': 3, 'verse': 16}
]
}
response = self.client.post('/api/v1/notes/', data)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)Always check user authentication in ViewSets:
def perform_create(self, serializer):
"""Assign current user to created object"""
if self.request.user.is_authenticated:
serializer.save(user=self.request.user)
else:
# Use guest user for unauthenticated requests
guest_user = User.objects.get(username='guest')
serializer.save(user=guest_user)Ensure users can only modify their own data:
def perform_update(self, serializer):
"""Prevent users from updating others' data"""
instance = serializer.instance
if (self.request.user.is_authenticated and
instance.user != self.request.user):
raise PermissionDenied(
"You do not have permission to update this note."
)
serializer.save()Validate all user input:
def validate_verse_references(self, value):
"""Ensure verse references are valid"""
if not value:
raise serializers.ValidationError(
"At least one verse reference is required."
)
for verse_ref in value:
if verse_ref['chapter'] < 1:
raise serializers.ValidationError(
"Chapter must be a positive number."
)
return valueNEVER commit sensitive data:
- API keys go in
config.yaml(gitignored) - Use
config_template.yamlfor examples - Database credentials in
config.yaml - Secret key in
config.yaml
Follow conventional commits with UPPERCASE type and capitalized message:
Feat: Add new featureFix: Resolve bugDocs: Update documentationRefactor: Restructure codeTest: Add testsChore: Update dependencies
Format: Type: Capitalized message description
Examples:
- ✅
Feat: Add Vercel Analytics integration - ✅
Fix: Resolve audio playback issue on Safari - ✅
Docs: Update DEVELOPER_GUIDE with caching strategy - ❌
feat: add analytics(lowercase - incorrect) - ❌
Feat: add analytics(message not capitalized - incorrect)
- Feat: New feature or functionality
- Fix: Bug fix or error correction
- Docs: Documentation changes only
- Refactor: Code restructuring without changing behavior
- Test: Adding or updating tests
- Chore: Maintenance tasks (dependencies, config, etc.)
- Style: Code formatting, whitespace, etc.
- Perf: Performance improvements
- Keep commits atomic: One logical change per commit
- Write descriptive messages: Explain what and why, not how
- Reference issues: Include issue numbers when applicable
- Example:
Fix: Resolve note deletion cascade issue (#42)
- Example:
- Use present tense: "Add feature" not "Added feature"
- Keep first line under 72 characters: For better git log display
- Update documentation: Always update
DEVELOPER_GUIDE.mdafter significant logic changes
Format: type/short-description
Examples:
feat/verse-highlightingfix/audio-playback-safaridocs/api-documentationrefactor/serializer-cleanup
IMPORTANT: After making significant logic changes, always update
DEVELOPER_GUIDE.md
When to update:
- Adding new API endpoints
- Changing API request/response formats
- Adding new models or fields
- Modifying authentication/authorization logic
- Adding new features or functionalities
- Changing database schema
- Updating external API integrations
What to update in DEVELOPER_GUIDE.md:
-
API Endpoints section: Add/update endpoint documentation
#### New Endpoint NameGET /api/v1/new-endpoint/ Authorization: Token
**Response**: ```json { "field": "value" } -
Database Schema section: Update model definitions
### ModelName Model ```python class ModelName(models.Model): new_field = CharField(max_length=100)
-
Core Functionalities section: Document new features
-
Common Issues section: Add troubleshooting for new features
Example commit with documentation:
# Make code changes
git add bible/views.py bible/serializers.py
# Update documentation
git add DEVELOPER_GUIDE.md
# Commit with both changes
git commit -m "Feat: Add verse search endpoint
- Add search view and serializer
- Update DEVELOPER_GUIDE.md with new endpoint documentation"- Update the model:
class Note(models.Model):
# ... existing fields ...
is_favorite = models.BooleanField(default=False) # New field- Create and apply migration:
python manage.py makemigrations
python manage.py migrate- Update serializer:
class NoteSerializer(serializers.ModelSerializer):
class Meta:
model = Note
fields = [
'id', 'note_text', 'is_favorite', # Add new field
'created_at', 'updated_at'
]- Update API documentation in
DEVELOPER_GUIDE.md
- Create view:
class FavoriteNotesView(APIView):
"""Get user's favorite notes"""
def get(self, request):
notes = Note.objects.filter(
user=request.user,
is_favorite=True
)
serializer = NoteSerializer(notes, many=True)
return Response(serializer.data)- Add URL pattern:
urlpatterns = [
path('notes/favorites/', FavoriteNotesView.as_view()),
]- Document in
DEVELOPER_GUIDE.md
-
Find translation code in DBT API documentation
-
Add to book mappings if needed:
# bible/utils/bible_books.py
AUDIO_BIBLE_MAPPINGS = {
'ENGESV': {
'1DA': 'ENGESVN1DA',
'2DA': 'ENGESVN2DA',
},
'ENGNIV': { # New translation
'1DA': 'ENGNIVN1DA',
'2DA': 'ENGNIVN2DA',
}
}- Test with API:
curl "http://localhost:8000/api/v1/bible/?passage=John+3&translation=ENGNIV"Use the DBTClient wrapper:
from bible.services.dbt.client import DBTClient
client = DBTClient()
# Get verses
verses = client.get_verses(
book='JHN',
chapter='3',
bible_id='ENGESV',
verse_start=16,
verse_end=17
)
# Search
results = client.search(
bible_id='ENGESV',
query='love'
)Handle API errors:
try:
verses = client.get_verses(book, chapter, bible_id)
except Exception as e:
logger.error(f"DBT API error: {str(e)}")
return Response(
{"error": "Failed to fetch verses"},
status=status.HTTP_503_SERVICE_UNAVAILABLE
)Cause: Missing or incorrect config.yaml
Solution:
cp config_template.yaml config.yaml
# Edit config.yaml and add your DBT_KEYCause: Verse doesn't exist in database Solution:
python manage.py import_esv_verses
# Or add verse manually:
python scripts/add_verse.pyCause: Test users not created Solution:
python scripts/create_test_user.pyCause: Multiple migration files for same change Solution:
# Delete conflicting migration files
# Recreate migrations
python manage.py makemigrationsEnable verbose logging:
import logging
logger = logging.getLogger(__name__)
logger.debug(f"Verse data: {verse_data}")
logger.info(f"User {user.username} created note")
logger.error(f"Failed to fetch verses: {str(e)}")Use Django shell for testing:
python manage.py shell
>>> from annotations.models import Note, Tag
>>> Note.objects.filter(user__username='testuser')
>>> Tag.objects.filter(parent_tag__isnull=True)Check database state:
python manage.py dbshell
SELECT * FROM annotations_note LIMIT 5;-
Don't exceed 79 characters per line in Python files
- This is a hard requirement from user rules
- Break long lines appropriately
-
Don't run
bundle exec rspecfor Ruby projects- Not applicable to this Python project, but noted in user rules
-
Don't commit sensitive data
- No API keys in code
- No passwords in code
- No database credentials in code
- Use
config.yaml(gitignored)
-
Don't modify migrations after they're applied
- Create new migrations instead
- Never edit existing migration files
-
Don't bypass authentication checks
- Always validate user permissions
- Use
perform_create,perform_update,perform_destroy
-
Don't use raw SQL queries
- Use Django ORM
- Exception: Complex queries where ORM is insufficient
-
Don't create circular imports
- Keep imports organized
- Use
get_user_model()instead of importing User directly
-
Don't hardcode configuration
- Use
settings.pyfor configuration - Use
config.yamlfor secrets
- Use
-
Don't ignore error handling
- Always handle external API failures
- Provide meaningful error messages
-
Don't skip input validation
- Validate all user input in serializers
- Check for edge cases
-
Database queries in loops
- Use
select_related()andprefetch_related() - Avoid N+1 query problems
- Use
-
External API calls
- Always handle timeouts
- Implement retry logic if needed
- Cache responses when appropriate
-
Deleting data
- Use
on_delete=models.PROTECTfor critical relationships - Implement soft deletes if needed
- Use
-
Changing model field types
- May require data migration
- Test thoroughly before applying
-
Write descriptive docstrings
- Explain what the function does
- Document parameters and return values
-
Use type hints
def get_verses( self, book: str, chapter: str, bible_id: str = "ENGESV" ) -> Dict[str, Any]:
-
Keep functions focused
- One function, one responsibility
- Extract complex logic into helper functions
-
Use meaningful variable names
# Good verse_references = data.get('verse_references', []) # Bad vr = data.get('vr', [])
-
Add helpful comments
# Convert book name to DBT book ID (e.g., "John" -> "JHN") book_id = get_dbt_book_id(book_name)
-
Test edge cases
- Empty inputs
- Invalid data
- Unauthenticated users
- Missing related objects
-
Keep serializers clean
- Separate read and write logic
- Use custom methods for complex transformations
-
Log important events
- User actions
- API calls
- Errors and exceptions
-
Update DEVELOPER_GUIDE.md after significant changes
- New API endpoints
- Model changes
- New features
- Authentication/authorization changes
bible_research/
├── bible/
│ ├── models.py # Verse model
│ ├── views.py # BiblePassageView
│ ├── serializers.py # BiblePassageSerializer
│ ├── services/dbt/client.py # DBT API client
│ └── utils/bible_books.py # Book mappings
├── annotations/
│ ├── models.py # Tag, Note, NoteVerse
│ ├── views.py # TagViewSet, NoteViewSet
│ └── serializers.py # Tag/Note serializers
├── bible_research/
│ ├── settings.py # Django settings
│ ├── urls.py # URL routing
│ └── authentication.py # Custom auth classes
├── scripts/
│ └── create_test_user.py # User creation script
├── config.yaml # Configuration (gitignored)
└── config_template.yaml # Configuration template
# Development
python manage.py runserver
python manage.py shell
python manage.py dbshell
# Database
python manage.py makemigrations
python manage.py migrate
python manage.py import_esv_verses
# Users
python scripts/create_test_user.py
# Testing
python manage.py test annotations
python manage.py test bible
# Deployment
python manage.py collectstatic# Verse
Verse.objects.get(book='John', chapter=3, verse=16)
# Tag
Tag.objects.filter(user=user, parent_tag__isnull=True)
# Note
Note.objects.filter(user=user, public=True)
# User
User.objects.get(username='testuser')Before submitting code, ensure:
- Code follows 79-character line limit
- All imports are organized correctly
- Docstrings are added for new functions/classes
- Type hints are used where appropriate
- Input validation is implemented
- Error handling is in place
- Authentication/authorization checks are correct
- Database migrations are created and tested
- API endpoints are RESTful
- Tests are written (if applicable)
- DEVELOPER_GUIDE.md is updated with changes
- No sensitive data is committed
- Logging is added for important events
- Code is tested locally
- Commit message follows format:
Type: Capitalized message
- Django Docs: https://docs.djangoproject.com/
- DRF Docs: https://www.django-rest-framework.org/
- DBT API: https://www.faithcomesbyhearing.com/bible-brain/api-reference
- PEP 8: https://pep8.org/
- Developer Guide: See
DEVELOPER_GUIDE.mdin this repository
Last Updated: 2025-12-28
Note: These guidelines are living documents. Update them as the project evolves and new patterns emerge.