Files
thrilltrack-explorer/django-backend/api/v1/endpoints/companies.py

651 lines
22 KiB
Python

"""
Company endpoints for API v1.
Provides CRUD operations for Company entities with filtering and search.
"""
from typing import List, Optional
from uuid import UUID
from django.shortcuts import get_object_or_404
from django.db.models import Q
from ninja import Router, Query
from ninja.pagination import paginate, PageNumberPagination
from apps.entities.models import Company
from apps.entities.services.company_submission import CompanySubmissionService
from apps.users.permissions import jwt_auth, require_auth
from ..schemas import (
CompanyCreate,
CompanyUpdate,
CompanyOut,
CompanyListOut,
ErrorResponse,
HistoryListResponse,
HistoryEventDetailSchema,
HistoryComparisonSchema,
HistoryDiffCurrentSchema,
FieldHistorySchema,
HistoryActivitySummarySchema,
RollbackRequestSchema,
RollbackResponseSchema,
ErrorSchema
)
from ..services.history_service import HistoryService
from django.core.exceptions import ValidationError
import logging
logger = logging.getLogger(__name__)
router = Router(tags=["Companies"])
class CompanyPagination(PageNumberPagination):
"""Custom pagination for companies."""
page_size = 50
@router.get(
"/",
response={200: List[CompanyOut]},
summary="List companies",
description="Get a paginated list of companies with optional filtering"
)
@paginate(CompanyPagination)
def list_companies(
request,
search: Optional[str] = Query(None, description="Search by company name"),
company_type: Optional[str] = Query(None, description="Filter by company type"),
location_id: Optional[UUID] = Query(None, description="Filter by location"),
ordering: Optional[str] = Query("-created", description="Sort by field (prefix with - for descending)")
):
"""
List all companies with optional filters.
**Filters:**
- search: Search company names (case-insensitive partial match)
- company_type: Filter by specific company type
- location_id: Filter by headquarters location
- ordering: Sort results (default: -created)
**Returns:** Paginated list of companies
"""
queryset = Company.objects.all()
# Apply search filter
if search:
queryset = queryset.filter(
Q(name__icontains=search) | Q(description__icontains=search)
)
# Apply company type filter
if company_type:
queryset = queryset.filter(company_types__contains=[company_type])
# Apply location filter
if location_id:
queryset = queryset.filter(location_id=location_id)
# Apply ordering
valid_order_fields = ['name', 'created', 'modified', 'founded_date', 'park_count', 'ride_count']
order_field = ordering.lstrip('-')
if order_field in valid_order_fields:
queryset = queryset.order_by(ordering)
else:
queryset = queryset.order_by('-created')
return queryset
@router.get(
"/{company_id}",
response={200: CompanyOut, 404: ErrorResponse},
summary="Get company",
description="Retrieve a single company by ID"
)
def get_company(request, company_id: UUID):
"""
Get a company by ID.
**Parameters:**
- company_id: UUID of the company
**Returns:** Company details
"""
company = get_object_or_404(Company, id=company_id)
return company
@router.post(
"/",
response={201: CompanyOut, 202: dict, 400: ErrorResponse, 401: ErrorResponse},
summary="Create company",
description="Create a new company through the Sacred Pipeline (requires authentication)"
)
@require_auth
def create_company(request, payload: CompanyCreate):
"""
Create a new company through the Sacred Pipeline.
**Authentication:** Required
**Parameters:**
- payload: Company data (name, company_types, headquarters, etc.)
**Returns:** Created company (moderators) or submission confirmation (regular users)
**Flow:**
- Moderators: Company created immediately (bypass moderation)
- Regular users: Submission created, enters moderation queue
**Note:** All companies flow through ContentSubmission pipeline for moderation.
"""
try:
user = request.auth
# Create company through Sacred Pipeline
submission, company = CompanySubmissionService.create_entity_submission(
user=user,
data=payload.dict(),
source='api',
ip_address=request.META.get('REMOTE_ADDR'),
user_agent=request.META.get('HTTP_USER_AGENT', '')
)
# If moderator bypass happened, Company was created immediately
if company:
logger.info(f"Company created (moderator): {company.id} by {user.email}")
return 201, company
# Regular user: submission pending moderation
logger.info(f"Company submission created: {submission.id} by {user.email}")
return 202, {
'submission_id': str(submission.id),
'status': submission.status,
'message': 'Company submission pending moderation. You will be notified when it is approved.',
}
except ValidationError as e:
return 400, {'detail': str(e)}
except Exception as e:
logger.error(f"Error creating company: {e}")
return 400, {'detail': str(e)}
@router.put(
"/{company_id}",
response={200: CompanyOut, 202: dict, 404: ErrorResponse, 400: ErrorResponse, 401: ErrorResponse},
summary="Update company",
description="Update an existing company through the Sacred Pipeline (requires authentication)"
)
@require_auth
def update_company(request, company_id: UUID, payload: CompanyUpdate):
"""
Update a company through the Sacred Pipeline.
**Authentication:** Required
**Parameters:**
- company_id: UUID of the company
- payload: Updated company data
**Returns:** Updated company (moderators) or submission confirmation (regular users)
**Flow:**
- Moderators: Updates applied immediately (bypass moderation)
- Regular users: Submission created, enters moderation queue
**Note:** All updates flow through ContentSubmission pipeline for moderation.
"""
try:
user = request.auth
company = get_object_or_404(Company, id=company_id)
data = payload.dict(exclude_unset=True)
# Update company through Sacred Pipeline
submission, updated_company = CompanySubmissionService.update_entity_submission(
entity=company,
user=user,
update_data=data,
source='api',
ip_address=request.META.get('REMOTE_ADDR'),
user_agent=request.META.get('HTTP_USER_AGENT', '')
)
# If moderator bypass happened, company was updated immediately
if updated_company:
logger.info(f"Company updated (moderator): {updated_company.id} by {user.email}")
return 200, updated_company
# Regular user: submission pending moderation
logger.info(f"Company update submission created: {submission.id} by {user.email}")
return 202, {
'submission_id': str(submission.id),
'status': submission.status,
'message': 'Company update pending moderation. You will be notified when it is approved.',
}
except ValidationError as e:
return 400, {'detail': str(e)}
except Exception as e:
logger.error(f"Error updating company: {e}")
return 400, {'detail': str(e)}
@router.patch(
"/{company_id}",
response={200: CompanyOut, 202: dict, 404: ErrorResponse, 400: ErrorResponse, 401: ErrorResponse},
summary="Partial update company",
description="Partially update an existing company through the Sacred Pipeline (requires authentication)"
)
@require_auth
def partial_update_company(request, company_id: UUID, payload: CompanyUpdate):
"""
Partially update a company through the Sacred Pipeline.
**Authentication:** Required
**Parameters:**
- company_id: UUID of the company
- payload: Fields to update (only provided fields are updated)
**Returns:** Updated company (moderators) or submission confirmation (regular users)
**Flow:**
- Moderators: Updates applied immediately (bypass moderation)
- Regular users: Submission created, enters moderation queue
**Note:** All updates flow through ContentSubmission pipeline for moderation.
"""
try:
user = request.auth
company = get_object_or_404(Company, id=company_id)
data = payload.dict(exclude_unset=True)
# Update company through Sacred Pipeline
submission, updated_company = CompanySubmissionService.update_entity_submission(
entity=company,
user=user,
update_data=data,
source='api',
ip_address=request.META.get('REMOTE_ADDR'),
user_agent=request.META.get('HTTP_USER_AGENT', '')
)
# If moderator bypass happened, company was updated immediately
if updated_company:
logger.info(f"Company partially updated (moderator): {updated_company.id} by {user.email}")
return 200, updated_company
# Regular user: submission pending moderation
logger.info(f"Company partial update submission created: {submission.id} by {user.email}")
return 202, {
'submission_id': str(submission.id),
'status': submission.status,
'message': 'Company update pending moderation. You will be notified when it is approved.',
}
except ValidationError as e:
return 400, {'detail': str(e)}
except Exception as e:
logger.error(f"Error partially updating company: {e}")
return 400, {'detail': str(e)}
@router.delete(
"/{company_id}",
response={200: dict, 202: dict, 404: ErrorResponse, 400: ErrorResponse, 401: ErrorResponse},
summary="Delete company",
description="Delete a company through the Sacred Pipeline (requires authentication)"
)
@require_auth
def delete_company(request, company_id: UUID):
"""
Delete a company through the Sacred Pipeline.
**Authentication:** Required
**Parameters:**
- company_id: UUID of the company
**Returns:** Deletion confirmation (moderators) or submission confirmation (regular users)
**Flow:**
- Moderators: Company hard-deleted immediately (removed from database)
- Regular users: Deletion request created, enters moderation queue
**Deletion Strategy:**
- Hard Delete: Removes company from database (Company has no status field for soft delete)
**Note:** All deletions flow through ContentSubmission pipeline for moderation.
**Warning:** Deleting a company may affect related parks and rides.
"""
try:
user = request.auth
company = get_object_or_404(Company, id=company_id)
# Delete company through Sacred Pipeline (hard delete - no status field)
submission, deleted = CompanySubmissionService.delete_entity_submission(
entity=company,
user=user,
deletion_type='hard', # Company has no status field
deletion_reason='',
source='api',
ip_address=request.META.get('REMOTE_ADDR'),
user_agent=request.META.get('HTTP_USER_AGENT', '')
)
# If moderator bypass happened, deletion was applied immediately
if deleted:
logger.info(f"Company deleted (moderator): {company_id} by {user.email}")
return 200, {
'message': 'Company deleted successfully',
'entity_id': str(company_id),
'deletion_type': 'hard'
}
# Regular user: deletion pending moderation
logger.info(f"Company deletion submission created: {submission.id} by {user.email}")
return 202, {
'submission_id': str(submission.id),
'status': submission.status,
'message': 'Company deletion request pending moderation. You will be notified when it is approved.',
'entity_id': str(company_id)
}
except ValidationError as e:
return 400, {'detail': str(e)}
except Exception as e:
logger.error(f"Error deleting company: {e}")
return 400, {'detail': str(e)}
@router.get(
"/{company_id}/parks",
response={200: List[dict], 404: ErrorResponse},
summary="Get company parks",
description="Get all parks operated by a company"
)
def get_company_parks(request, company_id: UUID):
"""
Get parks operated by a company.
**Parameters:**
- company_id: UUID of the company
**Returns:** List of parks
"""
company = get_object_or_404(Company, id=company_id)
parks = company.operated_parks.all().values('id', 'name', 'slug', 'status', 'park_type')
return list(parks)
@router.get(
"/{company_id}/rides",
response={200: List[dict], 404: ErrorResponse},
summary="Get company rides",
description="Get all rides manufactured by a company"
)
def get_company_rides(request, company_id: UUID):
"""
Get rides manufactured by a company.
**Parameters:**
- company_id: UUID of the company
**Returns:** List of rides
"""
company = get_object_or_404(Company, id=company_id)
rides = company.manufactured_rides.all().values('id', 'name', 'slug', 'status', 'ride_category')
return list(rides)
# ============================================================================
# History Endpoints
# ============================================================================
@router.get(
'/{company_id}/history/',
response={200: HistoryListResponse, 404: ErrorSchema},
summary="Get company history",
description="Get historical changes for a company"
)
def get_company_history(
request,
company_id: UUID,
page: int = Query(1, ge=1),
page_size: int = Query(50, ge=1, le=100),
date_from: Optional[str] = Query(None, description="Filter from date (YYYY-MM-DD)"),
date_to: Optional[str] = Query(None, description="Filter to date (YYYY-MM-DD)")
):
"""Get history for a company."""
from datetime import datetime
# Verify company exists
company = get_object_or_404(Company, id=company_id)
# Parse dates if provided
date_from_obj = datetime.fromisoformat(date_from).date() if date_from else None
date_to_obj = datetime.fromisoformat(date_to).date() if date_to else None
# Get history
offset = (page - 1) * page_size
events, accessible_count = HistoryService.get_history(
'company', str(company_id), request.user,
date_from=date_from_obj, date_to=date_to_obj,
limit=page_size, offset=offset
)
# Format events
formatted_events = []
for event in events:
formatted_events.append({
'id': event['id'],
'timestamp': event['timestamp'],
'operation': event['operation'],
'snapshot': event['snapshot'],
'changed_fields': event.get('changed_fields'),
'change_summary': event.get('change_summary', ''),
'can_rollback': HistoryService.can_rollback(request.user)
})
# Calculate pagination
total_pages = (accessible_count + page_size - 1) // page_size
return {
'entity_id': str(company_id),
'entity_type': 'company',
'entity_name': company.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': formatted_events,
'pagination': {
'page': page,
'page_size': page_size,
'total_pages': total_pages,
'total_items': accessible_count
}
}
@router.get(
'/{company_id}/history/{event_id}/',
response={200: HistoryEventDetailSchema, 404: ErrorSchema},
summary="Get specific company history event",
description="Get detailed information about a specific historical event"
)
def get_company_history_event(request, company_id: UUID, event_id: int):
"""Get a specific history event for a company."""
company = get_object_or_404(Company, id=company_id)
event = HistoryService.get_event('company', event_id, request.user)
if not event:
return 404, {"error": "Event not found or not accessible"}
return {
'id': event['id'],
'timestamp': event['timestamp'],
'operation': event['operation'],
'entity_id': str(company_id),
'entity_type': 'company',
'entity_name': company.name,
'snapshot': event['snapshot'],
'changed_fields': event.get('changed_fields'),
'metadata': event.get('metadata', {}),
'can_rollback': HistoryService.can_rollback(request.user),
'rollback_preview': None
}
@router.get(
'/{company_id}/history/compare/',
response={200: HistoryComparisonSchema, 400: ErrorSchema, 404: ErrorSchema},
summary="Compare two company history events",
description="Compare two historical events for a company"
)
def compare_company_history(
request,
company_id: UUID,
event1: int = Query(..., description="First event ID"),
event2: int = Query(..., description="Second event ID")
):
"""Compare two historical events for a company."""
company = get_object_or_404(Company, id=company_id)
try:
comparison = HistoryService.compare_events(
'company', event1, event2, request.user
)
if not comparison:
return 404, {"error": "One or both events not found"}
return {
'entity_id': str(company_id),
'entity_type': 'company',
'entity_name': company.name,
'event1': comparison['event1'],
'event2': comparison['event2'],
'differences': comparison['differences'],
'changed_field_count': comparison['changed_field_count'],
'unchanged_field_count': comparison['unchanged_field_count'],
'time_between': comparison['time_between']
}
except ValueError as e:
return 400, {"error": str(e)}
@router.get(
'/{company_id}/history/{event_id}/diff-current/',
response={200: HistoryDiffCurrentSchema, 404: ErrorSchema},
summary="Compare historical event with current state",
description="Compare a historical event with the current company state"
)
def diff_company_history_with_current(request, company_id: UUID, event_id: int):
"""Compare historical event with current company state."""
company = get_object_or_404(Company, id=company_id)
try:
diff = HistoryService.compare_with_current(
'company', event_id, company, request.user
)
if not diff:
return 404, {"error": "Event not found"}
return {
'entity_id': str(company_id),
'entity_type': 'company',
'entity_name': company.name,
'event': diff['event'],
'current_state': diff['current_state'],
'differences': diff['differences'],
'changed_field_count': diff['changed_field_count'],
'time_since': diff['time_since']
}
except ValueError as e:
return 404, {"error": str(e)}
@router.post(
'/{company_id}/history/{event_id}/rollback/',
response={200: RollbackResponseSchema, 400: ErrorSchema, 403: ErrorSchema},
summary="Rollback company to historical state",
description="Rollback company to a historical state (Moderators/Admins only)"
)
def rollback_company(request, company_id: UUID, event_id: int, payload: RollbackRequestSchema):
"""
Rollback company 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"}
company = get_object_or_404(Company, id=company_id)
try:
result = HistoryService.rollback_to_event(
company, 'company', event_id, request.user,
fields=payload.fields,
comment=payload.comment,
create_backup=payload.create_backup
)
return result
except (ValueError, PermissionError) as e:
return 400, {"error": str(e)}
@router.get(
'/{company_id}/history/field/{field_name}/',
response={200: FieldHistorySchema, 404: ErrorSchema},
summary="Get field-specific history",
description="Get history of changes to a specific company field"
)
def get_company_field_history(request, company_id: UUID, field_name: str):
"""Get history of changes to a specific company field."""
company = get_object_or_404(Company, id=company_id)
history = HistoryService.get_field_history(
'company', str(company_id), field_name, request.user
)
return {
'entity_id': str(company_id),
'entity_type': 'company',
'entity_name': company.name,
'field': field_name,
'field_type': 'CharField', # Could introspect this
**history
}
@router.get(
'/{company_id}/history/summary/',
response={200: HistoryActivitySummarySchema, 404: ErrorSchema},
summary="Get company activity summary",
description="Get activity summary for a company"
)
def get_company_activity_summary(request, company_id: UUID):
"""Get activity summary for a company."""
company = get_object_or_404(Company, id=company_id)
summary = HistoryService.get_activity_summary(
'company', str(company_id), request.user
)
return {
'entity_id': str(company_id),
'entity_type': 'company',
'entity_name': company.name,
**summary
}