rinogeek commited on
Commit
d42510a
·
0 Parent(s):

Initial commit: EduLab Backend for Hugging Face Spaces

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .env.exemple +53 -0
  2. .env.production +40 -0
  3. .gitignore +75 -0
  4. Dockerfile +42 -0
  5. GUIDE_COMPLET.MD +640 -0
  6. README.md +194 -0
  7. STRUCTURE_FILE.MD +508 -0
  8. apps/ai_tools/__init__.py +0 -0
  9. apps/ai_tools/admin.py +19 -0
  10. apps/ai_tools/apps.py +6 -0
  11. apps/ai_tools/migrations/0001_initial.py +99 -0
  12. apps/ai_tools/migrations/0002_initial.py +44 -0
  13. apps/ai_tools/migrations/__init__.py +0 -0
  14. apps/ai_tools/models.py +47 -0
  15. apps/ai_tools/serializers.py +21 -0
  16. apps/ai_tools/tests.py +3 -0
  17. apps/ai_tools/urls.py +18 -0
  18. apps/ai_tools/views.py +200 -0
  19. apps/analytics/__init__.py +2 -0
  20. apps/analytics/admin.py +35 -0
  21. apps/analytics/apps.py +7 -0
  22. apps/analytics/migrations/0001_initial.py +185 -0
  23. apps/analytics/migrations/__init__.py +0 -0
  24. apps/analytics/models.py +135 -0
  25. apps/analytics/serializers.py +24 -0
  26. apps/analytics/services.py +144 -0
  27. apps/analytics/tests.py +3 -0
  28. apps/analytics/urls.py +9 -0
  29. apps/analytics/views.py +117 -0
  30. apps/bookings/__init__.py +0 -0
  31. apps/bookings/admin.py +29 -0
  32. apps/bookings/apps.py +9 -0
  33. apps/bookings/migrations/0001_initial.py +150 -0
  34. apps/bookings/migrations/0002_initial.py +89 -0
  35. apps/bookings/migrations/__init__.py +0 -0
  36. apps/bookings/models.py +69 -0
  37. apps/bookings/serializers.py +142 -0
  38. apps/bookings/signals.py +58 -0
  39. apps/bookings/tasks.py +206 -0
  40. apps/bookings/tests.py +3 -0
  41. apps/bookings/urls.py +23 -0
  42. apps/bookings/views.py +102 -0
  43. apps/core/__init__.py +0 -0
  44. apps/core/admin.py +27 -0
  45. apps/core/apps.py +9 -0
  46. apps/core/exceptions.py +60 -0
  47. apps/core/management/commands/create_test_data.py +162 -0
  48. apps/core/management/commands/init_data.py +327 -0
  49. apps/core/management/commands/init_db.py +14 -0
  50. apps/core/middleware.py +38 -0
.env.exemple ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ==============================================
3
+ FICHIERS DE CONFIGURATION
4
+ ==============================================
5
+ """
6
+
7
+ # ============================================
8
+ # .env.example
9
+ # ============================================
10
+
11
+ # Django Settings
12
+ SECRET_KEY=your-secret-key-here-change-in-production
13
+ DEBUG=True
14
+ ALLOWED_HOSTS=localhost,127.0.0.1,yourdomain.com
15
+
16
+ # Database
17
+ DB_NAME=educonnect_db
18
+ DB_USER=postgres
19
+ DB_PASSWORD=your_db_password
20
+ DB_HOST=localhost
21
+ DB_PORT=5432
22
+
23
+ # CORS
24
+ CORS_ALLOWED_ORIGINS=http://localhost:3000,http://localhost:5173,https://yourdomain.com
25
+
26
+ # Redis (pour Celery et Channels)
27
+ REDIS_HOST=localhost
28
+ CELERY_BROKER_URL=redis://localhost:6379/0
29
+ CELERY_RESULT_BACKEND=redis://localhost:6379/0
30
+
31
+ # Email Configuration
32
+ EMAIL_HOST=smtp.gmail.com
33
+ EMAIL_PORT=587
34
+ EMAIL_HOST_USER=your-email@gmail.com
35
+ EMAIL_HOST_PASSWORD=your-email-password
36
+ DEFAULT_FROM_EMAIL=noreply@educonnect.africa
37
+
38
+ # AI APIs
39
+ GEMINI_API_KEY=your-gemini-api-key
40
+ OPENAI_API_KEY=your-openai-api-key
41
+
42
+ # Logging
43
+ DJANGO_LOG_LEVEL=INFO
44
+
45
+ # AWS S3 (si utilisé pour les fichiers)
46
+ AWS_ACCESS_KEY_ID=
47
+ AWS_SECRET_ACCESS_KEY=
48
+ AWS_STORAGE_BUCKET_NAME=
49
+ AWS_S3_REGION_NAME=
50
+
51
+ # Sentry (Monitoring - optionnel)
52
+ SENTRY_DSN=
53
+
.env.production ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ==============================================
2
+ # CONFIGURATION PRODUCTION - HUGGING FACE SPACES
3
+ # ==============================================
4
+
5
+ # Django Settings
6
+ DEBUG=False
7
+ SECRET_KEY=django-insecure-xdNTqmUzzdJbmHZ8fLr6B6v1NfShmDZM
8
+ ALLOWED_HOSTS=localhost,127.0.0.1,.hf.space
9
+
10
+ # CORS - Autoriser le frontend Hugging Face
11
+ CORS_ALLOWED_ORIGINS=https://rinogeek-edulabfrontend.hf.space
12
+
13
+ # Database - SQLite par défaut (pas de DB_HOST = utilise SQLite)
14
+ # Pour PostgreSQL, décommentez et configurez :
15
+ # DB_NAME=educonnect_db
16
+ # DB_USER=postgres
17
+ # DB_PASSWORD=your_db_password
18
+ # DB_HOST=your_db_host
19
+ # DB_PORT=5432
20
+
21
+ # Redis (pour Celery et Channels) - Désactivé sur HF Spaces gratuit
22
+ # REDIS_HOST=localhost
23
+ # CELERY_BROKER_URL=redis://localhost:6379/0
24
+ # CELERY_RESULT_BACKEND=redis://localhost:6379/0
25
+
26
+ # Email Configuration
27
+ EMAIL_HOST=smtp.gmail.com
28
+ EMAIL_PORT=587
29
+ EMAIL_HOST_USER=
30
+ EMAIL_HOST_PASSWORD=
31
+ DEFAULT_FROM_EMAIL=noreply@edulab.africa
32
+
33
+ # AI APIs
34
+ GEMINI_API_KEY=AIzaSyD-hnYdbQudiK8nfFtML05Mmlo26nLzxWQ
35
+
36
+ # Logging
37
+ DJANGO_LOG_LEVEL=INFO
38
+
39
+ # Security - HF gère SSL au niveau proxy
40
+ SECURE_SSL_REDIRECT=False
.gitignore ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ *.egg-info/
24
+ .installed.cfg
25
+ *.egg
26
+
27
+ # Django stuff:
28
+ *.log
29
+ *.pot
30
+ *.pyc
31
+ local_settings.py
32
+ db.sqlite3
33
+ db.sqlite3-journal
34
+
35
+ # Media files
36
+ media/
37
+
38
+ # Static files (collectstatic output)
39
+ staticfiles/
40
+
41
+ # Environment
42
+ .env
43
+ .env.local
44
+ venv/
45
+ ENV/
46
+ env/
47
+ .venv/
48
+
49
+ # IDE
50
+ .idea/
51
+ .vscode/
52
+ *.swp
53
+ *.swo
54
+
55
+ # Logs
56
+ logs/
57
+ *.log
58
+
59
+ # OS
60
+ .DS_Store
61
+ Thumbs.db
62
+
63
+ # Celery
64
+ celerybeat-schedule
65
+ celerybeat.pid
66
+
67
+ # Coverage
68
+ .coverage
69
+ htmlcov/
70
+
71
+ # pytest
72
+ .pytest_cache/
73
+
74
+ # mypy
75
+ .mypy_cache/
Dockerfile ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.12-slim
2
+
3
+ ENV PYTHONUNBUFFERED=1
4
+ ENV PYTHONDONTWRITEBYTECODE=1
5
+
6
+ WORKDIR /app
7
+
8
+ # Installer les dépendances système
9
+ RUN apt-get update && apt-get install -y \
10
+ postgresql-client \
11
+ gcc \
12
+ libpq-dev \
13
+ && rm -rf /var/lib/apt/lists/*
14
+
15
+ # Créer un utilisateur non-root pour Hugging Face Spaces (UID 1000)
16
+ RUN useradd -m -u 1000 user
17
+ USER user
18
+ ENV PATH="/home/user/.local/bin:${PATH}"
19
+
20
+ # Définir le répertoire de travail et s'assurer des permissions
21
+ WORKDIR /app
22
+ COPY --chown=user:user requirements.txt /app/
23
+ RUN pip install --no-cache-dir --upgrade pip && \
24
+ pip install --no-cache-dir -r requirements.txt
25
+
26
+ # Copier le reste du projet avec les bonnes permissions
27
+ COPY --chown=user:user . /app/
28
+
29
+ # Créer les dossiers nécessaires et s'assurer des permissions
30
+ RUN mkdir -p /app/logs /app/media /app/staticfiles
31
+
32
+ # Copier le fichier .env.production comme .env pour la production
33
+ RUN cp /app/.env.production /app/.env || true
34
+
35
+ # Collecter les fichiers statiques
36
+ RUN python manage.py collectstatic --noinput
37
+
38
+ # Exposer le port par défaut de Hugging Face Spaces
39
+ EXPOSE 7860
40
+
41
+ # Démarrer avec gunicorn
42
+ CMD ["gunicorn", "--bind", "0.0.0.0:7860", "--workers", "4", "educonnect.wsgi:application"]
GUIDE_COMPLET.MD ADDED
@@ -0,0 +1,640 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 📘 GUIDE COMPLET - EduConnect Africa API
2
+
3
+ ## 🎯 Vue d'Ensemble
4
+
5
+ API REST Django complète pour EduConnect Africa avec traçabilité maximale, soft delete, et versioning des données.
6
+
7
+ ---
8
+
9
+ ## 📁 Architecture du Projet
10
+
11
+ ```
12
+ educonnect-api/
13
+
14
+ ├── educonnect_api/ # Configuration Django
15
+ │ ├── __init__.py
16
+ │ ├── settings.py # Configuration complète
17
+ │ ├── urls.py # Routes principales
18
+ │ ├── asgi.py # Configuration ASGI (WebSockets)
19
+ │ ├── wsgi.py # Configuration WSGI
20
+ │ └── celery.py # Configuration Celery
21
+
22
+ ├── apps/ # Applications Django
23
+ │ │
24
+ │ ├── core/ # Utilitaires & Base
25
+ │ │ ├── models.py # Mixins (Timestamp, SoftDelete, Versioned)
26
+ │ │ ├── permissions.py # Permissions personnalisées
27
+ │ │ ├── exceptions.py # Gestionnaire d'exceptions
28
+ │ │ ├── middleware.py # Middleware d'activité
29
+ │ │ └── management/
30
+ │ │ └── commands/
31
+ │ │ ├── init_db.py # Initialisation DB
32
+ │ │ └── create_test_data.py # Données de test
33
+ │ │
34
+ │ ├── users/ # Authentification & Utilisateurs
35
+ │ │ ├── models.py # User, UserProfile, UserAvatar, etc.
36
+ │ │ ├── serializers.py # User, Registration, Login, Update
37
+ │ │ ├── views.py # AuthViewSet
38
+ │ │ ├── urls.py
39
+ │ │ └── tests.py
40
+ │ │
41
+ │ ├── mentors/ # Gestion Mentors
42
+ │ │ ├── models.py # MentorProfile, Bio, Specialties, etc.
43
+ │ │ ├── serializers.py # Mentor CRUD
44
+ │ │ ├── views.py # MentorViewSet
45
+ │ │ ├── urls.py
46
+ │ │ └── tests.py
47
+ │ │
48
+ │ ├── bookings/ # Système de Réservation
49
+ │ │ ├── models.py # Booking, Domains, Expectations, etc.
50
+ │ │ ├── serializers.py # Booking CRUD
51
+ │ │ ├── views.py # BookingViewSet
52
+ │ │ ├── urls.py
53
+ │ │ └── tests.py
54
+ │ │
55
+ │ ├── forum/ # Questions & Réponses
56
+ │ │ ├── models.py # Question, Answer (versionné)
57
+ │ │ ├── serializers.py # Forum CRUD
58
+ │ │ ├── views.py # QuestionViewSet, AnswerViewSet
59
+ │ │ ├── urls.py
60
+ │ │ └── tests.py
61
+ │ │
62
+ │ ├── gamification/ # Points & Badges
63
+ │ │ ├── models.py # Badge, UserBadge, PointsHistory
64
+ │ │ ├── serializers.py # Gamification
65
+ │ │ ├── views.py # GamificationViewSet
66
+ │ │ ├── services.py # BadgeService (logique métier)
67
+ │ │ ├── urls.py
68
+ │ │ └── tests.py
69
+ │ │
70
+ │ ├── messaging/ # Chat Temps Réel
71
+ │ │ ├── models.py # Conversation, Message
72
+ │ │ ├── serializers.py # Messaging
73
+ │ │ ├── views.py # ConversationViewSet
74
+ │ │ ├── consumers.py # WebSocket Consumer
75
+ │ │ ├── urls.py
76
+ │ │ └── tests.py
77
+ │ │
78
+ │ ├── notifications/ # Système de Notifications
79
+ │ │ ├── models.py # Notification
80
+ │ │ ├── serializers.py # Notification
81
+ │ │ ├── views.py # NotificationViewSet
82
+ │ │ ├── services.py # NotificationService
83
+ │ │ ├── urls.py
84
+ │ │ └── tests.py
85
+ │ │
86
+ │ ├── opportunities/ # Opportunités (Bourses, Stages)
87
+ │ │ ├── models.py # Opportunity
88
+ │ │ ├── serializers.py # Opportunity
89
+ │ │ ├── views.py # OpportunityViewSet
90
+ │ │ ├── urls.py
91
+ │ │ └── tests.py
92
+ │ │
93
+ │ ├── ai_tools/ # Tuteur IA
94
+ │ │ ├── models.py # AITutorSession, CodeSnippet
95
+ │ │ ├── serializers.py # AI Request/Response
96
+ │ │ ├── views.py # AIToolsViewSet
97
+ │ │ ├── urls.py
98
+ │ │ └── tests.py
99
+ │ │
100
+ │ └── analytics/ # Analytics (Optionnel)
101
+ │ ├── models.py # UserActivity, SystemMetric
102
+ │ └── views.py # AnalyticsViewSet
103
+
104
+ ├── logs/ # Logs application
105
+ ├── media/ # Fichiers uploadés
106
+ ├── staticfiles/ # Fichiers statiques collectés
107
+ ├── templates/ # Templates Django
108
+
109
+ ├── requirements.txt # Dépendances Python
110
+ ├── pytest.ini # Configuration pytest
111
+ ├── conftest.py # Fixtures pytest
112
+ ├── .env.example # Variables d'environnement
113
+ ├── Dockerfile # Docker image
114
+ ├── docker-compose.yml # Docker services
115
+ ├── deploy.sh # Script de déploiement
116
+ ├── manage.py # CLI Django
117
+ └── README.md # Documentation
118
+ ```
119
+
120
+ ---
121
+
122
+ ## 🚀 Installation & Démarrage
123
+
124
+ ### Option 1: Installation Locale
125
+
126
+ ```bash
127
+ # 1. Cloner le projet
128
+ git clone <repo-url>
129
+ cd educonnect-api
130
+
131
+ # 2. Environnement virtuel
132
+ python3.11 -m venv venv
133
+ source venv/bin/activate # Linux/Mac
134
+ # venv\Scripts\activate # Windows
135
+
136
+ # 3. Installer les dépendances
137
+ pip install -r requirements.txt
138
+
139
+ # 4. Configuration
140
+ cp .env.example .env
141
+ # Éditer .env avec vos paramètres
142
+
143
+ # 5. Base de données PostgreSQL
144
+ createdb educonnect_db
145
+
146
+ # 6. Migrations
147
+ python manage.py makemigrations
148
+ python manage.py migrate
149
+
150
+ # 7. Initialiser les données
151
+ python manage.py init_db
152
+
153
+ # 8. Créer un superuser
154
+ python manage.py createsuperuser
155
+
156
+ # 9. (Optionnel) Données de test
157
+ python manage.py create_test_data --users 50
158
+
159
+ # 10. Démarrer le serveur
160
+ python manage.py runserver
161
+
162
+ # 11. Démarrer Celery (dans un autre terminal)
163
+ celery -A educonnect_api worker -l info
164
+
165
+ # 12. Démarrer Channels/Daphne (dans un autre terminal)
166
+ daphne -b 0.0.0.0 -p 8001 educonnect_api.asgi:application
167
+ ```
168
+
169
+ ### Option 2: Avec Docker
170
+
171
+ ```bash
172
+ # Build et démarrer tous les services
173
+ docker-compose up --build
174
+
175
+ # Migrations
176
+ docker-compose exec web python manage.py migrate
177
+
178
+ # Initialiser
179
+ docker-compose exec web python manage.py init_db
180
+
181
+ # Créer superuser
182
+ docker-compose exec web python manage.py createsuperuser
183
+
184
+ # Voir les logs
185
+ docker-compose logs -f web
186
+ ```
187
+
188
+ ---
189
+
190
+ ## 📚 Endpoints API Complets
191
+
192
+ ### 🔐 Authentification `/api/auth/`
193
+
194
+ | Méthode | Endpoint | Description | Auth |
195
+ |---------|----------|-------------|------|
196
+ | POST | `/register/` | Inscription utilisateur | Non |
197
+ | POST | `/login/` | Connexion | Non |
198
+ | POST | `/token/refresh/` | Rafraîchir token | Non |
199
+ | GET | `/me/` | Profil actuel | Oui |
200
+ | PATCH | `/update_profile/` | Mise à jour profil | Oui |
201
+
202
+ **Exemple Inscription:**
203
+ ```json
204
+ POST /api/auth/register/
205
+ {
206
+ "email": "student@educonnect.africa",
207
+ "password": "SecurePass123!",
208
+ "password_confirm": "SecurePass123!",
209
+ "name": "Jean Dupont",
210
+ "role": "STUDENT",
211
+ "country": "Bénin",
212
+ "university": "Université d'Abomey-Calavi"
213
+ }
214
+ ```
215
+
216
+ **Réponse:**
217
+ ```json
218
+ {
219
+ "user": {
220
+ "id": 1,
221
+ "email": "student@educonnect.africa",
222
+ "role": "STUDENT",
223
+ "points": 0,
224
+ "profile": {
225
+ "name": "Jean Dupont",
226
+ "avatar": null,
227
+ "country": "Bénin",
228
+ "university": "Université d'Abomey-Calavi"
229
+ }
230
+ },
231
+ "tokens": {
232
+ "refresh": "eyJ0eXAiOiJKV1QiLCJhbGc...",
233
+ "access": "eyJ0eXAiOiJKV1QiLCJhbGc..."
234
+ }
235
+ }
236
+ ```
237
+
238
+ ---
239
+
240
+ ### 👨‍🏫 Mentors `/api/mentors/`
241
+
242
+ | Méthode | Endpoint | Description | Filtres |
243
+ |---------|----------|-------------|---------|
244
+ | GET | `/` | Liste mentors | ?search=, ?country=, ?specialty=, ?ordering= |
245
+ | GET | `/{id}/` | Détail mentor | - |
246
+ | GET | `/my_profile/` | Mon profil mentor | - |
247
+ | PATCH | `/my_profile/` | Mettre à jour | - |
248
+ | GET | `/{id}/reviews/` | Avis mentor | - |
249
+
250
+ **Exemple Liste:**
251
+ ```bash
252
+ GET /api/mentors/?country=Bénin&specialty=Informatique&ordering=-rating
253
+ ```
254
+
255
+ ---
256
+
257
+ ### 📅 Réservations `/api/bookings/`
258
+
259
+ | Méthode | Endpoint | Description |
260
+ |---------|----------|-------------|
261
+ | GET | `/` | Mes réservations |
262
+ | POST | `/` | Créer réservation |
263
+ | GET | `/{id}/` | Détail |
264
+ | PATCH | `/{id}/update_status/` | Accepter/Refuser |
265
+ | GET | `/mentor_requests/` | Demandes reçues (mentor) |
266
+
267
+ **Exemple Création:**
268
+ ```json
269
+ POST /api/bookings/
270
+ {
271
+ "mentor_id": 5,
272
+ "date": "2025-12-15",
273
+ "time": "14:00:00",
274
+ "domains": ["Informatique", "Programmation"],
275
+ "expectations": "Je souhaite apprendre les bases de Django",
276
+ "main_question": "Comment structurer un projet Django professionnel?"
277
+ }
278
+ ```
279
+
280
+ ---
281
+
282
+ ### 💬 Forum `/api/forum/`
283
+
284
+ #### Questions
285
+
286
+ | Méthode | Endpoint | Description | Filtres |
287
+ |---------|----------|-------------|---------|
288
+ | GET | `/questions/` | Liste | ?search=, ?filter=solved/unsolved, ?tag= |
289
+ | POST | `/questions/` | Créer | - |
290
+ | GET | `/questions/{id}/` | Détail | - |
291
+ | PATCH | `/questions/{id}/` | Modifier | - |
292
+ | DELETE | `/questions/{id}/` | Supprimer (soft) | - |
293
+ | POST | `/questions/{id}/vote/` | Voter | - |
294
+ | GET | `/questions/{id}/answers/` | Réponses | - |
295
+ | POST | `/questions/{id}/answers/` | Répondre | - |
296
+
297
+ **Exemple Création Question:**
298
+ ```json
299
+ POST /api/forum/questions/
300
+ {
301
+ "title": "Comment calculer une intégrale triple?",
302
+ "content": "Je bloque sur cet exercice: ∫∫∫ xyz dV...",
303
+ "tags": ["mathématiques", "intégrales", "calcul"]
304
+ }
305
+ ```
306
+
307
+ #### Réponses
308
+
309
+ | Méthode | Endpoint | Description |
310
+ |---------|----------|-------------|
311
+ | POST | `/answers/{id}/vote/` | Voter |
312
+ | POST | `/answers/{id}/accept/` | Accepter (auteur question) |
313
+
314
+ ---
315
+
316
+ ### 🎮 Gamification `/api/gamification/`
317
+
318
+ | Méthode | Endpoint | Description |
319
+ |---------|----------|-------------|
320
+ | GET | `/leaderboard/` | Classement |
321
+ | GET | `/my_badges/` | Mes badges |
322
+ | GET | `/all_badges/` | Tous les badges |
323
+ | GET | `/points_history/` | Historique points |
324
+ | GET | `/stats/` | Statistiques perso |
325
+
326
+ **Système de Points:**
327
+ - Question postée: **+10 points**
328
+ - Réponse postée: **+15 points**
329
+ - Réponse acceptée: **+50 points**
330
+ - Upvote reçu: **+5 points**
331
+ - Devenir mentor: **+100 points**
332
+ - Session complétée: **+25 points**
333
+
334
+ **Badges Disponibles:**
335
+ 1. 🌱 **Premier Pas** - Première question
336
+ 2. 🔍 **Curieux** - 100 points
337
+ 3. ⭐ **Engagé** - 500 points
338
+ 4. 🏆 **Expert** - 1000 points
339
+ 5. 👑 **Maître** - 2500 points
340
+ 6. 💎 **Légende** - 5000 points
341
+
342
+ ---
343
+
344
+ ### 💬 Messagerie `/api/messages/`
345
+
346
+ | Méthode | Endpoint | Description |
347
+ |---------|----------|-------------|
348
+ | GET | `/conversations/` | Mes conversations |
349
+ | POST | `/conversations/` | Créer conversation |
350
+ | GET | `/conversations/{id}/` | Détail |
351
+ | GET | `/conversations/{id}/messages/` | Messages |
352
+ | POST | `/conversations/{id}/send_message/` | Envoyer |
353
+
354
+ **WebSocket (Temps Réel):**
355
+ ```javascript
356
+ ws://localhost:8001/ws/chat/{conversation_id}/
357
+ ```
358
+
359
+ ---
360
+
361
+ ### 🔔 Notifications `/api/notifications/`
362
+
363
+ | Méthode | Endpoint | Description |
364
+ |---------|----------|-------------|
365
+ | GET | `/` | Mes notifications |
366
+ | GET | `/{id}/` | Détail |
367
+ | POST | `/mark_all_read/` | Tout marquer lu |
368
+ | POST | `/{id}/mark_read/` | Marquer lu |
369
+ | DELETE | `/{id}/delete_notification/` | Supprimer |
370
+ | GET | `/unread_count/` | Nombre non lues |
371
+
372
+ ---
373
+
374
+ ### 🎓 Opportunités `/api/opportunities/`
375
+
376
+ | Méthode | Endpoint | Description | Filtres |
377
+ |---------|----------|-------------|---------|
378
+ | GET | `/` | Liste | ?type=SCHOLARSHIP/CONTEST/INTERNSHIP/TRAINING |
379
+ | GET | `/{id}/` | Détail | - |
380
+
381
+ ---
382
+
383
+ ### 🤖 IA `/api/ai/`
384
+
385
+ | Méthode | Endpoint | Description |
386
+ |---------|----------|-------------|
387
+ | POST | `/tutor/` | Tuteur IA |
388
+
389
+ **Exemple:**
390
+ ```json
391
+ POST /api/ai/tutor/
392
+ {
393
+ "question": "Peux-tu m'expliquer le théorème de Pythagore?",
394
+ "subject": "Mathématiques",
395
+ "level": "Lycée"
396
+ }
397
+ ```
398
+
399
+ ---
400
+
401
+ ## 🧪 Tests
402
+
403
+ ```bash
404
+ # Lancer tous les tests
405
+ pytest
406
+
407
+ # Tests avec coverage
408
+ pytest --cov=apps --cov-report=html
409
+
410
+ # Tests spécifiques
411
+ pytest apps/users/tests.py
412
+ pytest apps/forum/tests.py::TestForum::test_create_question
413
+
414
+ # Tests verbeux
415
+ pytest -v
416
+
417
+ # Voir la sortie print
418
+ pytest -s
419
+ ```
420
+
421
+ ---
422
+
423
+ ## 🔒 Sécurité
424
+
425
+ ### Headers de Sécurité (Production)
426
+
427
+ ```python
428
+ SECURE_SSL_REDIRECT = True
429
+ SESSION_COOKIE_SECURE = True
430
+ CSRF_COOKIE_SECURE = True
431
+ SECURE_HSTS_SECONDS = 31536000
432
+ X_FRAME_OPTIONS = 'DENY'
433
+ ```
434
+
435
+ ### Rate Limiting (À implémenter)
436
+
437
+ ```python
438
+ # settings.py
439
+ REST_FRAMEWORK = {
440
+ 'DEFAULT_THROTTLE_CLASSES': [
441
+ 'rest_framework.throttling.AnonRateThrottle',
442
+ 'rest_framework.throttling.UserRateThrottle'
443
+ ],
444
+ 'DEFAULT_THROTTLE_RATES': {
445
+ 'anon': '100/hour',
446
+ 'user': '1000/hour'
447
+ }
448
+ }
449
+ ```
450
+
451
+ ---
452
+
453
+ ## 📊 Monitoring & Logs
454
+
455
+ ### Logs
456
+
457
+ ```bash
458
+ # Logs Django
459
+ tail -f logs/django.log
460
+
461
+ # Logs Gunicorn
462
+ tail -f /var/log/gunicorn/error.log
463
+
464
+ # Logs Nginx
465
+ tail -f /var/log/nginx/educonnect_error.log
466
+
467
+ # Logs Celery
468
+ tail -f /var/log/celery/worker.log
469
+ ```
470
+
471
+ ### Sentry (Monitoring Erreurs)
472
+
473
+ ```python
474
+ # settings.py
475
+ import sentry_sdk
476
+
477
+ sentry_sdk.init(
478
+ dsn="your-sentry-dsn",
479
+ environment="production",
480
+ traces_sample_rate=0.1
481
+ )
482
+ ```
483
+
484
+ ---
485
+
486
+ ## 🚀 Déploiement Production
487
+
488
+ ### 1. Préparer le Serveur
489
+
490
+ ```bash
491
+ # Installer dépendances système
492
+ sudo apt update
493
+ sudo apt install python3.11 python3.11-venv postgresql redis nginx
494
+
495
+ # Créer utilisateur
496
+ sudo useradd -m -s /bin/bash educonnect
497
+ ```
498
+
499
+ ### 2. Déployer l'Application
500
+
501
+ ```bash
502
+ # Cloner
503
+ cd /var/www
504
+ sudo git clone <repo> educonnect-api
505
+ sudo chown -R educonnect:educonnect educonnect-api
506
+
507
+ # Setup
508
+ cd educonnect-api
509
+ python3.11 -m venv venv
510
+ source venv/bin/activate
511
+ pip install -r requirements.txt
512
+
513
+ # Configuration
514
+ cp .env.example .env
515
+ # Éditer .env avec les vraies valeurs
516
+
517
+ # Migrations
518
+ python manage.py migrate
519
+ python manage.py collectstatic --noinput
520
+ ```
521
+
522
+ ### 3. Configurer Systemd
523
+
524
+ ```bash
525
+ # Copier les fichiers service
526
+ sudo cp systemd/gunicorn.service /etc/systemd/system/
527
+ sudo cp systemd/celery.service /etc/systemd/system/
528
+
529
+ # Activer et démarrer
530
+ sudo systemctl enable gunicorn celery
531
+ sudo systemctl start gunicorn celery
532
+ ```
533
+
534
+ ### 4. Configurer Nginx
535
+
536
+ ```bash
537
+ # Copier la config
538
+ sudo cp nginx/educonnect.conf /etc/nginx/sites-available/
539
+ sudo ln -s /etc/nginx/sites-available/educonnect.conf /etc/nginx/sites-enabled/
540
+
541
+ # Tester et recharger
542
+ sudo nginx -t
543
+ sudo systemctl reload nginx
544
+ ```
545
+
546
+ ### 5. SSL avec Let's Encrypt
547
+
548
+ ```bash
549
+ sudo apt install certbot python3-certbot-nginx
550
+ sudo certbot --nginx -d api.educonnect.africa
551
+ ```
552
+
553
+ ### 6. Script de Déploiement Automatique
554
+
555
+ ```bash
556
+ chmod +x deploy.sh
557
+ sudo ./deploy.sh
558
+ ```
559
+
560
+ ---
561
+
562
+ ## 🔄 Maintenance
563
+
564
+ ### Backup Base de Données
565
+
566
+ ```bash
567
+ # Backup manuel
568
+ pg_dump educonnect_db > backup_$(date +%Y%m%d).sql
569
+
570
+ # Backup automatique (cron)
571
+ 0 2 * * * pg_dump educonnect_db > /var/backups/educonnect/db_$(date +\%Y\%m\%d).sql
572
+ ```
573
+
574
+ ### Nettoyage
575
+
576
+ ```bash
577
+ # Supprimer anciennes versions (soft deleted)
578
+ python manage.py shell
579
+ >>> from django.utils import timezone
580
+ >>> from datetime import timedelta
581
+ >>> cutoff = timezone.now() - timedelta(days=90)
582
+ >>> # Archiver les données anciennes...
583
+ ```
584
+
585
+ ---
586
+
587
+ ## 📖 Documentation Interactive
588
+
589
+ Une fois l'API lancée:
590
+
591
+ - **Swagger UI**: http://localhost:8000/api/docs/
592
+ - **ReDoc**: http://localhost:8000/api/redoc/
593
+ - **Admin Django**: http://localhost:8000/admin/
594
+
595
+ ---
596
+
597
+ ## 🤝 Contribution
598
+
599
+ 1. Fork le projet
600
+ 2. Créer une branche feature (`git checkout -b feature/AmazingFeature`)
601
+ 3. Commit (`git commit -m 'Add AmazingFeature'`)
602
+ 4. Push (`git push origin feature/AmazingFeature`)
603
+ 5. Pull Request
604
+
605
+ ---
606
+
607
+ ## 📝 Notes Importantes
608
+
609
+ ### Traçabilité des Données
610
+
611
+ - **Aucune suppression physique** : Toutes les suppressions sont des soft deletes
612
+ - **Versioning** : Chaque modification crée un nouvel enregistrement
613
+ - **Audit Trail** : Timestamps et historiques complets
614
+ - **Récupération** : Possibilité de restaurer les données
615
+
616
+ ### Performance
617
+
618
+ - **Indexes** : Sur tous les champs fréquemment filtrés
619
+ - **Pagination** : 20 items par page par défaut
620
+ - **Caching Redis** : Pour sessions et Celery
621
+ - **Connection Pooling** : PostgreSQL avec `CONN_MAX_AGE=600`
622
+
623
+ ### Scalabilité
624
+
625
+ - **Horizontal** : Ajouter des workers Gunicorn/Celery
626
+ - **Vertical** : Augmenter ressources serveur
627
+ - **Database** : PostgreSQL supporte des millions de rows
628
+ - **CDN** : Pour fichiers statiques/media
629
+
630
+ ---
631
+
632
+ ## 📞 Support
633
+
634
+ - **Email**: support@educonnect.africa
635
+ - **Documentation**: https://docs.educonnect.africa
636
+ - **Issues**: https://github.com/hypee/educonnect-api/issues
637
+
638
+ ---
639
+
640
+ **Développé avec ❤️ par Initiative Hypee - Bénin 🇧🇯**
README.md ADDED
@@ -0,0 +1,194 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # EduConnect Africa API
2
+
3
+ API REST Django pour la plateforme EduConnect Africa - Initiative Hypee (Bénin)
4
+
5
+ ## 🚀 Technologies
6
+
7
+ - **Django 4.2** + **Django REST Framework 3.14**
8
+ - **PostgreSQL 15** (Base de données)
9
+ - **Redis** (Cache & Celery)
10
+ - **Channels** (WebSockets pour le chat)
11
+ - **JWT** (Authentification)
12
+ - **Celery** (Tâches asynchrones)
13
+ - **Docker** (Containerisation)
14
+
15
+ ## 📋 Prérequis
16
+
17
+ - Python 3.11+
18
+ - PostgreSQL 15+
19
+ - Redis 7+
20
+ - Docker & Docker Compose (optionnel)
21
+
22
+ ## ⚙️ Installation
23
+
24
+ ### Méthode 1: Installation locale
25
+
26
+ 1. **Cloner le projet**
27
+ ```bash
28
+ git clone <repo-url>
29
+ cd educonnect-api
30
+ ```
31
+
32
+ 2. **Créer un environnement virtuel**
33
+ ```bash
34
+ python -m venv venv
35
+ source venv/bin/activate # Linux/Mac
36
+ # ou
37
+ venv\\Scripts\\activate # Windows
38
+ ```
39
+
40
+ 3. **Installer les dépendances**
41
+ ```bash
42
+ pip install -r requirements.txt
43
+ ```
44
+
45
+ 4. **Configuration**
46
+ ```bash
47
+ cp .env.example .env
48
+ # Éditer .env avec vos paramètres
49
+ ```
50
+
51
+ 5. **Créer la base de données**
52
+ ```bash
53
+ createdb educonnect_db
54
+ ```
55
+
56
+ 6. **Migrations**
57
+ ```bash
58
+ python manage.py makemigrations
59
+ python manage.py migrate
60
+ ```
61
+
62
+ 7. **Initialiser les données**
63
+ ```bash
64
+ python manage.py init_db
65
+ python manage.py create_test_data --users 20
66
+ ```
67
+
68
+ 8. **Créer un superuser**
69
+ ```bash
70
+ python manage.py createsuperuser
71
+ ```
72
+
73
+ 9. **Lancer le serveur**
74
+ ```bash
75
+ python manage.py runserver
76
+ ```
77
+
78
+ ### Méthode 2: Avec Docker
79
+
80
+ ```bash
81
+ docker-compose up --build
82
+ docker-compose exec web python manage.py migrate
83
+ docker-compose exec web python manage.py init_db
84
+ docker-compose exec web python manage.py createsuperuser
85
+ ```
86
+
87
+ ## 📚 Documentation API
88
+
89
+ Une fois le serveur lancé:
90
+ - **Swagger UI**: http://localhost:8000/api/docs/
91
+ - **ReDoc**: http://localhost:8000/api/redoc/
92
+ - **Admin Django**: http://localhost:8000/admin/
93
+
94
+ ## 🔑 Endpoints Principaux
95
+
96
+ ### Authentification
97
+ - `POST /api/auth/register/` - Inscription
98
+ - `POST /api/auth/login/` - Connexion
99
+ - `GET /api/auth/me/` - Profil actuel
100
+ - `PATCH /api/auth/update_profile/` - Mise à jour profil
101
+
102
+ ### Mentors
103
+ - `GET /api/mentors/` - Liste des mentors
104
+ - `GET /api/mentors/{id}/` - Détail mentor
105
+ - `PATCH /api/mentors/my_profile/` - Mise à jour profil mentor
106
+
107
+ ### Forum
108
+ - `GET /api/forum/questions/` - Liste questions
109
+ - `POST /api/forum/questions/` - Créer question
110
+ - `POST /api/forum/questions/{id}/vote/` - Voter
111
+ - `POST /api/forum/questions/{id}/answers/` - Répondre
112
+
113
+ ### Réservations
114
+ - `POST /api/bookings/` - Créer réservation
115
+ - `GET /api/bookings/mentor_requests/` - Demandes reçues
116
+ - `PATCH /api/bookings/{id}/update_status/` - Accepter/Refuser
117
+
118
+ ### Gamification
119
+ - `GET /api/gamification/leaderboard/` - Classement
120
+ - `GET /api/gamification/my_badges/` - Mes badges
121
+ - `GET /api/gamification/stats/` - Statistiques
122
+
123
+ ### Messagerie
124
+ - `GET /api/messages/conversations/` - Conversations
125
+ - `POST /api/messages/conversations/{id}/send_message/` - Envoyer message
126
+ - WebSocket: `ws://localhost:8001/ws/chat/{conversation_id}/`
127
+
128
+ ### Notifications
129
+ - `GET /api/notifications/` - Mes notifications
130
+ - `POST /api/notifications/mark_all_read/` - Tout marquer lu
131
+
132
+ ### Opportunités
133
+ - `GET /api/opportunities/` - Liste opportunités
134
+
135
+ ### IA
136
+ - `POST /api/ai/tutor/` - Tuteur IA
137
+
138
+ ## 🧪 Tests
139
+
140
+ ```bash
141
+ pytest
142
+ pytest --cov=apps
143
+ ```
144
+
145
+ ## 🏗️ Structure du Projet
146
+
147
+ ```
148
+ educonnect-api/
149
+ ├── apps/
150
+ │ ├── users/ # Authentification & Utilisateurs
151
+ │ ├── mentors/ # Gestion mentors
152
+ │ ├── bookings/ # Système de réservation
153
+ │ ├── forum/ # Questions & Réponses
154
+ │ ├── gamification/ # Points & Badges
155
+ │ ├── messaging/ # Chat en temps réel
156
+ │ ├── notifications/ # Notifications
157
+ │ ├── opportunities/ # Opportunités
158
+ │ ├── ai_tools/ # Tuteur IA
159
+ │ ├── analytics/ # Analytics
160
+ │ └── core/ # Mixins, utilities
161
+ ├── educonnect_api/ # Configuration Django
162
+ ├── requirements.txt
163
+ ├── Dockerfile
164
+ └── docker-compose.yml
165
+ ```
166
+
167
+ ## 🔒 Sécurité
168
+
169
+ - Tokens JWT avec refresh
170
+ - Rate limiting
171
+ - CORS configuré
172
+ - Validation des entrées
173
+ - Soft delete pour traçabilité
174
+ - HTTPS obligatoire en production
175
+
176
+ ## 📊 Monitoring
177
+
178
+ Intégration Sentry pour le monitoring des erreurs en production.
179
+
180
+ ## 🤝 Contribution
181
+
182
+ 1. Fork le projet
183
+ 2. Créer une branche (`git checkout -b feature/AmazingFeature`)
184
+ 3. Commit (`git commit -m 'Add AmazingFeature'`)
185
+ 4. Push (`git push origin feature/AmazingFeature`)
186
+ 5. Pull Request
187
+
188
+ ## 📝 Licence
189
+
190
+ Propriétaire - Initiative Hypee Bénin
191
+
192
+ ## 👥 Équipe
193
+
194
+ Initiative Hypee - Bénin
STRUCTURE_FILE.MD ADDED
@@ -0,0 +1,508 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 📂 STRUCTURE COMPLÈTE DES FICHIERS - EduConnect Africa API
2
+
3
+ ## Checklist de Création des Fichiers
4
+
5
+ ### 🔧 Configuration Racine
6
+
7
+ ```
8
+ educonnect-api/
9
+ ├── 📄 manage.py
10
+ ├── 📄 requirements.txt
11
+ ├── 📄 .env.example
12
+ ├── 📄 .gitignore
13
+ ├── 📄 README.md
14
+ ├── 📄 pytest.ini
15
+ ├── 📄 conftest.py
16
+ ├── 📄 Dockerfile
17
+ ├── 📄 docker-compose.yml
18
+ ├── 📄 deploy.sh
19
+ └── 📄 .flake8
20
+ ```
21
+
22
+ ### 📦 Configuration Django (`educonnect_api/`)
23
+
24
+ ```
25
+ educonnect_api/
26
+ ├── 📄 __init__.py
27
+ ├── 📄 settings.py ✅ Créé
28
+ ├── 📄 urls.py ✅ Créé
29
+ ├── 📄 wsgi.py ✅ Créé
30
+ ├── 📄 asgi.py ✅ Créé
31
+ └── 📄 celery.py ✅ Créé
32
+ ```
33
+
34
+ ### 🎯 App: Core (`apps/core/`)
35
+
36
+ ```
37
+ apps/core/
38
+ ├── 📄 __init__.py
39
+ ├── 📄 apps.py
40
+ ├── 📄 models.py ✅ Créé (Mixins)
41
+ ├── 📄 permissions.py ✅ Créé
42
+ ├── 📄 exceptions.py ⚠️ À créer
43
+ ├── 📄 middleware.py ✅ Créé
44
+ ├── 📄 admin.py
45
+ ├── management/
46
+ │ └── commands/
47
+ │ ├── 📄 __init__.py
48
+ │ ├── 📄 init_db.py ✅ Créé
49
+ │ └── 📄 create_test_data.py ✅ Créé
50
+ └── tests/
51
+ └── 📄 test_models.py
52
+ ```
53
+
54
+ ### 👤 App: Users (`apps/users/`)
55
+
56
+ ```
57
+ apps/users/
58
+ ├── 📄 __init__.py
59
+ ├── 📄 apps.py
60
+ ├── 📄 models.py ✅ Créé
61
+ ├── 📄 serializers.py ✅ Créé
62
+ ├── 📄 views.py ✅ Créé
63
+ ├── 📄 urls.py ✅ Créé
64
+ ├── 📄 admin.py ⚠️ À créer
65
+ ├── 📄 tests.py ✅ Créé
66
+ └── migrations/
67
+ └── 📄 __init__.py
68
+ ```
69
+
70
+ ### 👨‍🏫 App: Mentors (`apps/mentors/`)
71
+
72
+ ```
73
+ apps/mentors/
74
+ ├── 📄 __init__.py
75
+ ├── 📄 apps.py
76
+ ├── 📄 models.py ✅ Créé
77
+ ├── 📄 serializers.py ✅ Créé
78
+ ├── 📄 views.py ✅ Créé
79
+ ├── 📄 urls.py ✅ Créé
80
+ ├── 📄 admin.py ⚠️ À créer
81
+ ├── 📄 tests.py ⚠️ À créer
82
+ └── migrations/
83
+ └── 📄 __init__.py
84
+ ```
85
+
86
+ ### 📅 App: Bookings (`apps/bookings/`)
87
+
88
+ ```
89
+ apps/bookings/
90
+ ├── 📄 __init__.py
91
+ ├── 📄 apps.py
92
+ ├── 📄 models.py ✅ Créé
93
+ ├── 📄 serializers.py ✅ Créé
94
+ ├── 📄 views.py ✅ Créé
95
+ ├── 📄 urls.py ✅ Créé
96
+ ├── 📄 admin.py ⚠️ À créer
97
+ ├── 📄 tests.py ⚠️ À créer
98
+ └── migrations/
99
+ └── 📄 __init__.py
100
+ ```
101
+
102
+ ### 💬 App: Forum (`apps/forum/`)
103
+
104
+ ```
105
+ apps/forum/
106
+ ├── 📄 __init__.py
107
+ ├── 📄 apps.py
108
+ ├── 📄 models.py ✅ Créé
109
+ ├── 📄 serializers.py ✅ Créé
110
+ ├── 📄 views.py ✅ Créé
111
+ ├── 📄 urls.py ✅ Créé
112
+ ├── 📄 admin.py ⚠️ À créer
113
+ ├── 📄 tests.py ✅ Créé
114
+ └── migrations/
115
+ └── 📄 __init__.py
116
+ ```
117
+
118
+ ### 🎮 App: Gamification (`apps/gamification/`)
119
+
120
+ ```
121
+ apps/gamification/
122
+ ├── 📄 __init__.py
123
+ ├── 📄 apps.py
124
+ ├── 📄 models.py ✅ Créé
125
+ ├── 📄 serializers.py ✅ Créé
126
+ ├── 📄 views.py ✅ Créé
127
+ ├── 📄 services.py ✅ Créé
128
+ ├── 📄 urls.py ✅ Créé
129
+ ├── 📄 admin.py ⚠️ À créer
130
+ ├── 📄 tests.py ✅ Créé
131
+ └── migrations/
132
+ └── 📄 __init__.py
133
+ ```
134
+
135
+ ### 💬 App: Messaging (`apps/messaging/`)
136
+
137
+ ```
138
+ apps/messaging/
139
+ ├── 📄 __init__.py
140
+ ├── 📄 apps.py
141
+ ├── 📄 models.py ✅ Créé
142
+ ├── 📄 serializers.py ✅ Créé
143
+ ├── 📄 views.py ✅ Créé
144
+ ├── 📄 consumers.py ✅ Créé (WebSocket)
145
+ ├── 📄 urls.py ✅ Créé
146
+ ├── 📄 admin.py ⚠️ À créer
147
+ ├── 📄 tests.py ⚠️ À créer
148
+ └── migrations/
149
+ └── 📄 __init__.py
150
+ ```
151
+
152
+ ### 🔔 App: Notifications (`apps/notifications/`)
153
+
154
+ ```
155
+ apps/notifications/
156
+ ├── 📄 __init__.py
157
+ ├── 📄 apps.py
158
+ ├── 📄 models.py ✅ Créé
159
+ ├── 📄 serializers.py ✅ Créé
160
+ ├── 📄 views.py ✅ Créé
161
+ ├── 📄 services.py ✅ Créé
162
+ ├── 📄 urls.py ✅ Créé
163
+ ├── 📄 admin.py ⚠️ À créer
164
+ ├── 📄 tests.py ⚠️ À créer
165
+ └── migrations/
166
+ └── 📄 __init__.py
167
+ ```
168
+
169
+ ### 🎓 App: Opportunities (`apps/opportunities/`)
170
+
171
+ ```
172
+ apps/opportunities/
173
+ ├── 📄 __init__.py
174
+ ├── 📄 apps.py
175
+ ├── 📄 models.py ✅ Créé
176
+ ├── 📄 serializers.py ✅ Créé
177
+ ├── 📄 views.py ✅ Créé
178
+ ├── 📄 urls.py ✅ Créé
179
+ ├── 📄 admin.py ⚠️ À créer
180
+ ├── 📄 tests.py ⚠️ À créer
181
+ └── migrations/
182
+ └── 📄 __init__.py
183
+ ```
184
+
185
+ ### 🤖 App: AI Tools (`apps/ai_tools/`)
186
+
187
+ ```
188
+ apps/ai_tools/
189
+ ├── 📄 __init__.py
190
+ ├── 📄 apps.py
191
+ ├── 📄 models.py ✅ Créé
192
+ ├── 📄 serializers.py ✅ Créé
193
+ ├── 📄 views.py ✅ Créé
194
+ ├── 📄 urls.py ✅ Créé
195
+ ├── 📄 admin.py ⚠️ À créer
196
+ ├── 📄 tests.py ⚠️ À créer
197
+ └── migrations/
198
+ └── 📄 __init__.py
199
+ ```
200
+
201
+ ### 📊 App: Analytics (`apps/analytics/`)
202
+
203
+ ```
204
+ apps/analytics/
205
+ ├── 📄 __init__.py
206
+ ├── 📄 apps.py
207
+ ├── 📄 models.py ✅ Créé
208
+ ├── 📄 views.py ✅ Créé
209
+ ├── 📄 urls.py ⚠️ À créer
210
+ ├── 📄 admin.py ⚠️ À créer
211
+ └── migrations/
212
+ └── 📄 __init__.py
213
+ ```
214
+
215
+ ### ⚙️ Systemd Services (`systemd/`)
216
+
217
+ ```
218
+ systemd/
219
+ ├── 📄 gunicorn.service ✅ Créé
220
+ └── 📄 celery.service ✅ Créé
221
+ ```
222
+
223
+ ### 🌐 Nginx Configuration (`nginx/`)
224
+
225
+ ```
226
+ nginx/
227
+ └── 📄 educonnect.conf ✅ Créé
228
+ ```
229
+
230
+ ### 🧪 GitHub Actions (`.github/workflows/`)
231
+
232
+ ```
233
+ .github/workflows/
234
+ └── 📄 ci.yml ✅ Créé
235
+ ```
236
+
237
+ ---
238
+
239
+ ## 📝 Fichiers Manquants à Créer
240
+
241
+ ### 1. `.gitignore`
242
+
243
+ ```gitignore
244
+ # Python
245
+ __pycache__/
246
+ *.py[cod]
247
+ *$py.class
248
+ *.so
249
+ .Python
250
+ venv/
251
+ env/
252
+ ENV/
253
+ *.egg-info/
254
+ dist/
255
+ build/
256
+
257
+ # Django
258
+ *.log
259
+ local_settings.py
260
+ db.sqlite3
261
+ media/
262
+ staticfiles/
263
+
264
+ # Environment
265
+ .env
266
+ .env.local
267
+
268
+ # IDE
269
+ .vscode/
270
+ .idea/
271
+ *.swp
272
+ *.swo
273
+ *~
274
+
275
+ # OS
276
+ .DS_Store
277
+ Thumbs.db
278
+
279
+ # Testing
280
+ .coverage
281
+ htmlcov/
282
+ .pytest_cache/
283
+
284
+ # Celery
285
+ celerybeat-schedule
286
+ celerybeat.pid
287
+ ```
288
+
289
+ ### 2. `.flake8`
290
+
291
+ ```ini
292
+ [flake8]
293
+ max-line-length = 127
294
+ exclude =
295
+ .git,
296
+ __pycache__,
297
+ venv,
298
+ migrations,
299
+ */migrations/*
300
+ ignore = E203, E266, W503
301
+ ```
302
+
303
+ ### 3. Fichiers `apps.py` pour chaque app
304
+
305
+ **Exemple `apps/users/apps.py`:**
306
+
307
+ ```python
308
+ from django.apps import AppConfig
309
+
310
+ class UsersConfig(AppConfig):
311
+ default_auto_field = 'django.db.models.BigAutoField'
312
+ name = 'apps.users'
313
+
314
+ def ready(self):
315
+ import apps.core.signals # Charger les signals
316
+ ```
317
+
318
+ ### 4. Fichiers `admin.py` pour chaque app
319
+
320
+ **Exemple `apps/users/admin.py`:**
321
+
322
+ ```python
323
+ from django.contrib import admin
324
+ from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
325
+ from apps.users.models import User, UserProfile
326
+
327
+ @admin.register(User)
328
+ class UserAdmin(BaseUserAdmin):
329
+ list_display = ['email', 'role', 'points', 'is_active', 'created_at']
330
+ list_filter = ['role', 'is_active', 'created_at']
331
+ search_fields = ['email']
332
+ ordering = ['-created_at']
333
+
334
+ fieldsets = (
335
+ (None, {'fields': ('email', 'password')}),
336
+ ('Informations', {'fields': ('role', 'points')}),
337
+ ('Permissions', {'fields': ('is_active', 'is_staff', 'is_superuser')}),
338
+ ('Dates', {'fields': ('created_at', 'updated_at')}),
339
+ )
340
+
341
+ readonly_fields = ['created_at', 'updated_at']
342
+
343
+ add_fieldsets = (
344
+ (None, {
345
+ 'classes': ('wide',),
346
+ 'fields': ('email', 'password1', 'password2', 'role'),
347
+ }),
348
+ )
349
+
350
+ @admin.register(UserProfile)
351
+ class UserProfileAdmin(admin.ModelAdmin):
352
+ list_display = ['user', 'name', 'is_current', 'created_at']
353
+ list_filter = ['is_current', 'created_at']
354
+ search_fields = ['user__email', 'name']
355
+ ```
356
+
357
+ ### 5. `apps/core/exceptions.py`
358
+
359
+ ```python
360
+ from rest_framework.views import exception_handler
361
+ from rest_framework.response import Response
362
+ from rest_framework import status
363
+ import logging
364
+
365
+ logger = logging.getLogger(__name__)
366
+
367
+ def custom_exception_handler(exc, context):
368
+ """Gestionnaire d'exceptions personnalisé"""
369
+
370
+ # Appeler le gestionnaire par défaut
371
+ response = exception_handler(exc, context)
372
+
373
+ # Logger l'erreur
374
+ logger.error(f"Exception: {exc}", exc_info=True)
375
+
376
+ if response is not None:
377
+ # Personnaliser le format de la réponse
378
+ custom_response_data = {
379
+ 'error': True,
380
+ 'message': str(exc),
381
+ 'status_code': response.status_code
382
+ }
383
+
384
+ if hasattr(exc, 'detail'):
385
+ custom_response_data['details'] = exc.detail
386
+
387
+ response.data = custom_response_data
388
+
389
+ return response
390
+ ```
391
+
392
+ ### 6. `apps/__init__.py`
393
+
394
+ Créer un fichier `__init__.py` vide dans chaque dossier d'app.
395
+
396
+ ---
397
+
398
+ ## 🚀 Script de Création Automatique
399
+
400
+ Pour créer tous les fichiers manquants automatiquement:
401
+
402
+ ```bash
403
+ #!/bin/bash
404
+
405
+ # create_structure.sh
406
+
407
+ echo "Création de la structure EduConnect Africa API..."
408
+
409
+ # Apps list
410
+ APPS=("users" "mentors" "bookings" "forum" "gamification" "messaging" "notifications" "opportunities" "ai_tools" "analytics" "core")
411
+
412
+ # Créer les dossiers principaux
413
+ mkdir -p apps logs media staticfiles templates systemd nginx .github/workflows
414
+
415
+ # Créer la structure pour chaque app
416
+ for app in "${APPS[@]}"; do
417
+ echo "Création de apps/$app..."
418
+ mkdir -p apps/$app/migrations
419
+ touch apps/$app/__init__.py
420
+ touch apps/$app/apps.py
421
+ touch apps/$app/models.py
422
+ touch apps/$app/serializers.py
423
+ touch apps/$app/views.py
424
+ touch apps/$app/urls.py
425
+ touch apps/$app/admin.py
426
+ touch apps/$app/tests.py
427
+ touch apps/$app/migrations/__init__.py
428
+ done
429
+
430
+ # Créer management commands
431
+ mkdir -p apps/core/management/commands
432
+ touch apps/core/management/__init__.py
433
+ touch apps/core/management/commands/__init__.py
434
+
435
+ # Configuration Django
436
+ mkdir -p educonnect_api
437
+ touch educonnect_api/__init__.py
438
+
439
+ # Fichiers racine
440
+ touch manage.py
441
+ touch pytest.ini
442
+ touch conftest.py
443
+ touch .gitignore
444
+ touch .flake8
445
+ touch README.md
446
+
447
+ echo "✅ Structure créée avec succès!"
448
+ echo "📝 N'oubliez pas de remplir les fichiers avec le code approprié"
449
+ ```
450
+
451
+ Rendre exécutable:
452
+ ```bash
453
+ chmod +x create_structure.sh
454
+ ./create_structure.sh
455
+ ```
456
+
457
+ ---
458
+
459
+ ## ✅ Checklist de Vérification
460
+
461
+ Avant de lancer l'application, vérifier:
462
+
463
+ - [ ] Tous les fichiers `__init__.py` sont créés
464
+ - [ ] `.env` est configuré avec les vraies valeurs
465
+ - [ ] PostgreSQL est installé et la DB créée
466
+ - [ ] Redis est installé et lancé
467
+ - [ ] Tous les fichiers de migrations sont présents
468
+ - [ ] Les fichiers `apps.py` sont configurés
469
+ - [ ] Les `admin.py` sont remplis
470
+ - [ ] Les tests passent (`pytest`)
471
+ - [ ] Le serveur démarre (`python manage.py runserver`)
472
+ - [ ] Celery fonctionne
473
+ - [ ] Les WebSockets fonctionnent (Channels/Daphne)
474
+
475
+ ---
476
+
477
+ ## 🔄 Ordre de Création Recommandé
478
+
479
+ 1. **Configuration de base**
480
+ - `settings.py`
481
+ - `urls.py`
482
+ - `.env`
483
+
484
+ 2. **Core & Users**
485
+ - `apps/core/models.py` (mixins)
486
+ - `apps/users/models.py`
487
+ - `apps/users/serializers.py`
488
+ - `apps/users/views.py`
489
+
490
+ 3. **Autres apps une par une**
491
+ - Models → Serializers → Views → URLs
492
+
493
+ 4. **Services & Signals**
494
+ - `apps/gamification/services.py`
495
+ - `apps/notifications/services.py`
496
+ - `apps/core/signals.py`
497
+
498
+ 5. **Tests**
499
+ - Écrire les tests au fur et à mesure
500
+
501
+ 6. **Configuration serveur**
502
+ - Systemd services
503
+ - Nginx
504
+ - SSL
505
+
506
+ ---
507
+
508
+ **🎯 Objectif: API REST complète, sécurisée, scalable et maintenable!**
apps/ai_tools/__init__.py ADDED
File without changes
apps/ai_tools/admin.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from django.contrib import admin
2
+ from .models import AITutorSession, AITutorQuestion, CodeSnippet
3
+
4
+ class AITutorQuestionInline(admin.StackedInline):
5
+ model = AITutorQuestion
6
+ extra = 0
7
+
8
+ @admin.register(AITutorSession)
9
+ class AITutorSessionAdmin(admin.ModelAdmin):
10
+ list_display = ('user', 'subject', 'level', 'total_questions', 'created_at')
11
+ list_filter = ('subject', 'level', 'created_at')
12
+ search_fields = ('user__email', 'subject')
13
+ inlines = [AITutorQuestionInline]
14
+
15
+ @admin.register(CodeSnippet)
16
+ class CodeSnippetAdmin(admin.ModelAdmin):
17
+ list_display = ('user', 'title', 'language', 'is_public', 'created_at')
18
+ list_filter = ('language', 'is_public', 'created_at')
19
+ search_fields = ('user__email', 'title', 'code')
apps/ai_tools/apps.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class AiToolsConfig(AppConfig):
5
+ default_auto_field = 'django.db.models.BigAutoField'
6
+ name = 'apps.ai_tools'
apps/ai_tools/migrations/0001_initial.py ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Generated by Django 4.2.7 on 2025-11-29 13:38
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+ initial = True
8
+
9
+ dependencies = []
10
+
11
+ operations = [
12
+ migrations.CreateModel(
13
+ name="AITutorQuestion",
14
+ fields=[
15
+ (
16
+ "id",
17
+ models.BigAutoField(
18
+ auto_created=True,
19
+ primary_key=True,
20
+ serialize=False,
21
+ verbose_name="ID",
22
+ ),
23
+ ),
24
+ ("created_at", models.DateTimeField(auto_now_add=True, db_index=True)),
25
+ ("updated_at", models.DateTimeField(auto_now=True)),
26
+ ("question", models.TextField()),
27
+ ("answer", models.TextField()),
28
+ ("model_used", models.CharField(max_length=50)),
29
+ ("tokens_used", models.IntegerField(blank=True, null=True)),
30
+ ("response_time", models.FloatField(blank=True, null=True)),
31
+ ],
32
+ options={
33
+ "db_table": "ai_tutor_questions",
34
+ "ordering": ["created_at"],
35
+ },
36
+ ),
37
+ migrations.CreateModel(
38
+ name="AITutorSession",
39
+ fields=[
40
+ (
41
+ "id",
42
+ models.BigAutoField(
43
+ auto_created=True,
44
+ primary_key=True,
45
+ serialize=False,
46
+ verbose_name="ID",
47
+ ),
48
+ ),
49
+ ("created_at", models.DateTimeField(auto_now_add=True, db_index=True)),
50
+ ("updated_at", models.DateTimeField(auto_now=True)),
51
+ (
52
+ "deleted_at",
53
+ models.DateTimeField(blank=True, db_index=True, null=True),
54
+ ),
55
+ ("is_active", models.BooleanField(db_index=True, default=True)),
56
+ ("subject", models.CharField(blank=True, max_length=100, null=True)),
57
+ ("level", models.CharField(blank=True, max_length=50, null=True)),
58
+ ("total_questions", models.IntegerField(default=0)),
59
+ ],
60
+ options={
61
+ "verbose_name": "Session Tuteur IA",
62
+ "verbose_name_plural": "Sessions Tuteur IA",
63
+ "db_table": "ai_tutor_sessions",
64
+ "ordering": ["-created_at"],
65
+ },
66
+ ),
67
+ migrations.CreateModel(
68
+ name="CodeSnippet",
69
+ fields=[
70
+ (
71
+ "id",
72
+ models.BigAutoField(
73
+ auto_created=True,
74
+ primary_key=True,
75
+ serialize=False,
76
+ verbose_name="ID",
77
+ ),
78
+ ),
79
+ ("created_at", models.DateTimeField(auto_now_add=True, db_index=True)),
80
+ ("updated_at", models.DateTimeField(auto_now=True)),
81
+ (
82
+ "deleted_at",
83
+ models.DateTimeField(blank=True, db_index=True, null=True),
84
+ ),
85
+ ("is_active", models.BooleanField(db_index=True, default=True)),
86
+ ("title", models.CharField(max_length=255)),
87
+ ("language", models.CharField(max_length=50)),
88
+ ("code", models.TextField()),
89
+ ("description", models.TextField(blank=True, null=True)),
90
+ ("is_public", models.BooleanField(default=False)),
91
+ ],
92
+ options={
93
+ "verbose_name": "Snippet de Code",
94
+ "verbose_name_plural": "Snippets de Code",
95
+ "db_table": "code_snippets",
96
+ "ordering": ["-created_at"],
97
+ },
98
+ ),
99
+ ]
apps/ai_tools/migrations/0002_initial.py ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Generated by Django 4.2.7 on 2025-11-29 13:38
2
+
3
+ from django.conf import settings
4
+ from django.db import migrations, models
5
+ import django.db.models.deletion
6
+
7
+
8
+ class Migration(migrations.Migration):
9
+ initial = True
10
+
11
+ dependencies = [
12
+ ("ai_tools", "0001_initial"),
13
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
14
+ ]
15
+
16
+ operations = [
17
+ migrations.AddField(
18
+ model_name="codesnippet",
19
+ name="user",
20
+ field=models.ForeignKey(
21
+ on_delete=django.db.models.deletion.CASCADE,
22
+ related_name="code_snippets",
23
+ to=settings.AUTH_USER_MODEL,
24
+ ),
25
+ ),
26
+ migrations.AddField(
27
+ model_name="aitutorsession",
28
+ name="user",
29
+ field=models.ForeignKey(
30
+ on_delete=django.db.models.deletion.CASCADE,
31
+ related_name="ai_sessions",
32
+ to=settings.AUTH_USER_MODEL,
33
+ ),
34
+ ),
35
+ migrations.AddField(
36
+ model_name="aitutorquestion",
37
+ name="session",
38
+ field=models.ForeignKey(
39
+ on_delete=django.db.models.deletion.CASCADE,
40
+ related_name="questions",
41
+ to="ai_tools.aitutorsession",
42
+ ),
43
+ ),
44
+ ]
apps/ai_tools/migrations/__init__.py ADDED
File without changes
apps/ai_tools/models.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ============================================
2
+ # apps/ai_tools/models.py - Historique IA
3
+ # ============================================
4
+ from django.db import models
5
+ from apps.core.models import TimestampMixin, SoftDeleteMixin
6
+ from apps.users.models import User
7
+
8
+ class AITutorSession(TimestampMixin, SoftDeleteMixin):
9
+ """Session de tuteur IA"""
10
+ user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='ai_sessions')
11
+ subject = models.CharField(max_length=100, null=True, blank=True)
12
+ level = models.CharField(max_length=50, null=True, blank=True)
13
+ total_questions = models.IntegerField(default=0)
14
+
15
+ class Meta:
16
+ db_table = 'ai_tutor_sessions'
17
+ verbose_name = 'Session Tuteur IA'
18
+ verbose_name_plural = 'Sessions Tuteur IA'
19
+ ordering = ['-created_at']
20
+
21
+ class AITutorQuestion(TimestampMixin):
22
+ """Question posée au tuteur IA"""
23
+ session = models.ForeignKey(AITutorSession, on_delete=models.CASCADE, related_name='questions')
24
+ question = models.TextField()
25
+ answer = models.TextField()
26
+ model_used = models.CharField(max_length=50) # gemini, gpt-4, etc.
27
+ tokens_used = models.IntegerField(null=True, blank=True)
28
+ response_time = models.FloatField(null=True, blank=True) # en secondes
29
+
30
+ class Meta:
31
+ db_table = 'ai_tutor_questions'
32
+ ordering = ['created_at']
33
+
34
+ class CodeSnippet(TimestampMixin, SoftDeleteMixin):
35
+ """Sauvegardes du sandbox de code"""
36
+ user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='code_snippets')
37
+ title = models.CharField(max_length=255)
38
+ language = models.CharField(max_length=50)
39
+ code = models.TextField()
40
+ description = models.TextField(null=True, blank=True)
41
+ is_public = models.BooleanField(default=False)
42
+
43
+ class Meta:
44
+ db_table = 'code_snippets'
45
+ verbose_name = 'Snippet de Code'
46
+ verbose_name_plural = 'Snippets de Code'
47
+ ordering = ['-created_at']
apps/ai_tools/serializers.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ============================================
2
+ # apps/ai_tools/serializers.py
3
+ # ============================================
4
+ from rest_framework import serializers
5
+
6
+ class AITutorRequestSerializer(serializers.Serializer):
7
+ question = serializers.CharField()
8
+ subject = serializers.CharField(max_length=100, required=False)
9
+ level = serializers.CharField(max_length=50, required=False)
10
+ style = serializers.CharField(max_length=100, required=False)
11
+ conversation_history = serializers.ListField(
12
+ child=serializers.DictField(),
13
+ required=False,
14
+ default=list
15
+ )
16
+
17
+ class AITutorResponseSerializer(serializers.Serializer):
18
+ answer = serializers.CharField()
19
+ session_id = serializers.IntegerField()
20
+ tokens_used = serializers.IntegerField(required=False)
21
+
apps/ai_tools/tests.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from django.test import TestCase
2
+
3
+ # Create your tests here.
apps/ai_tools/urls.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ============================================
2
+ # apps/ai_tools/urls.py
3
+ # ============================================
4
+ from django.urls import path, include
5
+ from rest_framework.routers import DefaultRouter
6
+ from apps.ai_tools.views import AIToolsViewSet
7
+
8
+ router = DefaultRouter()
9
+ router.register(r'', AIToolsViewSet, basename='ai_tools')
10
+
11
+ urlpatterns = [
12
+ path('', include(router.urls)),
13
+ ]
14
+
15
+ """
16
+ Endpoints disponibles:
17
+ - POST /api/ai/tutor/
18
+ """
apps/ai_tools/views.py ADDED
@@ -0,0 +1,200 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ============================================
2
+ # apps/ai_tools/views.py
3
+ # ============================================
4
+ from rest_framework import viewsets, status
5
+ from rest_framework.decorators import action
6
+ from rest_framework.response import Response
7
+ from rest_framework.permissions import IsAuthenticated
8
+ from django.conf import settings
9
+
10
+ from apps.ai_tools.serializers import (
11
+ AITutorRequestSerializer, AITutorResponseSerializer
12
+ )
13
+
14
+ class AIToolsViewSet(viewsets.GenericViewSet):
15
+ """Outils IA"""
16
+ permission_classes = [IsAuthenticated]
17
+
18
+ @action(detail=False, methods=['post'])
19
+ def tutor(self, request):
20
+ """POST /api/ai/tutor/ - Proxy vers l'API Gemini/OpenAI"""
21
+ serializer = AITutorRequestSerializer(data=request.data)
22
+ serializer.is_valid(raise_exception=True)
23
+
24
+ question = serializer.validated_data['question']
25
+ subject = serializer.validated_data.get('subject', '')
26
+ level = serializer.validated_data.get('level', '')
27
+ style = serializer.validated_data.get('style', '')
28
+
29
+ try:
30
+ # Utiliser Gemini si disponible, sinon OpenAI
31
+ if settings.GEMINI_API_KEY:
32
+ answer, tokens = self._call_gemini(question, subject, level, style)
33
+ elif settings.OPENAI_API_KEY:
34
+ answer, tokens = self._call_openai(question, subject, level, style)
35
+ else:
36
+ return Response(
37
+ {'error': 'Aucune API IA configurée'},
38
+ status=status.HTTP_503_SERVICE_UNAVAILABLE
39
+ )
40
+
41
+ # Sauvegarder dans l'historique
42
+ from apps.ai_tools.models import AITutorSession, AITutorQuestion
43
+ from django.utils import timezone
44
+ import time
45
+
46
+ start_time = time.time()
47
+
48
+ session = AITutorSession.objects.create(
49
+ user=request.user,
50
+ subject=subject,
51
+ level=level
52
+ )
53
+
54
+ AITutorQuestion.objects.create(
55
+ session=session,
56
+ question=question,
57
+ answer=answer,
58
+ model_used='gemini' if settings.GEMINI_API_KEY else 'openai',
59
+ tokens_used=tokens,
60
+ response_time=time.time() - start_time
61
+ )
62
+
63
+ session.total_questions += 1
64
+ session.save()
65
+
66
+ response_serializer = AITutorResponseSerializer({
67
+ 'answer': answer,
68
+ 'session_id': session.id,
69
+ 'tokens_used': tokens
70
+ })
71
+
72
+ return Response(response_serializer.data)
73
+
74
+ except Exception as e:
75
+ import traceback
76
+ error_details = traceback.format_exc()
77
+ print(f"GEMINI ERROR: {error_details}") # Pour les logs
78
+
79
+ return Response(
80
+ {
81
+ 'error': str(e),
82
+ 'details': 'Vérifiez que votre clé API Gemini est valide et configurée dans GEMINI_API_KEY',
83
+ 'type': type(e).__name__
84
+ },
85
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR
86
+ )
87
+
88
+ @action(detail=False, methods=['get'])
89
+ def history(self, request):
90
+ """GET /api/ai/history/ - Récupérer l'historique des sessions"""
91
+ from apps.ai_tools.models import AITutorSession
92
+
93
+ sessions = AITutorSession.objects.filter(
94
+ user=request.user,
95
+ is_active=True
96
+ ).prefetch_related('questions')[:20] # Dernières 20 sessions
97
+
98
+ sessions_data = []
99
+ for session in sessions:
100
+ questions_list = list(session.questions.all())
101
+ sessions_data.append({
102
+ 'id': session.id,
103
+ 'subject': session.subject,
104
+ 'level': session.level,
105
+ 'total_questions': session.total_questions,
106
+ 'created_at': session.created_at.isoformat(),
107
+ 'first_question': questions_list[0].question if questions_list else '',
108
+ 'questions': [
109
+ {
110
+ 'question': q.question,
111
+ 'answer': q.answer,
112
+ 'created_at': q.created_at.isoformat()
113
+ } for q in questions_list
114
+ ]
115
+ })
116
+
117
+ return Response(sessions_data)
118
+
119
+ def _call_gemini(self, question, subject, level, style=''):
120
+ """Appel à l'API Gemini"""
121
+ from google import genai
122
+ from django.conf import settings
123
+
124
+ # Le client récupère la clé API depuis settings ou variable d'environnement
125
+ # Ici on passe explicitement la clé si elle est dans settings
126
+ client = genai.Client(api_key=settings.GEMINI_API_KEY)
127
+
128
+ # Construction du prompt avec tous les paramètres
129
+ style_instruction = ""
130
+ if style:
131
+ if "Simple" in style or "Concis" in style:
132
+ style_instruction = "Sois concis et va droit au but. Utilise un langage simple."
133
+ elif "Détaillé" in style or "Académique" in style:
134
+ style_instruction = "Fournis une explication détaillée et académique avec des références théoriques."
135
+ elif "5 ans" in style or "ELI5" in style:
136
+ style_instruction = "Explique comme si tu parlais à un enfant de 5 ans. Utilise des analogies simples et amusantes."
137
+ elif "Pratique" in style or "Exemples" in style:
138
+ style_instruction = "Concentre-toi sur des exemples pratiques et concrets. Montre comment appliquer les concepts."
139
+ elif "Socratique" in style:
140
+ style_instruction = "Utilise la méthode socratique : pose des questions pour guider l'étudiant vers la réponse."
141
+
142
+ prompt = f"""Tu es un tuteur pédagogique bienveillant et patient pour EduLab Africa.
143
+ L'étudiant est basé en Afrique.
144
+
145
+ Contexte:
146
+ Matière: {subject if subject else 'Général'}
147
+ Niveau: {level if level else 'Adapté'}
148
+ {f'Style: {style_instruction}' if style_instruction else ''}
149
+
150
+ Question de l'étudiant: {question}
151
+
152
+ Réponds de manière claire, structurée et pédagogique. Utilise des exemples culturellement pertinents pour un étudiant africain si possible.
153
+
154
+ IMPORTANT - Formatage des formules :
155
+ - Pour les formules mathématiques inline (dans le texte), utilise la syntaxe : $formule$
156
+ Exemple: La formule $E = mc^2$ est célèbre
157
+ - Pour les formules mathématiques en bloc (centrées), utilise : $$formule$$
158
+ Exemple: $$\\int_{{a}}^{{b}} x^2 dx = \\frac{{b^3 - a^3}}{{3}}$$
159
+ - Pour les formules chimiques, utilise aussi $ : $H_2O$, $CO_2$, $C_6H_{{12}}O_6$
160
+ - Pour les équations chimiques : $$CH_4 + 2O_2 \\rightarrow CO_2 + 2H_2O$$
161
+ - Utilise TOUJOURS cette syntaxe LaTeX pour TOUTES les formules mathématiques et chimiques
162
+ """
163
+
164
+ response = client.models.generate_content(
165
+ model="gemini-flash-latest", contents=prompt
166
+ )
167
+
168
+ # Estimation des tokens (approximatif)
169
+ tokens = len(prompt.split()) + len(response.text.split())
170
+
171
+ return response.text, tokens
172
+
173
+
174
+ def _call_openai(self, question, subject, level, style=''):
175
+ """Appel à l'API OpenAI"""
176
+ import openai
177
+ from django.conf import settings
178
+
179
+ openai.api_key = settings.OPENAI_API_KEY
180
+
181
+ response = openai.ChatCompletion.create(
182
+ model="gpt-3.5-turbo",
183
+ messages=[
184
+ {
185
+ "role": "system",
186
+ "content": f"Tu es un tuteur pédagogique pour la matière {subject} au niveau {level}."
187
+ },
188
+ {
189
+ "role": "user",
190
+ "content": question
191
+ }
192
+ ]
193
+ )
194
+
195
+ answer = response.choices[0].message.content
196
+ tokens = response.usage.total_tokens
197
+
198
+ return answer, tokens
199
+
200
+
apps/analytics/__init__.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ # apps/analytics/__init__.py
2
+ default_app_config = 'apps.analytics.apps.AnalyticsConfig'
apps/analytics/admin.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from django.contrib import admin
2
+ from apps.analytics.models import SearchLog, PopularSearch
3
+
4
+ @admin.register(SearchLog)
5
+ class SearchLogAdmin(admin.ModelAdmin):
6
+ list_display = ['id', 'user', 'category', 'search_query', 'results_count', 'created_at']
7
+ list_filter = ['category', 'created_at']
8
+ search_fields = ['search_query', 'user__email']
9
+ readonly_fields = ['created_at', 'updated_at']
10
+ date_hierarchy = 'created_at'
11
+
12
+ fieldsets = (
13
+ ('Recherche', {
14
+ 'fields': ('user', 'category', 'search_query', 'filters_applied', 'results_count')
15
+ }),
16
+ ('Interaction', {
17
+ 'fields': ('clicked_result_id', 'clicked_result_position')
18
+ }),
19
+ ('Métadonnées', {
20
+ 'fields': ('session_id', 'ip_address', 'user_agent', 'page_url'),
21
+ 'classes': ('collapse',)
22
+ }),
23
+ ('Timestamps', {
24
+ 'fields': ('created_at', 'updated_at'),
25
+ 'classes': ('collapse',)
26
+ }),
27
+ )
28
+
29
+
30
+ @admin.register(PopularSearch)
31
+ class PopularSearchAdmin(admin.ModelAdmin):
32
+ list_display = ['search_query', 'category', 'search_count', 'last_searched']
33
+ list_filter = ['category']
34
+ search_fields = ['search_query']
35
+ ordering = ['-search_count', '-last_searched']
apps/analytics/apps.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class AnalyticsConfig(AppConfig):
5
+ default_auto_field = 'django.db.models.BigAutoField'
6
+ name = 'apps.analytics'
7
+ verbose_name = 'Analytics & Tracking'
apps/analytics/migrations/0001_initial.py ADDED
@@ -0,0 +1,185 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Generated by Django 4.2.7 on 2025-12-04 23:49
2
+
3
+ from django.conf import settings
4
+ from django.db import migrations, models
5
+ import django.db.models.deletion
6
+
7
+
8
+ class Migration(migrations.Migration):
9
+ initial = True
10
+
11
+ dependencies = [
12
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
13
+ ]
14
+
15
+ operations = [
16
+ migrations.CreateModel(
17
+ name="PopularSearch",
18
+ fields=[
19
+ (
20
+ "id",
21
+ models.BigAutoField(
22
+ auto_created=True,
23
+ primary_key=True,
24
+ serialize=False,
25
+ verbose_name="ID",
26
+ ),
27
+ ),
28
+ (
29
+ "category",
30
+ models.CharField(
31
+ choices=[
32
+ ("QUESTIONS", "Questions du Forum"),
33
+ ("MENTORS", "Recherche de Mentors"),
34
+ ("OPPORTUNITIES", "Opportunités"),
35
+ ("TOOLS", "Outils Pédagogiques"),
36
+ ("USERS", "Utilisateurs"),
37
+ ("GENERAL", "Recherche Générale"),
38
+ ],
39
+ max_length=20,
40
+ ),
41
+ ),
42
+ ("search_query", models.CharField(max_length=500)),
43
+ ("search_count", models.IntegerField(default=0)),
44
+ ("last_searched", models.DateTimeField(auto_now=True)),
45
+ ],
46
+ options={
47
+ "verbose_name": "Recherche Populaire",
48
+ "verbose_name_plural": "Recherches Populaires",
49
+ "db_table": "popular_searches",
50
+ "ordering": ["-search_count", "-last_searched"],
51
+ "unique_together": {("category", "search_query")},
52
+ },
53
+ ),
54
+ migrations.CreateModel(
55
+ name="SearchLog",
56
+ fields=[
57
+ (
58
+ "id",
59
+ models.BigAutoField(
60
+ auto_created=True,
61
+ primary_key=True,
62
+ serialize=False,
63
+ verbose_name="ID",
64
+ ),
65
+ ),
66
+ ("created_at", models.DateTimeField(auto_now_add=True, db_index=True)),
67
+ ("updated_at", models.DateTimeField(auto_now=True)),
68
+ (
69
+ "category",
70
+ models.CharField(
71
+ choices=[
72
+ ("QUESTIONS", "Questions du Forum"),
73
+ ("MENTORS", "Recherche de Mentors"),
74
+ ("OPPORTUNITIES", "Opportunités"),
75
+ ("TOOLS", "Outils Pédagogiques"),
76
+ ("USERS", "Utilisateurs"),
77
+ ("GENERAL", "Recherche Générale"),
78
+ ],
79
+ db_index=True,
80
+ help_text="Type de contenu recherché",
81
+ max_length=20,
82
+ ),
83
+ ),
84
+ (
85
+ "search_query",
86
+ models.CharField(
87
+ db_index=True,
88
+ help_text="Texte saisi par l'utilisateur",
89
+ max_length=500,
90
+ ),
91
+ ),
92
+ (
93
+ "filters_applied",
94
+ models.JSONField(
95
+ blank=True,
96
+ default=dict,
97
+ help_text='Filtres appliqués lors de la recherche (ex: {"status": "solved", "tags": ["math"]})',
98
+ ),
99
+ ),
100
+ (
101
+ "results_count",
102
+ models.IntegerField(
103
+ default=0, help_text="Nombre de résultats retournés"
104
+ ),
105
+ ),
106
+ (
107
+ "session_id",
108
+ models.CharField(
109
+ blank=True,
110
+ db_index=True,
111
+ help_text="ID de session pour regrouper les recherches d'une même visite",
112
+ max_length=100,
113
+ ),
114
+ ),
115
+ (
116
+ "ip_address",
117
+ models.GenericIPAddressField(
118
+ blank=True, help_text="Adresse IP de l'utilisateur", null=True
119
+ ),
120
+ ),
121
+ (
122
+ "user_agent",
123
+ models.TextField(blank=True, help_text="User agent du navigateur"),
124
+ ),
125
+ (
126
+ "page_url",
127
+ models.CharField(
128
+ blank=True,
129
+ help_text="URL de la page où la recherche a été effectuée",
130
+ max_length=500,
131
+ ),
132
+ ),
133
+ (
134
+ "clicked_result_id",
135
+ models.CharField(
136
+ blank=True,
137
+ help_text="ID du résultat cliqué (si applicable)",
138
+ max_length=100,
139
+ null=True,
140
+ ),
141
+ ),
142
+ (
143
+ "clicked_result_position",
144
+ models.IntegerField(
145
+ blank=True,
146
+ help_text="Position du résultat cliqué dans la liste",
147
+ null=True,
148
+ ),
149
+ ),
150
+ (
151
+ "user",
152
+ models.ForeignKey(
153
+ blank=True,
154
+ help_text="Utilisateur qui a effectué la recherche (null si anonyme)",
155
+ null=True,
156
+ on_delete=django.db.models.deletion.SET_NULL,
157
+ related_name="search_logs",
158
+ to=settings.AUTH_USER_MODEL,
159
+ ),
160
+ ),
161
+ ],
162
+ options={
163
+ "verbose_name": "Recherche",
164
+ "verbose_name_plural": "Recherches",
165
+ "db_table": "search_logs",
166
+ "ordering": ["-created_at"],
167
+ "indexes": [
168
+ models.Index(
169
+ fields=["category", "-created_at"],
170
+ name="search_logs_categor_24bab5_idx",
171
+ ),
172
+ models.Index(
173
+ fields=["user", "-created_at"],
174
+ name="search_logs_user_id_30eff5_idx",
175
+ ),
176
+ models.Index(
177
+ fields=["search_query"], name="search_logs_search__18b7a2_idx"
178
+ ),
179
+ models.Index(
180
+ fields=["-created_at"], name="search_logs_created_9a6547_idx"
181
+ ),
182
+ ],
183
+ },
184
+ ),
185
+ ]
apps/analytics/migrations/__init__.py ADDED
File without changes
apps/analytics/models.py ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ============================================
2
+ # apps/analytics/models.py - Analytics & Search Tracking
3
+ # ============================================
4
+ from django.db import models
5
+ from apps.core.models import TimestampMixin
6
+ from apps.users.models import User
7
+
8
+ class SearchLog(TimestampMixin):
9
+ """
10
+ Enregistre chaque recherche effectuée sur la plateforme
11
+ pour analyser les intérêts et comportements des utilisateurs
12
+ """
13
+ CATEGORY_CHOICES = [
14
+ ('QUESTIONS', 'Questions du Forum'),
15
+ ('MENTORS', 'Recherche de Mentors'),
16
+ ('OPPORTUNITIES', 'Opportunités'),
17
+ ('TOOLS', 'Outils Pédagogiques'),
18
+ ('USERS', 'Utilisateurs'),
19
+ ('GENERAL', 'Recherche Générale'),
20
+ ]
21
+
22
+ # Qui a effectué la recherche
23
+ user = models.ForeignKey(
24
+ User,
25
+ on_delete=models.SET_NULL,
26
+ null=True,
27
+ blank=True,
28
+ related_name='search_logs',
29
+ help_text="Utilisateur qui a effectué la recherche (null si anonyme)"
30
+ )
31
+
32
+ # Catégorie de la recherche
33
+ category = models.CharField(
34
+ max_length=20,
35
+ choices=CATEGORY_CHOICES,
36
+ db_index=True,
37
+ help_text="Type de contenu recherché"
38
+ )
39
+
40
+ # Terme de recherche
41
+ search_query = models.CharField(
42
+ max_length=500,
43
+ db_index=True,
44
+ help_text="Texte saisi par l'utilisateur"
45
+ )
46
+
47
+ # Filtres appliqués (JSON pour flexibilité)
48
+ filters_applied = models.JSONField(
49
+ default=dict,
50
+ blank=True,
51
+ help_text="Filtres appliqués lors de la recherche (ex: {\"status\": \"solved\", \"tags\": [\"math\"]})"
52
+ )
53
+
54
+ # Résultats
55
+ results_count = models.IntegerField(
56
+ default=0,
57
+ help_text="Nombre de résultats retournés"
58
+ )
59
+
60
+ # Métadonnées de session
61
+ session_id = models.CharField(
62
+ max_length=100,
63
+ blank=True,
64
+ db_index=True,
65
+ help_text="ID de session pour regrouper les recherches d'une même visite"
66
+ )
67
+
68
+ ip_address = models.GenericIPAddressField(
69
+ null=True,
70
+ blank=True,
71
+ help_text="Adresse IP de l'utilisateur"
72
+ )
73
+
74
+ user_agent = models.TextField(
75
+ blank=True,
76
+ help_text="User agent du navigateur"
77
+ )
78
+
79
+ # Page d'origine
80
+ page_url = models.CharField(
81
+ max_length=500,
82
+ blank=True,
83
+ help_text="URL de la page où la recherche a été effectuée"
84
+ )
85
+
86
+ # Interaction
87
+ clicked_result_id = models.CharField(
88
+ max_length=100,
89
+ null=True,
90
+ blank=True,
91
+ help_text="ID du résultat cliqué (si applicable)"
92
+ )
93
+
94
+ clicked_result_position = models.IntegerField(
95
+ null=True,
96
+ blank=True,
97
+ help_text="Position du résultat cliqué dans la liste"
98
+ )
99
+
100
+ class Meta:
101
+ db_table = 'search_logs'
102
+ verbose_name = 'Recherche'
103
+ verbose_name_plural = 'Recherches'
104
+ indexes = [
105
+ models.Index(fields=['category', '-created_at']),
106
+ models.Index(fields=['user', '-created_at']),
107
+ models.Index(fields=['search_query']),
108
+ models.Index(fields=['-created_at']),
109
+ ]
110
+ ordering = ['-created_at']
111
+
112
+ def __str__(self):
113
+ user_info = self.user.email if self.user else "Anonyme"
114
+ return f"{user_info} - {self.category}: '{self.search_query}'"
115
+
116
+
117
+ class PopularSearch(models.Model):
118
+ """
119
+ Vue matérialisée des recherches les plus populaires
120
+ Mise à jour périodiquement pour optimiser les performances
121
+ """
122
+ category = models.CharField(max_length=20, choices=SearchLog.CATEGORY_CHOICES)
123
+ search_query = models.CharField(max_length=500)
124
+ search_count = models.IntegerField(default=0)
125
+ last_searched = models.DateTimeField(auto_now=True)
126
+
127
+ class Meta:
128
+ db_table = 'popular_searches'
129
+ verbose_name = 'Recherche Populaire'
130
+ verbose_name_plural = 'Recherches Populaires'
131
+ unique_together = ['category', 'search_query']
132
+ ordering = ['-search_count', '-last_searched']
133
+
134
+ def __str__(self):
135
+ return f"{self.category}: '{self.search_query}' ({self.search_count}x)"
apps/analytics/serializers.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ============================================
2
+ # apps/analytics/serializers.py
3
+ # ============================================
4
+ from rest_framework import serializers
5
+ from apps.analytics.models import SearchLog, PopularSearch
6
+
7
+ class SearchLogSerializer(serializers.ModelSerializer):
8
+ """Serializer pour enregistrer une recherche"""
9
+
10
+ class Meta:
11
+ model = SearchLog
12
+ fields = [
13
+ 'id', 'category', 'search_query', 'filters_applied',
14
+ 'results_count', 'created_at'
15
+ ]
16
+ read_only_fields = ['id', 'created_at']
17
+
18
+
19
+ class PopularSearchSerializer(serializers.ModelSerializer):
20
+ """Serializer pour les recherches populaires"""
21
+
22
+ class Meta:
23
+ model = PopularSearch
24
+ fields = ['category', 'search_query', 'search_count', 'last_searched']
apps/analytics/services.py ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ============================================
2
+ # apps/analytics/services.py - Search Tracking Service
3
+ # ============================================
4
+ from apps.analytics.models import SearchLog, PopularSearch
5
+ from django.db.models import F
6
+
7
+ class SearchTrackingService:
8
+ """Service centralisé pour tracker les recherches"""
9
+
10
+ @staticmethod
11
+ def log_search(
12
+ category: str,
13
+ search_query: str,
14
+ user=None,
15
+ filters_applied: dict = None,
16
+ results_count: int = 0,
17
+ request=None
18
+ ):
19
+ """
20
+ Enregistre une recherche dans les logs
21
+
22
+ Args:
23
+ category: Catégorie de recherche (QUESTIONS, MENTORS, etc.)
24
+ search_query: Terme recherché
25
+ user: Utilisateur qui effectue la recherche (None si anonyme)
26
+ filters_applied: Dict des filtres appliqués
27
+ results_count: Nombre de résultats retournés
28
+ request: Objet request Django pour extraire métadonnées
29
+
30
+ Returns:
31
+ SearchLog instance
32
+ """
33
+ # Extraire métadonnées de la requête si disponible
34
+ session_id = ''
35
+ ip_address = None
36
+ user_agent = ''
37
+ page_url = ''
38
+
39
+ if request:
40
+ session_id = request.session.session_key or ''
41
+ ip_address = request.META.get('REMOTE_ADDR')
42
+ user_agent = request.META.get('HTTP_USER_AGENT', '')[:500]
43
+ page_url = request.META.get('HTTP_REFERER', '')[:500]
44
+
45
+ # Créer le log
46
+ search_log = SearchLog.objects.create(
47
+ user=user,
48
+ category=category,
49
+ search_query=search_query.strip()[:500],
50
+ filters_applied=filters_applied or {},
51
+ results_count=results_count,
52
+ session_id=session_id,
53
+ ip_address=ip_address,
54
+ user_agent=user_agent,
55
+ page_url=page_url
56
+ )
57
+
58
+ # Mettre à jour les recherches populaires
59
+ SearchTrackingService._update_popular_search(category, search_query.strip())
60
+
61
+ return search_log
62
+
63
+ @staticmethod
64
+ def _update_popular_search(category: str, search_query: str):
65
+ """Mise à jour incrémentale des recherches populaires"""
66
+ if not search_query or len(search_query) < 2:
67
+ return
68
+
69
+ popular, created = PopularSearch.objects.get_or_create(
70
+ category=category,
71
+ search_query=search_query[:500],
72
+ defaults={'search_count': 1}
73
+ )
74
+
75
+ if not created:
76
+ popular.search_count = F('search_count') + 1
77
+ popular.save(update_fields=['search_count', 'last_searched'])
78
+
79
+ @staticmethod
80
+ def log_result_click(search_log_id: int, result_id: str, position: int):
81
+ """
82
+ Enregistre le clic sur un résultat de recherche
83
+
84
+ Args:
85
+ search_log_id: ID du SearchLog
86
+ result_id: ID du résultat cliqué
87
+ position: Position dans la liste (0-indexed)
88
+ """
89
+ try:
90
+ search_log = SearchLog.objects.get(id=search_log_id)
91
+ search_log.clicked_result_id = str(result_id)
92
+ search_log.clicked_result_position = position
93
+ search_log.save(update_fields=['clicked_result_id', 'clicked_result_position'])
94
+ except SearchLog.DoesNotExist:
95
+ pass
96
+
97
+ @staticmethod
98
+ def get_popular_searches(category: str = None, limit: int = 10):
99
+ """
100
+ Récupère les recherches les plus populaires
101
+
102
+ Args:
103
+ category: Filtrer par catégorie (None = toutes)
104
+ limit: Nombre max de résultats
105
+
106
+ Returns:
107
+ QuerySet de PopularSearch
108
+ """
109
+ qs = PopularSearch.objects.all()
110
+
111
+ if category:
112
+ qs = qs.filter(category=category)
113
+
114
+ return qs[:limit]
115
+
116
+ @staticmethod
117
+ def get_trending_searches(category: str = None, days: int = 7, limit: int = 10):
118
+ """
119
+ Récupère les recherches tendances (populaires récemment)
120
+
121
+ Args:
122
+ category: Filtrer par catégorie
123
+ days: Nombre de jours à considérer
124
+ limit: Nombre max de résultats
125
+
126
+ Returns:
127
+ Liste de dicts avec query et count
128
+ """
129
+ from django.utils import timezone
130
+ from datetime import timedelta
131
+ from django.db.models import Count
132
+
133
+ since = timezone.now() - timedelta(days=days)
134
+
135
+ qs = SearchLog.objects.filter(created_at__gte=since)
136
+
137
+ if category:
138
+ qs = qs.filter(category=category)
139
+
140
+ trending = qs.values('search_query').annotate(
141
+ count=Count('id')
142
+ ).order_by('-count')[:limit]
143
+
144
+ return list(trending)
apps/analytics/tests.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from django.test import TestCase
2
+
3
+ # Create your tests here.
apps/analytics/urls.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ from django.urls import path
2
+ from apps.analytics import views
3
+
4
+ urlpatterns = [
5
+ path('search-log/', views.log_search, name='log-search'),
6
+ path('result-click/', views.log_result_click, name='log-result-click'),
7
+ path('popular-searches/', views.popular_searches, name='popular-searches'),
8
+ path('trending-searches/', views.trending_searches, name='trending-searches'),
9
+ ]
apps/analytics/views.py ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ============================================
2
+ # apps/analytics/views.py
3
+ # ============================================
4
+ from rest_framework import status
5
+ from rest_framework.decorators import api_view, permission_classes
6
+ from rest_framework.permissions import AllowAny
7
+ from rest_framework.response import Response
8
+ from apps.analytics.services import SearchTrackingService
9
+ from apps.analytics.serializers import SearchLogSerializer, PopularSearchSerializer
10
+
11
+ @api_view(['POST'])
12
+ @permission_classes([AllowAny])
13
+ def log_search(request):
14
+ """
15
+ Enregistrer une recherche
16
+ POST /api/analytics/search-log/
17
+
18
+ Body:
19
+ {
20
+ "category": "QUESTIONS",
21
+ "search_query": "python",
22
+ "filters_applied": {"status": "unsolved"},
23
+ "results_count": 15
24
+ }
25
+ """
26
+ serializer = SearchLogSerializer(data=request.data)
27
+
28
+ if serializer.is_valid():
29
+ user = request.user if request.user.is_authenticated else None
30
+
31
+ search_log = SearchTrackingService.log_search(
32
+ category=serializer.validated_data['category'],
33
+ search_query=serializer.validated_data['search_query'],
34
+ user=user,
35
+ filters_applied=serializer.validated_data.get('filters_applied', {}),
36
+ results_count=serializer.validated_data.get('results_count', 0),
37
+ request=request
38
+ )
39
+
40
+ return Response(
41
+ SearchLogSerializer(search_log).data,
42
+ status=status.HTTP_201_CREATED
43
+ )
44
+
45
+ return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
46
+
47
+
48
+ @api_view(['POST'])
49
+ @permission_classes([AllowAny])
50
+ def log_result_click(request):
51
+ """
52
+ Enregistrer le clic sur un résultat
53
+ POST /api/analytics/result-click/
54
+
55
+ Body:
56
+ {
57
+ "search_log_id": 123,
58
+ "result_id": "456",
59
+ "position": 2
60
+ }
61
+ """
62
+ search_log_id = request.data.get('search_log_id')
63
+ result_id = request.data.get('result_id')
64
+ position = request.data.get('position')
65
+
66
+ if not all([search_log_id, result_id, position is not None]):
67
+ return Response(
68
+ {'error': 'Missing required fields'},
69
+ status=status.HTTP_400_BAD_REQUEST
70
+ )
71
+
72
+ SearchTrackingService.log_result_click(
73
+ search_log_id=search_log_id,
74
+ result_id=result_id,
75
+ position=position
76
+ )
77
+
78
+ return Response({'status': 'success'}, status=status.HTTP_200_OK)
79
+
80
+
81
+ @api_view(['GET'])
82
+ @permission_classes([AllowAny])
83
+ def popular_searches(request):
84
+ """
85
+ Récupérer les recherches populaires
86
+ GET /api/analytics/popular-searches/?category=QUESTIONS&limit=10
87
+ """
88
+ category = request.query_params.get('category')
89
+ limit = int(request.query_params.get('limit', 10))
90
+
91
+ searches = SearchTrackingService.get_popular_searches(
92
+ category=category,
93
+ limit=min(limit, 50) # Max 50
94
+ )
95
+
96
+ serializer = PopularSearchSerializer(searches, many=True)
97
+ return Response(serializer.data)
98
+
99
+
100
+ @api_view(['GET'])
101
+ @permission_classes([AllowAny])
102
+ def trending_searches(request):
103
+ """
104
+ Récupérer les recherches tendances
105
+ GET /api/analytics/trending-searches/?category=QUESTIONS&days=7&limit=10
106
+ """
107
+ category = request.query_params.get('category')
108
+ days = int(request.query_params.get('days', 7))
109
+ limit = int(request.query_params.get('limit', 10))
110
+
111
+ trending = SearchTrackingService.get_trending_searches(
112
+ category=category,
113
+ days=min(days, 30), # Max 30 jours
114
+ limit=min(limit, 50)
115
+ )
116
+
117
+ return Response(trending)
apps/bookings/__init__.py ADDED
File without changes
apps/bookings/admin.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from django.contrib import admin
2
+ from .models import (
3
+ Booking, BookingDomain, BookingExpectation,
4
+ BookingMainQuestion, BookingStatusHistory
5
+ )
6
+
7
+ class BookingDomainInline(admin.TabularInline):
8
+ model = BookingDomain
9
+ extra = 0
10
+
11
+ class BookingExpectationInline(admin.StackedInline):
12
+ model = BookingExpectation
13
+ extra = 0
14
+
15
+ class BookingMainQuestionInline(admin.StackedInline):
16
+ model = BookingMainQuestion
17
+ extra = 0
18
+
19
+ @admin.register(Booking)
20
+ class BookingAdmin(admin.ModelAdmin):
21
+ list_display = ('id', 'student', 'mentor', 'date', 'time', 'status', 'created_at')
22
+ list_filter = ('status', 'date', 'created_at')
23
+ search_fields = ('student__email', 'mentor__email')
24
+ inlines = [BookingDomainInline, BookingExpectationInline, BookingMainQuestionInline]
25
+
26
+ @admin.register(BookingStatusHistory)
27
+ class BookingStatusHistoryAdmin(admin.ModelAdmin):
28
+ list_display = ('booking', 'previous_status', 'new_status', 'changed_by', 'created_at')
29
+ list_filter = ('new_status', 'created_at')
apps/bookings/apps.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class BookingsConfig(AppConfig):
5
+ default_auto_field = 'django.db.models.BigAutoField'
6
+ name = 'apps.bookings'
7
+
8
+ def ready(self):
9
+ import apps.bookings.signals # noqa
apps/bookings/migrations/0001_initial.py ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Generated by Django 4.2.7 on 2025-11-29 13:38
2
+
3
+ from django.db import migrations, models
4
+ import django.db.models.deletion
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+ initial = True
9
+
10
+ dependencies = []
11
+
12
+ operations = [
13
+ migrations.CreateModel(
14
+ name="Booking",
15
+ fields=[
16
+ (
17
+ "id",
18
+ models.BigAutoField(
19
+ auto_created=True,
20
+ primary_key=True,
21
+ serialize=False,
22
+ verbose_name="ID",
23
+ ),
24
+ ),
25
+ ("created_at", models.DateTimeField(auto_now_add=True, db_index=True)),
26
+ ("updated_at", models.DateTimeField(auto_now=True)),
27
+ (
28
+ "deleted_at",
29
+ models.DateTimeField(blank=True, db_index=True, null=True),
30
+ ),
31
+ ("is_active", models.BooleanField(db_index=True, default=True)),
32
+ ("date", models.DateField(db_index=True)),
33
+ ("time", models.TimeField()),
34
+ (
35
+ "status",
36
+ models.CharField(
37
+ choices=[
38
+ ("PENDING", "En attente"),
39
+ ("CONFIRMED", "Confirmé"),
40
+ ("REJECTED", "Refusé"),
41
+ ("COMPLETED", "Terminé"),
42
+ ("CANCELLED", "Annulé"),
43
+ ],
44
+ db_index=True,
45
+ default="PENDING",
46
+ max_length=20,
47
+ ),
48
+ ),
49
+ ],
50
+ options={
51
+ "verbose_name": "Réservation",
52
+ "verbose_name_plural": "Réservations",
53
+ "db_table": "bookings",
54
+ "ordering": ["-created_at"],
55
+ },
56
+ ),
57
+ migrations.CreateModel(
58
+ name="BookingDomain",
59
+ fields=[
60
+ (
61
+ "id",
62
+ models.BigAutoField(
63
+ auto_created=True,
64
+ primary_key=True,
65
+ serialize=False,
66
+ verbose_name="ID",
67
+ ),
68
+ ),
69
+ ("created_at", models.DateTimeField(auto_now_add=True, db_index=True)),
70
+ ("updated_at", models.DateTimeField(auto_now=True)),
71
+ ("domain", models.CharField(max_length=100)),
72
+ ],
73
+ options={
74
+ "db_table": "booking_domains",
75
+ },
76
+ ),
77
+ migrations.CreateModel(
78
+ name="BookingExpectation",
79
+ fields=[
80
+ (
81
+ "id",
82
+ models.BigAutoField(
83
+ auto_created=True,
84
+ primary_key=True,
85
+ serialize=False,
86
+ verbose_name="ID",
87
+ ),
88
+ ),
89
+ ("created_at", models.DateTimeField(auto_now_add=True, db_index=True)),
90
+ ("updated_at", models.DateTimeField(auto_now=True)),
91
+ ("is_current", models.BooleanField(db_index=True, default=True)),
92
+ ("expectation", models.TextField()),
93
+ ],
94
+ options={
95
+ "db_table": "booking_expectations",
96
+ },
97
+ ),
98
+ migrations.CreateModel(
99
+ name="BookingMainQuestion",
100
+ fields=[
101
+ (
102
+ "id",
103
+ models.BigAutoField(
104
+ auto_created=True,
105
+ primary_key=True,
106
+ serialize=False,
107
+ verbose_name="ID",
108
+ ),
109
+ ),
110
+ ("created_at", models.DateTimeField(auto_now_add=True, db_index=True)),
111
+ ("updated_at", models.DateTimeField(auto_now=True)),
112
+ ("is_current", models.BooleanField(db_index=True, default=True)),
113
+ ("question", models.TextField()),
114
+ ],
115
+ options={
116
+ "db_table": "booking_main_questions",
117
+ },
118
+ ),
119
+ migrations.CreateModel(
120
+ name="BookingStatusHistory",
121
+ fields=[
122
+ (
123
+ "id",
124
+ models.BigAutoField(
125
+ auto_created=True,
126
+ primary_key=True,
127
+ serialize=False,
128
+ verbose_name="ID",
129
+ ),
130
+ ),
131
+ ("created_at", models.DateTimeField(auto_now_add=True, db_index=True)),
132
+ ("updated_at", models.DateTimeField(auto_now=True)),
133
+ ("previous_status", models.CharField(max_length=20)),
134
+ ("new_status", models.CharField(max_length=20)),
135
+ ("reason", models.TextField(blank=True, null=True)),
136
+ (
137
+ "booking",
138
+ models.ForeignKey(
139
+ on_delete=django.db.models.deletion.CASCADE,
140
+ related_name="status_history",
141
+ to="bookings.booking",
142
+ ),
143
+ ),
144
+ ],
145
+ options={
146
+ "db_table": "booking_status_history",
147
+ "ordering": ["-created_at"],
148
+ },
149
+ ),
150
+ ]
apps/bookings/migrations/0002_initial.py ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Generated by Django 4.2.7 on 2025-11-29 13:38
2
+
3
+ from django.conf import settings
4
+ from django.db import migrations, models
5
+ import django.db.models.deletion
6
+
7
+
8
+ class Migration(migrations.Migration):
9
+ initial = True
10
+
11
+ dependencies = [
12
+ ("bookings", "0001_initial"),
13
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
14
+ ]
15
+
16
+ operations = [
17
+ migrations.AddField(
18
+ model_name="bookingstatushistory",
19
+ name="changed_by",
20
+ field=models.ForeignKey(
21
+ on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
22
+ ),
23
+ ),
24
+ migrations.AddField(
25
+ model_name="bookingmainquestion",
26
+ name="booking",
27
+ field=models.ForeignKey(
28
+ on_delete=django.db.models.deletion.CASCADE,
29
+ related_name="main_questions",
30
+ to="bookings.booking",
31
+ ),
32
+ ),
33
+ migrations.AddField(
34
+ model_name="bookingexpectation",
35
+ name="booking",
36
+ field=models.ForeignKey(
37
+ on_delete=django.db.models.deletion.CASCADE,
38
+ related_name="expectations",
39
+ to="bookings.booking",
40
+ ),
41
+ ),
42
+ migrations.AddField(
43
+ model_name="bookingdomain",
44
+ name="booking",
45
+ field=models.ForeignKey(
46
+ on_delete=django.db.models.deletion.CASCADE,
47
+ related_name="domains",
48
+ to="bookings.booking",
49
+ ),
50
+ ),
51
+ migrations.AddField(
52
+ model_name="booking",
53
+ name="mentor",
54
+ field=models.ForeignKey(
55
+ on_delete=django.db.models.deletion.CASCADE,
56
+ related_name="bookings_as_mentor",
57
+ to=settings.AUTH_USER_MODEL,
58
+ ),
59
+ ),
60
+ migrations.AddField(
61
+ model_name="booking",
62
+ name="student",
63
+ field=models.ForeignKey(
64
+ on_delete=django.db.models.deletion.CASCADE,
65
+ related_name="bookings_as_student",
66
+ to=settings.AUTH_USER_MODEL,
67
+ ),
68
+ ),
69
+ migrations.AddIndex(
70
+ model_name="booking",
71
+ index=models.Index(
72
+ fields=["student", "status", "is_active"],
73
+ name="bookings_student_84c0c2_idx",
74
+ ),
75
+ ),
76
+ migrations.AddIndex(
77
+ model_name="booking",
78
+ index=models.Index(
79
+ fields=["mentor", "status", "is_active"],
80
+ name="bookings_mentor__d43236_idx",
81
+ ),
82
+ ),
83
+ migrations.AddIndex(
84
+ model_name="booking",
85
+ index=models.Index(
86
+ fields=["date", "time"], name="bookings_date_44cf2c_idx"
87
+ ),
88
+ ),
89
+ ]
apps/bookings/migrations/__init__.py ADDED
File without changes
apps/bookings/models.py ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ============================================
2
+ # apps/bookings/models.py - Système Réservation
3
+ # ============================================
4
+ from django.db import models
5
+ from apps.core.models import TimestampMixin, SoftDeleteMixin, VersionedFieldMixin
6
+ from apps.users.models import User
7
+
8
+ class Booking(TimestampMixin, SoftDeleteMixin):
9
+ """Réservation de session avec mentor"""
10
+ STATUS_CHOICES = [
11
+ ('PENDING', 'En attente'),
12
+ ('CONFIRMED', 'Confirmé'),
13
+ ('REJECTED', 'Refusé'),
14
+ ('COMPLETED', 'Terminé'),
15
+ ('CANCELLED', 'Annulé'),
16
+ ]
17
+
18
+ student = models.ForeignKey(User, on_delete=models.CASCADE, related_name='bookings_as_student')
19
+ mentor = models.ForeignKey(User, on_delete=models.CASCADE, related_name='bookings_as_mentor')
20
+ date = models.DateField(db_index=True)
21
+ time = models.TimeField()
22
+ status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='PENDING', db_index=True)
23
+
24
+ class Meta:
25
+ db_table = 'bookings'
26
+ verbose_name = 'Réservation'
27
+ verbose_name_plural = 'Réservations'
28
+ indexes = [
29
+ models.Index(fields=['student', 'status', 'is_active']),
30
+ models.Index(fields=['mentor', 'status', 'is_active']),
31
+ models.Index(fields=['date', 'time']),
32
+ ]
33
+ ordering = ['-created_at']
34
+
35
+ def __str__(self):
36
+ return f"Booking #{self.id}: {self.student.email} -> {self.mentor.email}"
37
+
38
+ class BookingDomain(TimestampMixin):
39
+ booking = models.ForeignKey(Booking, on_delete=models.CASCADE, related_name='domains')
40
+ domain = models.CharField(max_length=100)
41
+
42
+ class Meta:
43
+ db_table = 'booking_domains'
44
+
45
+ class BookingExpectation(TimestampMixin, VersionedFieldMixin):
46
+ booking = models.ForeignKey(Booking, on_delete=models.CASCADE, related_name='expectations')
47
+ expectation = models.TextField()
48
+
49
+ class Meta:
50
+ db_table = 'booking_expectations'
51
+
52
+ class BookingMainQuestion(TimestampMixin, VersionedFieldMixin):
53
+ booking = models.ForeignKey(Booking, on_delete=models.CASCADE, related_name='main_questions')
54
+ question = models.TextField()
55
+
56
+ class Meta:
57
+ db_table = 'booking_main_questions'
58
+
59
+ class BookingStatusHistory(TimestampMixin):
60
+ """Traçabilité des changements de statut"""
61
+ booking = models.ForeignKey(Booking, on_delete=models.CASCADE, related_name='status_history')
62
+ previous_status = models.CharField(max_length=20)
63
+ new_status = models.CharField(max_length=20)
64
+ changed_by = models.ForeignKey(User, on_delete=models.CASCADE)
65
+ reason = models.TextField(null=True, blank=True)
66
+
67
+ class Meta:
68
+ db_table = 'booking_status_history'
69
+ ordering = ['-created_at']
apps/bookings/serializers.py ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ============================================
2
+ # apps/bookings/serializers.py
3
+ # ============================================
4
+ from rest_framework import serializers
5
+ from apps.bookings.models import (
6
+ Booking, BookingDomain, BookingExpectation, BookingMainQuestion
7
+ )
8
+ from apps.users.serializers import UserSerializer
9
+ from apps.core.serializers import HashIdField
10
+
11
+ class BookingSerializer(serializers.ModelSerializer):
12
+ student = UserSerializer(read_only=True)
13
+ mentor = UserSerializer(read_only=True)
14
+ domains = serializers.SerializerMethodField()
15
+ expectation = serializers.SerializerMethodField()
16
+ main_question = serializers.SerializerMethodField()
17
+ status_label = serializers.CharField(source='get_status_display', read_only=True)
18
+ id = HashIdField(read_only=True)
19
+
20
+ class Meta:
21
+ model = Booking
22
+ fields = [
23
+ 'id', 'student', 'mentor', 'date', 'time', 'status', 'status_label',
24
+ 'domains', 'expectation', 'main_question', 'created_at', 'updated_at'
25
+ ]
26
+ read_only_fields = ['id', 'student', 'created_at', 'updated_at']
27
+
28
+ def get_domains(self, obj):
29
+ return [d.domain for d in obj.domains.all()]
30
+
31
+ def get_expectation(self, obj):
32
+ exp = obj.expectations.filter(is_current=True).first()
33
+ return exp.expectation if exp else None
34
+
35
+ def get_main_question(self, obj):
36
+ q = obj.main_questions.filter(is_current=True).first()
37
+ return q.question if q else None
38
+
39
+ class BookingCreateSerializer(serializers.Serializer):
40
+ """Création d'une réservation"""
41
+ mentor_id = HashIdField()
42
+ date = serializers.DateField()
43
+ time = serializers.TimeField()
44
+ domains = serializers.ListField(
45
+ child=serializers.CharField(max_length=100),
46
+ min_length=1
47
+ )
48
+ expectations = serializers.CharField()
49
+ main_questions = serializers.CharField()
50
+
51
+ def validate_mentor_id(self, value):
52
+ from apps.users.models import User
53
+ try:
54
+ mentor = User.objects.get(id=value, role='MENTOR', is_active=True)
55
+ if not hasattr(mentor, 'mentor_profile'):
56
+ raise serializers.ValidationError("Ce mentor n'a pas de profil actif")
57
+ except User.DoesNotExist:
58
+ raise serializers.ValidationError("Mentor introuvable")
59
+ return value
60
+
61
+ def validate_date(self, value):
62
+ from datetime import date
63
+ if value < date.today():
64
+ raise serializers.ValidationError("La date doit être dans le futur")
65
+ return value
66
+
67
+ def create(self, validated_data):
68
+ from django.db import transaction
69
+ from apps.users.models import User
70
+
71
+ user = self.context['request'].user
72
+ mentor = User.objects.get(id=validated_data['mentor_id'])
73
+ domains = validated_data.pop('domains')
74
+ expectations = validated_data.pop('expectations')
75
+ main_question = validated_data.pop('main_questions')
76
+ validated_data.pop('mentor_id')
77
+
78
+ with transaction.atomic():
79
+ booking = Booking.objects.create(
80
+ student=user,
81
+ mentor=mentor,
82
+ **validated_data
83
+ )
84
+
85
+ # Ajouter domaines
86
+ for domain in domains:
87
+ BookingDomain.objects.create(booking=booking, domain=domain)
88
+
89
+ # Ajouter attentes
90
+ BookingExpectation.objects.create(
91
+ booking=booking,
92
+ expectation=expectations
93
+ )
94
+
95
+ # Ajouter question principale
96
+ BookingMainQuestion.objects.create(
97
+ booking=booking,
98
+ question=main_question
99
+ )
100
+
101
+ # Créer notification pour le mentor
102
+ from apps.notifications.services import NotificationService
103
+ NotificationService.create_booking_notification(booking)
104
+
105
+ return booking
106
+
107
+ class BookingStatusUpdateSerializer(serializers.Serializer):
108
+ """Mise à jour du statut"""
109
+ status = serializers.ChoiceField(choices=['CONFIRMED', 'REJECTED', 'COMPLETED', 'CANCELLED'])
110
+ reason = serializers.CharField(required=False, allow_blank=True)
111
+
112
+ def update(self, instance, validated_data):
113
+ from apps.bookings.models import BookingStatusHistory
114
+ from django.db import transaction
115
+
116
+ with transaction.atomic():
117
+ previous_status = instance.status
118
+ instance.status = validated_data['status']
119
+ instance.save()
120
+
121
+ # Historiser
122
+ BookingStatusHistory.objects.create(
123
+ booking=instance,
124
+ previous_status=previous_status,
125
+ new_status=validated_data['status'],
126
+ changed_by=self.context['request'].user,
127
+ reason=validated_data.get('reason', '')
128
+ )
129
+
130
+ # Créer notification pour l'étudiant
131
+ from apps.notifications.services import NotificationService
132
+ NotificationService.create_booking_status_notification(instance)
133
+
134
+ # Attribution de points si complété
135
+ if validated_data['status'] == 'COMPLETED':
136
+ from django.conf import settings
137
+ instance.student.add_points(
138
+ settings.GAMIFICATION_POINTS['BOOKING_COMPLETED'],
139
+ 'booking_completed'
140
+ )
141
+
142
+ return instance
apps/bookings/signals.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ============================================
2
+ # apps/bookings/signals.py - Signaux pour les réservations
3
+ # ============================================
4
+ from django.db.models.signals import post_save
5
+ from django.dispatch import receiver
6
+ import datetime
7
+ import logging
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ @receiver(post_save, sender='bookings.Booking')
13
+ def schedule_tasks_on_confirm(sender, instance, created, **kwargs):
14
+ """
15
+ Quand un booking passe en CONFIRMED:
16
+ 1. Planifie le déblocage des messages
17
+ 2. Planifie les rappels de rendez-vous
18
+ """
19
+ from django.utils import timezone
20
+
21
+ # Ne traiter que les bookings confirmés
22
+ if instance.status != 'CONFIRMED':
23
+ return
24
+
25
+ # Calculer l'heure de début
26
+ booking_start = datetime.datetime.combine(instance.date, instance.time)
27
+ booking_start = timezone.make_aware(booking_start)
28
+ now = timezone.now()
29
+
30
+ # === 1. Déblocage des messages ===
31
+ if booking_start <= now:
32
+ try:
33
+ from apps.messaging.tasks import unlock_messages_for_booking
34
+ # Exécuter immédiatement
35
+ unlock_messages_for_booking.delay(instance.id)
36
+ logger.info(f"Déblocage immédiat des messages pour booking {instance.id}")
37
+ except Exception as e:
38
+ logger.error(f"Erreur lors du déblocage immédiat: {e}")
39
+ else:
40
+ try:
41
+ from apps.messaging.tasks import unlock_messages_for_booking
42
+ # Planifier l'exécution à l'heure du booking
43
+ unlock_messages_for_booking.apply_async(
44
+ args=[instance.id],
45
+ eta=booking_start
46
+ )
47
+ logger.info(f"Déblocage planifié pour booking {instance.id} à {booking_start}")
48
+ except Exception as e:
49
+ logger.error(f"Erreur lors de la planification du déblocage: {e}")
50
+
51
+ # === 2. Planifier les rappels de rendez-vous ===
52
+ try:
53
+ from apps.bookings.tasks import schedule_all_reminders
54
+ # Planifier tous les rappels de manière asynchrone
55
+ schedule_all_reminders.delay(instance.id)
56
+ logger.info(f"Planification des rappels pour booking {instance.id}")
57
+ except Exception as e:
58
+ logger.error(f"Erreur lors de la planification des rappels: {e}")
apps/bookings/tasks.py ADDED
@@ -0,0 +1,206 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ============================================
2
+ # apps/bookings/tasks.py - Tâches Celery pour les réservations
3
+ # ============================================
4
+ import datetime
5
+ from celery import shared_task
6
+ from django.utils import timezone
7
+ import logging
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+ # Intervalles de rappel avant le rendez-vous (en minutes)
12
+ REMINDER_INTERVALS = [
13
+ (1440, "24 heures"), # 24h avant
14
+ (60, "1 heure"), # 1h avant
15
+ (30, "30 minutes"), # 30min avant
16
+ (15, "15 minutes"), # 15min avant
17
+ (5, "5 minutes"), # 5min avant
18
+ ]
19
+
20
+
21
+ @shared_task(name='bookings.send_booking_reminder')
22
+ def send_booking_reminder(booking_id, time_label):
23
+ """
24
+ Envoie une notification de rappel aux participants d'un booking.
25
+
26
+ Args:
27
+ booking_id: ID du booking
28
+ time_label: Texte décrivant le temps restant (ex: "30 minutes")
29
+ """
30
+ from apps.bookings.models import Booking
31
+ from apps.notifications.models import Notification, NotificationTitle, NotificationMessage
32
+ from apps.notifications.services import NotificationService
33
+
34
+ try:
35
+ booking = Booking.objects.get(id=booking_id)
36
+
37
+ # Vérifier que le booking est toujours confirmé
38
+ if booking.status != 'CONFIRMED':
39
+ logger.info(f"Booking {booking_id} n'est plus confirmé, skip reminder.")
40
+ return
41
+
42
+ # Récupérer les noms des participants
43
+ student_profile = booking.student.profiles.filter(is_current=True).first()
44
+ student_name = student_profile.name if student_profile else booking.student.email
45
+
46
+ mentor_profile = booking.mentor.profiles.filter(is_current=True).first()
47
+ mentor_name = mentor_profile.name if mentor_profile else booking.mentor.email
48
+
49
+ # Notification pour l'étudiant
50
+ notif_student = Notification.objects.create(
51
+ user=booking.student,
52
+ type='BOOKING',
53
+ link=f'/chat?partner={booking.mentor.id}'
54
+ )
55
+ NotificationTitle.objects.create(
56
+ notification=notif_student,
57
+ title=f'⏰ Rappel : Rendez-vous dans {time_label}'
58
+ )
59
+ NotificationMessage.objects.create(
60
+ notification=notif_student,
61
+ message=f'Votre session avec {mentor_name} commence dans {time_label}. Préparez-vous !'
62
+ )
63
+ NotificationService._send_ws_notification(notif_student)
64
+
65
+ # Notification pour le mentor
66
+ notif_mentor = Notification.objects.create(
67
+ user=booking.mentor,
68
+ type='BOOKING',
69
+ link=f'/chat?partner={booking.student.id}'
70
+ )
71
+ NotificationTitle.objects.create(
72
+ notification=notif_mentor,
73
+ title=f'⏰ Rappel : Rendez-vous dans {time_label}'
74
+ )
75
+ NotificationMessage.objects.create(
76
+ notification=notif_mentor,
77
+ message=f'Votre session avec {student_name} commence dans {time_label}. L\'étudiant vous attend !'
78
+ )
79
+ NotificationService._send_ws_notification(notif_mentor)
80
+
81
+ logger.info(f"Rappel envoyé pour booking {booking_id} ({time_label})")
82
+ return True
83
+
84
+ except Booking.DoesNotExist:
85
+ logger.error(f"Booking {booking_id} introuvable")
86
+ return False
87
+ except Exception as e:
88
+ logger.error(f"Erreur lors de l'envoi du rappel: {e}")
89
+ raise
90
+
91
+
92
+ @shared_task(name='bookings.send_booking_starting_now')
93
+ def send_booking_starting_now(booking_id):
94
+ """
95
+ Notification spéciale quand le rendez-vous commence.
96
+ """
97
+ from apps.bookings.models import Booking
98
+ from apps.notifications.models import Notification, NotificationTitle, NotificationMessage
99
+ from apps.notifications.services import NotificationService
100
+
101
+ try:
102
+ booking = Booking.objects.get(id=booking_id)
103
+
104
+ if booking.status != 'CONFIRMED':
105
+ return
106
+
107
+ # Récupérer les noms
108
+ student_profile = booking.student.profiles.filter(is_current=True).first()
109
+ student_name = student_profile.name if student_profile else booking.student.email
110
+
111
+ mentor_profile = booking.mentor.profiles.filter(is_current=True).first()
112
+ mentor_name = mentor_profile.name if mentor_profile else booking.mentor.email
113
+
114
+ # Notification pour l'étudiant
115
+ notif_student = Notification.objects.create(
116
+ user=booking.student,
117
+ type='BOOKING',
118
+ link=f'/chat?partner={booking.mentor.id}'
119
+ )
120
+ NotificationTitle.objects.create(
121
+ notification=notif_student,
122
+ title='🚀 C\'est l\'heure de votre session !'
123
+ )
124
+ NotificationMessage.objects.create(
125
+ notification=notif_student,
126
+ message=f'Votre session de mentorat avec {mentor_name} commence maintenant. Cliquez pour rejoindre le chat.'
127
+ )
128
+ NotificationService._send_ws_notification(notif_student)
129
+
130
+ # Notification pour le mentor
131
+ notif_mentor = Notification.objects.create(
132
+ user=booking.mentor,
133
+ type='BOOKING',
134
+ link=f'/chat?partner={booking.student.id}'
135
+ )
136
+ NotificationTitle.objects.create(
137
+ notification=notif_mentor,
138
+ title='🚀 C\'est l\'heure de votre session !'
139
+ )
140
+ NotificationMessage.objects.create(
141
+ notification=notif_mentor,
142
+ message=f'Votre session avec {student_name} commence maintenant. L\'étudiant vous attend !'
143
+ )
144
+ NotificationService._send_ws_notification(notif_mentor)
145
+
146
+ logger.info(f"Notification de démarrage envoyée pour booking {booking_id}")
147
+ return True
148
+
149
+ except Booking.DoesNotExist:
150
+ logger.error(f"Booking {booking_id} introuvable")
151
+ return False
152
+ except Exception as e:
153
+ logger.error(f"Erreur: {e}")
154
+ raise
155
+
156
+
157
+ @shared_task(name='bookings.schedule_all_reminders')
158
+ def schedule_all_reminders(booking_id):
159
+ """
160
+ Planifie tous les rappels pour un booking.
161
+ À appeler quand un booking est confirmé.
162
+ """
163
+ from apps.bookings.models import Booking
164
+
165
+ try:
166
+ booking = Booking.objects.get(id=booking_id)
167
+
168
+ if booking.status != 'CONFIRMED':
169
+ return
170
+
171
+ # Calculer l'heure de début
172
+ booking_start = datetime.datetime.combine(booking.date, booking.time)
173
+ booking_start = timezone.make_aware(booking_start)
174
+ now = timezone.now()
175
+
176
+ scheduled_count = 0
177
+
178
+ # Planifier chaque rappel
179
+ for minutes_before, label in REMINDER_INTERVALS:
180
+ reminder_time = booking_start - datetime.timedelta(minutes=minutes_before)
181
+
182
+ # Ne planifier que les rappels dans le futur
183
+ if reminder_time > now:
184
+ send_booking_reminder.apply_async(
185
+ args=[booking_id, label],
186
+ eta=reminder_time
187
+ )
188
+ scheduled_count += 1
189
+ logger.info(f"Rappel planifié pour booking {booking_id}: {label} (à {reminder_time})")
190
+
191
+ # Planifier la notification de démarrage
192
+ if booking_start > now:
193
+ send_booking_starting_now.apply_async(
194
+ args=[booking_id],
195
+ eta=booking_start
196
+ )
197
+ logger.info(f"Notification de démarrage planifiée pour booking {booking_id}")
198
+
199
+ return f"Planifié {scheduled_count} rappels pour booking {booking_id}"
200
+
201
+ except Booking.DoesNotExist:
202
+ logger.error(f"Booking {booking_id} introuvable")
203
+ return None
204
+ except Exception as e:
205
+ logger.error(f"Erreur lors de la planification: {e}")
206
+ raise
apps/bookings/tests.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from django.test import TestCase
2
+
3
+ # Create your tests here.
apps/bookings/urls.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ============================================
2
+ # apps/bookings/urls.py
3
+ # ============================================
4
+ from django.urls import path, include
5
+ from rest_framework.routers import DefaultRouter
6
+ from apps.bookings.views import BookingViewSet
7
+
8
+ router = DefaultRouter()
9
+ router.register(r'', BookingViewSet, basename='booking')
10
+
11
+ urlpatterns = [
12
+ path('', include(router.urls)),
13
+ ]
14
+
15
+ """
16
+ Endpoints disponibles:
17
+ - GET /api/bookings/ (mes réservations)
18
+ - POST /api/bookings/ (créer)
19
+ - GET /api/bookings/{id}/
20
+ - PATCH /api/bookings/{id}/update_status/
21
+ - GET /api/bookings/mentor_requests/
22
+ """
23
+
apps/bookings/views.py ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ============================================
2
+ # apps/bookings/views.py
3
+ # ============================================
4
+ from rest_framework import viewsets, status
5
+ from rest_framework.decorators import action
6
+ from rest_framework.permissions import IsAuthenticated
7
+ from rest_framework.response import Response
8
+
9
+ from apps.bookings.models import Booking
10
+ from apps.bookings.serializers import (
11
+ BookingSerializer, BookingCreateSerializer, BookingStatusUpdateSerializer
12
+ )
13
+
14
+ from apps.core.mixins import HashIdMixin
15
+
16
+ class BookingViewSet(HashIdMixin, viewsets.ModelViewSet):
17
+ """Gestion des réservations"""
18
+ permission_classes = [IsAuthenticated]
19
+ serializer_class = BookingSerializer
20
+
21
+ def get_queryset(self):
22
+ user = self.request.user
23
+ queryset = Booking.objects.filter(is_active=True)
24
+
25
+ # Étudiants voient leurs réservations
26
+ if user.role == 'STUDENT':
27
+ queryset = queryset.filter(student=user)
28
+ # Mentors voient les demandes reçues
29
+ elif user.role == 'MENTOR':
30
+ queryset = queryset.filter(mentor=user)
31
+
32
+ # Filtre par statut
33
+ status_filter = self.request.query_params.get('status')
34
+ if status_filter:
35
+ queryset = queryset.filter(status=status_filter)
36
+
37
+ return queryset.order_by('-created_at')
38
+
39
+ def get_serializer_class(self):
40
+ if self.action == 'create':
41
+ return BookingCreateSerializer
42
+ elif self.action == 'update_status':
43
+ return BookingStatusUpdateSerializer
44
+ return BookingSerializer
45
+
46
+ def create(self, request, *args, **kwargs):
47
+ serializer = self.get_serializer(data=request.data)
48
+ serializer.is_valid(raise_exception=True)
49
+ booking = serializer.save()
50
+
51
+ # Utiliser le serializer standard pour la réponse
52
+ response_serializer = BookingSerializer(booking, context=self.get_serializer_context())
53
+ headers = self.get_success_headers(response_serializer.data)
54
+ return Response(response_serializer.data, status=status.HTTP_201_CREATED, headers=headers)
55
+
56
+ def perform_create(self, serializer):
57
+ serializer.save()
58
+
59
+ @action(detail=True, methods=['patch'], permission_classes=[IsAuthenticated])
60
+ def update_status(self, request, pk=None):
61
+ """PATCH /api/bookings/{id}/update_status/"""
62
+ booking = self.get_object()
63
+
64
+ # Vérifier que c'est le mentor
65
+ if booking.mentor != request.user:
66
+ return Response(
67
+ {'error': 'Seul le mentor peut modifier le statut'},
68
+ status=status.HTTP_403_FORBIDDEN
69
+ )
70
+
71
+ serializer = BookingStatusUpdateSerializer(
72
+ booking,
73
+ data=request.data,
74
+ context={'request': request}
75
+ )
76
+ serializer.is_valid(raise_exception=True)
77
+ serializer.save()
78
+
79
+ return Response(BookingSerializer(booking).data)
80
+
81
+ @action(detail=False, methods=['get'], permission_classes=[IsAuthenticated])
82
+ def mentor_requests(self, request):
83
+ """GET /api/bookings/mentor_requests/ - Demandes reçues par le mentor"""
84
+ if request.user.role != 'MENTOR':
85
+ return Response(
86
+ {'error': 'Accessible uniquement aux mentors'},
87
+ status=status.HTTP_403_FORBIDDEN
88
+ )
89
+
90
+ bookings = Booking.objects.filter(
91
+ mentor=request.user,
92
+ is_active=True
93
+ ).order_by('date', 'time')
94
+
95
+ page = self.paginate_queryset(bookings)
96
+ if page is not None:
97
+ serializer = self.get_serializer(page, many=True)
98
+ return self.get_paginated_response(serializer.data)
99
+
100
+ serializer = self.get_serializer(bookings, many=True)
101
+ return Response(serializer.data)
102
+
apps/core/__init__.py ADDED
File without changes
apps/core/admin.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from django.contrib import admin
2
+ from .models import ImpactStat, LearningTool, Testimonial, SocialLink
3
+
4
+ @admin.register(ImpactStat)
5
+ class ImpactStatAdmin(admin.ModelAdmin):
6
+ list_display = ('title', 'value', 'icon', 'order', 'is_visible')
7
+ list_editable = ('order', 'is_visible')
8
+ search_fields = ('title', 'description')
9
+
10
+ @admin.register(LearningTool)
11
+ class LearningToolAdmin(admin.ModelAdmin):
12
+ list_display = ('title', 'category', 'level', 'status', 'order', 'is_visible')
13
+ list_editable = ('status', 'order', 'is_visible')
14
+ list_filter = ('category', 'level', 'status')
15
+ search_fields = ('title', 'description')
16
+
17
+ @admin.register(Testimonial)
18
+ class TestimonialAdmin(admin.ModelAdmin):
19
+ list_display = ('name', 'role', 'country', 'order', 'is_visible')
20
+ list_editable = ('order', 'is_visible')
21
+ search_fields = ('name', 'text', 'country')
22
+
23
+ @admin.register(SocialLink)
24
+ class SocialLinkAdmin(admin.ModelAdmin):
25
+ list_display = ('name', 'platform', 'url', 'order', 'is_visible')
26
+ list_editable = ('order', 'is_visible')
27
+ list_filter = ('platform',)
apps/core/apps.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class CoreConfig(AppConfig):
5
+ default_auto_field = 'django.db.models.BigAutoField'
6
+ name = 'apps.core'
7
+
8
+ def ready(self):
9
+ import apps.core.signals
apps/core/exceptions.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Custom exception handler for Django REST Framework
3
+ Développé par Marino ATOHOUN pour Hypee
4
+ """
5
+
6
+ from rest_framework.views import exception_handler
7
+ from rest_framework.response import Response
8
+ from rest_framework import status
9
+
10
+
11
+ def custom_exception_handler(exc, context):
12
+ """
13
+ Gestionnaire d'exceptions personnalisé pour DRF
14
+
15
+ Ajoute des informations supplémentaires aux réponses d'erreur
16
+ et standardise le format de retour.
17
+ """
18
+ # Appeler le gestionnaire par défaut de DRF
19
+ response = exception_handler(exc, context)
20
+
21
+ if response is not None:
22
+ # Standardiser le format de réponse d'erreur
23
+ custom_response_data = {
24
+ 'error': True,
25
+ 'status_code': response.status_code,
26
+ 'message': None,
27
+ 'details': response.data
28
+ }
29
+
30
+ # Essayer d'extraire un message clair
31
+ if isinstance(response.data, dict):
32
+ if 'detail' in response.data:
33
+ custom_response_data['message'] = response.data['detail']
34
+ elif 'non_field_errors' in response.data:
35
+ custom_response_data['message'] = response.data['non_field_errors'][0]
36
+ else:
37
+ # Prendre le premier message d'erreur trouvé
38
+ for field, errors in response.data.items():
39
+ if isinstance(errors, list) and errors:
40
+ custom_response_data['message'] = f"{field}: {errors[0]}"
41
+ break
42
+
43
+ # Message par défaut selon le code de statut
44
+ if not custom_response_data['message']:
45
+ if response.status_code == 400:
46
+ custom_response_data['message'] = "Données invalides"
47
+ elif response.status_code == 401:
48
+ custom_response_data['message'] = "Authentification requise"
49
+ elif response.status_code == 403:
50
+ custom_response_data['message'] = "Accès refusé"
51
+ elif response.status_code == 404:
52
+ custom_response_data['message'] = "Ressource non trouvée"
53
+ elif response.status_code == 500:
54
+ custom_response_data['message'] = "Erreur serveur interne"
55
+ else:
56
+ custom_response_data['message'] = "Une erreur est survenue"
57
+
58
+ response.data = custom_response_data
59
+
60
+ return response
apps/core/management/commands/create_test_data.py ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from django.core.management.base import BaseCommand
2
+ from django.contrib.auth import get_user_model
3
+ from apps.users.models import UserProfile, UserCountry, UserUniversity
4
+ from apps.mentors.models import (
5
+ MentorProfile, MentorBio, MentorSpecialty, MentorAvailability
6
+ )
7
+ from apps.forum.models import Question, QuestionTitle, QuestionContent, QuestionTag
8
+ from faker import Faker
9
+ import random
10
+
11
+ User = get_user_model()
12
+ fake = Faker(['fr_FR'])
13
+
14
+ class Command(BaseCommand):
15
+ help = 'Créer des données de test'
16
+
17
+ def add_arguments(self, parser):
18
+ parser.add_argument(
19
+ '--users',
20
+ type=int,
21
+ default=20,
22
+ help='Nombre d\'utilisateurs à créer'
23
+ )
24
+
25
+ def handle(self, *args, **options):
26
+ num_users = options['users']
27
+
28
+ self.stdout.write(f'Création de {num_users} utilisateurs de test...')
29
+
30
+ countries = ['Bénin', 'Sénégal', 'Côte d\'Ivoire', 'Togo', 'Burkina Faso', 'Mali']
31
+ universities = [
32
+ 'Université d\'Abomey-Calavi',
33
+ 'Université Cheikh Anta Diop',
34
+ 'Université Félix Houphouët-Boigny',
35
+ 'Université de Lomé'
36
+ ]
37
+
38
+ # Créer des étudiants
39
+ students = []
40
+ for i in range(num_users):
41
+ email = fake.email()
42
+ user = User.objects.create_user(
43
+ email=email,
44
+ password='password123',
45
+ role='STUDENT'
46
+ )
47
+
48
+ profile = UserProfile.objects.create(
49
+ user=user,
50
+ name=fake.name()
51
+ )
52
+
53
+ UserCountry.objects.create(
54
+ profile=profile,
55
+ country=random.choice(countries)
56
+ )
57
+
58
+ UserUniversity.objects.create(
59
+ profile=profile,
60
+ university=random.choice(universities)
61
+ )
62
+
63
+ students.append(user)
64
+
65
+ self.stdout.write(self.style.SUCCESS(f'{num_users} étudiants créés'))
66
+
67
+ # Créer des mentors
68
+ specialties_list = [
69
+ 'Mathématiques', 'Physique', 'Chimie', 'Informatique',
70
+ 'Économie', 'Gestion', 'Droit', 'Marketing', 'Finance'
71
+ ]
72
+
73
+ num_mentors = num_users // 4
74
+ mentors = []
75
+
76
+ for i in range(num_mentors):
77
+ email = f'mentor{i}@edulab.test'
78
+ user = User.objects.create_user(
79
+ email=email,
80
+ password='password123',
81
+ role='MENTOR'
82
+ )
83
+
84
+ profile = UserProfile.objects.create(
85
+ user=user,
86
+ name=fake.name()
87
+ )
88
+
89
+ UserCountry.objects.create(
90
+ profile=profile,
91
+ country=random.choice(countries)
92
+ )
93
+
94
+ mentor_profile = MentorProfile.objects.create(
95
+ user=user,
96
+ rating=round(random.uniform(3.5, 5.0), 1),
97
+ reviews_count=random.randint(5, 50),
98
+ is_verified=True
99
+ )
100
+
101
+ MentorBio.objects.create(
102
+ mentor_profile=mentor_profile,
103
+ bio=fake.text(max_nb_chars=200)
104
+ )
105
+
106
+ # Spécialités
107
+ for specialty in random.sample(specialties_list, k=random.randint(2, 4)):
108
+ MentorSpecialty.objects.create(
109
+ mentor_profile=mentor_profile,
110
+ specialty=specialty
111
+ )
112
+
113
+ # Disponibilités
114
+ days = ['MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', 'FRIDAY']
115
+ for day in random.sample(days, k=3):
116
+ MentorAvailability.objects.create(
117
+ mentor_profile=mentor_profile,
118
+ day_of_week=day,
119
+ start_time='14:00:00',
120
+ end_time='17:00:00'
121
+ )
122
+
123
+ mentors.append(user)
124
+
125
+ self.stdout.write(self.style.SUCCESS(f'{num_mentors} mentors créés'))
126
+
127
+ # Créer des questions
128
+ num_questions = num_users * 2
129
+ tags_list = ['aide', 'urgent', 'exercice', 'cours', 'examen', 'projet']
130
+
131
+ for i in range(num_questions):
132
+ author = random.choice(students)
133
+ profile = author.profiles.filter(is_current=True).first()
134
+
135
+ question = Question.objects.create(
136
+ author=author,
137
+ profile=profile,
138
+ votes=random.randint(0, 50)
139
+ )
140
+
141
+ QuestionTitle.objects.create(
142
+ question=question,
143
+ title=fake.sentence()
144
+ )
145
+
146
+ QuestionContent.objects.create(
147
+ question=question,
148
+ content=fake.text(max_nb_chars=500)
149
+ )
150
+
151
+ for tag in random.sample(tags_list, k=random.randint(1, 3)):
152
+ QuestionTag.objects.create(
153
+ question=question,
154
+ tag=tag
155
+ )
156
+
157
+ self.stdout.write(self.style.SUCCESS(f'{num_questions} questions créées'))
158
+
159
+ self.stdout.write(self.style.SUCCESS('Données de test créées avec succès!'))
160
+ self.stdout.write('\\nComptes de test:')
161
+ self.stdout.write(f' Étudiant: student@test.com / password123')
162
+ self.stdout.write(f' Mentor: mentor0@edulab.test / password123')
apps/core/management/commands/init_data.py ADDED
@@ -0,0 +1,327 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from django.core.management.base import BaseCommand
2
+ from apps.core.models import ImpactStat, LearningTool, Testimonial, SocialLink
3
+ from apps.opportunities.models import Opportunity
4
+ from apps.gamification.models import Badge
5
+
6
+ class Command(BaseCommand):
7
+ help = 'Initialize essential database data for the application startup'
8
+
9
+ def handle(self, *args, **options):
10
+ self.stdout.write(self.style.SUCCESS('Starting data initialization...'))
11
+
12
+ self.init_social_links()
13
+ self.init_impact_stats()
14
+ self.init_learning_tools()
15
+ self.init_testimonials()
16
+ self.init_opportunities()
17
+ self.init_badges()
18
+
19
+ self.stdout.write(self.style.SUCCESS('Data initialization completed successfully.'))
20
+
21
+ def init_social_links(self):
22
+ links = [
23
+ {
24
+ 'platform': 'facebook',
25
+ 'defaults': {
26
+ 'name': 'Facebook',
27
+ 'url': 'https://facebook.com/edulabafrica',
28
+ 'icon': 'Facebook',
29
+ 'order': 1
30
+ }
31
+ },
32
+ {
33
+ 'platform': 'twitter',
34
+ 'defaults': {
35
+ 'name': 'Twitter',
36
+ 'url': 'https://twitter.com/edulabafrica',
37
+ 'icon': 'Twitter',
38
+ 'order': 2
39
+ }
40
+ },
41
+ {
42
+ 'platform': 'linkedin',
43
+ 'defaults': {
44
+ 'name': 'LinkedIn',
45
+ 'url': 'https://linkedin.com/company/edulabafrica',
46
+ 'icon': 'Linkedin',
47
+ 'order': 3
48
+ }
49
+ },
50
+ {
51
+ 'platform': 'instagram',
52
+ 'defaults': {
53
+ 'name': 'Instagram',
54
+ 'url': 'https://instagram.com/edulabafrica',
55
+ 'icon': 'Instagram',
56
+ 'order': 4
57
+ }
58
+ },
59
+ {
60
+ 'platform': 'youtube',
61
+ 'defaults': {
62
+ 'name': 'YouTube',
63
+ 'url': 'https://youtube.com/c/edulabafrica',
64
+ 'icon': 'Youtube',
65
+ 'order': 5
66
+ }
67
+ }
68
+ ]
69
+
70
+ count = 0
71
+ for item in links:
72
+ obj, created = SocialLink.objects.get_or_create(
73
+ platform=item['platform'],
74
+ defaults=item['defaults']
75
+ )
76
+ if created:
77
+ count += 1
78
+
79
+ if count > 0:
80
+ self.stdout.write(self.style.SUCCESS(f'Created {count} new social links.'))
81
+ else:
82
+ self.stdout.write('Social links up to date.')
83
+
84
+ def init_impact_stats(self):
85
+ stats = [
86
+ {
87
+ 'title': 'Étudiants formés',
88
+ 'defaults': {
89
+ 'value': '5000+',
90
+ 'icon': 'Users',
91
+ 'description': 'Étudiants accompagnés à travers l\'Afrique',
92
+ 'order': 1
93
+ }
94
+ },
95
+ {
96
+ 'title': 'Taux de réussite',
97
+ 'defaults': {
98
+ 'value': '98%',
99
+ 'icon': 'CheckCircle',
100
+ 'description': 'Réussite aux examens nationaux',
101
+ 'order': 2
102
+ }
103
+ },
104
+ {
105
+ 'title': 'Mentors Experts',
106
+ 'defaults': {
107
+ 'value': '200+',
108
+ 'icon': 'Award',
109
+ 'description': 'Professionnels engagés',
110
+ 'order': 3
111
+ }
112
+ },
113
+ {
114
+ 'title': 'Outils Pratiques',
115
+ 'defaults': {
116
+ 'value': '50+',
117
+ 'icon': 'Wrench',
118
+ 'description': 'Simulateurs et laboratoires virtuels',
119
+ 'order': 4
120
+ }
121
+ }
122
+ ]
123
+
124
+ count = 0
125
+ for item in stats:
126
+ obj, created = ImpactStat.objects.get_or_create(
127
+ title=item['title'],
128
+ defaults=item['defaults']
129
+ )
130
+ if created:
131
+ count += 1
132
+
133
+ if count > 0:
134
+ self.stdout.write(self.style.SUCCESS(f'Created {count} new impact stats.'))
135
+ else:
136
+ self.stdout.write('Impact stats up to date.')
137
+
138
+ def init_learning_tools(self):
139
+ tools = [
140
+ {
141
+ 'tool_id': 'code',
142
+ 'defaults': {
143
+ 'title': 'Code Sandbox',
144
+ 'description': 'Environnement de développement intégré pour apprendre HTML, CSS et JS en temps réel.',
145
+ 'icon': 'Code',
146
+ 'category': 'Informatique',
147
+ 'level': 'Tous niveaux',
148
+ 'link': '/tools/code-sandbox',
149
+ 'color': 'bg-blue-100',
150
+ 'text_color': 'text-blue-600',
151
+ 'bg_gradient': 'from-blue-500 to-cyan-400',
152
+ 'order': 1
153
+ }
154
+ },
155
+ {
156
+ 'tool_id': 'geo',
157
+ 'defaults': {
158
+ 'title': 'Atlas Interactif',
159
+ 'description': 'Exploration géographique 3D avec données économiques et climatiques en temps réel.',
160
+ 'icon': 'Globe',
161
+ 'category': 'Sciences',
162
+ 'level': 'Collège',
163
+ 'link': '/tools/atlas',
164
+ 'color': 'bg-green-100',
165
+ 'text_color': 'text-green-600',
166
+ 'bg_gradient': 'from-green-500 to-emerald-400',
167
+ 'order': 2
168
+ }
169
+ },
170
+ {
171
+ 'tool_id': 'calc',
172
+ 'defaults': {
173
+ 'title': 'Calculatrice Scientifique',
174
+ 'description': 'Outil avancé pour les mathématiques, la physique et l\'ingénierie avec graphiques.',
175
+ 'icon': 'Calculator',
176
+ 'category': 'Sciences',
177
+ 'level': 'Lycée',
178
+ 'link': '/tools/calculator',
179
+ 'color': 'bg-purple-100',
180
+ 'text_color': 'text-purple-600',
181
+ 'bg_gradient': 'from-purple-500 to-indigo-400',
182
+ 'order': 3
183
+ }
184
+ },
185
+ {
186
+ 'tool_id': 'write',
187
+ 'defaults': {
188
+ 'title': 'Atelier d\'Écriture',
189
+ 'description': 'Assistant intelligent pour améliorer vos rédactions et votre style littéraire.',
190
+ 'icon': 'PenTool',
191
+ 'category': 'Langues',
192
+ 'level': 'Primaire',
193
+ 'link': '/tools/writing',
194
+ 'color': 'bg-orange-100',
195
+ 'text_color': 'text-orange-600',
196
+ 'bg_gradient': 'from-orange-500 to-amber-400',
197
+ 'order': 4
198
+ }
199
+ },
200
+ {
201
+ 'tool_id': 'art',
202
+ 'defaults': {
203
+ 'title': 'Atelier Créatif',
204
+ 'description': 'Espace de dessin et de coloriage numérique pour développer la créativité.',
205
+ 'icon': 'Palette',
206
+ 'category': 'Créativité',
207
+ 'level': 'Primaire',
208
+ 'link': '/tools/coloring',
209
+ 'color': 'bg-pink-100',
210
+ 'text_color': 'text-pink-600',
211
+ 'bg_gradient': 'from-pink-500 to-rose-400',
212
+ 'order': 5
213
+ }
214
+ }
215
+ ]
216
+
217
+ count = 0
218
+ for item in tools:
219
+ obj, created = LearningTool.objects.get_or_create(
220
+ tool_id=item['tool_id'],
221
+ defaults=item['defaults']
222
+ )
223
+ if created:
224
+ count += 1
225
+
226
+ if count > 0:
227
+ self.stdout.write(self.style.SUCCESS(f'Created {count} new learning tools.'))
228
+ else:
229
+ self.stdout.write('Learning tools up to date.')
230
+
231
+ def init_testimonials(self):
232
+ testimonials = [
233
+ {
234
+ 'name': 'Awa Ndiaye',
235
+ 'defaults': {
236
+ 'role': 'Étudiante en Médecine',
237
+ 'country': 'Sénégal',
238
+ 'text': 'EduLab m\'a permis de visualiser l\'anatomie en 3D, ce qui était impossible avec mes livres seulement. Une révolution !',
239
+ 'order': 1
240
+ }
241
+ },
242
+ {
243
+ 'name': 'Kofi Mensah',
244
+ 'defaults': {
245
+ 'role': 'Lycéen',
246
+ 'country': 'Ghana',
247
+ 'text': 'Grâce au tuteur IA, j\'ai enfin compris les intégrales. C\'est comme avoir un prof particulier disponible 24h/24.',
248
+ 'order': 2
249
+ }
250
+ },
251
+ {
252
+ 'name': 'Sarah Benali',
253
+ 'defaults': {
254
+ 'role': 'Développeuse Web',
255
+ 'country': 'Maroc',
256
+ 'text': 'Le Code Sandbox est incroyable pour tester des idées rapidement sans rien installer. J\'ai appris React grâce à ça.',
257
+ 'order': 3
258
+ }
259
+ }
260
+ ]
261
+
262
+ count = 0
263
+ for item in testimonials:
264
+ obj, created = Testimonial.objects.get_or_create(
265
+ name=item['name'],
266
+ defaults=item['defaults']
267
+ )
268
+ if created:
269
+ count += 1
270
+
271
+ if count > 0:
272
+ self.stdout.write(self.style.SUCCESS(f'Created {count} new testimonials.'))
273
+ else:
274
+ self.stdout.write('Testimonials up to date.')
275
+
276
+ def init_opportunities(self):
277
+ # For opportunities, we'll just check if the table is empty to avoid duplicates of the sample
278
+ # as they don't have unique keys like tool_id
279
+ if Opportunity.objects.exists():
280
+ self.stdout.write('Opportunities already exist. Skipping.')
281
+ return
282
+
283
+ from django.utils import timezone
284
+ from datetime import timedelta
285
+
286
+ # Create one sample opportunity
287
+ opp = Opportunity.objects.create(
288
+ type='SCHOLARSHIP',
289
+ deadline=timezone.now().date() + timedelta(days=30),
290
+ external_link='https://au.int/en/scholarship',
291
+ is_featured=True
292
+ )
293
+
294
+ # Add related data
295
+ opp.titles.create(title="Bourse d'Excellence Africaine 2025")
296
+ opp.providers.create(provider="Union Africaine")
297
+ opp.descriptions.create(description="Une opportunité unique pour les étudiants africains brillants de poursuivre leurs études supérieures avec une couverture financière complète.")
298
+ opp.locations.create(location="Addis Ababa, Éthiopie")
299
+
300
+ self.stdout.write(self.style.SUCCESS('Created 1 sample opportunity.'))
301
+
302
+ def init_badges(self):
303
+ badges = [
304
+ {'code': 'b1', 'name': 'Premier Pas', 'description': 'Première question posée', 'icon': '👣', 'color': 'bg-blue-100 text-blue-600'},
305
+ {'code': 'b2', 'name': 'Savant', 'description': '10 meilleures réponses', 'icon': '🦉', 'color': 'bg-yellow-100 text-yellow-600'},
306
+ {'code': 'b3', 'name': 'Mentor Star', 'description': 'Note moyenne de 5.0', 'icon': '⭐', 'color': 'bg-purple-100 text-purple-600'},
307
+ {'code': 'b4', 'name': 'Globe Trotter', 'description': 'Aidé des étudiants de 5 pays', 'icon': '🌍', 'color': 'bg-green-100 text-green-600'},
308
+ {'code': 'b5', 'name': 'Curieux', 'description': 'Avoir visité 50 questions', 'icon': '🔍', 'color': 'bg-indigo-100 text-indigo-600'},
309
+ {'code': 'b6', 'name': 'Tech Guru', 'description': 'Répondre à 20 questions Tech', 'icon': '💻', 'color': 'bg-pink-100 text-pink-600'},
310
+ {'code': 'b7', 'name': 'Philanthrope', 'description': 'Donner 5 sessions de mentorat', 'icon': '🤝', 'color': 'bg-orange-100 text-orange-600'},
311
+ {'code': 'b8', 'name': 'Légende', 'description': 'Atteindre 5000 points', 'icon': '👑', 'color': 'bg-red-100 text-red-600'},
312
+ ]
313
+
314
+ count = 0
315
+ for b in badges:
316
+ badge, created = Badge.objects.get_or_create(code=b['code'])
317
+ if created:
318
+ badge.names.create(name=b['name'])
319
+ badge.descriptions.create(description=b['description'])
320
+ badge.icons.create(icon=b['icon'])
321
+ badge.colors.create(color=b['color'])
322
+ count += 1
323
+
324
+ if count > 0:
325
+ self.stdout.write(self.style.SUCCESS(f'Created {count} new badges.'))
326
+ else:
327
+ self.stdout.write('Badges up to date.')
apps/core/management/commands/init_db.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from django.core.management.base import BaseCommand
2
+ from apps.gamification.services import BadgeService
3
+
4
+ class Command(BaseCommand):
5
+ help = 'Initialiser la base de données avec les données par défaut'
6
+
7
+ def handle(self, *args, **options):
8
+ self.stdout.write('Initialisation de la base de données...')
9
+
10
+ # Créer les badges par défaut
11
+ self.stdout.write('Création des badges par défaut...')
12
+ BadgeService.initialize_default_badges()
13
+
14
+ self.stdout.write(self.style.SUCCESS('Base de données initialisée avec succès!'))
apps/core/middleware.py ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ============================================
2
+ # apps/core/middleware.py (Optionnel)
3
+ # ============================================
4
+ import logging
5
+ from django.utils import timezone
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+ class ActivityTrackingMiddleware:
10
+ """Middleware pour tracker l'activité utilisateur"""
11
+
12
+ def __init__(self, get_response):
13
+ self.get_response = get_response
14
+
15
+ def __call__(self, request):
16
+ response = self.get_response(request)
17
+
18
+ # Logger l'activité si utilisateur authentifié
19
+ if request.user.is_authenticated and request.method in ['POST', 'PUT', 'PATCH', 'DELETE']:
20
+ from apps.analytics.models import UserActivity
21
+
22
+ try:
23
+ UserActivity.objects.create(
24
+ user=request.user,
25
+ activity_type=f"{request.method}_{request.path}",
26
+ activity_data={
27
+ 'path': request.path,
28
+ 'method': request.method,
29
+ 'status_code': response.status_code
30
+ },
31
+ ip_address=request.META.get('REMOTE_ADDR'),
32
+ user_agent=request.META.get('HTTP_USER_AGENT', '')[:500]
33
+ )
34
+ except Exception as e:
35
+ logger.error(f"Error tracking activity: {e}")
36
+
37
+ return response
38
+