Skip to content
Snippets Groups Projects
Commit 225c054a authored by COUCHET Thibaud's avatar COUCHET Thibaud
Browse files

Few upgrades

parent 0c8bf39a
No related branches found
No related tags found
No related merge requests found
Showing
with 278 additions and 88 deletions
......@@ -7,6 +7,8 @@
<title>Mes Défis</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
<script src="https://cdn.jsdelivr.net/npm/canvas-confetti@1.6.0/dist/confetti.browser.min.js"></script>
</head>
<body class="bg-[#dfdbec] flex flex-col items-center justify-between min-h-screen p-6">
......@@ -41,17 +43,44 @@
<!-- Title -->
<h2 class="text-3xl font-bold text-[#102564] text-center mb-6">Mes Challenges du Jour</h2>
<p id="successMessage" class="hidden text-center text-xl font-semibold text-green-600 mb-4 animate-bounce">
🎊 Tous les défis sont complétés ! Bravo !
</p>
<!-- Barre de progression -->
<div class="w-full bg-gray-200 rounded-full h-4 mb-6">
<div id="progressBar" class="bg-[#102564] h-4 rounded-full transition-all duration-500" style="width: 0%"></div>
</div>
<!-- Challenge List -->
<!-- Challenge List and put the icon -->
<ul class="space-y-4">
{% for challenge in challenges %}
<li class="flex items-center justify-between p-4 bg-gray-100 rounded-lg shadow-lg hover:shadow-xl transition duration-300">
<div class="flex flex-col">
<span class="text-lg font-medium text-[#102564]">{{ challenge.description }}</span>
<span class="text-sm text-gray-500">Niveau: {{ challenge.level }}</span>
<span class="text-sm text-gray-500">Lieu: {{ challenge.lieu }}</span>
<span class="text-sm text-gray-500">Points: {{ challenge.points }}</span>
<div class="flex items-center space-x-4">
<!-- Icon -->
<i class="fas fa-{{ challenge.icon }} text-2xl text-[#102564]"></i>
<!-- Challenge content -->
<div class="flex flex-col">
<div class="flex items-center space-x-2">
<span class="text-lg font-medium text-[#102564]">{{ challenge.description }}</span>
<!-- Génération du chemin de l'image -->
<i class="fas fa-info-circle text-blue-500 cursor-pointer hidden"
aria-label="Voir une illustration du défi"
id="info-icon-{{ challenge.id }}"
data-images="{% static 'img/activities/' %}{{ challenge.id }}.gif, {% static 'img/activities/' %}{{ challenge.id }}.jpg, {% static 'img/activities/' %}{{ challenge.id }}.png, {% static 'img/activities/' %}{{ challenge.id }}.webp"
onclick="showImage(this.getAttribute('data-image'))">
</i>
</div>
<span class="text-sm text-gray-500">Niveau: {{ challenge.level }}</span>
<span class="text-sm text-gray-500">Lieu: {{ challenge.lieu }}</span>
<span class="text-sm text-gray-500">Points: {{ challenge.points }}</span>
</div>
</div>
<!-- Validate Button -->
<button id="validate-btn-{{ challenge.id }}" onclick="completeChallenge('{{ challenge.id }}')" class="py-2 px-4 bg-[#102564] text-white font-semibold rounded-lg shadow-md hover:bg-[#000000] transition duration-300">Valider</button>
</li>
{% endfor %}
......@@ -81,9 +110,9 @@
<h2 class="text-xl font-bold text-[#102564] mb-4">Modifier le Profil</h2>
<label class="block text-sm text-gray-700">Niveau de difficulté</label>
<select id="difficulty" class="w-full p-2 border rounded-lg mb-4">
<option value="facile" {% if user_difficulty == "facile" %}selected{% endif %}>Facile</option>
<option value="moyen" {% if user_difficulty == "moyen" %}selected{% endif %}>Moyen</option>
<option value="difficile" {% if user_difficulty == "difficile" %}selected{% endif %}>Difficile</option>
<option value="Facile" {% if user_difficulty == "facile" %}selected{% endif %}>Facile</option>
<option value="Moyen" {% if user_difficulty == "moyen" %}selected{% endif %}>Moyen</option>
<option value="Difficile" {% if user_difficulty == "difficile" %}selected{% endif %}>Difficile</option>
</select>
<button onclick="saveProfileSettings()" class="w-full bg-[#102564] text-white py-2 rounded-lg hover:bg-[#000000] transition duration-300">Enregistrer</button>
<button onclick="toggleProfilePopup()" class="w-full mt-2 bg-gray-300 py-2 rounded-lg hover:bg-gray-400 transition duration-300">Annuler</button>
......@@ -98,6 +127,12 @@
<button onclick="closeConfirmationMessage()" class="w-full mt-2 bg-gray-300 py-2 rounded-lg hover:bg-gray-400 transition duration-300">OK</button>
</div>
</div>
<!-- Image Popup -->
<div id="imagePopup" class="hidden opacity-0 fixed inset-0 bg-black bg-opacity-75 flex justify-center items-center transition-opacity duration-300">
<img id="popupImage" class="transform scale-95 transition duration-300 max-w-full max-h-full rounded-lg shadow-lg">
<button onclick="hideImage()" class="absolute top-4 right-4 text-white text-3xl font-bold">&times;</button>
</div>
<script>
function toggleProfilePopup() {
......@@ -153,9 +188,12 @@
button.disabled = true;
button.classList.remove("hover:bg-[#000000]");
button.classList.add("bg-green-500", "cursor-not-allowed");
button.classList.add("animate-pulse");
updateProgressBar();
}
addValidatedChallenge(challengeId);
document.getElementById("confirmationText").innerText = "(" + data.points + " points)";
document.getElementById("confirmationText").innerText = `🎉 Bien joué ! Tu gagnes ${data.points} points !`;
document.getElementById("confirmationMessage").classList.remove("hidden");
} else {
alert(data.message);
......@@ -181,14 +219,112 @@
.then(data => {
if (data.status == "success") {
alert("Paramètres enregistrés avec succès");
location.reload();
toggleProfilePopup();
} else {
alert(data.message);
}
});
}
document.addEventListener("DOMContentLoaded", function () {
document.querySelectorAll("i[id^='info-icon-']").forEach(function (icon) {
let basePath = icon.getAttribute("data-images").split(",")[0]; // Récupère le chemin de base (sans format)
let formats = [".gif", ".jpg", ".png", ".webp"]; // Liste des formats à tester
let found = false;
for (let i = 0; i < formats.length; i++) {
if (found) break; // Arrêter dès qu'on trouve une image existante
let img = new Image();
let imgSrc = basePath.replace(".gif", formats[i]); // Remplace l'extension par le bon format
img.src = imgSrc;
img.onload = function () {
icon.setAttribute("data-image", imgSrc); // Stocke le bon format trouvé
icon.classList.remove("hidden"); // Affiche l'icône
found = true; // Marque l'image comme trouvée
};
}
});
});
function showImage(imageUrl) {
const popup = document.getElementById("imagePopup");
const image = document.getElementById("popupImage");
image.src = imageUrl;
popup.classList.remove("hidden");
requestAnimationFrame(() => {
popup.classList.add("opacity-100");
image.classList.remove("scale-95");
image.classList.add("scale-100");
});
}
function hideImage() {
const popup = document.getElementById("imagePopup");
const image = document.getElementById("popupImage");
popup.classList.remove("opacity-100");
popup.classList.add("opacity-0");
image.classList.remove("scale-100");
image.classList.add("scale-95");
setTimeout(() => {
popup.classList.add("hidden");
popup.classList.remove("opacity-0");
popup.classList.add("opacity-100");
}, 300);
}
function updateProgressBar() {
const total = document.querySelectorAll("button[id^='validate-btn-']").length;
const validated = document.querySelectorAll("button[disabled]").length;
const percentage = Math.round((validated / total) * 100);
document.getElementById("progressBar").style.width = percentage + "%";
// 🎉 Lancer les confettis quand tout est complété
if (percentage === 100 && !window.confettiLaunched) {
launchConfetti();
document.getElementById("successMessage").classList.remove("hidden");
window.confettiLaunched = true; // évite de rejouer plusieurs fois
}
}
function launchConfetti() {
confetti({
particleCount: 150,
spread: 70,
origin: { y: 0.6 },
});
// Optionnel : plusieurs salves pour effet waouh
setTimeout(() => confetti({ particleCount: 100, spread: 120, origin: { y: 0.4 } }), 400);
setTimeout(() => confetti({ particleCount: 80, spread: 100, origin: { y: 0.8 } }), 800);
}
// 1. Fermer avec la touche Escape
document.addEventListener("keydown", function (event) {
if (event.key === "Escape") {
hideImage();
}
});
// 2. Fermer en cliquant dans le fond (mais pas sur l'image)
document.getElementById("imagePopup").addEventListener("click", function (event) {
// Si on clique sur le fond (et pas sur l’image elle-même)
if (event.target === this) {
hideImage();
}
});
window.onload = checkValidatedButtons;
window.onload = () => {
checkValidatedButtons();
updateProgressBar();
};
</script>
</body>
</html>
......@@ -15,6 +15,8 @@ CSV_FILE = "scores.csv"
USER_FILE = "users.csv"
DIFFICULTY_FILE = "difficulties.csv"
DAILY_CHALLENGE_FILE = "daily_challenges.csv"
CATEGORIES = ["Au bureau (poste)", "Au bureau (espaces communs)", "Extérieur"]
LEVELS = ["Facile", "Moyen", "Difficile"]
def init_csv():
if not os.path.exists(CSV_FILE):
......@@ -33,7 +35,6 @@ def init_user_csv():
#################
def login_view(request):
# se log ou bien créer un compte si l'utilisateur n'existe pas sans mot de passe
if request.method == "POST":
username = request.POST.get("username")
team = request.POST.get("team")
......@@ -49,11 +50,10 @@ def login_view(request):
def add_user_to_csv(username, django_user_id):
"""Ajoute un utilisateur avec un ID Django dans le fichier 'users.csv'."""
user_id = django_user_id # Utiliser l'ID Django
user_id = django_user_id
with open(USER_FILE, 'a', newline='') as file:
writer = csv.writer(file)
writer.writerow([user_id, username]) # Ajouter l'ID Django et le nom d'utilisateur
writer.writerow([user_id, username])
return user_id
##################
......@@ -69,23 +69,15 @@ def logout_view(request):
##################
def ranking_view(request):
"""Afficher le classement des scores avec les nom d'utilisateur."""
# Lire les scores depuis le fichier CSV
scores = read_scores()
# Lire les utilisateurs depuis la base de données Django
users = read_users()
# Lire les équipes depuis la base de données Django
teams, user_teams = read_teams()
# Créer le classement en associant le score avec le nom d'utilisateur
ranking = []
for user_id, score in scores.items():
if int(user_id) in users: # Associer le user_id avec l'utilisateur dans le dictionnaire
if int(user_id) in users:
ranking.append({"username": users[int(user_id)], "score": score, "user_team": user_teams[int(user_id)] if user_teams[int(user_id)] else "No Team"})
# Construire un dictionnaire pour les scores d'équipe
team_scores = {}
for entry in ranking:
......@@ -94,7 +86,6 @@ def ranking_view(request):
team_scores[team] = 0
team_scores[team] += entry["score"]
# Convertir en liste pour le template
team_ranking = []
for team, score in team_scores.items():
team_ranking.append({
......@@ -104,31 +95,23 @@ def ranking_view(request):
team_ranking = [team for team in team_ranking if team["team"] != "No Team"]
# Trier le classement par score (du plus élevé au plus bas)
ranking.sort(key=lambda x: x['score'], reverse=True)
team_ranking.sort(key=lambda x: x["score"], reverse=True)
print(f"Classement: {ranking}") # Debug : Afficher le classement
# Passer le classement à la vue pour l'afficher
return render(request, "ranking.html", {"ranking": ranking, "team_ranking": team_ranking})
def read_users():
"""Lire les utilisateurs depuis django"""
users = {}
for user in User.objects.all():
users[user.id] = user.username
print(f"Utilisateurs: {users}")
return users
def read_teams():
"""Lire les équipes depuis django"""
teams = {}
for user in User.objects.all():
teams[user.id] = user.last_name
print(f"Équipes: {teams}")
return list(set(teams)), teams
#####################
......@@ -136,83 +119,65 @@ def read_teams():
#####################
def challenges_view(request):
# Redirection vers la page de login en cas de session non authentifié
if not request.user.is_authenticated:
return redirect("login")
user_id = request.session.get('user_id') # Utiliser l'ID stocké en session
today = datetime.today().strftime('%Y-%m-%d') # Récupérer la date du jour
# Vérifier si l'utilisateur a déjà généré ses défis pour aujourd'hui
user_challenges = read_user_daily_challenges(user_id, today)
user_id = request.session.get('user_id')
user_challenges = read_user_daily_challenges(user_id, get_today())
if user_challenges:
# Si les défis existent déjà pour aujourd'hui, on les récupère depuis le fichier
selected_challenges = user_challenges
print(f"Défis récupérés pour aujourd'hui: {len(selected_challenges)} défis")
else:
# Sinon, générer de nouveaux défis pour cet utilisateur
print("Aucun défi généré pour aujourd'hui, génération de nouveaux défis.")
selected_challenges = generate_daily_challenges(user_id) # Générer et enregistrer de nouveaux défis
# Rechercher la difficulté de l'user connecté
selected_challenges = generate_daily_challenges(user_id)
user_difficulty = get_user_difficulty(user_id)
print(f"Difficulté de l'utilisateur {user_id}: {user_difficulty}")
selected_challenges = read_challenges_from_ids(selected_challenges)
# Passer les défis du jour à la vue
return render(request, "challenges.html", {"challenges": selected_challenges, "user_difficulty": user_difficulty})
def get_user_difficulty(user_id):
"""Récupérer la difficulté de l'utilisateur à partir du fichier 'difficulties.csv'."""
difficulty = "moyen" # Valeur par défaut
difficulty = "moyen"
if os.path.exists(DIFFICULTY_FILE):
with open(DIFFICULTY_FILE, "r", newline="", encoding="utf-8") as file:
reader = csv.reader(file)
for row in reader:
if len(row) >= 2 and row[0] == str(user_id): # Vérifier l'ID utilisateur
difficulty = row[1] # Récupérer la difficulté
if len(row) >= 2 and row[0] == str(user_id):
difficulty = row[1]
break
return difficulty
return difficulty.capitalize()
def read_user_daily_challenges(user_id, today):
"""Lire les défis du jour pour un utilisateur donné depuis le fichier daily_challenges.csv."""
user_challenges = []
if os.path.exists(DAILY_CHALLENGE_FILE):
with open(DAILY_CHALLENGE_FILE, 'r', newline='', encoding='utf-8') as file:
reader = csv.reader(file)
for row in reader:
if len(row) >= 3 and row[1] == str(user_id) and row[0] == today:
user_challenges = row[2:] # Extraire les IDs des défis du jour
user_challenges = row[2:]
break
return user_challenges
def generate_daily_challenges(user_id):
"""Génère et enregistre les défis du jour pour l'utilisateur."""
challenges = read_challenges()
user_difficulty = get_user_difficulty(user_id)
user_difficulty = user_difficulty.capitalize()
daily_challenges = []
categories = ["Au bureau (poste)", "Au bureau (espaces communs)", "Extérieur"]
random.shuffle(categories) # Mélanger les catégories
categories = CATEGORIES.copy()
random.shuffle(categories)
for category in categories:
challenges_in_category = [c for c in challenges[user_difficulty] if c["lieu"] == category]
if challenges_in_category:
challenge = random.choice(challenges_in_category)
daily_challenges.append(challenge["id"])
save_daily_challenges_to_csv(user_id, daily_challenges) # Enregistrer les défis du jour
save_daily_challenges_to_csv(user_id, daily_challenges)
return daily_challenges
def save_daily_challenges_to_csv(user_id, challenge_ids):
"""Sauvegarder les défis du jour dans un fichier CSV"""
today = datetime.today().strftime('%Y-%m-%d')
with open(DAILY_CHALLENGE_FILE, 'a', newline='', encoding='utf-8') as file:
writer = csv.writer(file)
writer.writerow([today, user_id] + challenge_ids)
writer.writerow([get_today(), user_id] + challenge_ids)
def update_score(user_id, points):
"""Mettre à jour le score d'un utilisateur."""
scores = read_scores()
if str(user_id) in scores:
scores[str(user_id)] += points
......@@ -221,24 +186,23 @@ def update_score(user_id, points):
with open(CSV_FILE, 'w', newline='') as file:
writer = csv.writer(file)
writer.writerow(["user_id", "score"]) # En-tête
writer.writerow(["user_id", "score"])
for user_id, score in scores.items():
writer.writerow([user_id, score])
def complete_challenge(request):
if request.method == "POST":
challenge_id = request.POST.get("challenge_id")
user_id = request.session.get('user_id') # Récupérer l'ID utilisateur depuis la session
user_id = request.session.get('user_id')
points = get_challenge_points(challenge_id)
if points:
update_score(user_id, points) # Mettre à jour le score de l'utilisateur
update_score(user_id, points)
return JsonResponse({"status": "success", "points": points})
return JsonResponse({"status": "error", "message": "Invalid request"}, status=400)
def read_challenges():
"""Lire tous les défis depuis un fichier CSV avec des colonnes séparées par des virgules."""
challenges = {"Facile": [], "Moyen": [], "Difficile": []}
if os.path.exists("challenges.csv"):
with open("challenges.csv", 'r', newline='', encoding='utf-8') as file:
......@@ -257,7 +221,6 @@ def read_challenges():
return challenges
def read_challenges_from_ids(challenge_ids):
"""Récupérer les défis à partir de leurs IDs."""
challenges = read_challenges()
selected_challenges = []
for level in challenges:
......@@ -267,7 +230,6 @@ def read_challenges_from_ids(challenge_ids):
return selected_challenges
def get_challenge_points(challenge_id):
"""Récupérer les points associés à un défi donné."""
challenges = read_challenges()
for level in challenges:
for challenge in challenges[level]:
......@@ -275,28 +237,30 @@ def get_challenge_points(challenge_id):
return int(challenge["points"])
return None
@csrf_exempt
def update_difficulty(request):
if request.method == "POST":
user_id = request.session.get('user_id')
difficulty = request.POST.get("difficulty")
if difficulty in ["Facile", "Moyen", "Difficile"]:
save_user_difficulty(user_id, difficulty)
return JsonResponse({"status": "success"})
return JsonResponse({"status": "error", "message": "Invalid request"}, status=400)
#####################
##################################
# Page de Mise à jour difficulty #
#####################
##################################
@csrf_exempt
def update_difficulty(request):
if request.method == "POST":
user_id = request.session.get('user_id')
difficulty = request.POST.get("difficulty")
if difficulty in ["facile", "moyen", "difficile"]:
# Save user difficulty in file or database
if difficulty in ["Facile", "Moyen", "Difficile"]:
save_user_difficulty(user_id, difficulty)
today = get_today()
with open(DAILY_CHALLENGE_FILE, 'r', newline='', encoding='utf-8') as file:
reader = csv.reader(file)
data = []
for row in reader:
if len(row) >= 3 and row[1] == str(user_id) and row[0] == today:
continue
data.append(row)
with open(DAILY_CHALLENGE_FILE, 'w', newline='', encoding='utf-8') as file:
writer = csv.writer(file)
for row in data:
writer.writerow(row)
return JsonResponse({"status": "success"})
else:
return JsonResponse({"status": "error", "message": "Invalid difficulty"})
......@@ -312,14 +276,13 @@ def read_scores():
if os.path.exists(CSV_FILE):
with open(CSV_FILE, 'r', newline='') as file:
reader = csv.reader(file)
next(reader) # Ignorer l'en-tête
next(reader)
for row in reader:
if len(row) >= 2:
scores[row[0]] = int(row[1]) # Ajouter le score à un dictionnaire
scores[row[0]] = int(row[1])
return scores
def save_user_difficulty(user_id, difficulty):
"""Remplace si l'user à déjà sauvegarder sa difficulté sinon sauvegarde la le tout dans un fichier CSV."""
data = []
if os.path.exists(DIFFICULTY_FILE):
with open(DIFFICULTY_FILE, "r", newline="", encoding="utf-8") as file:
......@@ -336,3 +299,6 @@ def save_user_difficulty(user_id, difficulty):
writer.writerow(row)
if [user_id, difficulty] not in data:
writer.writerow([user_id, difficulty])
def get_today():
return datetime.today().strftime('%Y-%m-%d')
1,Étirements des bras et épaules (2 min),Facile,Au bureau (poste),5,arrows-up-down-left-right
2,Se tenir droit et contracter les abdos (5 min),Facile,Au bureau (poste),5,user
3,S'asseoir et se lever de sa chaise (10 fois),Facile,Au bureau (poste),5,circle-arrow-up
4,Tenir 30 sec en équilibre sur un pied,Facile,Au bureau (poste),5,universal-access
5,Rotations poignets et épaules (1 min),Facile,Au bureau (poste),5,arrows-rotate
6,Prendre les escaliers toute la journée,Facile,Au bureau (espaces communs),10,stairs
7,Marcher 5000 pas dans la journée,Facile,Au bureau (espaces communs),10,shoe-prints
8,Marcher 5 min après chaque réunion,Facile,Au bureau (espaces communs),10,clock
9,Flexions des genoux (10 fois),Facile,Au bureau (espaces communs),10,circle-arrow-down
10,Respiration profonde (5 fois),Facile,Au bureau (espaces communs),10,wind
11,Marche rapide (10 min) pause déjeuner,Facile,Extérieur,15,person-walking
12,Descendre un arrêt plus tôt,Facile,Extérieur,15,bus
13,Aller voir un collègue au lieu d’un e-mail,Facile,Extérieur,15,envelope-open-text
14,Balade autour du bureau,Facile,Extérieur,15,tree
15,20 montées de marches en extérieur,Facile,Extérieur,15,stairs
16,10 squats sans quitter son bureau,Moyen,Au bureau (poste),5,person-running
17,Gainage sur chaise (30 sec),Moyen,Au bureau (poste),5,grip-lines
18,20 extensions de mollets,Moyen,Au bureau (poste),5,person-walking
19,30 contractions abdos assis,Moyen,Au bureau (poste),5,user
20,Alterner assis/debout toutes les 30 min (2h),Moyen,Au bureau (poste),5,clock
21,Escaliers 3 jours d’affilée,Moyen,Au bureau (espaces communs),10,stairs
22,Se lever et marcher 5 min toutes les heures,Moyen,Au bureau (espaces communs),10,clock
23,10 montées de marches,Moyen,Au bureau (espaces communs),10,stairs
24,15 squats en salle de repos,Moyen,Au bureau (espaces communs),10,dumbbell
25,10 rotations de chevilles assis,Moyen,Au bureau (espaces communs),10,arrows-rotate
26,Marche rapide (15 min),Moyen,Extérieur,15,person-walking
27,10 flexions jambes en attendant métro,Moyen,Extérieur,15,bus
28,Aller au travail à pied/vélo (1x/semaine),Moyen,Extérieur,15,person-biking
29,7500 pas dans la journée,Moyen,Extérieur,15,shoe-prints
30,Balade de 20 min en fin de journée,Moyen,Extérieur,15,tree
31,Gainage contre mur (1 min),Difficile,Au bureau (poste),5,grip-lines
32,20 squats en plusieurs fois,Difficile,Au bureau (poste),5,person-running
33,50 extensions de mollets,Difficile,Au bureau (poste),5,person-walking
34,Planche sur chaise (2 min),Difficile,Au bureau (poste),5,grip-lines
35,50 contractions abdos assis,Difficile,Au bureau (poste),5,user
36,Escaliers uniquement (5 jours consécutifs),Difficile,Au bureau (espaces communs),10,stairs
37,Marche 10 min après déjeuner (3 jours),Difficile,Au bureau (espaces communs),10,person-walking
38,Défi collectif avec 3 collègues,Difficile,Au bureau (espaces communs),10,users
39,Se lever et marcher toutes les 30 min,Difficile,Au bureau (espaces communs),10,clock
40,3 min de montées de genoux salle de repos,Difficile,Au bureau (espaces communs),10,person-running
41,10000 pas dans la journée,Difficile,Extérieur,15,shoe-prints
42,Balade de 30 min après le travail,Difficile,Extérieur,15,tree
43,Aller au travail à vélo (3x/semaine),Difficile,Extérieur,15,person-biking
44,Marcher/courir 5 km en dehors du bureau,Difficile,Extérieur,15,shoe-prints
45,Séance collective sportive extérieure,Difficile,Extérieur,15,users
2025-03-21,33,27,21,20
2025-03-21,34,20,26,23
2025-03-21,23,17,30,23
2025-03-21,40,21,29,17
2025-03-21,41,26,17,25
2025-03-21,42,20,23,28
2025-03-21,43,17,28,25
No preview for this file type
2,difficile
17,difficile
22,difficile
35,facile
34,moyen
39,facile
23,moyen
41,Moyen
user_id,score
2,49
17,10
18,26
19,36
20,75
21,60
1,50
22,135
23,196
24,22
25,12
26,5
27,20
28,15
29,15
30,30
31,30
32,25
34,50
35,30
37,40
38,30
39,15
40,30
41,55
42,30
43,15
AppGCC/static/img/activities/1.gif

467 KiB

AppGCC/static/img/activities/16.gif

953 KiB

AppGCC/static/img/activities/17.png

7.89 KiB

AppGCC/static/img/activities/18.gif

585 KiB

AppGCC/static/img/activities/19.gif

1.67 MiB

AppGCC/static/img/activities/2.jpg

25.7 KiB

AppGCC/static/img/activities/24.gif

953 KiB

AppGCC/static/img/activities/25.gif

1.7 MiB

AppGCC/static/img/activities/27.gif

4.6 MiB

AppGCC/static/img/activities/31.gif

563 KiB

AppGCC/static/img/activities/32.gif

953 KiB

AppGCC/static/img/activities/33.gif

585 KiB

AppGCC/static/img/activities/34.jpg

68.8 KiB

0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment