Coverage for apps / legacy / views.py: 96%
115 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-02-14 19:13 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-02-14 19:13 +0000
1"""Views for legacy frontend."""
3from functools import wraps
5from django.shortcuts import render, redirect, get_object_or_404
7from apps.core.models import AppSettings
8from apps.profiles.models import Profile
9from apps.ai.models import AIPrompt
10from apps.ai.services.openrouter import OpenRouterService, AIUnavailableError, AIResponseError
11from apps.recipes.models import (
12 Recipe,
13 RecipeCollection,
14 RecipeFavorite,
15 RecipeViewHistory,
16)
19def require_profile(view_func):
20 """Decorator to ensure a profile is selected and valid.
22 Gets profile_id from session, validates it exists, and adds
23 the Profile instance to request.profile.
25 Redirects to profile_selector if:
26 - No profile_id in session
27 - Profile doesn't exist (also clears session)
28 """
30 @wraps(view_func)
31 def wrapper(request, *args, **kwargs):
32 profile_id = request.session.get("profile_id")
33 if not profile_id:
34 return redirect("legacy:profile_selector")
36 try:
37 request.profile = Profile.objects.get(id=profile_id)
38 except Profile.DoesNotExist:
39 del request.session["profile_id"]
40 return redirect("legacy:profile_selector")
42 return view_func(request, *args, **kwargs)
44 return wrapper
47def _is_ai_available() -> bool:
48 """Check if AI features are available (key configured AND valid)."""
49 settings = AppSettings.get()
50 if not settings.openrouter_api_key:
51 return False
52 is_valid, _ = OpenRouterService.validate_key_cached()
53 return is_valid
56def profile_selector(request):
57 """Profile selector screen."""
58 profiles = list(Profile.objects.all().values("id", "name", "avatar_color", "theme", "unit_preference"))
59 return render(
60 request,
61 "legacy/profile_selector.html",
62 {
63 "profiles": profiles,
64 },
65 )
68@require_profile
69def home(request):
70 """Home screen."""
71 profile = request.profile
73 # Get favorites for this profile
74 favorites_qs = RecipeFavorite.objects.filter(profile=profile).select_related("recipe").order_by("-created_at")
75 favorites_count = favorites_qs.count()
76 favorites = favorites_qs[:12]
78 # Get recently viewed for this profile
79 history_qs = RecipeViewHistory.objects.filter(profile=profile).select_related("recipe").order_by("-viewed_at")
80 history = history_qs[:6]
82 # Get total recipe count (for "My Recipes" link)
83 recipes_count = Recipe.objects.filter(profile=profile).count()
85 # Build favorite recipe IDs set for checking
86 favorite_recipe_ids = set(f.recipe_id for f in favorites)
88 # Check if AI features are available
89 ai_available = _is_ai_available()
91 return render(
92 request,
93 "legacy/home.html",
94 {
95 "profile": {
96 "id": profile.id,
97 "name": profile.name,
98 "avatar_color": profile.avatar_color,
99 },
100 "favorites": favorites,
101 "favorites_count": favorites_count,
102 "history": history,
103 "recipes_count": recipes_count,
104 "favorite_recipe_ids": favorite_recipe_ids,
105 "ai_available": ai_available,
106 },
107 )
110@require_profile
111def search(request):
112 """Search results screen."""
113 profile = request.profile
115 query = request.GET.get("q", "")
116 # Detect if query is a URL
117 is_url = query.strip().startswith("http://") or query.strip().startswith("https://")
119 return render(
120 request,
121 "legacy/search.html",
122 {
123 "profile": {
124 "id": profile.id,
125 "name": profile.name,
126 "avatar_color": profile.avatar_color,
127 },
128 "query": query,
129 "is_url": is_url,
130 },
131 )
134@require_profile
135def recipe_detail(request, recipe_id):
136 """Recipe detail screen."""
137 profile = request.profile
139 # Get the recipe (must belong to this profile)
140 recipe = get_object_or_404(Recipe, id=recipe_id, profile=profile)
142 # Record view history
143 RecipeViewHistory.objects.update_or_create(
144 profile=profile,
145 recipe=recipe,
146 defaults={}, # Just update viewed_at (auto_now)
147 )
149 # Check if recipe is favorited
150 is_favorite = RecipeFavorite.objects.filter(
151 profile=profile,
152 recipe=recipe,
153 ).exists()
155 # Get user's collections for the "add to collection" feature
156 collections = RecipeCollection.objects.filter(profile=profile)
158 # Check if AI features are available
159 ai_available = _is_ai_available()
161 # Prepare ingredient groups or flat list
162 has_ingredient_groups = bool(recipe.ingredient_groups)
164 # Prepare instructions
165 instructions = recipe.instructions
166 if not instructions and recipe.instructions_text:
167 instructions = [s.strip() for s in recipe.instructions_text.split("\n") if s.strip()]
169 # Build linked recipes for navigation
170 linked_recipes = []
171 if recipe.remixed_from_id:
172 # This is a remix - link to original
173 original = recipe.remixed_from
174 if original:
175 linked_recipes.append(
176 {
177 "id": original.id,
178 "title": original.title,
179 "relationship": "original",
180 }
181 )
182 # Also find sibling remixes (other remixes of the same original)
183 siblings = Recipe.objects.filter(
184 remixed_from=original,
185 profile=profile,
186 ).exclude(id=recipe.id)[:5]
187 for sibling in siblings:
188 linked_recipes.append(
189 {
190 "id": sibling.id,
191 "title": sibling.title,
192 "relationship": "sibling",
193 }
194 )
195 else:
196 # This is an original - find its remixes
197 remixes = Recipe.objects.filter(
198 remixed_from=recipe,
199 profile=profile,
200 )[:5]
201 for remix in remixes:
202 linked_recipes.append(
203 {
204 "id": remix.id,
205 "title": remix.title,
206 "relationship": "remix",
207 }
208 )
210 return render(
211 request,
212 "legacy/recipe_detail.html",
213 {
214 "profile": {
215 "id": profile.id,
216 "name": profile.name,
217 "avatar_color": profile.avatar_color,
218 },
219 "recipe": recipe,
220 "is_favorite": is_favorite,
221 "collections": collections,
222 "ai_available": ai_available,
223 "has_ingredient_groups": has_ingredient_groups,
224 "instructions": instructions,
225 "linked_recipes": linked_recipes,
226 },
227 )
230@require_profile
231def play_mode(request, recipe_id):
232 """Play mode / cooking mode screen."""
233 profile = request.profile
235 # Get the recipe (must belong to this profile)
236 recipe = get_object_or_404(Recipe, id=recipe_id, profile=profile)
238 # Check if AI features are available
239 ai_available = _is_ai_available()
241 # Prepare instructions
242 instructions = recipe.instructions
243 if not instructions and recipe.instructions_text:
244 instructions = [s.strip() for s in recipe.instructions_text.split("\n") if s.strip()]
246 return render(
247 request,
248 "legacy/play_mode.html",
249 {
250 "profile": {
251 "id": profile.id,
252 "name": profile.name,
253 "avatar_color": profile.avatar_color,
254 },
255 "recipe": recipe,
256 "instructions": instructions,
257 "instructions_json": instructions, # For JavaScript
258 "ai_available": ai_available,
259 },
260 )
263@require_profile
264def all_recipes(request):
265 """My Recipes screen - shows all recipes owned by this profile."""
266 profile = request.profile
268 # Get all recipes for this profile (imports + remixes)
269 recipes = Recipe.objects.filter(profile=profile).order_by("-scraped_at")
271 # Build set of favorite recipe IDs for display
272 favorite_recipe_ids = set(RecipeFavorite.objects.filter(profile=profile).values_list("recipe_id", flat=True))
274 return render(
275 request,
276 "legacy/all_recipes.html",
277 {
278 "profile": {
279 "id": profile.id,
280 "name": profile.name,
281 "avatar_color": profile.avatar_color,
282 },
283 "recipes": recipes,
284 "favorite_recipe_ids": favorite_recipe_ids,
285 },
286 )
289@require_profile
290def favorites(request):
291 """Favorites screen - shows all favorited recipes."""
292 profile = request.profile
294 # Get all favorites for this profile
295 favorites = RecipeFavorite.objects.filter(profile=profile).select_related("recipe").order_by("-created_at")
297 return render(
298 request,
299 "legacy/favorites.html",
300 {
301 "profile": {
302 "id": profile.id,
303 "name": profile.name,
304 "avatar_color": profile.avatar_color,
305 },
306 "favorites": favorites,
307 },
308 )
311@require_profile
312def collections(request):
313 """Collections list screen."""
314 profile = request.profile
316 # Get all collections for this profile
317 collections = (
318 RecipeCollection.objects.filter(profile=profile).prefetch_related("items__recipe").order_by("-updated_at")
319 )
321 return render(
322 request,
323 "legacy/collections.html",
324 {
325 "profile": {
326 "id": profile.id,
327 "name": profile.name,
328 "avatar_color": profile.avatar_color,
329 },
330 "collections": collections,
331 },
332 )
335@require_profile
336def collection_detail(request, collection_id):
337 """Collection detail screen - shows recipes in a collection."""
338 profile = request.profile
340 # Get the collection (must belong to this profile)
341 collection = get_object_or_404(RecipeCollection, id=collection_id, profile=profile)
343 # Get all items in this collection
344 items = collection.items.select_related("recipe").order_by("order", "-added_at")
346 # Build set of favorite recipe IDs for display
347 favorite_recipe_ids = set(RecipeFavorite.objects.filter(profile=profile).values_list("recipe_id", flat=True))
349 return render(
350 request,
351 "legacy/collection_detail.html",
352 {
353 "profile": {
354 "id": profile.id,
355 "name": profile.name,
356 "avatar_color": profile.avatar_color,
357 },
358 "collection": collection,
359 "items": items,
360 "favorite_recipe_ids": favorite_recipe_ids,
361 },
362 )
365@require_profile
366def settings(request):
367 """Settings screen - AI prompts and sources configuration."""
368 profile = request.profile
370 # Get app settings
371 app_settings = AppSettings.get()
373 # Check if AI features are available
374 ai_available = _is_ai_available()
376 # Get all AI prompts
377 prompts = list(AIPrompt.objects.all().order_by("name"))
379 # Get available models from OpenRouter
380 try:
381 service = OpenRouterService()
382 models = service.get_available_models()
383 except (AIUnavailableError, AIResponseError):
384 models = []
386 return render(
387 request,
388 "legacy/settings.html",
389 {
390 "profile": {
391 "id": profile.id,
392 "name": profile.name,
393 "avatar_color": profile.avatar_color,
394 },
395 "current_profile_id": profile.id,
396 "ai_available": ai_available,
397 "default_model": app_settings.default_ai_model,
398 "prompts": prompts,
399 "models": models,
400 },
401 )