Coverage for apps / core / api.py: 91%
87 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-11 00:40 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-11 00:40 +0000
1"""System API for administrative operations like database reset."""
3import os
4import shutil
6from django.conf import settings
7from django.core.cache import cache
8from django.core.management import call_command
9from django.contrib.sessions.models import Session
10from ninja import Router, Schema
12from apps.profiles.models import Profile
13from apps.recipes.models import (
14 Recipe,
15 RecipeFavorite,
16 RecipeCollection,
17 RecipeCollectionItem,
18 RecipeViewHistory,
19 SearchSource,
20 ServingAdjustment,
21 CachedSearchImage,
22)
23from apps.ai.models import AIDiscoverySuggestion, AIPrompt
25router = Router(tags=['system'])
28class HealthSchema(Schema):
29 status: str
30 database: str
33@router.get('/health/', response=HealthSchema)
34def health_check(request):
35 """Simple health check for container orchestration."""
36 from django.db import connection
37 try:
38 with connection.cursor() as cursor:
39 cursor.execute('SELECT 1')
40 return {'status': 'healthy', 'database': 'ok'}
41 except Exception:
42 return {'status': 'unhealthy', 'database': 'error'}
45class DataCountsSchema(Schema):
46 profiles: int
47 recipes: int
48 recipe_images: int
49 favorites: int
50 collections: int
51 collection_items: int
52 view_history: int
53 ai_suggestions: int
54 serving_adjustments: int
55 cached_search_images: int
58class ResetPreviewSchema(Schema):
59 data_counts: DataCountsSchema
60 preserved: list[str]
61 warnings: list[str]
64class ResetConfirmSchema(Schema):
65 confirmation_text: str # Must be "RESET"
68class ErrorSchema(Schema):
69 error: str
70 message: str
73class ResetSuccessSchema(Schema):
74 success: bool
75 message: str
76 actions_performed: list[str]
79@router.get('/reset-preview/', response=ResetPreviewSchema)
80def get_reset_preview(request):
81 """Get summary of data that will be deleted on reset."""
82 return {
83 'data_counts': {
84 'profiles': Profile.objects.count(),
85 'recipes': Recipe.objects.count(),
86 'recipe_images': Recipe.objects.exclude(image='').exclude(
87 image__isnull=True
88 ).count(),
89 'favorites': RecipeFavorite.objects.count(),
90 'collections': RecipeCollection.objects.count(),
91 'collection_items': RecipeCollectionItem.objects.count(),
92 'view_history': RecipeViewHistory.objects.count(),
93 'ai_suggestions': AIDiscoverySuggestion.objects.count(),
94 'serving_adjustments': ServingAdjustment.objects.count(),
95 'cached_search_images': CachedSearchImage.objects.count(),
96 },
97 'preserved': [
98 'Search source configurations',
99 'AI prompt templates',
100 'Application settings',
101 ],
102 'warnings': [
103 'All user data will be permanently deleted',
104 'All recipe images will be removed from storage',
105 'This action cannot be undone',
106 ],
107 }
110@router.post('/reset/', response={200: ResetSuccessSchema, 400: ErrorSchema})
111def reset_database(request, data: ResetConfirmSchema):
112 """
113 Completely reset the database to factory state.
115 Requires confirmation_text="RESET" to proceed.
116 """
117 if data.confirmation_text != 'RESET':
118 return 400, {
119 'error': 'invalid_confirmation',
120 'message': 'Type RESET to confirm',
121 }
123 try:
124 # 1. Clear database tables (order matters for FK constraints)
125 # Start with leaf tables that depend on others
126 AIDiscoverySuggestion.objects.all().delete()
127 ServingAdjustment.objects.all().delete()
128 RecipeViewHistory.objects.all().delete()
129 RecipeCollectionItem.objects.all().delete()
130 RecipeCollection.objects.all().delete()
131 RecipeFavorite.objects.all().delete()
132 CachedSearchImage.objects.all().delete()
134 # Delete all recipes (this will cascade to related items)
135 Recipe.objects.all().delete()
137 # Delete all profiles
138 Profile.objects.all().delete()
140 # Reset SearchSource failure counters (keep selectors)
141 SearchSource.objects.all().update(
142 consecutive_failures=0,
143 needs_attention=False,
144 last_validated_at=None,
145 )
147 # 2. Clear recipe images
148 images_dir = os.path.join(settings.MEDIA_ROOT, 'recipe_images')
149 if os.path.exists(images_dir):
150 shutil.rmtree(images_dir)
151 os.makedirs(images_dir) # Recreate empty directory
153 # Clear cached search images
154 search_images_dir = os.path.join(settings.MEDIA_ROOT, 'search_images')
155 if os.path.exists(search_images_dir):
156 shutil.rmtree(search_images_dir)
157 os.makedirs(search_images_dir) # Recreate empty directory
159 # 3. Clear Django cache
160 cache.clear()
162 # 4. Clear all sessions
163 Session.objects.all().delete()
165 # 5. Re-run migrations (ensures clean state)
166 call_command('migrate', verbosity=0)
168 # 6. Re-seed default data
169 try:
170 call_command('seed_search_sources', verbosity=0)
171 except Exception:
172 pass # Command may not exist yet
174 try:
175 call_command('seed_ai_prompts', verbosity=0)
176 except Exception:
177 pass # Command may not exist yet
179 return {
180 'success': True,
181 'message': 'Database reset complete',
182 'actions_performed': [
183 'Deleted all user profiles',
184 'Deleted all recipes and images',
185 'Cleared all favorites and collections',
186 'Cleared all view history',
187 'Cleared all AI cache data',
188 'Cleared all cached search images',
189 'Reset search source counters',
190 'Cleared application cache',
191 'Cleared all sessions',
192 'Re-ran database migrations',
193 'Restored default seed data',
194 ],
195 }
197 except Exception as e:
198 return 400, {'error': 'reset_failed', 'message': str(e)}