Add comment system with models, forms, and UI integration for lessons

This commit is contained in:
mrtoine 2025-12-10 22:22:17 +01:00
parent c22622ebc1
commit 95111240bc
26 changed files with 1001 additions and 77 deletions

View file

@ -0,0 +1,8 @@
{% extends 'layout.html' %}
{% block content %}
<section>
{% include 'courses/partials/_course_header.html' %}
{% include 'courses/partials/_course_toc.html' %}
</section>
{% endblock %}

View file

@ -11,7 +11,7 @@
<img class="thumbnails" src="{{ course.thumbnail.url }}" alt="Image des cours sur {{ course.name }}" class="src">
</div>
<div class="card-body">
<h2><a href="{% url 'courses:show' course.id course.name|slugify %}">{{ course.name }}</a></h2>
<h2><a href="{% url 'courses:show' course.name|slugify course.id %}">{{ course.name }}</a></h2>
{{ course.content|truncatechars_html:250 }}
</div>
</div>

View file

@ -0,0 +1,6 @@
<h1>{{ course.name }}</h1>
<p class="def-author">
<img src="/{{ course.author.profile.avatar }}" alt="Profile Picture" class="profile-picture-mini">
Un cours proposé par&nbsp;<a href="{% url 'another_profile' course.author.id %}">{{ course.author }}</a>
</p>
<p>{{ course.content }}</p>

View file

@ -0,0 +1,216 @@
{% load comment_format %}
<nav class="courseToc" aria-label="Sommaire du cours">
{% regroup lessons by module as modules %}
<ol class="tocModules">
{% for group in modules %}
<li class="tocModule">
<div class="tocModuleHeader">
<span class="tocModuleIndex">{{ forloop.counter }}</span>
<span class="tocModuleTitle">{{ group.grouper.name }}</span>
</div>
<ol class="tocLessons">
{% for item in group.list %}
<li class="tocLesson{% if lesson and lesson.id == item.id %} current{% endif %}">
<a class="tocLink" href="{% url 'courses:lesson_detail' course.slug item.module.slug item.slug %}">
{{ item.name }}
{% if lesson and lesson.id == item.id %}<span class="tocCurrentTag">(cours actuel)</span>{% endif %}
</a>
{% if lesson and lesson.id == item.id %}
<div class="lessonInline">
<article class="lesson">
{% if lesson.video_id %}
VIDEO {{ lesson.video_id }}<br />
{% endif %}
{{ lesson.content|safe }}
</article>
<h3 id="comments">Commentaires</h3>
<div class="lessonComments">
{% if comments %}
<ol class="commentsList">
{% for c in comments %}
<li class="commentItem" id="comment-{{ c.id }}">
<div class="commentHeader">
<span class="commentAuthor">{{ c.user.username }}</span>
<time class="commentDate" datetime="{{ c.created_at|date:'c' }}">{{ c.created_at|date:'d/m/Y H:i' }}</time>
</div>
<div class="commentContent">{{ c.content|comment_markdown }}</div>
{# Replies #}
{% if c.replies.all %}
{% with replies=c.replies.all %}
{% if replies %}
<ol class="commentsList" style="margin-top: var(--space-4); margin-left: var(--space-6);">
{% for r in replies %}
<li class="commentItem" id="comment-{{ r.id }}">
<div class="commentHeader">
<span class="commentAuthor">{{ r.user.username }}</span>
<time class="commentDate" datetime="{{ r.created_at|date:'c' }}">{{ r.created_at|date:'d/m/Y H:i' }}</time>
</div>
<div class="commentContent">{{ r.content|comment_markdown }}</div>
</li>
{% endfor %}
</ol>
{% endif %}
{% endwith %}
{% endif %}
{# Reply toggle + form #}
{% if user.is_authenticated %}
<button type="button" class="btn btnReply" aria-expanded="false" style="margin-top: var(--space-3);">Répondre</button>
<div class="commentFormBlock" style="display:none; margin-top: var(--space-3);">
<form method="post" action="{% url 'courses:lesson_detail' course.slug module.slug lesson.slug %}#comments" class="commentForm">
{% csrf_token %}
<input type="hidden" name="parent" value="{{ c.id }}" />
<div class="commentToolbar" role="toolbar" aria-label="Outils de mise en forme des réponses">
<button class="btnTool" type="button" data-wrap="**|**" title="Gras (Ctrl/Cmd+B)"><i class="fa-solid fa-bold"></i></button>
<button class="btnTool" type="button" data-wrap="*|*" title="Italique (Ctrl/Cmd+I)"><i class="fa-solid fa-italic"></i></button>
<button class="btnTool" type="button" data-wrap="`|`" title="Code inline"><i class="fa-solid fa-code"></i></button>
<button class="btnTool" type="button" data-insert="[texte](https://exemple.com)" title="Lien"><i class="fa-solid fa-link"></i></button>
<button class="btnTool" type="button" data-prefix="- " title="Liste à puces"><i class="fa-solid fa-list-ul"></i></button>
<button class="btnTool" type="button" data-fence="```
|\n```" title="Bloc de code"><i class="fa-solid fa-square-code"></i></button>
</div>
<div class="commentField">
<textarea name="content" rows="2" placeholder="Répondre à ce commentaire…"></textarea>
</div>
<div class="formActions">
<button type="submit" class="btn">Publier</button>
</div>
</form>
</div>
{% else %}
<p>
<a href="{% url 'login' %}?next={% url 'courses:lesson_detail' course.slug module.slug lesson.slug %}#comments">Se connecter pour répondre</a>
</p>
{% endif %}
</li>
{% endfor %}
</ol>
{% else %}
<p class="noComments">Aucun commentaire pour le moment.</p>
{% endif %}
<div class="commentFormBlock">
{% if user.is_authenticated %}
<form method="post" action="{% url 'courses:lesson_detail' course.slug module.slug lesson.slug %}#comments" class="commentForm">
{% csrf_token %}
<div class="commentToolbar" role="toolbar" aria-label="Outils de mise en forme des commentaires">
<button class="btnTool" type="button" data-wrap="**|**" title="Gras (Ctrl/Cmd+B)"><i class="fa-solid fa-bold"></i></button>
<button class="btnTool" type="button" data-wrap="*|*" title="Italique (Ctrl/Cmd+I)"><i class="fa-solid fa-italic"></i></button>
<button class="btnTool" type="button" data-wrap="`|`" title="Code inline"><i class="fa-solid fa-code"></i></button>
<button class="btnTool" type="button" data-insert="[texte](https://exemple.com)" title="Lien"><i class="fa-solid fa-link"></i></button>
<button class="btnTool" type="button" data-prefix="- " title="Liste à puces"><i class="fa-solid fa-list-ul"></i></button>
<button class="btnTool" type="button" data-fence="```\n|\n```" title="Bloc de code"><i class="fa-solid fa-square-code"></i></button>
</div>
<div class="commentField">
{{ comment_form.content }}
</div>
<p class="commentHelp">
Astuce: vous pouvez utiliser une mise en forme simple — **gras**, *italique*, `code`, listes (- item) et blocs de code avec ```.
</p>
<div class="formActions">
<button type="submit" class="btn">Publier</button>
</div>
</form>
{% else %}
<p>Vous devez être connecté pour publier un commentaire.
<a href="{% url 'login' %}?next={% url 'courses:lesson_detail' course.slug module.slug lesson.slug %}#comments">Se connecter</a>
</p>
{% endif %}
</div>
<script>
(function(){
const getTextareaFromButton = (btn) => {
const form = btn.closest('.commentForm');
if (!form) return document.getElementById('id_content');
return form.querySelector('textarea') || document.getElementById('id_content');
};
const wrap = (area, before, after) => {
if (!area) return;
const start = area.selectionStart || 0;
const end = area.selectionEnd || 0;
const sel = area.value.substring(start, end) || '';
const bef = area.value.substring(0, start);
const aft = area.value.substring(end);
area.value = bef + before + sel + (after ?? '') + aft;
const pos = (bef + before + sel).length;
area.focus();
area.setSelectionRange(pos, pos);
};
const addPrefix = (area, prefix) => {
if (!area) return;
const start = area.selectionStart || 0;
const end = area.selectionEnd || 0;
const text = area.value;
const sel = text.substring(start, end) || '';
const lines = sel.split('\n');
const out = lines.map(l => l ? (prefix + l) : l).join('\n');
const bef = text.substring(0, start);
const aft = text.substring(end);
area.value = bef + out + aft;
const pos = (bef + out).length;
area.focus();
area.setSelectionRange(pos, pos);
};
document.addEventListener('click', function(e){
const b = e.target.closest('.btnTool');
if (b) {
e.preventDefault();
const area = getTextareaFromButton(b);
const wrapSpec = b.getAttribute('data-wrap');
const insertSpec = b.getAttribute('data-insert');
const prefix = b.getAttribute('data-prefix');
const fence = b.getAttribute('data-fence');
if (wrapSpec) {
const [before, after] = wrapSpec.split('|');
wrap(area, before, after);
} else if (insertSpec) {
wrap(area, insertSpec, '');
} else if (prefix) {
addPrefix(area, prefix);
} else if (fence) {
const [before, after] = fence.split('|');
wrap(area, before, after);
}
return;
}
// Toggle reply form when clicking "Répondre"
const replyBtn = e.target.closest('.btnReply');
if (replyBtn) {
e.preventDefault();
const formBlock = replyBtn.nextElementSibling;
if (!formBlock || !formBlock.classList.contains('commentFormBlock')) return;
const isHidden = formBlock.style.display === 'none' || getComputedStyle(formBlock).display === 'none';
formBlock.style.display = isHidden ? '' : 'none';
replyBtn.setAttribute('aria-expanded', isHidden ? 'true' : 'false');
if (isHidden) {
const ta = formBlock.querySelector('textarea');
if (ta) ta.focus();
}
}
});
// Keyboard shortcuts for any textarea within comment forms
document.addEventListener('keydown', function(e){
const target = e.target;
if (!(target instanceof HTMLElement)) return;
if (!target.closest('.commentForm') || target.tagName !== 'TEXTAREA') return;
if ((e.metaKey || e.ctrlKey) && (e.key === 'b' || e.key === 'i')) {
e.preventDefault();
const isBold = e.key === 'b';
const spec = isBold ? ['**','**'] : ['*','*'];
wrap(target, spec[0], spec[1]);
}
});
})();
</script>
</div>
</div>
{% endif %}
</li>
{% endfor %}
</ol>
</li>
{% endfor %}
</ol>
</nav>

View file

@ -8,7 +8,7 @@
<img class="thumbnails" src="{{ course.thumbnail.url }}" alt="Image des cours sur {{ course.name }}" class="src">
</div>
<div class="card-body">
<h2><a href="{% url 'courses:show' course.id course.name|slugify %}">{{ course.name }}</a></h2>
<h2><a href="{% url 'courses:show' course.name|slugify course.id %}">{{ course.name }}</a></h2>
{{ course.content|truncatechars_html:250 }}
</div>
</div>

View file

@ -2,39 +2,8 @@
{% block content %}
<section>
<h1>{{ course.name }}</h1>
<p class="def-author"><img src="/{{ course.author.profile.avatar }}" alt="Profile Picture" class="profile-picture-mini">Un cours proposé par&nbsp;<a href="{% url 'another_profile' course.author.id %}">{{ course.author }}</a></p>
<p>{{ course.content }}</p>
<aside class="courseNav">
<ol>
{% for lesson in lessons %}
<li><a href="#{{ lesson.name|slugify }}">{{ lesson.name }}</a></li>
{% endfor %}
<li><a href="#comments">Commentaires</a></li>
</ol>
</aside>
{% for lesson in lessons %}
<h2 id="{{ lesson.name|slugify }}">{{ lesson.name }}</h2>
{{ lesson.content|safe }}
{% endfor %}
<h1 id="comments">Commentaires</h1>
<div id="disqus_thread"></div>
<script>
/**
* RECOMMENDED CONFIGURATION VARIABLES: EDIT AND UNCOMMENT THE SECTION BELOW TO INSERT DYNAMIC VALUES FROM YOUR PLATFORM OR CMS.
* LEARN WHY DEFINING THESE VARIABLES IS IMPORTANT: https://disqus.com/admin/universalcode/#configuration-variables */
/**/
var disqus_config = function () {
this.page.url = "{{ request.build_absolute_uri|safe }}"; // Replace PAGE_URL with your page's canonical URL variable
this.page.identifier = "{{ course.id }}"; // Replace PAGE_IDENTIFIER with your page's unique identifier variable
};
(function() { // DON'T EDIT BELOW THIS LINE
var d = document, s = d.createElement('script');
s.src = 'https://partirdezero.disqus.com/embed.js';
s.setAttribute('data-timestamp', +new Date());
(d.head || d.body).appendChild(s);
})();
</script>
<noscript>Please enable JavaScript to view the <a href="https://disqus.com/?ref_noscript">comments powered by Disqus.</a></noscript>
{% include 'courses/partials/_course_header.html' %}
{% include 'courses/partials/_course_toc.html' %}
<p>Sélectionnez une leçon dans le sommaire pour commencer.</p>
</section>
{% endblock %}

View file

@ -114,7 +114,7 @@
<h2>Cours en vedette</h2>
<div class="carousel-track" role="list">
{% for course in courses|slice:":6" %}
<a class="carousel-item card" href="{% url 'courses:show' course.id course.name|slugify %}" role="listitem">
<a class="carousel-item card" href="{% url 'courses:show' course.name|slugify course.id %}" role="listitem">
<div class="ratio-16x9">
<img src="{{ course.thumbnail.url }}" alt="{{ course.name }}" loading="lazy">
</div>

View file

@ -8,7 +8,7 @@
<a href="{% url 'courses:list' %}">Les cours</a>
<ul>
{% for course in courses %}
<li><a href="{% url 'courses:show' course.id course.name|slugify %}">{{ course.name }}</a></li>
<li><a href="{% url 'courses:show' course.name|slugify course.id %}">{{ course.name }}</a></li>
{% endfor %}
</ul>
</li>
@ -32,6 +32,11 @@
{% endif %}
</ul>
</li>
{% if user.is_authenticated and user.is_staff %}
<li>
<a href="{% url 'admin:index' %}" class="button button-danger" title="Administration">Admin</a>
</li>
{% endif %}
<li>
{% if user.is_authenticated %}
<!-- On affiche le pseudo -->

View file

@ -7,10 +7,10 @@
<div class="profile-grid">
<div class="profile-card">
<h3>Paramètres du compte</h3>
<form method="post">
<form method="post" class="form">
{% csrf_token %}
{{ user_form.as_p }}
<div class="text-right" style="margin-top:12px; display:flex; gap:8px; justify-content:flex-end;">
<div class="form-actions">
<a href="{% url 'profile' %}" class="btn btn-secondary">Annuler</a>
<button type="submit" class="btn btn-primary">Enregistrer</button>
</div>

View file

@ -4,10 +4,12 @@
<section class="form-section">
<h2>Complète ton profil</h2>
<p>La nouvelle mise à jour du site web te permet d'avoir un profil personnel. Tu peux remplir les champs suivant pour l'activer.</p>
<form method="post">
<form method="post" class="form">
{% csrf_token %}
{{ form.as_p }}
<button type="submit" class="btn-submit">Save</button>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Enregistrer</button>
</div>
</form>
</section>
{% endblock %}

View file

@ -1,11 +1,13 @@
{% extends 'layout.html' %}
{% block content %}
<section>
<section class="form-section">
<h2>Create Post</h2>
<form method="post">
<form method="post" class="form">
{% csrf_token %}
<!-- Add form fields for post creation here -->
<button type="submit">Create Post</button>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Create Post</button>
</div>
</form>
</section>
{% endblock %}

View file

@ -4,10 +4,12 @@
<h2>Login</h2>
<p>Pas encore inscrit ? <a href="{% url 'register' %}">Inscrivez-vous</a></p>
<p class="login-info">En vous connectant, vous aurez accès à des fonctionnalités exclusives, telles que la gestion de vos cours, la participation aux discussions et la personnalisation de votre profil. Rejoignez notre communauté et profitez pleinement de tout ce que notre site a à offrir.</p>
<form method="post" class="login-form">
<form method="post" class="login-form form">
{% csrf_token %}
{{ form.as_p }}
<button type="submit" class="btn-submit">Login</button>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Login</button>
</div>
</form>
</section>
{% endblock %}

View file

@ -6,10 +6,10 @@
{% endblock %}
<div class="profile-details">
<h2>Mes cours</h2>
<p>Retrouvez ici la liste de tous les cours que vous avez rédigés.</p>
<p>Retrouvez ici la liste de tous les cours que vous suivez.</p>
<ul>
{% for course in user_courses %}
<li><a href="{% url 'courses:show' course.id course.name|slugify %}">{{ course.name }}</a></li>
<li><a href="{% url 'courses:show' course.name|slugify course.id %}">{{ course.name }}</a></li>
{% endfor %}
</ul>
</div>

View file

@ -38,7 +38,7 @@
<a class="btn btn-secondary btn-sm" href="{% url 'user_courses' %}">Voir tous mes cours</a>
</div>
{% else %}
<p class="muted">Aucun cours publié pour le moment.</p>
<p class="muted">Aucun cours suivi pour le moment.</p>
{% endif %}
{% endwith %}
</div>

View file

@ -15,10 +15,10 @@
</div>
</div>
<form method="post" enctype="multipart/form-data">
<form method="post" enctype="multipart/form-data" class="form">
{% csrf_token %}
{{ profile_form.as_p }}
<div class="text-right" style="margin-top:12px; display:flex; gap:8px; justify-content:flex-end;">
<div class="form-actions">
<a href="{% url 'profile' %}" class="btn btn-secondary">Annuler</a>
<button type="submit" class="btn btn-primary">Enregistrer</button>
</div>

View file

@ -2,10 +2,12 @@
{% block content %}
<section class="form-section">
<h2>Register</h2>
<form method="post" class="login-form">
<form method="post" class="login-form form">
{% csrf_token %}
{{ form.as_p }}
<button type="submit" class="btn-submit">Register</button>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Register</button>
</div>
</form>
</section>
{% endblock %}