Refactor code structure and remove redundant changes

This commit is contained in:
pacnpal
2025-11-09 16:31:34 -05:00
parent 2884bc23ce
commit eb68cf40c6
1080 changed files with 27361 additions and 56687 deletions

View File

@@ -0,0 +1,633 @@
# Priority 5: History API Implementation Guide
**Date:** 2025-11-08
**Status:** 🚧 IN PROGRESS - Service Layer Complete
## Overview
Implementation of comprehensive history API using pghistory Event models to replace the old custom versioning system. Provides history tracking, comparison, and rollback capabilities with role-based access control.
---
## ✅ Completed
### 1. Service Layer (`django/api/v1/services/history_service.py`)
**Status:** ✅ COMPLETE
**Features Implemented:**
- `get_history()` - Query entity history with access control
- `get_event()` - Retrieve specific historical event
- `compare_events()` - Compare two historical snapshots
- `compare_with_current()` - Compare historical state with current
- `rollback_to_event()` - Rollback entity to historical state (admin only)
- `get_field_history()` - Track changes to specific field
- `get_activity_summary()` - Activity statistics
**Access Control:**
- Unauthenticated: Last 30 days
- Authenticated: Last 1 year
- Moderators/Admins/Superusers: Unlimited
**Models Supported:**
- Park → ParkEvent
- Ride → RideEvent
- Company → CompanyEvent
- RideModel → RideModelEvent
- Review → ReviewEvent
---
## 📋 Remaining Implementation Tasks
### Phase 1: API Schemas
**File:** `django/api/v1/schemas.py`
**Add the following schemas:**
```python
# History Event Schema
class HistoryEventSchema(BaseModel):
"""Schema for a single history event."""
id: int
timestamp: datetime
operation: str # 'INSERT' or 'UPDATE'
snapshot: dict
changed_fields: Optional[dict] = None
change_summary: str
can_rollback: bool
class Config:
from_attributes = True
# History List Response
class HistoryListResponse(BaseModel):
"""Response for list history endpoint."""
entity_id: UUID
entity_type: str
entity_name: str
total_events: int
accessible_events: int
access_limited: bool
access_reason: str
events: List[HistoryEventSchema]
pagination: dict
# Event Detail Response
class HistoryEventDetailSchema(BaseModel):
"""Detailed event with rollback preview."""
id: int
timestamp: datetime
operation: str
entity_id: UUID
entity_type: str
entity_name: str
snapshot: dict
changed_fields: Optional[dict] = None
metadata: dict
can_rollback: bool
rollback_preview: Optional[dict] = None
class Config:
from_attributes = True
# Comparison Response
class HistoryComparisonSchema(BaseModel):
"""Response for event comparison."""
entity_id: UUID
entity_type: str
entity_name: str
event1: dict
event2: dict
differences: dict
changed_field_count: int
unchanged_field_count: int
time_between: str
# Diff with Current Response
class HistoryDiffCurrentSchema(BaseModel):
"""Response for comparing event with current state."""
entity_id: UUID
entity_type: str
entity_name: str
event: dict
current_state: dict
differences: dict
changed_field_count: int
time_since: str
# Field History Response
class FieldHistorySchema(BaseModel):
"""Response for field-specific history."""
entity_id: UUID
entity_type: str
entity_name: str
field: str
field_type: str
history: List[dict]
total_changes: int
first_value: Any
current_value: Any
# Activity Summary Response
class HistoryActivitySummarySchema(BaseModel):
"""Response for activity summary."""
entity_id: UUID
entity_type: str
entity_name: str
total_events: int
accessible_events: int
summary: dict
most_changed_fields: Optional[List[dict]] = None
recent_activity: List[dict]
# Rollback Request
class RollbackRequestSchema(BaseModel):
"""Request body for rollback operation."""
fields: Optional[List[str]] = None
comment: str = ""
create_backup: bool = True
# Rollback Response
class RollbackResponseSchema(BaseModel):
"""Response for rollback operation."""
success: bool
message: str
entity_id: UUID
rollback_event_id: int
new_event_id: Optional[int]
fields_changed: dict
backup_event_id: Optional[int]
```
---
### Phase 2: Generic History Endpoints
**File:** `django/api/v1/endpoints/history.py` (CREATE NEW)
**Implementation:**
```python
"""
Generic history endpoints for all entity types.
Provides cross-entity history operations and utilities.
"""
from typing import Optional
from uuid import UUID
from django.shortcuts import get_object_or_404
from django.http import Http404
from ninja import Router, Query
from api.v1.services.history_service import HistoryService
from api.v1.schemas import (
HistoryEventDetailSchema,
HistoryComparisonSchema,
ErrorSchema
)
router = Router(tags=['History'])
@router.get(
'/events/{event_id}',
response={200: HistoryEventDetailSchema, 404: ErrorSchema},
summary="Get event by ID",
description="Retrieve any historical event by its ID (requires entity_type parameter)"
)
def get_event_by_id(
request,
event_id: int,
entity_type: str = Query(..., description="Entity type (park, ride, company, ridemodel, review)")
):
"""Get a specific historical event by ID."""
try:
event = HistoryService.get_event(entity_type, event_id, request.user)
if not event:
return 404, {"error": "Event not found or not accessible"}
# Build response
# ... (format event data)
return response_data
except ValueError as e:
return 404, {"error": str(e)}
@router.get(
'/compare',
response={200: HistoryComparisonSchema, 400: ErrorSchema, 404: ErrorSchema},
summary="Compare two events",
description="Compare two historical events (must be same entity)"
)
def compare_events(
request,
entity_type: str = Query(...),
event1: int = Query(...),
event2: int = Query(...)
):
"""Compare two historical events."""
try:
comparison = HistoryService.compare_events(
entity_type, event1, event2, request.user
)
# Format response
# ... (build comparison response)
return response_data
except ValueError as e:
return 400, {"error": str(e)}
```
---
### Phase 3: Entity-Specific History Routes
**Add to each entity endpoint file:**
#### Parks (`django/api/v1/endpoints/parks.py`)
```python
@router.get(
'/{park_id}/history/',
response={200: HistoryListResponse, 404: ErrorSchema},
summary="Get park history"
)
def get_park_history(
request,
park_id: UUID,
page: int = Query(1, ge=1),
page_size: int = Query(50, ge=1, le=100),
date_from: Optional[date] = Query(None),
date_to: Optional[date] = Query(None)
):
"""Get history for a park."""
# Verify park exists
park = get_object_or_404(Park, id=park_id)
# Get history
offset = (page - 1) * page_size
events, accessible_count = HistoryService.get_history(
'park', str(park_id), request.user,
date_from=date_from, date_to=date_to,
limit=page_size, offset=offset
)
# Format response
return {
'entity_id': str(park_id),
'entity_type': 'park',
'entity_name': park.name,
'total_events': accessible_count,
'accessible_events': accessible_count,
'access_limited': HistoryService.is_access_limited(request.user),
'access_reason': HistoryService.get_access_reason(request.user),
'events': [/* format each event */],
'pagination': {/* pagination info */}
}
@router.get(
'/{park_id}/history/{event_id}/',
response={200: HistoryEventDetailSchema, 404: ErrorSchema},
summary="Get specific park history event"
)
def get_park_history_event(request, park_id: UUID, event_id: int):
"""Get a specific history event for a park."""
park = get_object_or_404(Park, id=park_id)
event = HistoryService.get_event('park', event_id, request.user)
if not event:
return 404, {"error": "Event not found or not accessible"}
# Format and return event details
# ...
@router.get(
'/{park_id}/history/compare/',
response={200: HistoryComparisonSchema, 400: ErrorSchema},
summary="Compare two park history events"
)
def compare_park_history(
request,
park_id: UUID,
event1: int = Query(...),
event2: int = Query(...)
):
"""Compare two historical events for a park."""
park = get_object_or_404(Park, id=park_id)
try:
comparison = HistoryService.compare_events(
'park', event1, event2, request.user
)
# Format and return comparison
# ...
except ValueError as e:
return 400, {"error": str(e)}
@router.get(
'/{park_id}/history/{event_id}/diff-current/',
response={200: HistoryDiffCurrentSchema, 404: ErrorSchema},
summary="Compare historical event with current state"
)
def diff_park_history_with_current(request, park_id: UUID, event_id: int):
"""Compare historical event with current park state."""
park = get_object_or_404(Park, id=park_id)
try:
diff = HistoryService.compare_with_current(
'park', event_id, park, request.user
)
# Format and return diff
# ...
except ValueError as e:
return 404, {"error": str(e)}
@router.post(
'/{park_id}/history/{event_id}/rollback/',
response={200: RollbackResponseSchema, 400: ErrorSchema, 403: ErrorSchema},
summary="Rollback park to historical state"
)
def rollback_park(request, park_id: UUID, event_id: int, payload: RollbackRequestSchema):
"""
Rollback park to a historical state.
**Permission:** Moderators, Admins, Superusers only
"""
# Check authentication
if not request.user or not request.user.is_authenticated:
return 401, {"error": "Authentication required"}
# Check rollback permission
if not HistoryService.can_rollback(request.user):
return 403, {"error": "Only moderators and administrators can perform rollbacks"}
park = get_object_or_404(Park, id=park_id)
try:
result = HistoryService.rollback_to_event(
park, 'park', event_id, request.user,
fields=payload.fields,
comment=payload.comment,
create_backup=payload.create_backup
)
return result
except (ValueError, PermissionDenied) as e:
return 400, {"error": str(e)}
@router.get(
'/{park_id}/history/field/{field_name}/',
response={200: FieldHistorySchema, 404: ErrorSchema},
summary="Get field-specific history"
)
def get_park_field_history(request, park_id: UUID, field_name: str):
"""Get history of changes to a specific park field."""
park = get_object_or_404(Park, id=park_id)
history = HistoryService.get_field_history(
'park', str(park_id), field_name, request.user
)
return {
'entity_id': str(park_id),
'entity_type': 'park',
'entity_name': park.name,
'field': field_name,
'field_type': 'CharField', # Could introspect this
**history
}
@router.get(
'/{park_id}/history/summary/',
response={200: HistoryActivitySummarySchema, 404: ErrorSchema},
summary="Get park activity summary"
)
def get_park_activity_summary(request, park_id: UUID):
"""Get activity summary for a park."""
park = get_object_or_404(Park, id=park_id)
summary = HistoryService.get_activity_summary(
'park', str(park_id), request.user
)
return {
'entity_id': str(park_id),
'entity_type': 'park',
'entity_name': park.name,
**summary
}
```
**Repeat similar patterns for:**
- `rides.py`
- `companies.py`
- `ride_models.py`
- `reviews.py`
---
### Phase 4: Register Routes
**File:** `django/api/v1/api.py`
**Add:**
```python
from .endpoints.history import router as history_router
# After other routers:
api.add_router("/history", history_router)
```
---
### Phase 5: Documentation
**File:** `django/API_HISTORY_ENDPOINTS.md` (CREATE NEW)
Document all history endpoints with:
- Endpoint URLs
- Request/response schemas
- Authentication requirements
- Access control rules
- Example requests/responses
- Rollback safety guidelines
---
## 🔒 Security Considerations
### Rollback Protection
1. **Permission Checks:** Only moderators/admins can rollback
2. **Audit Trail:** Every rollback creates new event
3. **Backup Option:** create_backup flag preserves pre-rollback state
4. **Validation:** Ensure entity exists and event matches entity
### Access Control
1. **Time-Based Limits:**
- Public: 30 days
- Authenticated: 1 year
- Privileged: Unlimited
2. **Event Visibility:** Users can only access events within their time window
3. **Rate Limiting:** Consider adding rate limits for rollback operations
---
## 🧪 Testing Checklist
### Unit Tests
- [ ] HistoryService access control rules
- [ ] Event comparison logic
- [ ] Field history tracking
- [ ] Rollback functionality
- [ ] Access level determination
### Integration Tests
- [ ] List history with different user types
- [ ] Get specific events
- [ ] Compare events
- [ ] Field-specific history
- [ ] Activity summaries
- [ ] Rollback operations (mocked)
### API Tests
- [ ] All GET endpoints return correct data
- [ ] Pagination works correctly
- [ ] Filtering (date range, etc.) works
- [ ] POST rollback requires authentication
- [ ] POST rollback requires proper permissions
- [ ] Invalid requests return appropriate errors
---
## 📊 Performance Optimization
### Database
1. **Indexes:** pghistory automatically indexes `pgh_obj_id` and `pgh_created_at`
2. **Query Optimization:** Use `.only()` to fetch minimal fields
3. **Pagination:** Always paginate large result sets
### Caching
Consider caching:
- Recent history for popular entities (e.g., last 10 events)
- Activity summaries (TTL: 1 hour)
- Field statistics
### Limits
- Max page_size: 100 events
- Max field_history: 100 changes
- Max activity summary: Last 10 events
---
## 🚀 Deployment Checklist
- [ ] All schemas added to `schemas.py`
- [ ] History service tested and working
- [ ] Generic history endpoints created
- [ ] Entity-specific routes added to all 5 entity types
- [ ] Routes registered in `api.py`
- [ ] Documentation complete
- [ ] Tests passing
- [ ] API documentation updated
- [ ] Security review completed
- [ ] Performance tested with large datasets
---
## 📖 Usage Examples
### Get Park History
```bash
# Public user (last 30 days)
GET /api/v1/parks/{park_id}/history/
# Authenticated user (last 1 year)
GET /api/v1/parks/{park_id}/history/
Authorization: Bearer {token}
# With pagination
GET /api/v1/parks/{park_id}/history/?page=2&page_size=50
# With date filtering
GET /api/v1/parks/{park_id}/history/?date_from=2024-01-01&date_to=2024-12-31
```
### Compare Events
```bash
GET /api/v1/parks/{park_id}/history/compare/?event1=12340&event2=12345
Authorization: Bearer {token}
```
### Rollback (Admin Only)
```bash
POST /api/v1/parks/{park_id}/history/{event_id}/rollback/
Authorization: Bearer {admin_token}
Content-Type: application/json
{
"fields": ["status", "description"],
"comment": "Reverting accidental changes",
"create_backup": true
}
```
### Field History
```bash
GET /api/v1/parks/{park_id}/history/field/status/
```
### Activity Summary
```bash
GET /api/v1/parks/{park_id}/history/summary/
```
---
## 🎯 Next Steps
1. **Implement Schemas** - Add all history schemas to `schemas.py`
2. **Create Generic Endpoints** - Implement `history.py`
3. **Add Entity Routes** - Add history routes to each entity endpoint file
4. **Register Routes** - Update `api.py`
5. **Test** - Write and run tests
6. **Document** - Create API documentation
7. **Deploy** - Roll out to production
---
## 📞 Support
For questions or issues with the history API:
1. Review this implementation guide
2. Check the HistoryService docstrings
3. Review pghistory documentation: https://django-pghistory.readthedocs.io/
---
**Status:** Service layer complete. API endpoints and schemas ready for implementation.
**Next Action:** Add history schemas to `schemas.py`, then implement endpoint routes.