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
8
templates/courses/lesson.html
Normal file
8
templates/courses/lesson.html
Normal 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 %}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
6
templates/courses/partials/_course_header.html
Normal file
6
templates/courses/partials/_course_header.html
Normal 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 <a href="{% url 'another_profile' course.author.id %}">{{ course.author }}</a>
|
||||
</p>
|
||||
<p>{{ course.content }}</p>
|
||||
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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 <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 %}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 -->
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
@ -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 %}
|
||||
|
|
@ -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 %}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
Loading…
Add table
Add a link
Reference in a new issue