django-lucy-assist 1.2.2__py3-none-any.whl → 1.2.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -160,6 +160,8 @@ class CRUDService:
160
160
  """
161
161
  Appelle une vue du projet via RequestFactory.
162
162
 
163
+ Utilise la découverte dynamique des vues (cache mémoire 5 min).
164
+
163
165
  Args:
164
166
  model_name: Nom du modèle
165
167
  action: Action CRUD ('create', 'update', 'delete', 'list', 'detail')
@@ -169,13 +171,10 @@ class CRUDService:
169
171
  Returns:
170
172
  Dict avec 'success', 'response', 'messages' ou None si pas de vue
171
173
  """
172
- view_info = self.config.get_crud_view_for_model(model_name, action)
173
-
174
- # Essayer aussi avec des variations du nom de modèle
175
- if not view_info:
176
- from lucy_assist.services.view_discovery_service import ViewDiscoveryService
177
- service = ViewDiscoveryService()
178
- view_info = service.get_view_info(model_name, action)
174
+ # Utiliser la découverte dynamique des vues (cache mémoire)
175
+ from lucy_assist.services.view_discovery_service import ViewDiscoveryService
176
+ service = ViewDiscoveryService()
177
+ view_info = service.get_view_info(model_name, action)
179
178
 
180
179
  if not view_info:
181
180
  LogUtils.info(f"[CRUD] Pas de vue trouvée pour {model_name}.{action}")
@@ -187,6 +186,36 @@ class CRUDService:
187
186
 
188
187
  LogUtils.info(f"[CRUD] Appel vue {url_name} ({method}) pour {model_name}.{action}")
189
188
 
189
+ # Vérifier que l'utilisateur est authentifié
190
+ if not self.user or not self.user.is_authenticated:
191
+ LogUtils.warning(f"[CRUD] Utilisateur non authentifié pour {model_name}.{action}")
192
+ return {
193
+ 'success': False,
194
+ 'error': "Vous devez être connecté pour effectuer cette action.",
195
+ 'permission_denied': True,
196
+ 'messages': []
197
+ }
198
+
199
+ # Vérifier les permissions Django standard (optionnel mais rapide)
200
+ action_to_perm = {
201
+ 'create': 'add',
202
+ 'update': 'change',
203
+ 'delete': 'delete',
204
+ 'detail': 'view',
205
+ 'list': 'view',
206
+ }
207
+ perm_action = action_to_perm.get(action)
208
+ if perm_action and not self.user.is_superuser:
209
+ # Trouver l'app_label du modèle
210
+ model = self.get_model('', model_name)
211
+ if model:
212
+ app_label = model._meta.app_label
213
+ perm_codename = f"{app_label}.{perm_action}_{model_name.lower()}"
214
+ if not self.user.has_perm(perm_codename):
215
+ LogUtils.warning(f"[CRUD] Permission Django manquante: {perm_codename}")
216
+ # On ne bloque pas ici car la vue peut avoir des permissions custom
217
+ # mais on log pour le debug
218
+
190
219
  try:
191
220
  # Construire l'URL
192
221
  if requires_pk and object_id:
@@ -226,21 +255,49 @@ class CRUDService:
226
255
  self._cleanup_thread_local()
227
256
 
228
257
  # Analyser la réponse
229
- success = response.status_code in [200, 201, 302]
258
+ redirect_url = response.get('Location', '') if response.status_code == 302 else ''
230
259
  messages = self._extract_messages(request)
231
260
 
232
- # Détecter les erreurs dans la réponse
261
+ # Vérifier si c'est une redirection vers login (permission refusée)
262
+ is_permission_denied = False
263
+ if response.status_code == 302 and redirect_url:
264
+ login_patterns = ['/login', '/connexion', '/auth', 'accounts/login']
265
+ if any(pattern in redirect_url.lower() for pattern in login_patterns):
266
+ is_permission_denied = True
267
+ LogUtils.warning(f"[CRUD] Permission refusée - redirection vers {redirect_url}")
268
+
269
+ # Vérifier si c'est une erreur 403 (Forbidden)
270
+ if response.status_code == 403:
271
+ is_permission_denied = True
272
+ LogUtils.warning(f"[CRUD] Permission refusée - HTTP 403")
273
+
274
+ if is_permission_denied:
275
+ return {
276
+ 'success': False,
277
+ 'status_code': response.status_code,
278
+ 'error': "Vous n'avez pas les permissions nécessaires pour effectuer cette action.",
279
+ 'permission_denied': True,
280
+ 'messages': messages
281
+ }
282
+
283
+ # Déterminer le succès
284
+ success = response.status_code in [200, 201, 302]
285
+
286
+ # Détecter les erreurs dans la réponse HTML
233
287
  if hasattr(response, 'content'):
234
288
  content = response.content.decode('utf-8', errors='replace')
235
289
  # Chercher des indicateurs d'erreur dans le HTML
236
290
  if 'class="error' in content or 'class="invalid' in content:
237
291
  success = False
292
+ # Détecter les messages d'erreur de permission dans le contenu
293
+ if 'permission' in content.lower() and ('denied' in content.lower() or 'refusé' in content.lower()):
294
+ success = False
238
295
 
239
296
  return {
240
297
  'success': success,
241
298
  'status_code': response.status_code,
242
299
  'messages': messages,
243
- 'redirect_url': response.get('Location') if response.status_code == 302 else None
300
+ 'redirect_url': redirect_url if response.status_code == 302 else None
244
301
  }
245
302
 
246
303
  except NoReverseMatch as e:
@@ -480,7 +537,16 @@ class CRUDService:
480
537
  'via_view': True
481
538
  }
482
539
 
483
- # Fallback: création directe via le modèle
540
+ # Si CRUD_VIEWS_ONLY est activé, refuser l'opération
541
+ if lucy_assist_settings.get('CRUD_VIEWS_ONLY', True):
542
+ LogUtils.info(f"[CRUD] Aucune vue trouvée pour {model_name}.create et CRUD_VIEWS_ONLY=True")
543
+ return {
544
+ 'success': False,
545
+ 'errors': [f"Aucune vue de création trouvée pour {model_name}. L'opération CRUD n'est pas disponible pour ce modèle."],
546
+ 'no_view_found': True
547
+ }
548
+
549
+ # Fallback: création directe via le modèle (si autorisé)
484
550
  LogUtils.info(f"[CRUD] Fallback création directe pour {model_name}")
485
551
  return self._create_object_direct(app_name, model_name, data)
486
552
 
@@ -594,7 +660,16 @@ class CRUDService:
594
660
  'via_view': True
595
661
  }
596
662
 
597
- # Fallback: mise à jour directe via le modèle
663
+ # Si CRUD_VIEWS_ONLY est activé, refuser l'opération
664
+ if lucy_assist_settings.get('CRUD_VIEWS_ONLY', True):
665
+ LogUtils.info(f"[CRUD] Aucune vue trouvée pour {model_name}.update et CRUD_VIEWS_ONLY=True")
666
+ return {
667
+ 'success': False,
668
+ 'errors': [f"Aucune vue de modification trouvée pour {model_name}. L'opération CRUD n'est pas disponible pour ce modèle."],
669
+ 'no_view_found': True
670
+ }
671
+
672
+ # Fallback: mise à jour directe via le modèle (si autorisé)
598
673
  LogUtils.info(f"[CRUD] Fallback mise à jour directe pour {model_name}")
599
674
  return self._update_object_direct(app_name, model_name, object_id, data)
600
675
 
@@ -715,7 +790,16 @@ class CRUDService:
715
790
  'via_view': True
716
791
  }
717
792
 
718
- # Fallback: suppression directe via le modèle
793
+ # Si CRUD_VIEWS_ONLY est activé, refuser l'opération
794
+ if lucy_assist_settings.get('CRUD_VIEWS_ONLY', True):
795
+ LogUtils.info(f"[CRUD] Aucune vue trouvée pour {model_name}.delete et CRUD_VIEWS_ONLY=True")
796
+ return {
797
+ 'success': False,
798
+ 'errors': [f"Aucune vue de suppression trouvée pour {model_name}. L'opération CRUD n'est pas disponible pour ce modèle."],
799
+ 'no_view_found': True
800
+ }
801
+
802
+ # Fallback: suppression directe via le modèle (si autorisé)
719
803
  LogUtils.info(f"[CRUD] Fallback suppression directe pour {model_name}")
720
804
  return self._delete_object_direct(app_name, model_name, object_id)
721
805
 
@@ -35,6 +35,26 @@ class GitLabService:
35
35
  """Construit l'URL de l'API."""
36
36
  return f"{self.base_url}/api/v4{endpoint}"
37
37
 
38
+ def get_default_branch(self) -> str:
39
+ """
40
+ Recupere la branche par defaut du projet.
41
+
42
+ Returns:
43
+ Nom de la branche par defaut ou 'main' si erreur
44
+ """
45
+ if not self.token or not self.project_id:
46
+ return 'main'
47
+
48
+ try:
49
+ url = self._api_url(f"/projects/{self.project_id}")
50
+ response = requests.get(url, headers=self.headers, timeout=10)
51
+ response.raise_for_status()
52
+ project_info = response.json()
53
+ return project_info.get('default_branch', 'main')
54
+ except requests.RequestException as e:
55
+ LogUtils.error(f"Erreur lors de la recuperation de la branche par defaut: {e}")
56
+ return 'main'
57
+
38
58
  def search_code(
39
59
  self,
40
60
  query: str,
@@ -76,14 +96,14 @@ class GitLabService:
76
96
  def get_file_content(
77
97
  self,
78
98
  file_path: str,
79
- ref: str = 'main'
99
+ ref: str = None
80
100
  ) -> Optional[str]:
81
101
  """
82
- Récupère le contenu d'un fichier.
102
+ Recupere le contenu d'un fichier.
83
103
 
84
104
  Args:
85
105
  file_path: Chemin du fichier dans le repo
86
- ref: Branche ou tag
106
+ ref: Branche ou tag (None = branche par defaut)
87
107
 
88
108
  Returns:
89
109
  Contenu du fichier ou None
@@ -91,6 +111,10 @@ class GitLabService:
91
111
  if not self.token or not self.project_id:
92
112
  return None
93
113
 
114
+ # Utiliser la branche par defaut si non specifiee
115
+ if ref is None:
116
+ ref = self.get_default_branch()
117
+
94
118
  try:
95
119
  encoded_path = quote(file_path, safe='')
96
120
  url = self._api_url(
@@ -104,16 +128,16 @@ class GitLabService:
104
128
  return response.text
105
129
 
106
130
  except requests.RequestException as e:
107
- LogUtils.error(f"Erreur lors de la récupération du fichier {file_path}: {e}")
131
+ LogUtils.error(f"Erreur lors de la recuperation du fichier {file_path}: {e}")
108
132
  return None
109
133
 
110
134
  def get_file_blame(
111
135
  self,
112
136
  file_path: str,
113
- ref: str = 'main'
137
+ ref: str = None
114
138
  ) -> List[Dict]:
115
139
  """
116
- Récupère le blame d'un fichier (qui a modifié quoi).
140
+ Recupere le blame d'un fichier (qui a modifie quoi).
117
141
 
118
142
  Returns:
119
143
  Liste des blocs de blame
@@ -121,6 +145,10 @@ class GitLabService:
121
145
  if not self.token or not self.project_id:
122
146
  return []
123
147
 
148
+ # Utiliser la branche par defaut si non specifiee
149
+ if ref is None:
150
+ ref = self.get_default_branch()
151
+
124
152
  try:
125
153
  encoded_path = quote(file_path, safe='')
126
154
  url = self._api_url(
@@ -218,6 +246,51 @@ class GitLabService:
218
246
 
219
247
  return result
220
248
 
249
+ def get_repository_tree(
250
+ self,
251
+ path: str = '',
252
+ ref: str = None,
253
+ recursive: bool = True,
254
+ per_page: int = 100
255
+ ) -> List[Dict]:
256
+ """
257
+ Liste les fichiers et dossiers du repository.
258
+
259
+ Args:
260
+ path: Chemin du dossier (vide = racine)
261
+ ref: Branche ou tag (None = branche par defaut)
262
+ recursive: Inclure les sous-dossiers
263
+ per_page: Nombre de resultats par page
264
+
265
+ Returns:
266
+ Liste des fichiers/dossiers avec 'name', 'path', 'type'
267
+ """
268
+ if not self.token or not self.project_id:
269
+ return []
270
+
271
+ # Utiliser la branche par defaut si non specifiee
272
+ if ref is None:
273
+ ref = self.get_default_branch()
274
+
275
+ try:
276
+ url = self._api_url(f"/projects/{self.project_id}/repository/tree")
277
+ params = {
278
+ 'ref': ref,
279
+ 'recursive': str(recursive).lower(),
280
+ 'per_page': per_page
281
+ }
282
+ if path:
283
+ params['path'] = path
284
+
285
+ response = requests.get(url, headers=self.headers, params=params, timeout=15)
286
+ response.raise_for_status()
287
+
288
+ return response.json()
289
+
290
+ except requests.RequestException as e:
291
+ LogUtils.error(f"Erreur lors de la recuperation de l'arborescence: {e}")
292
+ return []
293
+
221
294
  def get_recent_commits(self, per_page: int = 10) -> List[Dict]:
222
295
  """
223
296
  Récupère les commits récents.
@@ -60,6 +60,8 @@ class MistralService:
60
60
  Utilise le prompt stocke en base de donnees et le cache
61
61
  pour reduire la redondance des informations sur le projet.
62
62
  """
63
+ from lucy_assist.services.context_service import ContextService
64
+
63
65
  # Recuperer les permissions utilisateur (compressees)
64
66
  user_permissions = []
65
67
  if hasattr(user, 'get_all_permissions'):
@@ -69,25 +71,43 @@ class MistralService:
69
71
 
70
72
  # Recuperer le contexte projet optimise depuis le cache
71
73
  page_url = page_context.get('page_url', page_context.get('url', ''))
72
- optimized_context = self.project_context_service.get_optimized_context(
73
- page_url=page_url,
74
- user_question=user_question
75
- )
76
74
 
77
- # Fusionner le contexte de page avec le contexte projet cache
75
+ # Utiliser ContextService pour obtenir les modeles pertinents (optimisation tokens)
76
+ context_service = ContextService(user)
77
+ relevant_models = context_service.get_relevant_models(page_url)
78
+
79
+ # Formater les modeles pertinents (avec details)
80
+ models_description = []
81
+ for model in relevant_models:
82
+ prefix = "[PAGE ACTUELLE] " if model.get('is_current') else ""
83
+ fields_str = ", ".join(model.get('fields', []))
84
+ models_description.append(
85
+ f"{prefix}{model['name']} ({model['name_lower']}): {fields_str}"
86
+ )
87
+
88
+ # Ajouter la liste complete de TOUS les modeles disponibles (juste les noms)
89
+ all_model_names = context_service.get_all_model_names()
90
+ if all_model_names:
91
+ models_description.append("")
92
+ models_description.append("Liste complete des modeles existants: " + ", ".join(all_model_names))
93
+
94
+ available_models = "\n".join(models_description) if models_description else "Aucun modele"
95
+
96
+ # Enrichir le contexte de page avec l'aide contextuelle
97
+ page_help = context_service.get_page_help(page_url)
78
98
  enriched_context = {
79
- 'page': page_context,
80
- 'projet': optimized_context.get('relevant_info', {}),
81
- 'cache_stats': optimized_context.get('stats', {})
99
+ 'url': page_url,
100
+ 'type_page': page_help.get('page_type'),
101
+ 'modele_actuel': page_help.get('current_model'),
102
+ 'aide': page_help.get('help_message'),
103
+ 'actions_suggerees': page_help.get('suggested_actions', []),
104
+ 'liens_utiles': page_help.get('useful_links', [])
82
105
  }
83
106
 
84
107
  # Recuperer la configuration avec le prompt systeme
85
108
  from lucy_assist.models import ConfigurationLucyAssist
86
109
  config = ConfigurationLucyAssist.get_config()
87
110
 
88
- # Generer la description des modeles disponibles
89
- available_models = config.get_available_models_description()
90
-
91
111
  # Construire le prompt depuis la configuration
92
112
  prompt = config.get_system_prompt(
93
113
  page_context=json.dumps(enriched_context, ensure_ascii=False, indent=2),
@@ -54,6 +54,9 @@ class ToolExecutorService:
54
54
  'get_form_fields': self._handle_get_form_fields,
55
55
  'navigate_to_page': self._handle_navigate_to_page,
56
56
  'analyze_bug': self._handle_analyze_bug,
57
+ 'get_page_help': self._handle_get_page_help,
58
+ 'search_code_gitlab': self._handle_search_code_gitlab,
59
+ 'get_file_content_gitlab': self._handle_get_file_content_gitlab,
57
60
  }
58
61
 
59
62
  handler = handlers.get(tool_name)
@@ -371,6 +374,94 @@ class ToolExecutorService:
371
374
  }
372
375
 
373
376
 
377
+ def _handle_get_page_help(self, params: Dict) -> Dict:
378
+ """
379
+ Recupere l'aide contextuelle pour la page actuelle.
380
+ """
381
+ url_path = params.get('url_path', '')
382
+
383
+ if not url_path:
384
+ return {'error': 'url_path est requis'}
385
+
386
+ help_info = self.context_service.get_page_help(url_path)
387
+
388
+ return {
389
+ 'success': True,
390
+ **help_info
391
+ }
392
+
393
+ def _handle_search_code_gitlab(self, params: Dict) -> Dict:
394
+ """
395
+ Recherche dans le code source du projet via GitLab.
396
+ """
397
+ query = params.get('query', '')
398
+ file_type = params.get('file_type', 'all')
399
+
400
+ if not query:
401
+ return {'error': 'query est requis'}
402
+
403
+ # Adapter la recherche selon le type de fichier
404
+ search_query = query
405
+ if file_type != 'all':
406
+ # Ajouter un filtre de chemin pour cibler le type de fichier
407
+ search_query = f"{query} path:{file_type}"
408
+
409
+ results = self.gitlab_service.search_code(search_query, per_page=10)
410
+
411
+ if not results:
412
+ return {
413
+ 'success': True,
414
+ 'count': 0,
415
+ 'message': f'Aucun resultat trouve pour "{query}"',
416
+ 'results': []
417
+ }
418
+
419
+ # Formater les resultats
420
+ formatted_results = []
421
+ for result in results[:10]:
422
+ formatted_results.append({
423
+ 'file': result.get('filename', ''),
424
+ 'content_preview': result.get('data', '')[:500],
425
+ 'ref': result.get('ref', '')
426
+ })
427
+
428
+ return {
429
+ 'success': True,
430
+ 'count': len(formatted_results),
431
+ 'query': query,
432
+ 'results': formatted_results
433
+ }
434
+
435
+ def _handle_get_file_content_gitlab(self, params: Dict) -> Dict:
436
+ """
437
+ Recupere le contenu complet d'un fichier depuis GitLab.
438
+ """
439
+ file_path = params.get('file_path', '')
440
+
441
+ if not file_path:
442
+ return {'error': 'file_path est requis'}
443
+
444
+ content = self.gitlab_service.get_file_content(file_path)
445
+
446
+ if content is None:
447
+ return {
448
+ 'success': False,
449
+ 'error': f'Fichier non trouve: {file_path}'
450
+ }
451
+
452
+ # Limiter la taille du contenu retourne
453
+ max_content_size = 10000
454
+ truncated = len(content) > max_content_size
455
+
456
+ return {
457
+ 'success': True,
458
+ 'file_path': file_path,
459
+ 'content': content[:max_content_size],
460
+ 'truncated': truncated,
461
+ 'total_size': len(content)
462
+ }
463
+
464
+
374
465
  def create_tool_executor(user):
375
466
  """
376
467
  Factory function pour créer un callable d'exécution de tools.
@@ -274,6 +274,71 @@ LUCY_ASSIST_TOOLS = [
274
274
  "required": ["user_description"]
275
275
  }
276
276
  }
277
+ },
278
+ {
279
+ "type": "function",
280
+ "function": {
281
+ "name": "get_page_help",
282
+ "description": """Recupere l'aide contextuelle pour la page actuelle.
283
+ Utilise ce tool pour obtenir des informations sur ce que l'utilisateur peut faire sur la page.
284
+ Retourne les actions suggerees, liens utiles et conseils.""",
285
+ "parameters": {
286
+ "type": "object",
287
+ "properties": {
288
+ "url_path": {
289
+ "type": "string",
290
+ "description": "URL de la page actuelle (disponible dans le contexte)"
291
+ }
292
+ },
293
+ "required": ["url_path"]
294
+ }
295
+ }
296
+ },
297
+ {
298
+ "type": "function",
299
+ "function": {
300
+ "name": "search_code_gitlab",
301
+ "description": """USAGE INTERNE - Recherche dans le code source pour comprendre le fonctionnement.
302
+ Utilise ce tool pour comprendre comment fonctionne une fonctionnalite.
303
+ IMPORTANT: Les resultats sont pour TON usage interne uniquement.
304
+ Ne JAMAIS montrer les chemins de fichiers ou le code a l'utilisateur.
305
+ Utilise ces informations pour repondre en termes simples avec des liens vers les pages.""",
306
+ "parameters": {
307
+ "type": "object",
308
+ "properties": {
309
+ "query": {
310
+ "type": "string",
311
+ "description": "Terme de recherche (nom de classe, fonction, variable, texte)"
312
+ },
313
+ "file_type": {
314
+ "type": "string",
315
+ "enum": ["models", "views", "urls", "forms", "admin", "all"],
316
+ "description": "Type de fichier a rechercher (defaut: all)"
317
+ }
318
+ },
319
+ "required": ["query"]
320
+ }
321
+ }
322
+ },
323
+ {
324
+ "type": "function",
325
+ "function": {
326
+ "name": "get_file_content_gitlab",
327
+ "description": """USAGE INTERNE - Recupere le contenu d'un fichier pour comprendre le fonctionnement.
328
+ IMPORTANT: Les resultats sont pour TON usage interne uniquement.
329
+ Ne JAMAIS montrer le code source ou les chemins de fichiers a l'utilisateur.
330
+ Utilise ces informations pour repondre en termes simples avec des liens vers les pages.""",
331
+ "parameters": {
332
+ "type": "object",
333
+ "properties": {
334
+ "file_path": {
335
+ "type": "string",
336
+ "description": "Chemin du fichier dans le repository"
337
+ }
338
+ },
339
+ "required": ["file_path"]
340
+ }
341
+ }
277
342
  }
278
343
  ]
279
344
 
@@ -16,8 +16,16 @@ from lucy_assist.conf import lucy_assist_settings
16
16
  class ViewDiscoveryService:
17
17
  """
18
18
  Service pour découvrir automatiquement les vues CRUD d'un projet Django.
19
+
20
+ La découverte est mise en cache en mémoire pour éviter de scanner
21
+ les URLs à chaque requête.
19
22
  """
20
23
 
24
+ # Cache en mémoire pour les vues découvertes (partagé entre instances)
25
+ _views_cache = None
26
+ _cache_timestamp = None
27
+ CACHE_TTL = 300 # 5 minutes
28
+
21
29
  # Patterns de nommage courants pour les vues CRUD
22
30
  # L'ordre est important : les patterns plus spécifiques doivent être en premier
23
31
  CRUD_PATTERNS = {
@@ -65,6 +73,35 @@ class ViewDiscoveryService:
65
73
  def __init__(self):
66
74
  self.apps_prefix = lucy_assist_settings.PROJECT_APPS_PREFIX or ''
67
75
 
76
+ @classmethod
77
+ def get_cached_views(cls) -> Dict:
78
+ """
79
+ Retourne les vues CRUD depuis le cache mémoire.
80
+ Redécouvre automatiquement si le cache est expiré.
81
+ """
82
+ import time
83
+
84
+ now = time.time()
85
+
86
+ # Vérifier si le cache est valide
87
+ if (cls._views_cache is not None and
88
+ cls._cache_timestamp is not None and
89
+ now - cls._cache_timestamp < cls.CACHE_TTL):
90
+ return cls._views_cache
91
+
92
+ # Redécouvrir les vues
93
+ service = cls()
94
+ cls._views_cache = service.discover_crud_views()
95
+ cls._cache_timestamp = now
96
+
97
+ return cls._views_cache
98
+
99
+ @classmethod
100
+ def invalidate_cache(cls):
101
+ """Force la redécouverte des vues au prochain appel."""
102
+ cls._views_cache = None
103
+ cls._cache_timestamp = None
104
+
68
105
  def discover_crud_views(self) -> Dict:
69
106
  """
70
107
  Découvre toutes les vues CRUD du projet.
@@ -293,6 +330,9 @@ class ViewDiscoveryService:
293
330
  """
294
331
  Récupère les informations d'une vue pour un modèle et une action.
295
332
 
333
+ Utilise le cache mémoire pour éviter de scanner les URLs à chaque requête.
334
+ Le cache est automatiquement rafraîchi après CACHE_TTL secondes.
335
+
296
336
  Args:
297
337
  model_name: Nom du modèle
298
338
  action: Action CRUD ('list', 'create', 'detail', 'update', 'delete')
@@ -300,11 +340,8 @@ class ViewDiscoveryService:
300
340
  Returns:
301
341
  Dict avec 'url_name', 'url', 'method', 'requires_pk' ou None
302
342
  """
303
- from lucy_assist.models import ConfigurationLucyAssist
304
- config = ConfigurationLucyAssist.get_config()
305
-
306
- if not config.crud_views_mapping:
307
- config.refresh_crud_views_mapping()
343
+ # Utiliser le cache mémoire (découverte dynamique)
344
+ crud_views_mapping = self.get_cached_views()
308
345
 
309
346
  model_lower = model_name.lower()
310
347
 
@@ -317,10 +354,12 @@ class ViewDiscoveryService:
317
354
  ]
318
355
 
319
356
  for variant in variations:
320
- model_views = config.crud_views_mapping.get(variant, {})
357
+ model_views = crud_views_mapping.get(variant, {})
321
358
  if model_views and action in model_views:
359
+ LogUtils.info(f"[ViewDiscovery] Vue trouvée: {variant}.{action} -> {model_views[action].get('url_name')}")
322
360
  return model_views.get(action)
323
361
 
362
+ LogUtils.info(f"[ViewDiscovery] Aucune vue trouvée pour {model_name}.{action}")
324
363
  return None
325
364
 
326
365
  def get_all_discovered_models(self) -> List[str]:
@@ -330,10 +369,5 @@ class ViewDiscoveryService:
330
369
  Returns:
331
370
  Liste des noms de modèles
332
371
  """
333
- from lucy_assist.models import ConfigurationLucyAssist
334
- config = ConfigurationLucyAssist.get_config()
335
-
336
- if not config.crud_views_mapping:
337
- config.refresh_crud_views_mapping()
338
-
339
- return list(config.crud_views_mapping.keys())
372
+ crud_views_mapping = self.get_cached_views()
373
+ return list(crud_views_mapping.keys())