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 %}

    Commentaires

    @@ -251,6 +258,64 @@ }); })(); + + +
    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');