Spaces:
Sleeping
Sleeping
K2MAR commited on
Commit ·
e1d3c92
1
Parent(s): 0f7dfed
feat: Notifications complètes - rappels examens + détection nouveautés
Browse files- Rappels examens: 7j, 5j, 3j, 2j, 1j, 1h avant
- Détection nouveaux projets/groupes automatique
- Route check-exam-reminders pour rappels multi-jours
- Route check-new-items pour polling des nouveautés
- Timestamp lastCheck pour éviter doublons
app.py
CHANGED
|
@@ -194,7 +194,9 @@ def home():
|
|
| 194 |
'new-groups': 'POST /api/notifications/new-groups (token requis)',
|
| 195 |
'group-assignment': 'POST /api/notifications/group-assignment (token requis)',
|
| 196 |
'check-deadlines': 'POST /api/notifications/check-deadlines (token requis)',
|
| 197 |
-
'check-examens': 'POST /api/notifications/check-examens (token requis)'
|
|
|
|
|
|
|
| 198 |
}
|
| 199 |
}
|
| 200 |
})
|
|
@@ -1362,6 +1364,205 @@ def check_upcoming_examens():
|
|
| 1362 |
'message': str(e)
|
| 1363 |
}), 500
|
| 1364 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1365 |
# ==================== DÉMARRAGE DU SERVEUR ====================
|
| 1366 |
|
| 1367 |
if __name__ == '__main__':
|
|
|
|
| 194 |
'new-groups': 'POST /api/notifications/new-groups (token requis)',
|
| 195 |
'group-assignment': 'POST /api/notifications/group-assignment (token requis)',
|
| 196 |
'check-deadlines': 'POST /api/notifications/check-deadlines (token requis)',
|
| 197 |
+
'check-examens': 'POST /api/notifications/check-examens (token requis)',
|
| 198 |
+
'check-exam-reminders': 'POST /api/notifications/check-exam-reminders (token requis)',
|
| 199 |
+
'check-new-items': 'POST /api/notifications/check-new-items (token requis)'
|
| 200 |
}
|
| 201 |
}
|
| 202 |
})
|
|
|
|
| 1364 |
'message': str(e)
|
| 1365 |
}), 500
|
| 1366 |
|
| 1367 |
+
@app.route('/api/notifications/check-exam-reminders', methods=['POST'])
|
| 1368 |
+
@token_required
|
| 1369 |
+
def check_exam_reminders():
|
| 1370 |
+
"""Vérifier les examens à venir et envoyer rappels (7j, 5j, 3j, 2j, 1j)"""
|
| 1371 |
+
try:
|
| 1372 |
+
# Récupérer l'emploi du temps depuis Firebase
|
| 1373 |
+
emploi_response = requests.get(f'{FIREBASE_DB_URL}/emploi_du_temps.json')
|
| 1374 |
+
|
| 1375 |
+
if emploi_response.status_code != 200:
|
| 1376 |
+
return jsonify({
|
| 1377 |
+
'success': False,
|
| 1378 |
+
'message': 'Erreur récupération emploi du temps'
|
| 1379 |
+
}), 500
|
| 1380 |
+
|
| 1381 |
+
emploi_data = emploi_response.json()
|
| 1382 |
+
|
| 1383 |
+
if not emploi_data or not emploi_data.get('semaines'):
|
| 1384 |
+
return jsonify({
|
| 1385 |
+
'success': True,
|
| 1386 |
+
'message': 'Aucun emploi du temps',
|
| 1387 |
+
'reminders': []
|
| 1388 |
+
}), 200
|
| 1389 |
+
|
| 1390 |
+
now = datetime.now()
|
| 1391 |
+
reminders = []
|
| 1392 |
+
|
| 1393 |
+
# Jours à checker : 7, 5, 3, 2, 1
|
| 1394 |
+
days_to_check = [7, 5, 3, 2, 1]
|
| 1395 |
+
|
| 1396 |
+
# Parcourir toutes les semaines
|
| 1397 |
+
for semaine in emploi_data.get('semaines', []):
|
| 1398 |
+
for jour_id, cours_list in semaine.get('cours_par_jour', {}).items():
|
| 1399 |
+
for cours in cours_list:
|
| 1400 |
+
# Vérifier si c'est un examen
|
| 1401 |
+
cours_type = cours.get('details', ['', '', '', '', ''])[4] if cours.get('details') else ''
|
| 1402 |
+
|
| 1403 |
+
if cours_type.lower() not in ['examen', 'evaluation', 'évaluation', 'test', 'contrôle']:
|
| 1404 |
+
continue
|
| 1405 |
+
|
| 1406 |
+
# Extraire la date (format: "lun 03 mars")
|
| 1407 |
+
date_str = cours.get('date', '')
|
| 1408 |
+
|
| 1409 |
+
try:
|
| 1410 |
+
# Parser la date complète avec l'année actuelle
|
| 1411 |
+
# Format: "lun 03 mars"
|
| 1412 |
+
parts = date_str.split()
|
| 1413 |
+
if len(parts) >= 3:
|
| 1414 |
+
day_num = int(parts[1])
|
| 1415 |
+
month_name = parts[2].lower()
|
| 1416 |
+
|
| 1417 |
+
# Map mois français
|
| 1418 |
+
mois = {
|
| 1419 |
+
'janvier': 1, 'février': 2, 'mars': 3, 'avril': 4,
|
| 1420 |
+
'mai': 5, 'juin': 6, 'juillet': 7, 'août': 8,
|
| 1421 |
+
'septembre': 9, 'octobre': 10, 'novembre': 11, 'décembre': 12
|
| 1422 |
+
}
|
| 1423 |
+
|
| 1424 |
+
month_num = mois.get(month_name)
|
| 1425 |
+
if not month_num:
|
| 1426 |
+
continue
|
| 1427 |
+
|
| 1428 |
+
# Créer la date de l'examen
|
| 1429 |
+
year = now.year
|
| 1430 |
+
examen_date = datetime(year, month_num, day_num)
|
| 1431 |
+
|
| 1432 |
+
# Si la date est passée, essayer l'année prochaine
|
| 1433 |
+
if examen_date < now:
|
| 1434 |
+
examen_date = datetime(year + 1, month_num, day_num)
|
| 1435 |
+
|
| 1436 |
+
# Calculer jours restants
|
| 1437 |
+
days_until = (examen_date - now).days
|
| 1438 |
+
|
| 1439 |
+
# Vérifier si on doit envoyer un rappel
|
| 1440 |
+
if days_until in days_to_check:
|
| 1441 |
+
cours_nom = cours.get('details', [''])[0] if cours.get('details') else 'Examen'
|
| 1442 |
+
salle = cours.get('details', ['', '', ''])[2] if cours.get('details') and len(cours.get('details', [])) > 2 else ''
|
| 1443 |
+
horaire = cours.get('horaire', '')
|
| 1444 |
+
|
| 1445 |
+
reminders.append({
|
| 1446 |
+
'type': cours_type,
|
| 1447 |
+
'nom': cours_nom,
|
| 1448 |
+
'date': date_str,
|
| 1449 |
+
'horaire': horaire,
|
| 1450 |
+
'salle': salle,
|
| 1451 |
+
'daysRemaining': days_until
|
| 1452 |
+
})
|
| 1453 |
+
|
| 1454 |
+
except Exception as e:
|
| 1455 |
+
print(f"⚠️ Erreur parsing date {date_str}: {e}")
|
| 1456 |
+
continue
|
| 1457 |
+
|
| 1458 |
+
return jsonify({
|
| 1459 |
+
'success': True,
|
| 1460 |
+
'reminders': reminders
|
| 1461 |
+
}), 200
|
| 1462 |
+
|
| 1463 |
+
except Exception as e:
|
| 1464 |
+
print(f"❌ Erreur vérification rappels examens: {e}")
|
| 1465 |
+
return jsonify({
|
| 1466 |
+
'success': False,
|
| 1467 |
+
'message': str(e)
|
| 1468 |
+
}), 500
|
| 1469 |
+
|
| 1470 |
+
@app.route('/api/notifications/check-new-items', methods=['POST'])
|
| 1471 |
+
@token_required
|
| 1472 |
+
def check_new_items():
|
| 1473 |
+
"""Détecter les nouveaux projets et groupes créés depuis dernière vérification"""
|
| 1474 |
+
try:
|
| 1475 |
+
data = request.get_json()
|
| 1476 |
+
last_check = data.get('lastCheck') # Timestamp ISO
|
| 1477 |
+
|
| 1478 |
+
if not last_check:
|
| 1479 |
+
return jsonify({
|
| 1480 |
+
'success': False,
|
| 1481 |
+
'message': 'lastCheck requis'
|
| 1482 |
+
}), 400
|
| 1483 |
+
|
| 1484 |
+
last_check_time = datetime.fromisoformat(last_check.replace('Z', '+00:00'))
|
| 1485 |
+
|
| 1486 |
+
new_projets = []
|
| 1487 |
+
new_groupes = []
|
| 1488 |
+
|
| 1489 |
+
# 1. Vérifier nouveaux projets
|
| 1490 |
+
projets_response = requests.get(f'{FIREBASE_DB_URL}/projets.json')
|
| 1491 |
+
if projets_response.status_code == 200:
|
| 1492 |
+
all_projets = projets_response.json()
|
| 1493 |
+
|
| 1494 |
+
if all_projets:
|
| 1495 |
+
for projet_id, projet_data in all_projets.items():
|
| 1496 |
+
if not projet_data or not projet_data.get('createdAt'):
|
| 1497 |
+
continue
|
| 1498 |
+
|
| 1499 |
+
try:
|
| 1500 |
+
created_at = datetime.fromisoformat(projet_data['createdAt'].replace('Z', '+00:00'))
|
| 1501 |
+
|
| 1502 |
+
if created_at > last_check_time:
|
| 1503 |
+
new_projets.append({
|
| 1504 |
+
'projetId': projet_id,
|
| 1505 |
+
'subjectName': projet_data.get('subjectName', 'Projet'),
|
| 1506 |
+
'deadline': projet_data.get('deadline', ''),
|
| 1507 |
+
'profName': projet_data.get('profName', ''),
|
| 1508 |
+
'createdAt': projet_data.get('createdAt')
|
| 1509 |
+
})
|
| 1510 |
+
except Exception as e:
|
| 1511 |
+
print(f"⚠️ Erreur parsing projet {projet_id}: {e}")
|
| 1512 |
+
continue
|
| 1513 |
+
|
| 1514 |
+
# 2. Vérifier nouveaux groupes
|
| 1515 |
+
groupes_response = requests.get(f'{FIREBASE_DB_URL}/groupes.json')
|
| 1516 |
+
if groupes_response.status_code == 200:
|
| 1517 |
+
all_groupes = groupes_response.json()
|
| 1518 |
+
|
| 1519 |
+
if all_groupes:
|
| 1520 |
+
for batch_id, batch_data in all_groupes.items():
|
| 1521 |
+
if not batch_data or not batch_data.get('createdAt'):
|
| 1522 |
+
continue
|
| 1523 |
+
|
| 1524 |
+
try:
|
| 1525 |
+
created_at = datetime.fromisoformat(batch_data['createdAt'].replace('Z', '+00:00'))
|
| 1526 |
+
|
| 1527 |
+
if created_at > last_check_time:
|
| 1528 |
+
# Vérifier si l'utilisateur actuel est dans un des groupes
|
| 1529 |
+
user_email = request.current_user.get('email')
|
| 1530 |
+
user_groups = []
|
| 1531 |
+
|
| 1532 |
+
for groupe in batch_data.get('groups', []):
|
| 1533 |
+
for member in groupe.get('members', []):
|
| 1534 |
+
if member.get('email') == user_email:
|
| 1535 |
+
user_groups.append({
|
| 1536 |
+
'groupName': groupe.get('name', 'Groupe'),
|
| 1537 |
+
'members': [f"{m.get('firstName', '')} {m.get('lastName', '')}" for m in groupe.get('members', [])]
|
| 1538 |
+
})
|
| 1539 |
+
break
|
| 1540 |
+
|
| 1541 |
+
if user_groups:
|
| 1542 |
+
new_groupes.append({
|
| 1543 |
+
'batchId': batch_id,
|
| 1544 |
+
'subjectName': batch_data.get('subjectName', 'Projet'),
|
| 1545 |
+
'deadline': batch_data.get('deadline', ''),
|
| 1546 |
+
'userGroups': user_groups,
|
| 1547 |
+
'createdAt': batch_data.get('createdAt')
|
| 1548 |
+
})
|
| 1549 |
+
except Exception as e:
|
| 1550 |
+
print(f"⚠️ Erreur parsing groupe {batch_id}: {e}")
|
| 1551 |
+
continue
|
| 1552 |
+
|
| 1553 |
+
return jsonify({
|
| 1554 |
+
'success': True,
|
| 1555 |
+
'newProjets': new_projets,
|
| 1556 |
+
'newGroupes': new_groupes
|
| 1557 |
+
}), 200
|
| 1558 |
+
|
| 1559 |
+
except Exception as e:
|
| 1560 |
+
print(f"❌ Erreur vérification nouveautés: {e}")
|
| 1561 |
+
return jsonify({
|
| 1562 |
+
'success': False,
|
| 1563 |
+
'message': str(e)
|
| 1564 |
+
}), 500
|
| 1565 |
+
|
| 1566 |
# ==================== DÉMARRAGE DU SERVEUR ====================
|
| 1567 |
|
| 1568 |
if __name__ == '__main__':
|