Add comment system with models, forms, and UI integration for lessons
This commit is contained in:
parent
c22622ebc1
commit
95111240bc
26 changed files with 1001 additions and 77 deletions
216
templates/courses/partials/_course_toc.html
Normal file
216
templates/courses/partials/_course_toc.html
Normal 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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue