From ac8ef6894daa85baa59f2110d13175d1da10f21c Mon Sep 17 00:00:00 2001
From: mrtoine
Date: Mon, 15 Dec 2025 22:34:02 +0100
Subject: [PATCH] =?UTF-8?q?Ajout=20des=20fonctionnalit=C3=A9s=20de=20gesti?=
=?UTF-8?q?on=20de=20progression=20des=20cours=20:=20vue=20d=C3=A9di=C3=A9?=
=?UTF-8?q?e=20pour=20le=20toggle=20des=20le=C3=A7ons,=20mise=20=C3=A0=20j?=
=?UTF-8?q?our=20des=20templates=20pour=20afficher=20la=20progression,=20i?=
=?UTF-8?q?nt=C3=A9gration=20des=20routes=20Ajax,=20styles=20associ=C3=A9s?=
=?UTF-8?q?,=20et=20ajustements=20des=20vues=20et=20mod=C3=A8les=20pour=20?=
=?UTF-8?q?g=C3=A9rer=20les=20donn=C3=A9es=20utilisateur.?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
courses/views.py | 29 +++++
devart/urls.py | 2 +-
progression/urls.py | 5 +-
progression/views.py | 42 +++++++-
static/css/app.css | 102 +++++++++++++++++-
.../courses/partials/_course_header.html | 19 ++++
templates/courses/partials/_course_toc.html | 67 +++++++++++-
templates/layout.html | 4 +-
8 files changed, 261 insertions(+), 9 deletions(-)
diff --git a/courses/views.py b/courses/views.py
index 8a33bbb..ca9fb22 100644
--- a/courses/views.py
+++ b/courses/views.py
@@ -3,6 +3,7 @@ from django.urls import reverse
from django.views import generic
from django.db.models import Prefetch
from .models import Course, Lesson, Module, Comment
+from progression.models import Progression
from .forms import CommentForm
def list_courses(request):
@@ -23,9 +24,23 @@ def show(request, course_name, course_id):
.order_by('order')
)
+ # Récupération de la progression de l'utilisateur sur le cours
+ user_progress = None
+ completed_lesson_ids = [] # On prépare une liste vide par défaut
+
+ if request.user.is_authenticated:
+ user_progress = Progression.objects.filter(user=request.user, course=course).first()
+
+ # 2. S'il y a une progression, on extrait juste les IDs des leçons finies
+ if user_progress:
+ # values_list renvoie une liste simple : [1, 5, 12] au lieu d'objets lourds
+ completed_lesson_ids = user_progress.completed_lessons.values_list('id', flat=True)
+
context = {
'course': course,
'lessons': lessons,
+ 'user_progress': user_progress,
+ 'completed_lesson_ids': completed_lesson_ids,
}
return render(request, 'courses/show.html', context)
@@ -105,6 +120,18 @@ def lesson_detail(request, course_slug, module_slug, lesson_slug):
.order_by('created_at')
)
+ # Récupération de la progression de l'utilisateur sur le cours
+ user_progress = None
+ completed_lesson_ids = [] # On prépare une liste vide par défaut
+
+ if request.user.is_authenticated:
+ user_progress = Progression.objects.filter(user=request.user, course=course).first()
+
+ # 2. S'il y a une progression, on extrait juste les IDs des leçons finies
+ if user_progress:
+ # values_list renvoie une liste simple : [1, 5, 12] au lieu d'objets lourds
+ completed_lesson_ids = user_progress.completed_lessons.values_list('id', flat=True)
+
context = {
'course': course,
'module': module,
@@ -114,5 +141,7 @@ def lesson_detail(request, course_slug, module_slug, lesson_slug):
'comment_form': form,
'prev_lesson': prev_lesson,
'next_lesson': next_lesson,
+ 'user_progress': user_progress,
+ 'completed_lesson_ids': completed_lesson_ids,
}
return render(request, 'courses/lesson.html', context)
\ No newline at end of file
diff --git a/devart/urls.py b/devart/urls.py
index b46feda..17cb4e7 100644
--- a/devart/urls.py
+++ b/devart/urls.py
@@ -43,7 +43,7 @@ urlpatterns = [
path('', include('home.urls')),
path('courses/', include('courses.urls')),
path('users/', include('users.urls')),
-
+ path('progression/', include('progression.urls')),
path('blog/', include('blog.urls')),
path('sitemap.xml', sitemap, {'sitemaps': sitemaps_dict}, name='django.contrib.sitemaps.views.sitemap'),
diff --git a/progression/urls.py b/progression/urls.py
index a2fd910..cef076c 100644
--- a/progression/urls.py
+++ b/progression/urls.py
@@ -2,6 +2,9 @@ from django.urls import path
from . import views
app_name = 'progression'
-urlpatterns = [
+urlpatterns = [
+ # On ne garde QUE la route AJAX ici.
+ # Les routes 'list', 'detail', etc. doivent rester dans courses/urls.py
+ path('ajax/toggle-lesson/', views.toggle_lesson_completion, name='toggle_lesson'),
]
\ No newline at end of file
diff --git a/progression/views.py b/progression/views.py
index 91ea44a..1cee11c 100644
--- a/progression/views.py
+++ b/progression/views.py
@@ -1,3 +1,41 @@
-from django.shortcuts import render
+from django.shortcuts import render, get_object_or_404
+import json
+from django.http import JsonResponse
+from django.views.decorators.http import require_POST
+from django.contrib.auth.decorators import login_required
+from progression.models import Progression
+from courses.models import Lesson
-# Create your views here.
+@login_required
+@require_POST
+def toggle_lesson_completion(request):
+ data = json.loads(request.body)
+ lesson_id = data.get('lesson_id')
+
+ lesson = get_object_or_404(Lesson, id=lesson_id)
+ # On remonte au cours via le module (Lesson -> Module -> Course)
+ course = lesson.module.course
+
+ # On récupère ou crée la progression
+ progression, created = Progression.objects.get_or_create(
+ user=request.user,
+ course=course
+ )
+
+ # La logique du Toggle
+ if lesson in progression.completed_lessons.all():
+ progression.completed_lessons.remove(lesson)
+ is_completed = False
+ else:
+ progression.completed_lessons.add(lesson)
+ is_completed = True
+
+ # Mise à jour de la dernière leçon vue
+ progression.last_viewed_lesson = lesson
+ progression.save()
+
+ return JsonResponse({
+ 'status': 'success',
+ 'is_completed': is_completed,
+ 'new_percent': progression.percent_completed
+ })
\ No newline at end of file
diff --git a/static/css/app.css b/static/css/app.css
index d95a712..367520c 100644
--- a/static/css/app.css
+++ b/static/css/app.css
@@ -247,9 +247,9 @@ html { box-sizing: border-box; }
gap: 6px;
padding: 2px 8px;
margin-left: 8px;
- border-radius: 999px;
+ border-radius: 2px;
background: var(--accent);
- color: var(--warning-contrast);
+ color: var(--text);
font-size: 11px;
font-weight: 800;
letter-spacing: .3px;
@@ -259,6 +259,40 @@ html { box-sizing: border-box; }
white-space: nowrap;
}
+.current-tag {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ padding: 2px 8px;
+ border-radius: 2px;
+ background: var(--primary);
+ color: var(--primary-contrast);
+ font-size: 11px;
+ font-weight: 800;
+ letter-spacing: .3px;
+ line-height: 1.2;
+ vertical-align: middle;
+ text-transform: uppercase;
+ white-space: nowrap;
+}
+
+.completed-tag {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ padding: 2px 8px;
+ border-radius: 2px;
+ background: var(--success);
+ color: var(--success-contrast);
+ font-size: 11px;
+ font-weight: 800;
+ letter-spacing: .3px;
+ line-height: 1.2;
+ vertical-align: middle;
+ text-transform: uppercase;
+ white-space: nowrap;
+}
+
.courseToc .tocLink.disabled {
color: var(--text-muted);
background: transparent;
@@ -269,6 +303,70 @@ html { box-sizing: border-box; }
font-weight: 500;
}
+/* Progression des cours */
+
+.progress-container {
+ display: flex;
+ flex-direction: row;
+}
+
+.progress-bar {
+ display: flex;
+ border-radius: var(--r-2);
+ border: 1px solid var(--border);
+ height: 20px;
+ background: var(--neutral-900);
+}
+
+.progress-text {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 4px;
+ color: var(--text);
+ font-size: 12px;
+ margin-left: 10px;
+}
+
+.progress-bar-fill {
+ background: var(--success);
+ height: 100%;
+ border-radius: var(--r-2) 0 0 var(--r-2);
+}
+
+.course-completed {
+ display: flex;
+ flex-direction: row;
+ border-radius: var(--r-2);
+ border: 1px solid var(--border);
+ margin: var(--space-2) 0;
+ background: var(--success);
+}
+
+.course-completed .container {
+ display: flex;
+ flex-direction: column;
+ padding: var(--space-2);
+ height: 100%;
+ vertical-align: middle;
+}
+
+.course-completed .container .icon {
+ color: var(--success-contrast);
+ font-size: 50px;
+}
+
+.course-completed .container .title {
+ color: var(--success-contrast);
+ font-size: 28px;
+ font-weight: 600;
+}
+
+.course-completed .container .content {
+ color: var(--success-contrast);
+ font-size: 14px;
+}
+
[data-theme='light'] {
/* Palette: plus nuancé, moins "blanc" */
--bg: #eef3f7; /* fond légèrement teinté bleu-gris */
diff --git a/templates/courses/partials/_course_header.html b/templates/courses/partials/_course_header.html
index bff069f..33fbd36 100644
--- a/templates/courses/partials/_course_header.html
+++ b/templates/courses/partials/_course_header.html
@@ -4,3 +4,22 @@
Un cours proposé par {{ course.author }}
{{ course.content }}
+Progression pour ce cours
+{% if user_progress.percent_completed == 100 %}
+
+
+
+
Félicitation
+
Tu as terminé(e) ce cours ! Tu peux réellement être fier(e) de toi !!
+
+
+{% else %}
+
+
+
{{ user_progress.percent_completed }}%
+
+{% endif %}
diff --git a/templates/courses/partials/_course_toc.html b/templates/courses/partials/_course_toc.html
index 9039157..1389e55 100644
--- a/templates/courses/partials/_course_toc.html
+++ b/templates/courses/partials/_course_toc.html
@@ -13,7 +13,8 @@
{{ item.name }} {% if item.is_premium %}PREMIUM{% endif %}
- {% if lesson and lesson.id == item.id %}(cours actuel){% endif %}
+ {% if lesson and lesson.id == item.id %}(cours actuel){% endif %}
+ {% if item.id in completed_lesson_ids %}Terminé{% endif %}
{% if lesson and lesson.id == item.id %}
@@ -69,6 +70,12 @@
Ce que l'on voit durant ce cours :
{{ lesson.content|comment_markdown }}
+
+ {% if lesson.id in completed_lesson_ids %}
+
+ {% else %}
+
+ {% endif %}
diff --git a/templates/layout.html b/templates/layout.html
index 2c3189d..2e7a521 100644
--- a/templates/layout.html
+++ b/templates/layout.html
@@ -39,8 +39,8 @@
(function() {
try {
var stored = localStorage.getItem('pdz-theme');
- var prefersLight = window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches;
- var theme = stored || (prefersLight ? 'light' : 'dark');
+ var prefersLight = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
+ var theme = stored || (prefersLight ? 'dark' : 'light');
document.documentElement.setAttribute('data-theme', theme);
if (theme === 'light') {
var link = document.getElementById('theme-css');