diff --git a/courses/admin.py b/courses/admin.py index 0840729..b34bc19 100644 --- a/courses/admin.py +++ b/courses/admin.py @@ -1,16 +1,56 @@ from django.contrib import admin -from .models import * +from .models import Course, Module, Lesson, Comment -# Register your models here. -class CategoriesAdmin(admin.ModelAdmin): - fieldsets = [ - ('Nom', {'fields': ['name']}), - ('Description', {'fields': ['content']}), - ('Date de cr"ation', {'fields': ['created_at']}), - ] - list_display = ('name', 'content', 'type') - list_filter = ['type', 'name'] - search_fields = ['name', 'content'] -admin.site.register(Course) -admin.site.register(Lesson) \ No newline at end of file +class LessonInline(admin.TabularInline): + model = Lesson + extra = 0 + fields = ("name", "slug", "is_premium", "order") + ordering = ("order",) + prepopulated_fields = {"slug": ("name",)} + + +class ModuleInline(admin.TabularInline): + model = Module + extra = 0 + fields = ("name", "slug", "enable", "order") + ordering = ("order",) + prepopulated_fields = {"slug": ("name",)} + + +@admin.register(Course) +class CourseAdmin(admin.ModelAdmin): + list_display = ("name", "slug", "author", "enable", "created_at", "updated_at") + list_filter = ("enable", "author", "created_at", "updated_at") + search_fields = ("name", "tags", "description", "author__username") + prepopulated_fields = {"slug": ("name",)} + readonly_fields = ("created_at", "updated_at") + inlines = [ModuleInline] + + +@admin.register(Module) +class ModuleAdmin(admin.ModelAdmin): + list_display = ("name", "slug", "course", "enable", "order", "created_at", "updated_at") + list_filter = ("enable", "course", "created_at", "updated_at") + search_fields = ("name", "description", "course__name") + prepopulated_fields = {"slug": ("name",)} + readonly_fields = ("created_at", "updated_at") + ordering = ("course", "order") + inlines = [LessonInline] + + +@admin.register(Lesson) +class LessonAdmin(admin.ModelAdmin): + list_display = ("name", "slug", "module", "is_premium", "order") + list_filter = ("is_premium", "module") + search_fields = ("name", "content", "module__name") + prepopulated_fields = {"slug": ("name",)} + ordering = ("module", "order") + + +@admin.register(Comment) +class CommentAdmin(admin.ModelAdmin): + list_display = ("lesson", "user", "created_at", "is_active") + list_filter = ("is_active", "created_at") + search_fields = ("content", "user__username", "lesson__name") + readonly_fields = ("created_at", "updated_at") \ No newline at end of file diff --git a/courses/forms.py b/courses/forms.py new file mode 100644 index 0000000..ee2c66d --- /dev/null +++ b/courses/forms.py @@ -0,0 +1,13 @@ +from django import forms + + +class CommentForm(forms.Form): + content = forms.CharField( + label="", + widget=forms.Textarea(attrs={ + "rows": 3, + "placeholder": "Écrire un commentaire…", + }), + max_length=5000, + ) + parent = forms.IntegerField(required=False, widget=forms.HiddenInput()) diff --git a/courses/migrations/0002_comment.py b/courses/migrations/0002_comment.py new file mode 100644 index 0000000..c552f67 --- /dev/null +++ b/courses/migrations/0002_comment.py @@ -0,0 +1,31 @@ +# Generated by Django 6.0 on 2025-12-10 20:35 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('courses', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Comment', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('content', models.TextField()), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('is_active', models.BooleanField(default=True)), + ('lesson', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='courses.lesson')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='lesson_comments', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ('created_at',), + }, + ), + ] diff --git a/courses/migrations/0003_comment_parent.py b/courses/migrations/0003_comment_parent.py new file mode 100644 index 0000000..cfbe40a --- /dev/null +++ b/courses/migrations/0003_comment_parent.py @@ -0,0 +1,19 @@ +# Generated by Django 6.0 on 2025-12-10 22:15 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('courses', '0002_comment'), + ] + + operations = [ + migrations.AddField( + model_name='comment', + name='parent', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='replies', to='courses.comment'), + ), + ] diff --git a/courses/models.py b/courses/models.py index 83ced4d..1c5a096 100644 --- a/courses/models.py +++ b/courses/models.py @@ -41,4 +41,20 @@ class Lesson(models.Model): self.content = self.content.replace('?>', '?>') def __str__(self): - return self.name \ No newline at end of file + return self.name + + +class Comment(models.Model): + lesson = models.ForeignKey(Lesson, on_delete=models.CASCADE, related_name='comments') + user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='lesson_comments') + content = models.TextField() + parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True, blank=True, related_name='replies') + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + is_active = models.BooleanField(default=True) + + class Meta: + ordering = ("created_at",) + + def __str__(self): + return f"Comment by {self.user} on {self.lesson}" \ No newline at end of file diff --git a/courses/templatetags/__init__.py b/courses/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/courses/templatetags/comment_format.py b/courses/templatetags/comment_format.py new file mode 100644 index 0000000..2fbfd7e --- /dev/null +++ b/courses/templatetags/comment_format.py @@ -0,0 +1,108 @@ +from django import template +from django.utils.html import conditional_escape, escape +from django.utils.safestring import mark_safe +import re + +register = template.Library() + + +def _link_repl(match): + text = match.group(1) + url = match.group(2) + # Allow only http/https links + if not re.match(r"^(https?://)", url, re.IGNORECASE): + # The whole input has already been HTML-escaped upstream. + return text + return ( + f'{text}' + ) + + +def _format_inline(text: str) -> str: + # code spans first + text = re.sub( + r"`([^`]+)`", + # Content is already HTML-escaped by conditional_escape; avoid double-escaping + lambda m: f"{m.group(1)}", + text, + ) + # bold **text** + text = re.sub( + r"\*\*([^*]+)\*\*", + lambda m: f"{m.group(1)}", + text, + ) + # italic *text* or _text_ + text = re.sub( + r"(?{m.group(1)}", + text, + ) + text = re.sub( + r"_([^_]+)_", + lambda m: f"{m.group(1)}", + text, + ) + # links [text](url) + text = re.sub(r"\[([^\]]+)\]\(([^)]+)\)", _link_repl, text) + return text + + +def _format_lists(lines): + html_lines = [] + in_ul = False + for line in lines: + m = re.match(r"^\s*[-*]\s+(.+)$", line) + if m: + if not in_ul: + html_lines.append("") + in_ul = False + html_lines.append(_format_inline(line)) + if in_ul: + html_lines.append("") + return html_lines + + +@register.filter(name="comment_markdown") +def comment_markdown(value: str): + """A very small Markdown subset renderer for comments with HTML escaping. + Supports: paragraphs, lists (- or *), **bold**, *italic* or _italic_, `code`, [text](url). + Fenced code blocks ```...``` will render as
.
+    All HTML is escaped.
+    """
+    if not value:
+        return ""
+
+    text = str(value)
+
+    # Normalize newlines
+    text = text.replace("\r\n", "\n").replace("\r", "\n")
+
+    # Escape HTML entirely first
+    text = conditional_escape(text)
+
+    # Handle fenced code blocks ```
+    parts = re.split(r"```\n?", text)
+    rendered = []
+    for i, part in enumerate(parts):
+        if i % 2 == 1:
+            # inside code block
+            rendered.append(f"
{part}
") + else: + # regular markdown in this chunk + # Split into paragraphs by blank lines + paragraphs = re.split(r"\n\s*\n", part) + for p in paragraphs: + lines = p.split("\n") + formatted_lines = _format_lists(lines) + html = "
".join(l for l in formatted_lines if l != "") + if html.strip(): + rendered.append(f"

{html}

") + html_out = "\n".join(rendered) + return mark_safe(html_out) diff --git a/courses/urls.py b/courses/urls.py index 4bc6314..11c4623 100644 --- a/courses/urls.py +++ b/courses/urls.py @@ -4,5 +4,7 @@ from . import views app_name = 'courses' urlpatterns = [ path('', views.list, name='list'), - path('-/', views.show, name="show"), + # Lesson detail: /courses//// + path('///', views.lesson_detail, name='lesson_detail'), + path('-/', views.show, name="show"), ] \ No newline at end of file diff --git a/courses/views.py b/courses/views.py index d9a6f0c..37131cb 100644 --- a/courses/views.py +++ b/courses/views.py @@ -1,12 +1,102 @@ -from django.shortcuts import render, get_object_or_404 +from django.shortcuts import render, get_object_or_404, redirect +from django.urls import reverse from django.views import generic -from .models import Course, Lesson +from django.db.models import Prefetch +from .models import Course, Lesson, Module, Comment +from .forms import CommentForm def list(request): courses = Course.objects.all() return render(request, 'courses/list.html', {'courses': courses}) def show(request, course_name, course_id): - course = get_object_or_404(Course, pk=course_id) - lessons = Lesson.objects.filter(course_id=course_id) - return render(request, 'courses/show.html', {'course' : course, 'lessons': lessons}) \ No newline at end of file + # Optimized course fetch with related author and profile (if present) + course = get_object_or_404( + Course.objects.select_related('author', 'author__profile'), + pk=course_id, + ) + + # Fetch lessons through module -> course relation and keep a stable order + lessons = ( + Lesson.objects.filter(module__course_id=course_id) + .select_related('module') + .order_by('order') + ) + + context = { + 'course': course, + 'lessons': lessons, + } + return render(request, 'courses/show.html', context) + + +def lesson_detail(request, course_slug, module_slug, lesson_slug): + # Fetch course by slug + course = get_object_or_404( + Course.objects.select_related('author', 'author__profile'), + slug=course_slug, + ) + + # Fetch module within course + module = get_object_or_404( + Module.objects.filter(course=course), + slug=module_slug, + ) + + # Fetch lesson within module + lesson = get_object_or_404( + Lesson.objects.select_related('module').filter(module=module), + slug=lesson_slug, + ) + + # Handle comment submission + if request.method == 'POST': + if not request.user.is_authenticated: + login_url = reverse('login') + return redirect(f"{login_url}?next={request.path}") + form = CommentForm(request.POST) + if form.is_valid(): + parent = None + parent_id = form.cleaned_data.get('parent') + if parent_id: + try: + parent = Comment.objects.get(id=parent_id, lesson=lesson, is_active=True) + except Comment.DoesNotExist: + parent = None + + Comment.objects.create( + lesson=lesson, + user=request.user, + content=form.cleaned_data['content'], + parent=parent, + ) + # Redirect to anchor to show the new comment + return redirect(request.path + "#comments") + else: + form = CommentForm() + + # Lessons list for TOC (entire course ordered) + lessons = ( + Lesson.objects.filter(module__course=course) + .select_related('module') + .order_by('order') + ) + + # Public comments for the current lesson (top-level) and their replies + replies_qs = Comment.objects.filter(is_active=True).select_related('user').order_by('created_at') + comments = ( + Comment.objects.filter(lesson=lesson, is_active=True, parent__isnull=True) + .select_related('user') + .prefetch_related(Prefetch('replies', queryset=replies_qs)) + .order_by('created_at') + ) + + context = { + 'course': course, + 'module': module, + 'lesson': lesson, + 'lessons': lessons, + 'comments': comments, + 'comment_form': form, + } + return render(request, 'courses/lesson.html', context) \ No newline at end of file diff --git a/static/css/app.css b/static/css/app.css index 8a02811..8a5d52d 100644 --- a/static/css/app.css +++ b/static/css/app.css @@ -109,6 +109,103 @@ } /* Light theme values — applied via colors_light.css inclusion */ + +/* ========================== + Cours — Sommaire (TOC) + ========================== */ +.courseToc { + background: var(--surface); + border: 1px solid var(--border-subtle); + border-radius: var(--r-3); + box-shadow: var(--shadow-1); + /* Make the TOC span the full width of its section */ + width: 100%; + margin: var(--space-5) 0; + padding: var(--space-5); + max-width: none; +} + +.courseToc .tocModules, +.courseToc .tocLessons { + list-style: none; + margin: 0; + padding: 0; +} + +.courseToc .tocModule + .tocModule { + margin-top: var(--space-5); + padding-top: var(--space-5); + border-top: 2px dashed var(--border-subtle); +} + +.courseToc .tocModuleHeader { + display: flex; + align-items: center; + gap: var(--space-3); + color: var(--text); + font-weight: 600; + letter-spacing: .2px; + margin-bottom: var(--space-3); +} + +.courseToc .tocModuleIndex { + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border-radius: 999px; + background: var(--primary); + color: var(--primary-contrast); + font-size: 14px; + font-weight: 700; + box-shadow: 0 0 0 3px rgba(78,158,214,0.15); +} + +.courseToc .tocModuleTitle { + font-size: 18px; +} + +.courseToc .tocLessons { + display: grid; + grid-template-columns: 1fr; + gap: 6px; +} + +.courseToc .tocLesson { + position: relative; +} + +.courseToc .tocLink { + display: block; + padding: 10px 12px; + border-radius: var(--r-2); + text-decoration: none; + color: var(--link); + background: rgba(255,255,255,0.02); + border: 1px solid transparent; + transition: all var(--transition-1); +} + +.courseToc .tocLink:hover { + color: var(--link-hover); + background: rgba(78,158,214,0.08); + border-color: var(--border-subtle); +} + +.courseToc .tocLesson.current .tocLink { + color: var(--success); + background: rgba(47,168,108,0.12); + border-color: rgba(47,168,108,0.35); + font-weight: 700; +} + +.courseToc .tocCurrentTag { + color: var(--text-muted); + font-weight: 600; + margin-left: 8px; + font-size: 12px; +} [data-theme='light'] { /* Palette: plus nuancé, moins "blanc" */ --bg: #eef3f7; /* fond légèrement teinté bleu-gris */ @@ -719,7 +816,227 @@ pre { /* Reduced motion */ @media (prefers-reduced-motion: reduce) { - * { transition: none !important; animation: none !important; } + * { transition: none !important; animation: none !important; } +} + +/* --------------------------------------------- + Container Global + --------------------------------------------- */ +.lessonComments { + margin-top: var(--space-6); + background: var(--surface); /* Assure-toi que c'est une couleur un peu plus claire que le fond de page */ + border: 1px solid var(--border-subtle); + border-radius: var(--r-3); + /* On enlève le padding global pour coller les éléments aux bords si besoin, + ou on le garde pour un effet "carte" */ + padding: var(--space-5); + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); +} + +.lessonComments h3 { + font-size: 1.1rem; + font-weight: 700; + color: var(--text); + margin-bottom: var(--space-4); + display: flex; + align-items: center; + gap: 10px; +} + +/* --------------------------------------------- + Liste des commentaires + --------------------------------------------- */ +.commentsList { + list-style: none; + padding: 0; + margin: 0 0 var(--space-6) 0; + display: flex; + flex-direction: column; + gap: var(--space-4); /* Espace entre les commentaires */ +} + +.commentItem { + background: var(--neutral-100); /* Un fond très léger pour détacher le commentaire */ + border: 1px solid var(--border-subtle); + border-radius: var(--r-2); + padding: var(--space-4); + transition: border-color 0.2s ease; +} + +.commentItem:hover { + border-color: var(--border-strong); +} + +/* En-tête du commentaire (Auteur + Date) */ +.commentHeader { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: var(--space-3); + padding-bottom: var(--space-3); + border-bottom: 1px solid var(--border-subtle); +} + +.commentAuthor { + font-weight: 700; + color: var(--primary); /* Couleur primaire pour l'auteur */ + font-size: 0.95rem; + display: flex; + align-items: center; + gap: 8px; +} + +/* Petit point décoratif avant le nom */ +.commentAuthor::before { + content: ''; + display: inline-block; + width: 8px; + height: 8px; + background-color: var(--primary); + border-radius: 50%; + opacity: 0.6; +} + +.commentDate { + font-size: 0.8rem; + color: var(--text-muted); +} + +/* Contenu du texte */ +.commentContent { + color: var(--text); + font-size: 0.95rem; + line-height: 1.6; +} + +/* Markdown Styles dans les commentaires */ +.commentContent p { margin-bottom: 0.8em; } +.commentContent p:last-child { margin-bottom: 0; } + +.commentContent code { + font-family: 'Fira Code', monospace; + background: rgba(124, 58, 237, 0.1); /* Petite teinte violette légère */ + color: var(--primary); + padding: 2px 5px; + border-radius: 4px; + font-size: 0.9em; +} + +.commentContent pre { + background: #1e1e2e; /* Fond sombre style IDE */ + color: #cdd6f4; + padding: 15px; + border-radius: 8px; + overflow-x: auto; + margin: 10px 0; + border: 1px solid rgba(255,255,255,0.05); +} + +.commentContent a { + color: var(--primary); + text-decoration: none; + border-bottom: 1px dotted var(--primary); +} +.commentContent a:hover { border-bottom-style: solid; } + +/* --------------------------------------------- + Formulaire "Éditeur Riche" + --------------------------------------------- */ +.commentFormBlock { + margin-top: var(--space-4); +} + +.commentForm { + background: var(--surface); + border: 1px solid var(--border-strong); /* Bordure plus visible */ + border-radius: var(--r-2); + overflow: hidden; /* Pour que les enfants ne dépassent pas des coins */ + transition: box-shadow 0.2s; +} + +.commentForm:focus-within { + box-shadow: 0 0 0 2px var(--primary-focus, rgba(124, 58, 237, 0.3)); + border-color: var(--primary); +} + +/* La barre d'outils collée au textarea */ +.commentToolbar { + background: var(--neutral-200); /* Fond gris clair/sombre pour la barre */ + padding: 8px 12px; + border-bottom: 1px solid var(--border-subtle); + display: flex; + gap: 8px; +} + +.commentToolbar .btnTool { + background: transparent; + border: none; + color: var(--text-muted); + cursor: pointer; + padding: 6px; + border-radius: 4px; + transition: all 0.2s; + display: flex; + align-items: center; + justify-content: center; +} + +.commentToolbar .btnTool:hover { + background: rgba(0,0,0,0.05); /* Ou blanc semi-transparent en dark mode */ + color: var(--primary); +} + +/* Le champ texte */ +.commentField { + padding: 0; /* On enlève le padding du container */ +} + +.commentField textarea { + width: 100%; /* Prend toute la largeur */ + border: none; /* Pas de bordure, c'est le container .commentForm qui gère */ + background: transparent; + color: var(--text); + padding: 15px; + min-height: 120px; + resize: vertical; + font-family: inherit; + outline: none; /* Le focus est géré par le parent */ + display: block; /* Évite les espaces fantômes */ + line-height: 1.5; +} + +/* Footer du formulaire (Aide + Bouton) */ +.formActions { + padding: 10px 15px; + background: var(--neutral-100); /* Pied de formulaire légèrement différent */ + border-top: 1px solid var(--border-subtle); + display: flex; + justify-content: space-between; /* Aide à gauche, Bouton à droite */ + align-items: center; +} + +.commentHelp { + margin: 0; + font-size: 0.75rem; + color: var(--text-muted); + font-style: italic; +} + +.lessonComments .btn { + background: var(--primary); + color: #fff; + border: none; + padding: 8px 20px; + border-radius: 6px; + font-weight: 600; + cursor: pointer; + transition: background 0.2s; + font-size: 0.9rem; +} + +.lessonComments .btn:hover { + background: var(--bg, #6d28d9); + transform: translateY(-1px); } /* Responsive */ @@ -1345,7 +1662,7 @@ footer { background-color: var(--card); color: var(--text-muted); } form { display: flex; flex-direction: column; - width: 50%; + width: 90%; margin: 20px auto; } @@ -1360,7 +1677,7 @@ form { border-radius: 10px; /* Bordure arrondie */ box-shadow: var(--shadow-1); /* Ombre légère */ margin: 20px auto; - width: 50%; + width: min(680px, 92%); } .login-form { @@ -1425,6 +1742,82 @@ input[type="text"], input[type="email"], input[type="password"], textarea { color: var(--fg); } +/* + Système de formulaires générique — pour un rendu cohérent avec le site + À appliquer avec class="form" sur
(fonctionne aussi avec {{ form.as_p }}) +*/ + +.form { + width: 100%; +} + +.form p { /* Django {{ form.as_p }} */ + display: flex; + flex-direction: column; + gap: 6px; + margin: 0 0 14px 0; +} + +.form label { + font-weight: 600; + color: var(--text); +} + +.form input[type="text"], +.form input[type="email"], +.form input[type="password"], +.form input[type="url"], +.form input[type="number"], +.form input[type="file"], +.form input[type="search"], +.form input[type="tel"], +.form select, +.form textarea { + width: 100%; + padding: 10px 12px; + border: 1px solid var(--border); + border-radius: var(--r-2); + background-color: var(--surface); + color: var(--text); + box-shadow: inset var(--shadow-1); + transition: border-color var(--transition-1), box-shadow var(--transition-1), background-color var(--transition-1); +} + +.form input:focus, +.form select:focus, +.form textarea:focus { + outline: none; + box-shadow: var(--focus-ring); + border-color: var(--primary); +} + +.form .helptext, +.form small.helptext { + color: var(--text-muted); + font-size: 0.9rem; +} + +/* Erreurs Django: ul.errorlist > li */ +.form ul.errorlist { + list-style: none; + margin: 0 0 6px 0; + padding: 8px 10px; + border: 1px solid color-mix(in oklab, var(--danger) 60%, var(--border)); + background: color-mix(in oklab, var(--danger) 12%, var(--surface)); + color: var(--danger); + border-radius: var(--r-2); +} + +.form ul.errorlist li { margin: 0; } + +.form .actions, +.form .form-actions { + display: flex; + gap: 8px; + justify-content: flex-end; + margin-top: 12px; +} + /* Système de boutons — harmonisé avec le thème Utilise les tokens de couleurs et de rayons définis en haut de fichier. diff --git a/templates/courses/lesson.html b/templates/courses/lesson.html new file mode 100644 index 0000000..1b98dd1 --- /dev/null +++ b/templates/courses/lesson.html @@ -0,0 +1,8 @@ +{% extends 'layout.html' %} + +{% block content %} +
+ {% include 'courses/partials/_course_header.html' %} + {% include 'courses/partials/_course_toc.html' %} +
+{% endblock %} diff --git a/templates/courses/list.html b/templates/courses/list.html index 87396f4..ec5b92f 100644 --- a/templates/courses/list.html +++ b/templates/courses/list.html @@ -11,7 +11,7 @@ Image des cours sur {{ course.name }}
-

{{ course.name }}

+

{{ course.name }}

{{ course.content|truncatechars_html:250 }}
diff --git a/templates/courses/partials/_course_header.html b/templates/courses/partials/_course_header.html new file mode 100644 index 0000000..bff069f --- /dev/null +++ b/templates/courses/partials/_course_header.html @@ -0,0 +1,6 @@ +

{{ course.name }}

+

+ Profile Picture + Un cours proposé par {{ course.author }} +

+

{{ course.content }}

diff --git a/templates/courses/partials/_course_toc.html b/templates/courses/partials/_course_toc.html new file mode 100644 index 0000000..3feeea3 --- /dev/null +++ b/templates/courses/partials/_course_toc.html @@ -0,0 +1,216 @@ +{% load comment_format %} +
diff --git a/templates/courses/partials/list.html b/templates/courses/partials/list.html index 851a8b7..12a8853 100644 --- a/templates/courses/partials/list.html +++ b/templates/courses/partials/list.html @@ -8,7 +8,7 @@ Image des cours sur {{ course.name }}
-

{{ course.name }}

+

{{ course.name }}

{{ course.content|truncatechars_html:250 }}
diff --git a/templates/courses/show.html b/templates/courses/show.html index a8b0719..08f20ae 100644 --- a/templates/courses/show.html +++ b/templates/courses/show.html @@ -2,39 +2,8 @@ {% block content %}
-

{{ course.name }}

-

Profile PictureUn cours proposé par {{ course.author }}

-

{{ course.content }}

- - {% for lesson in lessons %} -

{{ lesson.name }}

- {{ lesson.content|safe }} - {% endfor %} -

Commentaires

-
- - + {% include 'courses/partials/_course_header.html' %} + {% include 'courses/partials/_course_toc.html' %} +

Sélectionnez une leçon dans le sommaire pour commencer.

{% endblock %} \ No newline at end of file diff --git a/templates/home.html b/templates/home.html index 4701afe..090f14d 100644 --- a/templates/home.html +++ b/templates/home.html @@ -114,7 +114,7 @@

Cours en vedette

-
+ {% csrf_token %} {{ profile_form.as_p }} -
+
Annuler
diff --git a/templates/users/register.html b/templates/users/register.html index 4417cf1..be887ad 100644 --- a/templates/users/register.html +++ b/templates/users/register.html @@ -2,10 +2,12 @@ {% block content %}

Register

- + {% csrf_token %} {{ form.as_p }} - +
+ +
{% endblock %} \ No newline at end of file