First Commit

This commit is contained in:
mrtoine 2025-09-12 11:11:44 +02:00
commit ce0758fbbb
496 changed files with 52062 additions and 0 deletions

0
forum/__init__.py Executable file
View file

33
forum/admin.py Executable file
View file

@ -0,0 +1,33 @@
from django.contrib import admin
from .models import Category, Forum, Topic, Post
class CategoryAdmin(admin.ModelAdmin):
list_display = ('name', 'description', 'created', 'updated')
list_filter = ('created', 'updated')
search_fields = ('name', 'description')
ordering = ('-created',)
fields = ('name', 'description')
class ForumAdmin(admin.ModelAdmin):
list_display = ('name', 'author', 'category', 'created', 'updated')
list_filter = ('category', 'author', 'created', 'updated')
search_fields = ('name', 'description')
ordering = ('-created',)
fields = ('category', 'author', 'name', 'description')
class TopicAdmin(admin.ModelAdmin):
list_display = ('title', 'author', 'created', 'updated')
list_filter = ('forum', 'author', 'created', 'updated', 'state')
search_fields = ('title',)
ordering = ('-created',)
fields = ('forum', 'author', 'title', 'state')
class PostAdmin(admin.ModelAdmin):
list_display = ('type', 'topic', 'author', 'created', 'updated')
list_filter = ('topic', 'author', 'created', 'updated', 'type')
fields = ('topic', 'author', 'content', 'type')
admin.site.register(Category, CategoryAdmin)
admin.site.register(Forum, ForumAdmin)
admin.site.register(Topic, TopicAdmin)
admin.site.register(Post, PostAdmin)

6
forum/apps.py Executable file
View file

@ -0,0 +1,6 @@
from django.apps import AppConfig
class ForumConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "forum"

26
forum/forms.py Executable file
View file

@ -0,0 +1,26 @@
from django import forms
class CreateTopic(forms.Form):
TOPIC_TYPE_CHOICES = (
('normal', 'Normal'),
('announce', 'Annonce'),
)
title = forms.CharField(
max_length=150,
label='',
widget=forms.TextInput(attrs={'placeholder': 'Titre du sujet'})
)
content = forms.CharField(
label='',
widget=forms.Textarea(attrs={'placeholder': 'Contenu du sujet'})
)
type = forms.ChoiceField(choices=TOPIC_TYPE_CHOICES, label='Type de sujet')
class CreatePost(forms.Form):
content = forms.CharField(
label='',
widget=forms.Textarea(attrs={'placeholder': 'Contenu du message'})
)
class EditPost(forms.Form):
content = forms.CharField(widget=forms.Textarea, label="Message")

47
forum/middleware.py Executable file
View file

@ -0,0 +1,47 @@
from django.utils.deprecation import MiddlewareMixin
from .models import Forum, Topic, Post
from users.models import User
from django.db.models import Count
class ForumStatsMiddleware(MiddlewareMixin):
# On récupère les statistiques du forum pour les affiché dans le menu
def process_request(self, request):
# Nombre total de forums
total_forums = Forum.objects.count()
# Nombre total de topics
total_topics = Topic.objects.count()
# Nombre total de posts
total_posts = Post.objects.count()
# Utilisateur ayant posté le plus de messages
user_with_most_posts = User.objects.annotate(num_posts=Count('post')).order_by('-num_posts').first()
# Nombre de messages de l'utilisateur ayant posté le plus de messages
most_posts = user_with_most_posts.num_posts if user_with_most_posts else 0
# Utilisateur ayant créé le plus de topics
user_with_most_topics = None
most_topics = 0
for user in User.objects.all():
if user.topic_set.count() > most_topics:
user_with_most_topics = user
most_topics = user.topic_set.count()
# Dernier message posté
last_post = Post.objects.latest('created') if total_posts > 0 else None
# Derneir topic créé
last_topic = Topic.objects.latest('created') if total_topics > 0 else None
# Ajouter les variables à l'objet request
request.total_forums = total_forums
request.total_topics = total_topics
request.total_posts = total_posts
request.user_with_most_posts = user_with_most_posts
request.most_posts = most_posts
request.user_with_most_topics = user_with_most_topics
request.most_topics = most_topics
request.last_post = last_post
request.last_topic = last_topic

View file

@ -0,0 +1,98 @@
# Generated by Django 4.2.16 on 2024-10-22 11:02
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="Category",
fields=[
("id", models.AutoField(primary_key=True, serialize=False)),
("name", models.CharField(max_length=50)),
("description", models.CharField(max_length=100)),
("created", models.DateTimeField(auto_now_add=True)),
("updated", models.DateTimeField(auto_now=True)),
],
),
migrations.CreateModel(
name="Forum",
fields=[
("id", models.AutoField(primary_key=True, serialize=False)),
("name", models.CharField(max_length=50)),
("description", models.CharField(max_length=100)),
("created", models.DateTimeField(auto_now_add=True)),
("updated", models.DateTimeField(auto_now=True)),
(
"author",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
(
"category",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="forum.category"
),
),
],
),
migrations.CreateModel(
name="Topic",
fields=[
("id", models.AutoField(primary_key=True, serialize=False)),
("title", models.CharField(max_length=50)),
("created", models.DateTimeField(auto_now_add=True)),
("updated", models.DateTimeField(auto_now=True)),
("state", models.CharField(default="open", max_length=10)),
(
"author",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
(
"forum",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="forum.forum"
),
),
],
),
migrations.CreateModel(
name="Post",
fields=[
("id", models.AutoField(primary_key=True, serialize=False)),
("content", models.CharField(max_length=100)),
("created", models.DateTimeField(auto_now_add=True)),
("updated", models.DateTimeField(auto_now=True)),
("type", models.CharField(default="post", max_length=10)),
("active", models.BooleanField(default=True)),
(
"author",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="forum_author",
to=settings.AUTH_USER_MODEL,
),
),
(
"topic",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="forum.topic"
),
),
],
),
]

View file

@ -0,0 +1,51 @@
# Generated by Django 4.2.16 on 2024-10-22 18:36
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("forum", "0001_initial"),
]
operations = [
migrations.AlterModelOptions(
name="category",
options={"verbose_name": "Catégorie", "verbose_name_plural": "Catégories"},
),
migrations.CreateModel(
name="TopicRead",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("last_read", models.DateTimeField(auto_now=True)),
(
"topic",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="forum.topic"
),
),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"unique_together": {("user", "topic")},
},
),
]

View file

@ -0,0 +1,23 @@
# Generated by Django 4.2.16 on 2024-10-22 18:49
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("forum", "0002_alter_category_options_topicread"),
]
operations = [
migrations.AlterField(
model_name="post",
name="topic",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="posts",
to="forum.topic",
),
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 4.2.17 on 2024-12-23 16:48
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('forum', '0003_alter_post_topic'),
]
operations = [
migrations.AlterField(
model_name='post',
name='content',
field=models.TextField(),
),
]

0
forum/migrations/__init__.py Executable file
View file

87
forum/models.py Executable file
View file

@ -0,0 +1,87 @@
from django.db import models
from users.models import User
class Category(models.Model):
id = models.AutoField(primary_key=True)
name = models.CharField(max_length=50)
description = models.CharField(max_length=100)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = 'Catégorie'
verbose_name_plural = 'Catégories'
def __str__(self):
return f'ForumCategory({self.id}, {self.name}, {self.description})'
class Forum(models.Model):
id = models.AutoField(primary_key=True)
category = models.ForeignKey(Category, on_delete=models.CASCADE)
author = models.ForeignKey(User, on_delete=models.CASCADE)
name = models.CharField(max_length=50)
description = models.CharField(max_length=100)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
def __str__(self):
return f'Forum({self.id}, {self.category}, {self.name}, {self.description})'
class Topic(models.Model):
id = models.AutoField(primary_key=True)
forum = models.ForeignKey(Forum, on_delete=models.CASCADE)
author = models.ForeignKey(User, on_delete=models.CASCADE)
title = models.CharField(max_length=50)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
state = models.CharField(max_length=10, default='open')
def save(self, *args, **kwargs):
if not self.id:
self.state = 'open'
super(Topic, self).save(*args, **kwargs)
def __str__(self):
return f'Topic({self.id}, {self.forum}, {self.title})'
def is_unread(self, user):
if not user.is_authenticated:
return False
last_read = TopicRead.objects.filter(
user=user,
topic=self
).first()
if not last_read:
return True
last_post = Post.objects.filter(topic=self).order_by('-created').first()
if not last_post:
return False
return last_post.created > last_read.last_read
class Post(models.Model):
id = models.AutoField(primary_key=True)
topic = models.ForeignKey(Topic, on_delete=models.CASCADE, related_name='posts')
author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='forum_author')
content = models.TextField()
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
type = models.CharField(max_length=10, default='post')
active = models.BooleanField(default=True)
def __str__(self):
return f'Post({self.id}, {self.topic}, {self.author}, {self.content})'
class TopicRead(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
topic = models.ForeignKey(Topic, on_delete=models.CASCADE)
last_read = models.DateTimeField(auto_now=True)
class Meta:
unique_together = ['user', 'topic']
def __str__(self):
return f'TopicRead({self.id}, {self.topic}, {self.user})'

0
forum/templatetags/__init__.py Executable file
View file

View file

@ -0,0 +1,9 @@
from django import template
from commons.bbcode_parser import BBCodeParser
register = template.Library()
@register.filter(name='bbcode')
def bbcode(value):
parser = BBCodeParser()
return parser.parse(value)

View file

@ -0,0 +1,7 @@
from django import template
register = template.Library()
@register.filter
def get_item(dictionary, key):
return dictionary.get(key)

View file

@ -0,0 +1,7 @@
from django import template
register = template.Library()
@register.inclusion_tag('components/paginator.html', takes_context=True)
def paginate(context):
return context

3
forum/tests.py Executable file
View file

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

20
forum/urls.py Executable file
View file

@ -0,0 +1,20 @@
from django.urls import path
from django.conf.urls.static import static
from passion_retro import settings
from . import views
urlpatterns = [
path("", views.forum_home, name="forum_home"),
path("<int:forum_id>/", views.topic_list, name="topic_list"),
path("<int:forum_id>/create_topic/", views.create_topic, name="create_topic"),
path("<int:forum_id>/<int:topic_id>/", views.topic_detail, name="post_list"),
path("<int:topic_id>/lock/", views.lock_topic, name="lock_topic"),
path("<int:topic_id>/unlock/", views.unlock_topic, name="unlock_topic"),
path("<int:forum_id>/<int:topic_id>/deactivate/", views.deactivate_topic, name="deactivate_topic"),
path("<int:forum_id>/<int:topic_id>/activate/", views.activate_topic, name="activate_topic"),
path("<int:post_id>/deactivate/", views.deactivate_post, name="deactivate_post"),
path("<int:post_id>/activate/", views.activate_post, name="activate_post"),
path('post/<int:post_id>/edit/', views.edit_post, name='forum_edit_post'),
]

303
forum/views.py Executable file
View file

@ -0,0 +1,303 @@
from django.core.paginator import Paginator
from django.shortcuts import render, redirect
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.http import HttpResponseForbidden
from django.utils import timezone
from .models import Category, Forum, Topic, Post, TopicRead
from .forms import CreateTopic, CreatePost, EditPost
from django.db.models import Max, F
from users.decorators import groups_required
from users.models import UserLevel
def forum_home(request):
categories = Category.objects.all()
forums = Forum.objects.all()
last_posts = {}
count_topics = {}
unread_topics = {}
for forum in forums:
# Dernier post du forum avec une seule requête
last_post = Post.objects.select_related(
'topic',
'author',
'topic__forum'
).filter(
topic__forum=forum
).order_by('-created').first()
last_posts[forum.id] = last_post
# Nombre de topics (pas de changement)
count_topic = Topic.objects.filter(forum=forum).count()
count_topics[forum.id] = count_topic
# Optimisation de la vérification des topics non lus
if request.user.is_authenticated:
# Récupérer le dernier post de chaque topic du forum en une seule requête
latest_posts_per_topic = Post.objects.filter(
topic__forum=forum
).values('topic_id').annotate(
last_post_date=Max('created')
)
# Récupérer les dernières lectures de l'utilisateur pour tous les topics du forum
topic_reads = TopicRead.objects.filter(
user=request.user,
topic__forum=forum
).values_list('topic_id', 'last_read')
# Convertir en dictionnaire pour un accès plus rapide
read_dates = dict(topic_reads)
# Un forum est non lu si au moins un topic a un post plus récent que la dernière lecture
is_unread = False
for post_info in latest_posts_per_topic:
topic_id = post_info['topic_id']
last_post_date = post_info['last_post_date']
# Si le topic n'a jamais été lu
if topic_id not in read_dates:
is_unread = True
break
# Si le dernier post est plus récent que la dernière lecture
if last_post_date > read_dates[topic_id]:
is_unread = True
break
unread_topics[forum.id] = is_unread
else:
unread_topics[forum.id] = False
print(unread_topics)
context = {
'categories': categories,
'forums': forums,
'last_posts': last_posts,
'count_topics': count_topics,
'unread_topics': unread_topics,
}
return render(request, "components/forum_home.html", context)
def topic_list(request, forum_id):
forum = Forum.objects.get(id=forum_id)
topics = Topic.objects.filter(forum=forum).order_by('-created')
paginator = Paginator(topics, 20)
page_number = request.GET.get('page')
topics = paginator.get_page(page_number)
count_posts = {}
last_posts = {}
page_numbers = {}
unread_topics = {}
for topic in topics:
count_post = Post.objects.filter(topic=topic).count()
count_posts[topic.id] = count_post
last_post = Post.objects.filter(topic=topic).order_by('-created').first()
last_posts[topic.id] = last_post
if request.user.is_authenticated:
# Récupérer le dernier post du topic
latest_post = Post.objects.filter(topic=topic).aggregate(last_post_date=Max('created'))['last_post_date']
# Récupérer la dernière lecture de l'utilisateur pour ce topic
topic_read = TopicRead.objects.filter(user=request.user, topic=topic).first()
# Un topic est non lu si le dernier post est plus récent que la dernière lecture
if topic_read:
unread_topics[topic.id] = latest_post > topic_read.last_read
else:
unread_topics[topic.id] = True
else:
unread_topics[topic.id] = False
if last_post:
# Position du dernier message (nombre de messages jusqu'à ce message)
post_position = Post.objects.filter(
topic=topic,
created__lte=last_post.created
).count()
# Calcul du numéro de page (20 messages par page comme dans topic_detail)
page_number = (post_position + 19) // 20 # équivalent à ceil(post_position/20)
page_numbers[topic.id] = page_number
context = {
'forum': forum,
'topics': topics,
'count_posts': count_posts,
'last_posts': last_posts,
'page_numbers': page_numbers,
'unread_topics': unread_topics,
'is_paginated': topics.has_other_pages(),
'page_obj': topics,
'paginator': paginator,
}
return render(request, "forum/topic_list.html", context)
@login_required
def create_topic(request, forum_id):
if not request.user.is_active:
return HttpResponseForbidden("Vous n'êtes pas autorisé à créer un sujet.")
if request.method == 'POST':
topic_form = request.POST
# On créer le topic via le model Topic
topic = Topic(
forum=Forum.objects.get(id=forum_id),
author=request.user,
title=topic_form['title'],
)
# Puis le premier post du topic
post = Post(
topic=topic,
author=request.user,
content=topic_form['content'],
)
topic.save()
post.save()
messages.success(request, 'Topic créer avec succès.')
UserLevel.objects.update(user=request.user, experience=F('experience') + 15)
# on renvoie l'utilisateur sur la page du topic contenu l'id du forum et du topic créer
return redirect('post_list', forum_id=forum_id, topic_id=topic.id)
forum = Forum.objects.get(id=forum_id)
topic_form = CreateTopic()
context = {
'forum': forum,
'topic_form': topic_form,
}
return render(request, "forum/create_topic.html", context)
def topic_detail(request, topic_id, forum_id):
if request.method == 'POST':
if not request.user.is_active or not request.user.is_authenticated:
return HttpResponseForbidden("Vous n'êtes pas autorisé à poster un message.")
post_form = CreatePost(request.POST)
if post_form.is_valid():
post = Post(
topic=Topic.objects.get(id=topic_id),
author=request.user,
content=post_form.cleaned_data['content'],
)
post.save()
UserLevel.objects.update(user=request.user, experience=F('experience') + 10)
messages.success(request, 'Message posté avec succès.')
topic = Topic.objects.get(id=topic_id)
posts = Post.objects.filter(topic=topic, active=True)
paginator = Paginator(posts, 20)
page_number = request.GET.get('page')
posts = paginator.get_page(page_number)
# Marquer comme lu si l'utilisateur est connecté
if request.user.is_authenticated:
TopicRead.objects.update_or_create(
user=request.user,
topic=topic,
defaults={'last_read': timezone.now()}
)
# On compte le nombre de posts de l'auteur
count_posts = {}
for post in posts:
count_post = Post.objects.filter(author=post.author).count()
count_posts[post.author.id] = count_post
context = {
'topic': topic,
'posts': posts,
'count_posts': count_posts,
'post_form': CreatePost(),
'is_paginated': posts.has_other_pages(),
'page_obj': posts,
'paginator': paginator,
}
return render(request, "forum/topic_detail.html", context)
@groups_required('Modérateur', 'Admininistrateur', 'Super Admin')
def lock_topic(request, topic_id):
topic = Topic.objects.get(id=topic_id)
topic.state = 'closed'
topic.save()
messages.success(request, 'Sujet verrouillé avec succès.')
return redirect('post_list', forum_id=topic.forum.id, topic_id=topic_id)
@groups_required('Modérateur', 'Admininistrateur', 'Super Admin')
def unlock_topic(request, topic_id):
topic = Topic.objects.get(id=topic_id)
topic.state = 'open'
topic.save()
messages.success(request, 'Sujet déverrouillé avec succès.')
return redirect('post_list', forum_id=topic.forum.id, topic_id=topic_id)
@groups_required('Modérateur', 'Admininistrateur', 'Super Admin')
def deactivate_post(request, post_id):
post = Post.objects.get(id=post_id)
post.active = False
post.save()
messages.success(request, 'Message désactivé avec succès.')
return redirect('post_list', forum_id=post.topic.forum.id, topic_id=post.topic.id)
@groups_required('Modérateur', 'Admininistrateur', 'Super Admin')
def activate_post(request, post_id):
post = Post.objects.get(id=post_id)
post.active = True
post.save()
messages.success(request, 'Message activé avec succès.')
return redirect('post_list', forum_id=post.topic.forum.id, topic_id=post.topic.id)
@groups_required('Modérateur', 'Admininistrateur', 'Super Admin')
def deactivate_topic(request, topic_id):
topic = Topic.objects.get(id=topic_id)
topic.active = False
topic.save()
messages.success(request, 'Sujet désactivé avec succès.')
return redirect('topic_list', forum_id=topic.forum.id)
@groups_required('Modérateur', 'Admininistrateur', 'Super Admin')
def activate_topic(request, topic_id):
topic = Topic.objects.get(id=topic_id)
topic.active = True
topic.save()
messages.success(request, 'Sujet activé avec succès.')
return redirect('topic_list', forum_id=topic.forum.id)
@login_required
def edit_post(request, post_id):
post = Post.objects.get(id=post_id)
# Vérifier si l'utilisateur a le droit d'éditer
if not (request.user == post.author or request.user.groups.filter(name__in=['Administrateur', 'Super Admin']).exists()):
return HttpResponseForbidden("Vous n'êtes pas autorisé à éditer ce message.")
if request.method == 'POST':
form = EditPost(request.POST)
if form.is_valid():
post.content = form.cleaned_data['content']
post.updated = timezone.now()
post.save()
messages.success(request, 'Message modifié avec succès.')
return redirect('post_list', forum_id=post.topic.forum.id, topic_id=post.topic.id)
else:
form = EditPost(initial={'content': post.content})
context = {
'form': form,
'post': post
}
return render(request, "forum/edit_post.html", context)