django-lucy-assist 0.1.0__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.
- django_lucy_assist-0.1.0.dist-info/METADATA +206 -0
- django_lucy_assist-0.1.0.dist-info/RECORD +44 -0
- django_lucy_assist-0.1.0.dist-info/WHEEL +5 -0
- django_lucy_assist-0.1.0.dist-info/top_level.txt +1 -0
- lucy_assist/__init__.py +11 -0
- lucy_assist/admin.py +22 -0
- lucy_assist/apps.py +10 -0
- lucy_assist/conf.py +103 -0
- lucy_assist/constantes.py +120 -0
- lucy_assist/context_processors.py +65 -0
- lucy_assist/migrations/0001_initial.py +92 -0
- lucy_assist/migrations/__init__.py +0 -0
- lucy_assist/models/__init__.py +14 -0
- lucy_assist/models/base.py +54 -0
- lucy_assist/models/configuration.py +175 -0
- lucy_assist/models/conversation.py +54 -0
- lucy_assist/models/message.py +45 -0
- lucy_assist/models/project_context_cache.py +213 -0
- lucy_assist/services/__init__.py +21 -0
- lucy_assist/services/bug_notification_service.py +183 -0
- lucy_assist/services/claude_service.py +417 -0
- lucy_assist/services/context_service.py +350 -0
- lucy_assist/services/crud_service.py +364 -0
- lucy_assist/services/gitlab_service.py +248 -0
- lucy_assist/services/project_context_service.py +412 -0
- lucy_assist/services/tool_executor_service.py +343 -0
- lucy_assist/services/tools_definition.py +229 -0
- lucy_assist/signals.py +25 -0
- lucy_assist/static/lucy_assist/css/lucy-assist.css +160 -0
- lucy_assist/static/lucy_assist/image/icon-lucy.png +0 -0
- lucy_assist/static/lucy_assist/js/lucy-assist.js +824 -0
- lucy_assist/templates/lucy_assist/chatbot_sidebar.html +419 -0
- lucy_assist/templates/lucy_assist/partials/documentation_content.html +107 -0
- lucy_assist/tests/__init__.py +0 -0
- lucy_assist/tests/factories/__init__.py +15 -0
- lucy_assist/tests/factories/lucy_assist_factories.py +109 -0
- lucy_assist/tests/test_lucy_assist.py +186 -0
- lucy_assist/urls.py +36 -0
- lucy_assist/utils/__init__.py +7 -0
- lucy_assist/utils/log_utils.py +59 -0
- lucy_assist/utils/message_utils.py +130 -0
- lucy_assist/utils/token_utils.py +87 -0
- lucy_assist/views/__init__.py +13 -0
- lucy_assist/views/api_views.py +595 -0
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Service pour exécuter des opérations CRUD via Lucy Assist.
|
|
3
|
+
"""
|
|
4
|
+
import logging
|
|
5
|
+
from typing import Dict, List, Optional, Any
|
|
6
|
+
|
|
7
|
+
from django.apps import apps
|
|
8
|
+
from django.db import transaction
|
|
9
|
+
from django.forms import modelform_factory
|
|
10
|
+
|
|
11
|
+
from lucy_assist.utils.log_utils import LogUtils
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CRUDService:
|
|
15
|
+
"""Service pour les opérations CRUD pilotées par Lucy Assist."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, user):
|
|
18
|
+
self.user = user
|
|
19
|
+
|
|
20
|
+
def can_perform_action(self, app_name: str, model_name: str, action: str) -> bool:
|
|
21
|
+
"""
|
|
22
|
+
Vérifie si l'utilisateur peut effectuer l'action.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
app_name: Nom de l'app Django
|
|
26
|
+
model_name: Nom du modèle
|
|
27
|
+
action: 'add', 'change', 'delete', 'view'
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
True si autorisé
|
|
31
|
+
"""
|
|
32
|
+
if self.user.is_superuser:
|
|
33
|
+
return True
|
|
34
|
+
|
|
35
|
+
permission = f"{app_name}.{action}_{model_name.lower()}"
|
|
36
|
+
return self.user.has_perm(permission)
|
|
37
|
+
|
|
38
|
+
def get_model(self, app_name: str, model_name: str):
|
|
39
|
+
"""Récupère une classe de modèle Django."""
|
|
40
|
+
try:
|
|
41
|
+
return apps.get_model(app_name, model_name)
|
|
42
|
+
except LookupError:
|
|
43
|
+
# Essayer de trouver dans toutes les apps
|
|
44
|
+
for app_config in apps.get_app_configs():
|
|
45
|
+
try:
|
|
46
|
+
return app_config.get_model(model_name)
|
|
47
|
+
except LookupError:
|
|
48
|
+
continue
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
def get_form_class(self, model):
|
|
52
|
+
"""Récupère ou crée une classe de formulaire pour le modèle."""
|
|
53
|
+
# Essayer de trouver un formulaire existant
|
|
54
|
+
app_label = model._meta.app_label
|
|
55
|
+
|
|
56
|
+
# Chercher le formulaire dans l'app
|
|
57
|
+
try:
|
|
58
|
+
forms_module = __import__(
|
|
59
|
+
f'apps.{app_label}.forms',
|
|
60
|
+
fromlist=[f'{model.__name__}Form']
|
|
61
|
+
)
|
|
62
|
+
form_class = getattr(forms_module, f'{model.__name__}Form', None)
|
|
63
|
+
if form_class:
|
|
64
|
+
return form_class
|
|
65
|
+
except (ImportError, AttributeError):
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
# Créer un formulaire automatique
|
|
69
|
+
return modelform_factory(model, fields='__all__')
|
|
70
|
+
|
|
71
|
+
def get_required_fields(self, model) -> List[Dict]:
|
|
72
|
+
"""
|
|
73
|
+
Retourne les champs requis pour créer un objet.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
Liste de dicts avec 'name', 'verbose_name', 'type', 'choices'
|
|
77
|
+
"""
|
|
78
|
+
required = []
|
|
79
|
+
|
|
80
|
+
for field in model._meta.get_fields():
|
|
81
|
+
# Ignorer les relations inverses et les champs auto
|
|
82
|
+
if not hasattr(field, 'blank'):
|
|
83
|
+
continue
|
|
84
|
+
|
|
85
|
+
if field.name in ['id', 'pk', 'created_date', 'updated_date', 'created_user', 'updated_user']:
|
|
86
|
+
continue
|
|
87
|
+
|
|
88
|
+
if not field.blank or (hasattr(field, 'null') and not field.null and not field.has_default()):
|
|
89
|
+
field_info = {
|
|
90
|
+
'name': field.name,
|
|
91
|
+
'verbose_name': str(field.verbose_name) if hasattr(field, 'verbose_name') else field.name,
|
|
92
|
+
'type': field.get_internal_type() if hasattr(field, 'get_internal_type') else 'text',
|
|
93
|
+
'required': True
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
# Ajouter les choix si c'est un champ avec choices
|
|
97
|
+
if hasattr(field, 'choices') and field.choices:
|
|
98
|
+
field_info['choices'] = [
|
|
99
|
+
{'value': c[0], 'label': c[1]} for c in field.choices
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
required.append(field_info)
|
|
103
|
+
|
|
104
|
+
return required
|
|
105
|
+
|
|
106
|
+
def get_optional_fields(self, model) -> List[Dict]:
|
|
107
|
+
"""Retourne les champs optionnels."""
|
|
108
|
+
optional = []
|
|
109
|
+
|
|
110
|
+
for field in model._meta.get_fields():
|
|
111
|
+
if not hasattr(field, 'blank'):
|
|
112
|
+
continue
|
|
113
|
+
|
|
114
|
+
if field.name in ['id', 'pk', 'created_date', 'updated_date', 'created_user', 'updated_user']:
|
|
115
|
+
continue
|
|
116
|
+
|
|
117
|
+
if field.blank:
|
|
118
|
+
field_info = {
|
|
119
|
+
'name': field.name,
|
|
120
|
+
'verbose_name': str(field.verbose_name) if hasattr(field, 'verbose_name') else field.name,
|
|
121
|
+
'type': field.get_internal_type() if hasattr(field, 'get_internal_type') else 'text',
|
|
122
|
+
'required': False
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if hasattr(field, 'choices') and field.choices:
|
|
126
|
+
field_info['choices'] = [
|
|
127
|
+
{'value': c[0], 'label': c[1]} for c in field.choices
|
|
128
|
+
]
|
|
129
|
+
|
|
130
|
+
optional.append(field_info)
|
|
131
|
+
|
|
132
|
+
return optional
|
|
133
|
+
|
|
134
|
+
@transaction.atomic
|
|
135
|
+
def create_object(
|
|
136
|
+
self,
|
|
137
|
+
app_name: str,
|
|
138
|
+
model_name: str,
|
|
139
|
+
data: Dict[str, Any]
|
|
140
|
+
) -> Dict:
|
|
141
|
+
"""
|
|
142
|
+
Crée un nouvel objet.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
app_name: Nom de l'app
|
|
146
|
+
model_name: Nom du modèle
|
|
147
|
+
data: Données du formulaire
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
Dict avec 'success', 'object_id', 'errors'
|
|
151
|
+
"""
|
|
152
|
+
# Vérifier les permissions
|
|
153
|
+
if not self.can_perform_action(app_name, model_name, 'add'):
|
|
154
|
+
return {
|
|
155
|
+
'success': False,
|
|
156
|
+
'errors': ['Vous n\'avez pas les droits pour créer cet objet.']
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
# Récupérer le modèle
|
|
160
|
+
model = self.get_model(app_name, model_name)
|
|
161
|
+
if not model:
|
|
162
|
+
return {
|
|
163
|
+
'success': False,
|
|
164
|
+
'errors': [f'Modèle {model_name} non trouvé.']
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
# Récupérer le formulaire
|
|
168
|
+
form_class = self.get_form_class(model)
|
|
169
|
+
|
|
170
|
+
try:
|
|
171
|
+
form = form_class(data=data)
|
|
172
|
+
|
|
173
|
+
if form.is_valid():
|
|
174
|
+
obj = form.save(commit=False)
|
|
175
|
+
|
|
176
|
+
# Ajouter l'utilisateur créateur si le champ existe
|
|
177
|
+
if hasattr(obj, 'created_user'):
|
|
178
|
+
obj.created_user = self.user
|
|
179
|
+
|
|
180
|
+
obj.save()
|
|
181
|
+
form.save_m2m() # Sauvegarder les relations many-to-many
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
'success': True,
|
|
185
|
+
'object_id': obj.pk,
|
|
186
|
+
'object_str': str(obj),
|
|
187
|
+
'message': f'{model_name} créé avec succès.'
|
|
188
|
+
}
|
|
189
|
+
else:
|
|
190
|
+
return {
|
|
191
|
+
'success': False,
|
|
192
|
+
'errors': [f"{field}: {', '.join(errors)}" for field, errors in form.errors.items()]
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
except Exception as e:
|
|
196
|
+
LogUtils.error(f"Erreur lors de la création de {model_name}")
|
|
197
|
+
return {
|
|
198
|
+
'success': False,
|
|
199
|
+
'errors': [str(e)]
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
@transaction.atomic
|
|
203
|
+
def update_object(
|
|
204
|
+
self,
|
|
205
|
+
app_name: str,
|
|
206
|
+
model_name: str,
|
|
207
|
+
object_id: int,
|
|
208
|
+
data: Dict[str, Any]
|
|
209
|
+
) -> Dict:
|
|
210
|
+
"""
|
|
211
|
+
Met à jour un objet existant.
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
Dict avec 'success', 'errors'
|
|
215
|
+
"""
|
|
216
|
+
# Vérifier les permissions
|
|
217
|
+
if not self.can_perform_action(app_name, model_name, 'change'):
|
|
218
|
+
return {
|
|
219
|
+
'success': False,
|
|
220
|
+
'errors': ['Vous n\'avez pas les droits pour modifier cet objet.']
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
# Récupérer le modèle et l'objet
|
|
224
|
+
model = self.get_model(app_name, model_name)
|
|
225
|
+
if not model:
|
|
226
|
+
return {
|
|
227
|
+
'success': False,
|
|
228
|
+
'errors': [f'Modèle {model_name} non trouvé.']
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
try:
|
|
232
|
+
obj = model.objects.get(pk=object_id)
|
|
233
|
+
except model.DoesNotExist:
|
|
234
|
+
return {
|
|
235
|
+
'success': False,
|
|
236
|
+
'errors': [f'{model_name} #{object_id} non trouvé.']
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
# Récupérer le formulaire
|
|
240
|
+
form_class = self.get_form_class(model)
|
|
241
|
+
|
|
242
|
+
try:
|
|
243
|
+
form = form_class(data=data, instance=obj)
|
|
244
|
+
|
|
245
|
+
if form.is_valid():
|
|
246
|
+
obj = form.save(commit=False)
|
|
247
|
+
|
|
248
|
+
# Mettre à jour l'utilisateur modificateur si le champ existe
|
|
249
|
+
if hasattr(obj, 'updated_user'):
|
|
250
|
+
obj.updated_user = self.user
|
|
251
|
+
|
|
252
|
+
obj.save()
|
|
253
|
+
form.save_m2m()
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
'success': True,
|
|
257
|
+
'object_id': obj.pk,
|
|
258
|
+
'message': f'{model_name} mis à jour avec succès.'
|
|
259
|
+
}
|
|
260
|
+
else:
|
|
261
|
+
return {
|
|
262
|
+
'success': False,
|
|
263
|
+
'errors': [f"{field}: {', '.join(errors)}" for field, errors in form.errors.items()]
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
except Exception as e:
|
|
267
|
+
LogUtils.error(f"Erreur lors de la mise à jour de {model_name}")
|
|
268
|
+
return {
|
|
269
|
+
'success': False,
|
|
270
|
+
'errors': [str(e)]
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
@transaction.atomic
|
|
274
|
+
def delete_object(
|
|
275
|
+
self,
|
|
276
|
+
app_name: str,
|
|
277
|
+
model_name: str,
|
|
278
|
+
object_id: int
|
|
279
|
+
) -> Dict:
|
|
280
|
+
"""
|
|
281
|
+
Supprime un objet.
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
Dict avec 'success', 'errors'
|
|
285
|
+
"""
|
|
286
|
+
# Vérifier les permissions
|
|
287
|
+
if not self.can_perform_action(app_name, model_name, 'delete'):
|
|
288
|
+
return {
|
|
289
|
+
'success': False,
|
|
290
|
+
'errors': ['Vous n\'avez pas les droits pour supprimer cet objet.']
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
# Récupérer le modèle
|
|
294
|
+
model = self.get_model(app_name, model_name)
|
|
295
|
+
if not model:
|
|
296
|
+
return {
|
|
297
|
+
'success': False,
|
|
298
|
+
'errors': [f'Modèle {model_name} non trouvé.']
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
try:
|
|
302
|
+
obj = model.objects.get(pk=object_id)
|
|
303
|
+
obj_str = str(obj)
|
|
304
|
+
obj.delete()
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
'success': True,
|
|
308
|
+
'message': f'{model_name} "{obj_str}" supprimé avec succès.'
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
except model.DoesNotExist:
|
|
312
|
+
return {
|
|
313
|
+
'success': False,
|
|
314
|
+
'errors': [f'{model_name} #{object_id} non trouvé.']
|
|
315
|
+
}
|
|
316
|
+
except Exception as e:
|
|
317
|
+
LogUtils.error(f"Erreur lors de la suppression de {model_name}")
|
|
318
|
+
return {
|
|
319
|
+
'success': False,
|
|
320
|
+
'errors': [str(e)]
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
def get_object(
|
|
324
|
+
self,
|
|
325
|
+
app_name: str,
|
|
326
|
+
model_name: str,
|
|
327
|
+
object_id: int
|
|
328
|
+
) -> Optional[Dict]:
|
|
329
|
+
"""
|
|
330
|
+
Récupère les détails d'un objet.
|
|
331
|
+
|
|
332
|
+
Returns:
|
|
333
|
+
Dict avec les données de l'objet ou None
|
|
334
|
+
"""
|
|
335
|
+
# Vérifier les permissions
|
|
336
|
+
if not self.can_perform_action(app_name, model_name, 'view'):
|
|
337
|
+
return None
|
|
338
|
+
|
|
339
|
+
model = self.get_model(app_name, model_name)
|
|
340
|
+
if not model:
|
|
341
|
+
return None
|
|
342
|
+
|
|
343
|
+
try:
|
|
344
|
+
obj = model.objects.get(pk=object_id)
|
|
345
|
+
|
|
346
|
+
# Construire un dict avec les données
|
|
347
|
+
data = {'id': obj.pk, 'str': str(obj)}
|
|
348
|
+
|
|
349
|
+
for field in model._meta.get_fields():
|
|
350
|
+
if hasattr(field, 'verbose_name'):
|
|
351
|
+
try:
|
|
352
|
+
value = getattr(obj, field.name)
|
|
353
|
+
# Gérer les FK
|
|
354
|
+
if hasattr(value, 'pk'):
|
|
355
|
+
data[field.name] = {'id': value.pk, 'str': str(value)}
|
|
356
|
+
else:
|
|
357
|
+
data[field.name] = value
|
|
358
|
+
except Exception:
|
|
359
|
+
pass
|
|
360
|
+
|
|
361
|
+
return data
|
|
362
|
+
|
|
363
|
+
except model.DoesNotExist:
|
|
364
|
+
return None
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Service d'intégration avec GitLab pour l'analyse de code.
|
|
3
|
+
"""
|
|
4
|
+
import re
|
|
5
|
+
from typing import Dict, List, Optional
|
|
6
|
+
from urllib.parse import quote
|
|
7
|
+
|
|
8
|
+
from django.conf import settings
|
|
9
|
+
|
|
10
|
+
import requests
|
|
11
|
+
|
|
12
|
+
from lucy_assist.utils.log_utils import LogUtils
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class GitLabService:
|
|
16
|
+
"""Service pour interagir avec l'API GitLab."""
|
|
17
|
+
|
|
18
|
+
def __init__(self):
|
|
19
|
+
self.token = getattr(settings, 'GITLAB_TOKEN', None)
|
|
20
|
+
self.base_url = getattr(settings, 'GITLAB_URL', 'https://gitlab.com')
|
|
21
|
+
self.project_id = getattr(settings, 'GITLAB_PROJECT_ID', None)
|
|
22
|
+
|
|
23
|
+
if not self.token:
|
|
24
|
+
LogUtils.error("GITLAB_TOKEN non configuré")
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def headers(self) -> Dict[str, str]:
|
|
28
|
+
"""Headers pour les requêtes API."""
|
|
29
|
+
return {
|
|
30
|
+
'PRIVATE-TOKEN': self.token,
|
|
31
|
+
'Content-Type': 'application/json'
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
def _api_url(self, endpoint: str) -> str:
|
|
35
|
+
"""Construit l'URL de l'API."""
|
|
36
|
+
return f"{self.base_url}/api/v4{endpoint}"
|
|
37
|
+
|
|
38
|
+
def search_code(
|
|
39
|
+
self,
|
|
40
|
+
query: str,
|
|
41
|
+
scope: str = 'blobs',
|
|
42
|
+
per_page: int = 10
|
|
43
|
+
) -> List[Dict]:
|
|
44
|
+
"""
|
|
45
|
+
Recherche dans le code source du projet.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
query: Terme de recherche
|
|
49
|
+
scope: 'blobs' (fichiers), 'commits', 'issues'
|
|
50
|
+
per_page: Nombre de résultats
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Liste des résultats de recherche
|
|
54
|
+
"""
|
|
55
|
+
if not self.token or not self.project_id:
|
|
56
|
+
LogUtils.error("GitLab non configuré pour la recherche de code")
|
|
57
|
+
return []
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
url = self._api_url(f"/projects/{self.project_id}/search")
|
|
61
|
+
params = {
|
|
62
|
+
'scope': scope,
|
|
63
|
+
'search': query,
|
|
64
|
+
'per_page': per_page
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
response = requests.get(url, headers=self.headers, params=params, timeout=10)
|
|
68
|
+
response.raise_for_status()
|
|
69
|
+
|
|
70
|
+
return response.json()
|
|
71
|
+
|
|
72
|
+
except requests.RequestException as e:
|
|
73
|
+
LogUtils.error(f"Erreur lors de la recherche GitLab: {e}")
|
|
74
|
+
return []
|
|
75
|
+
|
|
76
|
+
def get_file_content(
|
|
77
|
+
self,
|
|
78
|
+
file_path: str,
|
|
79
|
+
ref: str = 'main'
|
|
80
|
+
) -> Optional[str]:
|
|
81
|
+
"""
|
|
82
|
+
Récupère le contenu d'un fichier.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
file_path: Chemin du fichier dans le repo
|
|
86
|
+
ref: Branche ou tag
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
Contenu du fichier ou None
|
|
90
|
+
"""
|
|
91
|
+
if not self.token or not self.project_id:
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
encoded_path = quote(file_path, safe='')
|
|
96
|
+
url = self._api_url(
|
|
97
|
+
f"/projects/{self.project_id}/repository/files/{encoded_path}/raw"
|
|
98
|
+
)
|
|
99
|
+
params = {'ref': ref}
|
|
100
|
+
|
|
101
|
+
response = requests.get(url, headers=self.headers, params=params, timeout=10)
|
|
102
|
+
response.raise_for_status()
|
|
103
|
+
|
|
104
|
+
return response.text
|
|
105
|
+
|
|
106
|
+
except requests.RequestException as e:
|
|
107
|
+
LogUtils.error(f"Erreur lors de la récupération du fichier {file_path}: {e}")
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
def get_file_blame(
|
|
111
|
+
self,
|
|
112
|
+
file_path: str,
|
|
113
|
+
ref: str = 'main'
|
|
114
|
+
) -> List[Dict]:
|
|
115
|
+
"""
|
|
116
|
+
Récupère le blame d'un fichier (qui a modifié quoi).
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
Liste des blocs de blame
|
|
120
|
+
"""
|
|
121
|
+
if not self.token or not self.project_id:
|
|
122
|
+
return []
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
encoded_path = quote(file_path, safe='')
|
|
126
|
+
url = self._api_url(
|
|
127
|
+
f"/projects/{self.project_id}/repository/files/{encoded_path}/blame"
|
|
128
|
+
)
|
|
129
|
+
params = {'ref': ref}
|
|
130
|
+
|
|
131
|
+
response = requests.get(url, headers=self.headers, params=params, timeout=10)
|
|
132
|
+
response.raise_for_status()
|
|
133
|
+
|
|
134
|
+
return response.json()
|
|
135
|
+
|
|
136
|
+
except requests.RequestException as e:
|
|
137
|
+
LogUtils.error(f"Erreur lors du blame du fichier {file_path}: {e}")
|
|
138
|
+
return []
|
|
139
|
+
|
|
140
|
+
def find_view_for_url(self, url_path: str) -> Optional[Dict]:
|
|
141
|
+
"""
|
|
142
|
+
Trouve la vue Django correspondant à une URL.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
url_path: Chemin de l'URL (ex: /membre/list)
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Dict avec 'file_path', 'view_name', 'code' ou None
|
|
149
|
+
"""
|
|
150
|
+
# Extraire le nom de l'app et la vue potentielle
|
|
151
|
+
parts = url_path.strip('/').split('/')
|
|
152
|
+
if not parts:
|
|
153
|
+
return None
|
|
154
|
+
|
|
155
|
+
app_name = parts[0]
|
|
156
|
+
|
|
157
|
+
# Chercher dans les fichiers urls.py et views.py
|
|
158
|
+
search_results = self.search_code(f"path.*{parts[-1] if len(parts) > 1 else app_name}")
|
|
159
|
+
|
|
160
|
+
for result in search_results:
|
|
161
|
+
if 'urls.py' in result.get('filename', ''):
|
|
162
|
+
# Trouver le nom de la vue associée
|
|
163
|
+
content = self.get_file_content(result['filename'])
|
|
164
|
+
if content:
|
|
165
|
+
# Chercher la vue correspondante
|
|
166
|
+
view_match = re.search(
|
|
167
|
+
rf'path\(["\'][^"\']*{re.escape(parts[-1] if len(parts) > 1 else "")}["\'][^)]*views\.(\w+)',
|
|
168
|
+
content
|
|
169
|
+
)
|
|
170
|
+
if view_match:
|
|
171
|
+
view_name = view_match.group(1)
|
|
172
|
+
# Chercher le fichier de la vue
|
|
173
|
+
views_path = result['filename'].replace('urls.py', f'views/{view_name.lower()}_views.py')
|
|
174
|
+
view_content = self.get_file_content(views_path)
|
|
175
|
+
|
|
176
|
+
if not view_content:
|
|
177
|
+
# Essayer le fichier views.py principal
|
|
178
|
+
views_path = result['filename'].replace('urls.py', 'views.py')
|
|
179
|
+
view_content = self.get_file_content(views_path)
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
'file_path': views_path,
|
|
183
|
+
'view_name': view_name,
|
|
184
|
+
'code': view_content
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
def find_model_and_form(self, model_name: str) -> Dict:
|
|
190
|
+
"""
|
|
191
|
+
Trouve le modèle et le formulaire correspondant.
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
Dict avec 'model_file', 'model_code', 'form_file', 'form_code'
|
|
195
|
+
"""
|
|
196
|
+
result = {
|
|
197
|
+
'model_file': None,
|
|
198
|
+
'model_code': None,
|
|
199
|
+
'form_file': None,
|
|
200
|
+
'form_code': None
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
# Chercher le modèle
|
|
204
|
+
model_search = self.search_code(f"class {model_name}(")
|
|
205
|
+
for item in model_search:
|
|
206
|
+
if 'models' in item.get('filename', ''):
|
|
207
|
+
result['model_file'] = item['filename']
|
|
208
|
+
result['model_code'] = self.get_file_content(item['filename'])
|
|
209
|
+
break
|
|
210
|
+
|
|
211
|
+
# Chercher le formulaire
|
|
212
|
+
form_search = self.search_code(f"class {model_name}Form(")
|
|
213
|
+
for item in form_search:
|
|
214
|
+
if 'forms' in item.get('filename', ''):
|
|
215
|
+
result['form_file'] = item['filename']
|
|
216
|
+
result['form_code'] = self.get_file_content(item['filename'])
|
|
217
|
+
break
|
|
218
|
+
|
|
219
|
+
return result
|
|
220
|
+
|
|
221
|
+
def get_recent_commits(self, per_page: int = 10) -> List[Dict]:
|
|
222
|
+
"""
|
|
223
|
+
Récupère les commits récents.
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
Liste des commits avec 'id', 'message', 'author', 'date'
|
|
227
|
+
"""
|
|
228
|
+
if not self.token or not self.project_id:
|
|
229
|
+
return []
|
|
230
|
+
|
|
231
|
+
try:
|
|
232
|
+
url = self._api_url(f"/projects/{self.project_id}/repository/commits")
|
|
233
|
+
params = {'per_page': per_page}
|
|
234
|
+
|
|
235
|
+
response = requests.get(url, headers=self.headers, params=params, timeout=10)
|
|
236
|
+
response.raise_for_status()
|
|
237
|
+
|
|
238
|
+
commits = response.json()
|
|
239
|
+
return [{
|
|
240
|
+
'id': c['short_id'],
|
|
241
|
+
'message': c['title'],
|
|
242
|
+
'author': c['author_name'],
|
|
243
|
+
'date': c['created_at']
|
|
244
|
+
} for c in commits]
|
|
245
|
+
|
|
246
|
+
except requests.RequestException as e:
|
|
247
|
+
LogUtils.error(f"Erreur lors de la récupération des commits: {e}")
|
|
248
|
+
return []
|