Ajout des fonctionnalités de gestion de progression des cours : vue dédiée pour le toggle des leçons, mise à jour des templates pour afficher la progression, intégration des routes Ajax, styles associés, et ajustements des vues et modèles pour gérer les données utilisateur.
This commit is contained in:
parent
609745a723
commit
ac8ef6894d
8 changed files with 261 additions and 9 deletions
|
|
@ -3,6 +3,7 @@ from django.urls import reverse
|
||||||
from django.views import generic
|
from django.views import generic
|
||||||
from django.db.models import Prefetch
|
from django.db.models import Prefetch
|
||||||
from .models import Course, Lesson, Module, Comment
|
from .models import Course, Lesson, Module, Comment
|
||||||
|
from progression.models import Progression
|
||||||
from .forms import CommentForm
|
from .forms import CommentForm
|
||||||
|
|
||||||
def list_courses(request):
|
def list_courses(request):
|
||||||
|
|
@ -23,9 +24,23 @@ def show(request, course_name, course_id):
|
||||||
.order_by('order')
|
.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 = {
|
context = {
|
||||||
'course': course,
|
'course': course,
|
||||||
'lessons': lessons,
|
'lessons': lessons,
|
||||||
|
'user_progress': user_progress,
|
||||||
|
'completed_lesson_ids': completed_lesson_ids,
|
||||||
}
|
}
|
||||||
return render(request, 'courses/show.html', context)
|
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')
|
.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 = {
|
context = {
|
||||||
'course': course,
|
'course': course,
|
||||||
'module': module,
|
'module': module,
|
||||||
|
|
@ -114,5 +141,7 @@ def lesson_detail(request, course_slug, module_slug, lesson_slug):
|
||||||
'comment_form': form,
|
'comment_form': form,
|
||||||
'prev_lesson': prev_lesson,
|
'prev_lesson': prev_lesson,
|
||||||
'next_lesson': next_lesson,
|
'next_lesson': next_lesson,
|
||||||
|
'user_progress': user_progress,
|
||||||
|
'completed_lesson_ids': completed_lesson_ids,
|
||||||
}
|
}
|
||||||
return render(request, 'courses/lesson.html', context)
|
return render(request, 'courses/lesson.html', context)
|
||||||
|
|
@ -43,7 +43,7 @@ urlpatterns = [
|
||||||
path('', include('home.urls')),
|
path('', include('home.urls')),
|
||||||
path('courses/', include('courses.urls')),
|
path('courses/', include('courses.urls')),
|
||||||
path('users/', include('users.urls')),
|
path('users/', include('users.urls')),
|
||||||
|
path('progression/', include('progression.urls')),
|
||||||
path('blog/', include('blog.urls')),
|
path('blog/', include('blog.urls')),
|
||||||
|
|
||||||
path('sitemap.xml', sitemap, {'sitemaps': sitemaps_dict}, name='django.contrib.sitemaps.views.sitemap'),
|
path('sitemap.xml', sitemap, {'sitemaps': sitemaps_dict}, name='django.contrib.sitemaps.views.sitemap'),
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,9 @@ from django.urls import path
|
||||||
from . import views
|
from . import views
|
||||||
|
|
||||||
app_name = 'progression'
|
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'),
|
||||||
]
|
]
|
||||||
|
|
@ -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
|
||||||
|
})
|
||||||
|
|
@ -247,9 +247,9 @@ html { box-sizing: border-box; }
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
padding: 2px 8px;
|
padding: 2px 8px;
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
border-radius: 999px;
|
border-radius: 2px;
|
||||||
background: var(--accent);
|
background: var(--accent);
|
||||||
color: var(--warning-contrast);
|
color: var(--text);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
letter-spacing: .3px;
|
letter-spacing: .3px;
|
||||||
|
|
@ -259,6 +259,40 @@ html { box-sizing: border-box; }
|
||||||
white-space: nowrap;
|
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 {
|
.courseToc .tocLink.disabled {
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
background: transparent;
|
background: transparent;
|
||||||
|
|
@ -269,6 +303,70 @@ html { box-sizing: border-box; }
|
||||||
font-weight: 500;
|
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'] {
|
[data-theme='light'] {
|
||||||
/* Palette: plus nuancé, moins "blanc" */
|
/* Palette: plus nuancé, moins "blanc" */
|
||||||
--bg: #eef3f7; /* fond légèrement teinté bleu-gris */
|
--bg: #eef3f7; /* fond légèrement teinté bleu-gris */
|
||||||
|
|
|
||||||
|
|
@ -4,3 +4,22 @@
|
||||||
Un cours proposé par <a href="{% url 'another_profile' course.author.id %}">{{ course.author }}</a>
|
Un cours proposé par <a href="{% url 'another_profile' course.author.id %}">{{ course.author }}</a>
|
||||||
</p>
|
</p>
|
||||||
<p>{{ course.content }}</p>
|
<p>{{ course.content }}</p>
|
||||||
|
<strong>Progression pour ce cours</strong>
|
||||||
|
{% if user_progress.percent_completed == 100 %}
|
||||||
|
<div class="course-completed">
|
||||||
|
<div class="container">
|
||||||
|
<div class="icon"><i class="fa-solid fa-check"></i></div>
|
||||||
|
</div>
|
||||||
|
<div class="container">
|
||||||
|
<div class="title">Félicitation</div>
|
||||||
|
<div class="content">Tu as terminé(e) ce cours ! Tu peux réellement être fier(e) de toi !!</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="progress-container">
|
||||||
|
<div class="progress-bar" style="width: 100%;">
|
||||||
|
<div id="progress-bar-fill" class="progress-bar-fill" style="width: {{ user_progress.percent_completed }}%; height: 100%; transition: width 0.3s;"></div>
|
||||||
|
</div>
|
||||||
|
<div id="progress-text" class="progress-text">{{ user_progress.percent_completed }}%</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,8 @@
|
||||||
<li class="tocLesson{% if lesson and lesson.id == item.id %} current{% endif %}">
|
<li class="tocLesson{% if lesson and lesson.id == item.id %} current{% endif %}">
|
||||||
<a class="tocLink {% if item.is_premium and user.profile.is_premium == False %}premium{% endif %}" href="{% url 'courses:lesson_detail' course.slug item.module.slug item.slug %}">
|
<a class="tocLink {% if item.is_premium and user.profile.is_premium == False %}premium{% endif %}" href="{% url 'courses:lesson_detail' course.slug item.module.slug item.slug %}">
|
||||||
{{ item.name }} {% if item.is_premium %}<span class="premium-tag">PREMIUM</span>{% endif %}
|
{{ item.name }} {% if item.is_premium %}<span class="premium-tag">PREMIUM</span>{% endif %}
|
||||||
{% if lesson and lesson.id == item.id %}<span class="tocCurrentTag">(cours actuel)</span>{% endif %}
|
{% if lesson and lesson.id == item.id %}<span class="current-tag">(cours actuel)</span>{% endif %}
|
||||||
|
{% if item.id in completed_lesson_ids %}<span class="completed-tag">Terminé</span>{% endif %}
|
||||||
</a>
|
</a>
|
||||||
{% if lesson and lesson.id == item.id %}
|
{% if lesson and lesson.id == item.id %}
|
||||||
<div class="lessonInline">
|
<div class="lessonInline">
|
||||||
|
|
@ -69,6 +70,12 @@
|
||||||
<div class="content-lesson">
|
<div class="content-lesson">
|
||||||
<div class="lessonTitle">Ce que l'on voit durant ce cours : </div>
|
<div class="lessonTitle">Ce que l'on voit durant ce cours : </div>
|
||||||
{{ lesson.content|comment_markdown }}
|
{{ lesson.content|comment_markdown }}
|
||||||
|
|
||||||
|
{% if lesson.id in completed_lesson_ids %}
|
||||||
|
<button id="btn-complete" data-lesson-id="{{ lesson.id }}" class="btn btn-success">✅ Terminée</button>
|
||||||
|
{% else %}
|
||||||
|
<button id="btn-complete" data-lesson-id="{{ lesson.id }}" class="btn btn-secondary">Marquer comme terminé</button>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<h3 id="comments">Commentaires</h3>
|
<h3 id="comments">Commentaires</h3>
|
||||||
<div class="lessonComments">
|
<div class="lessonComments">
|
||||||
|
|
@ -251,6 +258,64 @@
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- Progression Bar -->
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const btn = document.getElementById('btn-complete');
|
||||||
|
const progressBar = document.getElementById('progress-bar-fill');
|
||||||
|
const progressText = document.getElementById('progress-text');
|
||||||
|
|
||||||
|
btn.addEventListener('click', function() {
|
||||||
|
const lessonId = this.getAttribute('data-lesson-id');
|
||||||
|
|
||||||
|
// 1. On prépare la requête
|
||||||
|
fetch("{% url 'progression:toggle_lesson' %}", { // Assure-toi que c'est le bon nom dans urls.py
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRFToken': getCookie('csrftoken') // Fonction indispensable pour Django
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
'lesson_id': lessonId
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.status === 'success') {
|
||||||
|
// 2. On met à jour le visuel de la barre
|
||||||
|
progressBar.style.width = data.new_percent + '%';
|
||||||
|
progressText.innerText = data.new_percent + '%';
|
||||||
|
|
||||||
|
// 3. On change l'aspect du bouton
|
||||||
|
if (data.is_completed) {
|
||||||
|
btn.innerText = "✅ Terminée";
|
||||||
|
btn.style.backgroundColor = "green";
|
||||||
|
} else {
|
||||||
|
btn.innerText = "Marquer comme terminé";
|
||||||
|
btn.style.backgroundColor = "gray";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Fonction Helper pour récupérer le cookie CSRF de Django ---
|
||||||
|
function getCookie(name) {
|
||||||
|
let cookieValue = null;
|
||||||
|
if (document.cookie && document.cookie !== '') {
|
||||||
|
const cookies = document.cookie.split(';');
|
||||||
|
for (let i = 0; i < cookies.length; i++) {
|
||||||
|
const cookie = cookies[i].trim();
|
||||||
|
if (cookie.substring(0, name.length + 1) === (name + '=')) {
|
||||||
|
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cookieValue;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
</div> <!-- /.lessonComments -->
|
</div> <!-- /.lessonComments -->
|
||||||
</div> <!-- /.lesson -->
|
</div> <!-- /.lesson -->
|
||||||
</div> <!-- /.lessonInline -->
|
</div> <!-- /.lessonInline -->
|
||||||
|
|
|
||||||
|
|
@ -39,8 +39,8 @@
|
||||||
(function() {
|
(function() {
|
||||||
try {
|
try {
|
||||||
var stored = localStorage.getItem('pdz-theme');
|
var stored = localStorage.getItem('pdz-theme');
|
||||||
var prefersLight = window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches;
|
var prefersLight = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
var theme = stored || (prefersLight ? 'light' : 'dark');
|
var theme = stored || (prefersLight ? 'dark' : 'light');
|
||||||
document.documentElement.setAttribute('data-theme', theme);
|
document.documentElement.setAttribute('data-theme', theme);
|
||||||
if (theme === 'light') {
|
if (theme === 'light') {
|
||||||
var link = document.getElementById('theme-css');
|
var link = document.getElementById('theme-css');
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue