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

8
.gitignore vendored Normal file
View file

@ -0,0 +1,8 @@
.venv
.env
.DS_Store
venv
staticfiles/**/*
media/**/*
*.sqlite3
**/__pycache__/*

82
commons/bbcode_parser.py Normal file
View file

@ -0,0 +1,82 @@
import re
class BBCodeParser:
def __init__(self):
# On défini ici les balises BBcode et leur équivalent en html
self.bbcode_patterns = {
r'\[b\](.*?)\[/b\]': r'<strong>\1</strong>',
r'\[i\](.*?)\[/i\]': r'<i>\1</i>',
r'\[u\](.*?)\[/u\]': r'<u>\1</u>',
r'\[s\](.*?)\[/s\]': r'<strike>\1</strike>',
r'\[url=(.*?)\](.*?)\[/url\]': r'<a href="\1">\2</a>',
r'\[url\](.*?)\[/url\]': r'<a href="\1">\1</a>',
r'\[url=(.*?)(?:\s+class=(.*?))?\](.*?)\[/url\]': lambda m: f'<a href="{m.group(1)}"{f" class={m.group(2)}" if m.group(2) else ""}>{{m.group(3)}}</a>',
r'\[url\](?:\s+class=(.*?))?\](.*?)\[/url\]': lambda m: f'<a href="{m.group(2)}"{f" class={m.group(1)}" if m.group(1) else ""}>{{m.group(2)}}</a>',
r'\[img alt=(.*?)\](.*?)\[/img\]': r'<img src="\2" alt="\1">',
r'\[img\](.*?)\[/img\]': r'<img src="\1" alt="Image insérer par un utilisateur">',
r'\[list\](.*?)\[/list\]': r'<ul class="bbcode-list">\1</ul>',
r'\[\*\](.*?)': r'<li class="bbcode-list">\1</li>',
r'\[t1\](.*?)\[/t1\]': r'<span style="font-size:2rem;font-weight:800;">\1</span>',
r'\[t2\](.*?)\[/t2\]': r'<span style="font-size:1.6rem;font-weight:600;">\1</span>',
r'\[t3\](.*?)\[/t3\]': r'<span style="font-size:1.4rem;font-weight:400;">\1</span>',
r'\[citation\](.*?)\[/citation\]': r'<fieldset class="quote-bbcode">\1</fieldset>',
r'\[citation=(.*?)\](.*?)\[/citation\]': r'<fieldset class="quote-bbcode"><legend>\1</legend>\2</fieldset>',
r'\[quote\](.*?)\[/quote\]': r'<fieldset class="quote-bbcode">\1</fieldset>',
r'\[quote=(.*?)\](.*?)\[/quote\]': r'<fieldset class="quote-bbcode"><legend>\1</legend>\2</fieldset>',
r'\[color=(.*?)\](.*?)\[/color\]': r'<span style="color: \1;">\2</span>',
r'\[size=(.*?)\](.*?)\[/size\]': r'<span style="font-size: \1px;">\2</span>',
r'\[p](.*?)\[/p\]': r'<p>\1</p>',
r'\[center\](.*?)\[/center\]': r'<div style="text-align:center;">\1</div>',
r'\[right\](.*?)\[/right\]': r'<div style="text-align:right;">\1</div>',
r'\[hr\]': r'<hr>',
r'\[block\](.*?)\[/block\]': r'<div>\1</div>',
r'\[block style=(.*?)\](.*?)\[/block\]': r'<div style="\1">\2</div>',
r'\[block class=(.*?)\](.*?)\[/block\]': r'<div class="\1">\2</div>',
r'\[block style=(.*?) class=(.*?)\](.*?)\[/block\]': r'<div style="\1" class="\2">\3</div>',
r'\[code\](.*?)\[/code\]': lambda m: self._handle_code(m.group(1)),
r'\[code=(.*?)\](.*?)\[/code\]': lambda m: self._handle_code(m.group(2), m.group(1)),
}
def _handle_code(self, content, language='none'):
# Convertit les retours à la ligne en \n littéraux
escaped_content = (
content
.replace('&', '&amp;')
.replace('<', '&lt;')
.replace('>', '&gt;')
.replace('"', '&quot;')
.replace("'", '&#39;')
.replace('\r\n', '\n') # Normalise les retours à la ligne Windows
.replace('\r', '\n') # Normalise les retours à la ligne Mac
)
# Enveloppe dans pre/code sans modifier les \n
return f'''<pre class="language-{language}"><code class="language-{language}">{escaped_content}</code></pre>'''
def parse(self, text):
# Protège temporairement le contenu des balises [code]
code_blocks = []
def save_code(match):
code_blocks.append(match.group(0))
return f'@@CODE_BLOCK_{len(code_blocks)-1}@@'
text = re.sub(r'\[code(?:=.*?)?\].*?\[/code\]', save_code, text, flags=re.DOTALL)
# Parse les autres BBCodes
for bbcode, html in self.bbcode_patterns.items():
if callable(html):
text = re.sub(bbcode, html, text, flags=re.DOTALL)
else:
text = re.sub(bbcode, html, text, flags=re.DOTALL)
# Restaure les blocs de code
for i, code_block in enumerate(code_blocks):
if '=' in code_block: # code avec langage spécifié
lang = re.match(r'\[code=(.*?)\]', code_block).group(1)
content = re.search(r'\[code=.*?\](.*?)\[/code\]', code_block, re.DOTALL).group(1)
else: # code sans langage
lang = 'none'
content = re.search(r'\[code\](.*?)\[/code\]', code_block, re.DOTALL).group(1)
text = text.replace(f'@@CODE_BLOCK_{i}@@', self._handle_code(content, lang))
return text

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)

0
gallery/__init__.py Normal file
View file

3
gallery/admin.py Normal file
View file

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

6
gallery/apps.py Normal file
View file

@ -0,0 +1,6 @@
from django.apps import AppConfig
class GalleryConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'gallery'

17
gallery/forms.py Normal file
View file

@ -0,0 +1,17 @@
from django import forms
class AddImgGallery(forms.Form):
image = forms.FileField(
label='Image',
required=True,
widget=forms.ClearableFileInput(attrs={'class': 'form-inline'})
)
def clean_img(self):
img = self.cleaned_data.get('image')
if img:
if not img.name.lower().endswith(('.jpg', '.jpeg', '.png')):
raise forms.ValidationError('Seul les fichiers JPG, JPEG, PNG sont autorisés.')
if img.size > 5 * 1024 * 1024:
raise forms.ValidationError('La taille de l\'image ne dois pas dépasser 5 Mo.')
return img

View file

3
gallery/models.py Normal file
View file

@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

3
gallery/tests.py Normal file
View file

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

10
gallery/urls.py Executable file
View file

@ -0,0 +1,10 @@
from django.urls import path
from django.conf.urls.static import static
from passion_retro import settings
from . import views
urlpatterns = [
path("", views.home_gallery, name="home_gallery"),
path("import", views.import_img, name="import"),
]

52
gallery/views.py Normal file
View file

@ -0,0 +1,52 @@
from django.shortcuts import render
from django.contrib import messages
from .forms import AddImgGallery
from django.contrib.auth.decorators import login_required
import os
from django.conf import settings
@login_required()
def home_gallery(request):
user_id = request.user.id
user_directory = os.path.join(settings.MEDIA_ROOT, 'galleries', str(user_id))
img_urls = []
if os.path.exists(user_directory):
for filename in os.listdir(user_directory):
if filename.lower().endswith(('.png', '.jpg', '.jpeg', '.gif')):
img_urls.append(f"{settings.MEDIA_URL}galleries/{user_id}/{filename}")
return render(request, 'gallery/home.html', {'images': img_urls})
@login_required()
def import_img(request):
if request.method == "POST":
form = AddImgGallery(request.POST, request.FILES)
if form.is_valid():
image = form.clean_img()
user_id = request.user.id
saved_image_url = save_image_to_gallery(image, user_id)
messages.success(request, 'Votre image a bien été ajoutée à la galerie')
form = AddImgGallery()
return render(request, 'gallery/form.html', {'form': form})
def save_image_to_gallery(image, user_id):
user_directory = os.path.join(settings.MEDIA_ROOT, 'galleries', str(user_id))
os.makedirs(user_directory, exist_ok=True)
# Créer un fichier index.html vide pour sécuriser le répertoire
index_file_path = os.path.join(user_directory, 'index.html')
if not os.path.exists(index_file_path): # Vérifie si le fichier n'existe pas déjà
with open(index_file_path, 'w') as index_file:
index_file.write('') # Écrit un fichier vide
# Définir le chemin complet du fichier
file_path = os.path.join(user_directory, image.name)
# Enregistrer le fichier
with open(file_path, 'wb+') as destination:
for chunk in image.chunks():
destination.write(chunk)
return f"{settings.MEDIA_URL}galleries/{user_id}/{image.name}"

0
games/__init__.py Normal file
View file

4
games/admin.py Normal file
View file

@ -0,0 +1,4 @@
from django.contrib import admin
from .models import LittleBacCategories
admin.site.register(LittleBacCategories)

6
games/apps.py Normal file
View file

@ -0,0 +1,6 @@
from django.apps import AppConfig
class GamesConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'games'

View file

@ -0,0 +1,64 @@
# Generated by Django 4.2.17 on 2024-12-30 09:48
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='LittleBacCategories',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=100)),
('description', models.TextField(default='')),
],
),
migrations.CreateModel(
name='LittleBacGames',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=155)),
('status', models.CharField(choices=[('waiting', 'En attente'), ('in_progress', 'En cours'), ('finished', 'Terminée')], default='waiting', max_length=20)),
('created', models.DateTimeField(auto_now_add=True)),
('updated', models.DateTimeField(auto_now=True)),
],
),
migrations.CreateModel(
name='LittleBacRounds',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('letter', models.CharField(max_length=1)),
('round_counter', models.IntegerField(default=1)),
('game', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='little_bac_rounds', to='games.littlebacgames')),
],
),
migrations.CreateModel(
name='LittleBacPlayers',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('score', models.IntegerField()),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='little_bac_players', to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='LittleBacAnswers',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('answer', models.CharField(max_length=100)),
('is_valid', models.BooleanField(default=False)),
('point', models.IntegerField(default=0)),
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='little_bac_answers', to='games.littlebaccategories')),
('player', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='little_bac_answers', to='games.littlebacplayers')),
('round', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='little_bac_answers', to='games.littlebacrounds')),
],
),
]

View file

@ -0,0 +1,20 @@
# Generated by Django 4.2.17 on 2024-12-30 10:51
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('games', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='littlebacplayers',
name='game',
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='little_bac_players', to='games.littlebacgames'),
preserve_default=False,
),
]

View file

@ -0,0 +1,22 @@
# Generated by Django 4.2.17 on 2024-12-30 12:10
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),
('games', '0002_littlebacplayers_game'),
]
operations = [
migrations.AddField(
model_name='littlebacgames',
name='author',
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='little_bac_games', to=settings.AUTH_USER_MODEL),
preserve_default=False,
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 4.2.17 on 2024-12-30 12:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('games', '0003_littlebacgames_author'),
]
operations = [
migrations.AddField(
model_name='littlebacplayers',
name='is_ready',
field=models.BooleanField(default=False),
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 4.2.17 on 2024-12-30 15:24
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('games', '0004_littlebacplayers_is_ready'),
]
operations = [
migrations.AddField(
model_name='littlebacgames',
name='players',
field=models.ManyToManyField(related_name='little_bac_games', to='games.littlebacplayers'),
),
]

View file

@ -0,0 +1,17 @@
# Generated by Django 4.2.17 on 2024-12-30 15:37
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('games', '0005_littlebacgames_players'),
]
operations = [
migrations.RemoveField(
model_name='littlebacgames',
name='players',
),
]

View file

@ -0,0 +1,61 @@
# Generated by Django 4.2.17 on 2024-12-30 19:24
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),
('games', '0006_remove_littlebacgames_players'),
]
operations = [
migrations.AddField(
model_name='littlebacgames',
name='countdown_started',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='littlebacgames',
name='countdown_time',
field=models.IntegerField(default=0),
),
migrations.AlterField(
model_name='littlebacanswers',
name='category',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='answers', to='games.littlebaccategories'),
),
migrations.AlterField(
model_name='littlebacanswers',
name='player',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='answers', to='games.littlebacplayers'),
),
migrations.AlterField(
model_name='littlebacanswers',
name='round',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='answers', to='games.littlebacrounds'),
),
migrations.AlterField(
model_name='littlebacgames',
name='author',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='games', to=settings.AUTH_USER_MODEL),
),
migrations.AlterField(
model_name='littlebacplayers',
name='game',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='players', to='games.littlebacgames'),
),
migrations.AlterField(
model_name='littlebacplayers',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='players', to=settings.AUTH_USER_MODEL),
),
migrations.AlterField(
model_name='littlebacrounds',
name='game',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rounds', to='games.littlebacgames'),
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 4.2.17 on 2024-12-30 19:46
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('games', '0007_littlebacgames_countdown_started_and_more'),
]
operations = [
migrations.AddField(
model_name='littlebacgames',
name='countdown_start_time',
field=models.DateTimeField(blank=True, null=True),
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 4.2.17 on 2024-12-31 08:54
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('games', '0008_littlebacgames_countdown_start_time'),
]
operations = [
migrations.AlterField(
model_name='littlebacgames',
name='countdown_start_time',
field=models.DateTimeField(blank=True, default=None, null=True),
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 4.2.17 on 2024-12-31 12:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('games', '0009_alter_littlebacgames_countdown_start_time'),
]
operations = [
migrations.AddField(
model_name='littlebacplayers',
name='status',
field=models.CharField(choices=[('playing', 'Joue'), ('overed', 'A fini')], default='playing', max_length=20),
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 4.2.17 on 2025-01-02 13:49
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('games', '0010_littlebacplayers_status'),
]
operations = [
migrations.AddField(
model_name='littlebacgames',
name='current_phase',
field=models.CharField(blank=True, default='ready_game', max_length=60, null=True),
),
]

View file

49
games/models.py Normal file
View file

@ -0,0 +1,49 @@
from django.db import models
from users.models import User
class LittleBacGames(models.Model):
id = models.AutoField(primary_key=True)
author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='games')
name = models.CharField(max_length=155)
status = models.CharField(max_length=20 ,choices=[
('waiting', 'En attente'),
('in_progress', 'En cours'),
('finished', 'Terminée')
], default='waiting')
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
countdown_started = models.BooleanField(default=False)
countdown_time = models.IntegerField(default=0)
countdown_start_time = models.DateTimeField(default=None, null=True, blank=True)
current_phase = models.CharField(max_length=60, default="ready_game", null=True, blank=True)
class LittleBacPlayers(models.Model):
id = models.AutoField(primary_key=True)
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='players')
game = models.ForeignKey(LittleBacGames, on_delete=models.CASCADE, related_name='players')
score = models.IntegerField()
is_ready = models.BooleanField(default=False)
status = models.CharField(max_length=20, choices=[
('playing', 'Joue'),
('overed', 'A fini')
], default="playing")
class LittleBacCategories(models.Model):
id = models.AutoField(primary_key=True)
name = models.CharField(max_length=100)
description = models.TextField(default="")
class LittleBacRounds(models.Model):
id = models.AutoField(primary_key=True)
game = models.ForeignKey(LittleBacGames, on_delete=models.CASCADE, related_name='rounds')
letter = models.CharField(max_length=1)
round_counter = models.IntegerField(default=1)
class LittleBacAnswers(models.Model):
id = models.AutoField(primary_key=True)
round = models.ForeignKey(LittleBacRounds, on_delete=models.CASCADE, related_name='answers')
player = models.ForeignKey(LittleBacPlayers, on_delete=models.CASCADE, related_name='answers')
category = models.ForeignKey(LittleBacCategories, on_delete=models.CASCADE, related_name='answers')
answer = models.CharField(max_length=100)
is_valid = models.BooleanField(default=False)
point = models.IntegerField(default=0)

View file

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

3
games/tests.py Normal file
View file

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

25
games/urls.py Normal file
View file

@ -0,0 +1,25 @@
from django.urls import path
from django.conf.urls.static import static
from . import views
urlpatterns = [
# API REST
path('api/<int:game_id>/start_countdown', views.game_start_countdown, name='game_start_countdown'),
path('api/<int:game_id>/countdown_status', views.game_countdown_status, name='game_countdown_status'),
path('api/bac/<int:game_id>/info', views.game_infos_little_bac, name='game_infos_little_bac'),
path('api/bac/<int:game_id>/info_party', views.party_infos_little_bac, name='party_infos_little_bac'),
path('api/bac/<int:game_id>/players', views.game_players_little_bac, name='game_players_little_bac'),
path('api/bac/<int:game_id>/end_game', views.game_liitle_bac_end_game, name='end_game'),
path('api/bac/<int:game_id>/player/<int:player_id>/toggle_ready', views.toggle_ready_status_little_bac, name='toggle_ready_status_little_bac'),
path("", views.portal, name="portal_games"),
path("bac", views.little_bac_home, name="bac_games"),
path("bac/party", views.little_bac_start, name="bac_start_games"),
path("bac/party/<str:party_id>", views.little_bac_party, name="bac_party_games"),
path("bac/party/<str:party_id>/join", views.little_bac_party_join, name="bac_party_join_games"),
path("bac/party/<str:party_id>/play", views.little_bac_party_play, name="bac_party_play_games"),
path("bac/party/<str:party_id>/results", views.game_little_bac_results, name="bac_party_results_games"),
path('party/<int:game_id>/new_round/', views.game_little_bac_start_new_round, name='bac_start_new_round'),
]

433
games/views.py Normal file
View file

@ -0,0 +1,433 @@
from django.shortcuts import render, redirect, get_object_or_404
from django.http import JsonResponse
from django.contrib.auth.decorators import login_required
from .models import *
from users.models import UserLevel
from django.utils.timezone import now
from django.contrib import messages
from django.db.models import Count, Sum, F
from django.db.models.functions import Lower
def portal(request):
last_party = LittleBacGames.objects.filter().last()
nb_parties = LittleBacGames.objects.filter(status="finished").count()
if not request.user.is_authenticated:
return render(request, 'games/portal.html', {'last_party': last_party, 'nb_parties': nb_parties})
games = LittleBacGames.objects.filter(author=request.user, status='waiting')
return render(request, 'games/portal.html', {'games': games, 'last_party': last_party, 'nb_parties': nb_parties})
def little_bac_home(request):
return render(request, 'games/littlebac/home.html')
@login_required()
def little_bac_start(request):
import random
import string
game = LittleBacGames.objects.create(name=f"Partie de {request.user.username}", author=request.user)
LittleBacPlayers.objects.create(
user=request.user,
game=game,
score=0
)
# Liste des lettres de l'alphabet
alphabet = string.ascii_uppercase # 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
# Sélection aléatoire
random_letter = random.choice(alphabet)
LittleBacRounds.objects.create(
game=game,
letter=random_letter,
round_counter=1
)
return redirect('bac_party_games', party_id=game.id)
@login_required()
def little_bac_party(request, party_id):
game = LittleBacGames.objects.get(id=party_id)
players = LittleBacPlayers.objects.filter(game=game)
rounds = LittleBacRounds.objects.filter(game=game)
if players.filter(user=request.user).exists():
current_round = rounds.last()
return render(request, 'games/littlebac/game.html', {'game': game, 'players': players, 'round': current_round})
else:
return redirect('bac_games')
@login_required()
def little_bac_party_join(request, party_id):
game = LittleBacGames.objects.get(id=party_id)
players = LittleBacPlayers.objects.filter(game=game)
if players.filter(user=request.user).exists():
return redirect('bac_games')
LittleBacPlayers.objects.create(
user=request.user,
game=game,
score=0
)
return redirect('bac_party_games', party_id=game.id)
@login_required()
def little_bac_party_play(request, party_id):
game = LittleBacGames.objects.get(id=party_id)
players = LittleBacPlayers.objects.filter(game=game)
if not players.filter(user=request.user).exists():
return redirect('bac_games')
# Vérifie si la partie est en attente et met à jour le statut
if game.status == 'waiting':
game.status = 'in_progress'
game.save()
# Récupère le round actuel (dernier round)
current_round = LittleBacRounds.objects.filter(game=game).last()
categories = LittleBacCategories.objects.all()
player = players.get(user=request.user)
if request.method == "POST":
print("POST")
# On vérifie que les réponses commencent par la lettre du round
round_letter = current_round.letter.upper()
all_valid = True
for category in categories:
answer = request.POST.get(f"col-{category.id}", "").strip()
if answer and answer[0].upper() != round_letter:
all_valid = False
break
if all_valid:
player = LittleBacPlayers.objects.get(game=game, user_id=request.user.id)
player.status = "overed"
player.save()
# Récupérer les réponses soumises par le joueur
answers = {
f"col-{category.id}": request.POST.get(f"col-{category.id}")
for category in categories
}
responses = []
# Enregistre chaque réponse en base de données
for category in categories:
answer = answers.get(f"col-{category.id}", "").strip() # Récupère la réponse ou une chaîne vide
if answer: # Vérifie si une réponse est fournie
response = LittleBacAnswers.objects.create(
round=current_round,
player=players.get(user=request.user),
category=category,
answer=answer,
is_valid=False
)
responses.append(response)
return render(request, 'games/littlebac/finish.html', {
'responses': responses,
'round': current_round,
'categories': categories,
'player': player
})
else:
messages.error(request, "Les réponses doivent commencer par la lettre du tour.")
return render(request, 'games/littlebac/play.html', {
'game': game,
'round': current_round,
'categories': categories,
'countdown_remaining': countdown_remaining,
})
# Passe les informations du décompte au template
countdown_remaining = max(
0, game.countdown_time - int((now() - game.countdown_start_time).total_seconds())
) if game.countdown_started else None
return render(request, 'games/littlebac/play.html', {
'game': game,
'round': current_round,
'categories': categories,
'countdown_remaining': countdown_remaining,
'player': player
})
@login_required()
def game_little_bac_results(request, party_id):
game = get_object_or_404(LittleBacGames, id=party_id)
players = LittleBacPlayers.objects.filter(game=game)
rounds = LittleBacRounds.objects.filter(game=game)
categories = LittleBacCategories.objects.all()
all_organized_answers = {}
scores_by_round = {round.id: {} for round in rounds}
total_scores = {player.id: 0 for player in players}
for round in rounds:
answers = LittleBacAnswers.objects.filter(round=round)
# On détermine qu'un mot est valide s'il est unique pour une catégorie donnée
for category in categories:
valid_answers = answers.filter(category=category).annotate(
lower_answer=Lower('answer')
).values('lower_answer').annotate(
count=Count('lower_answer')
).filter(count=1)
for answer in valid_answers:
answers.filter(category=category, answer__iexact=answer['lower_answer']).update(is_valid=True, point=5)
# Marquer les réponses dupliquées et leur attribuer 1 point
duplicate_answers = answers.filter(category=category).annotate(
lower_answer=Lower('answer')
).values('lower_answer').annotate(
count=Count('lower_answer')
).filter(count__gt=1)
for answer in duplicate_answers:
answers.filter(category=category, answer__iexact=answer['lower_answer']).update(is_valid=False, point=1)
# Calcule des points pour chaque joueur pour ce round
for player in players:
player_score = answers.filter(
player=player
).aggregate(
total=Sum('point')
)['total'] or 0
scores_by_round[round.id][player.id] = player_score
total_scores[player.id] += player_score
# On ajoute X experience au joueur (X: score du round)
user_level = UserLevel.objects.get(user=player.user)
last_round_updated = request.session.get(f'last_round_updated_{player.user.id}', 0)
if last_round_updated < round.id:
user_level.experience += player_score
user_level.save()
request.session[f'last_round_updated_{player.user.id}'] = round.id
# Organiser les réponses par joueur et par catégorie pour chaque round
for round in rounds:
answers = LittleBacAnswers.objects.filter(round=round)
organized_answers = {}
for player in players:
organized_answers[player.id] = {}
for category in categories:
answer = answers.filter(player=player, category=category).first()
if answer:
organized_answers[player.id][category.id] = answer.answer
else:
organized_answers[player.id][category.id] = ""
all_organized_answers[round.id] = organized_answers
# Mettre à jour les scores totaux des joueurs
for player in players:
player.score = total_scores[player.id]
player.save()
return render(request, 'games/littlebac/results.html', {
'game': game,
'players': players,
'rounds': rounds,
'categories': categories,
'all_organized_answers': all_organized_answers,
'scores_by_round': scores_by_round,
'total_scores': total_scores
})
login_required()
def game_little_bac_start_new_round(request, game_id):
import random
import string
game = LittleBacGames.objects.get(id=game_id)
if game.author != request.user:
return redirect('bac_party_games', party_id=game_id)
players = LittleBacPlayers.objects.filter(game=game)
game.status = 'waiting'
game.countdown_started = False
game.countdown_start_time = None
game.countdown_time = 0
game.current_phase = "ready_game"
game.save()
players.update(is_ready=False, status='playing')
# Sélectionne une lettre aléatoire pour le nouveau round
alphabet = string.ascii_uppercase
letter = random.choice(alphabet)
# Détermine le numéro du nouveau round
round_counter = game.rounds.count() + 1
# Crée un nouveau round
new_round = LittleBacRounds.objects.create(
game=game,
letter=letter,
round_counter=round_counter
)
return redirect('bac_party_games', party_id=game_id)
# API REST DES JEUX
@login_required()
def game_players_little_bac(request, game_id):
try:
game = LittleBacGames.objects.get(id=game_id)
players = LittleBacPlayers.objects.filter(game=game)
players_list = [{"id": player.id, "username": player.user.username, "score": player.score} for player in players]
return JsonResponse({"game_id": game_id, "players": players_list})
except LittleBacGames.DoesNotExist:
return JsonResponse({"error": "Game not found"}, status=404)
@login_required()
def toggle_ready_status_little_bac(request, game_id, player_id):
try:
game = LittleBacGames.objects.get(id=game_id)
player = LittleBacPlayers.objects.get(game=game, user_id=player_id)
player.is_ready = not player.is_ready
player.save()
# Vérifie si tous les joueurs de la partie sont prêts
game = player.game
print("Joueurs de la partie",game)
all_ready = LittleBacPlayers.objects.filter(game=game, is_ready=True).count()
print(all_ready)
return JsonResponse({"is_ready": player.is_ready, "all_ready": all_ready})
except LittleBacPlayers.DoesNotExist:
return JsonResponse({"error": "Player not found"}, status=404)
def game_infos_little_bac(request, game_id):
try:
game = LittleBacGames.objects.get(id=game_id)
return JsonResponse({
"name": game.name,
"status": game.status,
"created": game.created,
"all_ready": game.players.filter(is_ready=True).count()
})
except LittleBacGames.DoesNotExist:
return JsonResponse({"error": "Party not found"}, status=404)
@login_required()
def party_infos_little_bac(request, game_id):
from django.utils.timezone import now
try:
game = LittleBacGames.objects.get(id=game_id)
players = LittleBacPlayers.objects.filter(game=game)
players_data = [
{"id": player.id, "username": player.user.username, "status": player.status, "score": player.score}
for player in players
]
countdown_remaining = None
if game.countdown_started and game.countdown_start_time:
elapsed_time = (now() - game.countdown_start_time).total_seconds()
countdown_remaining = max(0, game.countdown_time - int(elapsed_time))
print(game.status)
return JsonResponse({
"game_status": game.status,
"players": players_data,
"countdown_time": countdown_remaining,
"countdown_started": game.countdown_started,
"current_phase": game.current_phase if hasattr(game, 'current_phase') else None
})
except LittleBacGames.DoesNotExist:
return JsonResponse({"error": "Game not found"}, status=404)
@login_required()
def game_start_countdown(request, game_id):
from django.utils.timezone import now
# Récupère le type de décompte (ready_game ou finish_game)
countdown_type = request.GET.get("type", "ready_game")
print(f"Type de décompte reçu: {countdown_type}") # Ajout de cette ligne pour vérifier le type de décompte
if countdown_type not in ["ready_game", "finish_game"]:
return JsonResponse({"success": False, "error": "Type de décompte invalide."}, status=400)
try:
game = LittleBacGames.objects.get(id=game_id)
print(f"Statut du jeu: {game.status}") # Ajout de cette ligne pour vérifier le statut du jeu
# Vérification des conditions pour chaque décompte
if countdown_type == "ready_game" and not game.countdown_started and game.status == "waiting":
game.countdown_started = True
game.countdown_start_time = now()
game.countdown_time = 5
game.save()
elif countdown_type == "finish_game" and game.status == "in_progress":
print("Finish game")
# Vérifie si un décompte précédent n'est pas actif
elapsed_time = (now() - game.countdown_start_time).total_seconds()
if not game.countdown_started or elapsed_time >= game.countdown_time:
print("Start countdown")
game.countdown_started = True
game.countdown_start_time = now()
game.countdown_time = 60
game.current_phase = "finish_game"
game.save()
else:
return JsonResponse({"success": False, "error": "Décompte déjà en cours."}, status=400)
else:
return JsonResponse({"success": False, "error": "Condition de décompte non remplie."}, status=400)
return JsonResponse({
"success": True,
"countdown_started": game.countdown_started,
"countdown_time": game.countdown_time
})
except LittleBacGames.DoesNotExist:
return JsonResponse({"success": False, "error": "Game not found"}, status=404)
def game_countdown_status(request, game_id):
from django.utils.timezone import now
game = LittleBacGames.objects.get(id=game_id)
if game.countdown_started and game.countdown_start_time:
# Temps écoulé en secondes
elapsed_time = (now() - game.countdown_start_time).total_seconds()
remaining_time = max(0, game.countdown_time - int(elapsed_time))
print(f"Elapsed time: {elapsed_time}, Remaining time: {remaining_time}") # Vérification
else:
print("Ouuups")
remaining_time = game.countdown_time
return JsonResponse({
"countdown_started": game.countdown_started,
"countdown_time": remaining_time
})
@login_required()
def game_liitle_bac_end_game(request, game_id):
from django.utils.timezone import now
try:
game = LittleBacGames.objects.get(id=game_id)
# Mettre à jour l'état du jeu à "finished"
game.status = 'finished'
game.updated = now()
game.save()
# Optionnel : Mettez à jour tous les joueurs pour les marquer comme ayant terminé
LittleBacPlayers.objects.filter(game=game).update(status='overed')
return JsonResponse({"success": True, "message": "Game ended successfully."})
except LittleBacGames.DoesNotExist:
return JsonResponse({"error": "Game not found"}, status=404)

0
guestbook/__init__.py Executable file
View file

3
guestbook/admin.py Executable file
View file

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

6
guestbook/apps.py Executable file
View file

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

12
guestbook/forms.py Executable file
View file

@ -0,0 +1,12 @@
from django import forms
class CreateGuestbook(forms.Form):
author = forms.CharField(
max_length=150,
label='',
widget=forms.TextInput(attrs={'placeholder': 'Nom'})
)
content = forms.CharField(
label='',
widget=forms.Textarea(attrs={'placeholder': 'Message'})
)

14
guestbook/middleware.py Executable file
View file

@ -0,0 +1,14 @@
from django.utils.deprecation import MiddlewareMixin
from guestbook.models import Guestbook
class GuestbookMiddleware(MiddlewareMixin):
def process_request(self, request):
# On récupère les messages du livre d'or et les auteurs
guestbook = Guestbook.objects.all().order_by('-created')[:5]
# On compte le nombre de messages
total_guestbook = Guestbook.objects.count()
# On ajoute les variables à l'objet request
request.guestbook = guestbook
request.total_guestbook = total_guestbook

View file

@ -0,0 +1,23 @@
# Generated by Django 4.2.16 on 2024-10-22 11:02
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name="Guestbook",
fields=[
("id", models.AutoField(primary_key=True, serialize=False)),
("author", models.CharField(default="Visiteur", max_length=50)),
("content", models.CharField(max_length=100)),
("created", models.DateTimeField(auto_now_add=True)),
("active", models.BooleanField(default=True)),
],
),
]

View file

11
guestbook/models.py Executable file
View file

@ -0,0 +1,11 @@
from django.db import models
class Guestbook(models.Model):
id = models.AutoField(primary_key=True)
author = models.CharField(max_length=50, default='Visiteur')
content = models.CharField(max_length=100)
created = models.DateTimeField(auto_now_add=True)
active = models.BooleanField(default=True)
def __str__(self):
return f'Guestbook({self.id}, {self.author}, {self.content}, {self.created})'

3
guestbook/tests.py Executable file
View file

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

9
guestbook/urls.py Executable file
View file

@ -0,0 +1,9 @@
from django.urls import path
from django.conf.urls.static import static
from passion_retro import settings
from . import views
urlpatterns = [
path("", views.guestbook_home, name="guestbook_home"),
]

29
guestbook/views.py Executable file
View file

@ -0,0 +1,29 @@
from django.core.paginator import Paginator
from django.shortcuts import render
from .models import Guestbook
from .forms import CreateGuestbook
def guestbook_home(request):
# Si un message est envoyé on le traite
if request.method == "POST":
form = CreateGuestbook(request.POST)
if form.is_valid():
author = form.cleaned_data['author']
content = form.cleaned_data['content']
Guestbook.objects.create(author=author, content=content)
guestbook = Guestbook.objects.all().order_by('-created')
paginator = Paginator(guestbook, 10)
page_number = request.GET.get('page')
guestbook = paginator.get_page(page_number)
context = {
'guestbook': guestbook,
'is_paginated': guestbook.has_other_pages(),
'page_obj': guestbook,
'paginator': paginator,
'form': CreateGuestbook(),
}
return render(request, "components/guestbook_home.html", context)

0
home/__init__.py Executable file
View file

3
home/admin.py Executable file
View file

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

6
home/apps.py Executable file
View file

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

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

3
home/models.py Executable file
View file

@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

3
home/tests.py Executable file
View file

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

12
home/urls.py Executable file
View file

@ -0,0 +1,12 @@
from django.urls import path
from django.conf.urls.static import static
from passion_retro import settings
from . import views
urlpatterns = [
path("", views.home, name="home"),
]
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

16
home/views.py Executable file
View file

@ -0,0 +1,16 @@
from django.shortcuts import render
from posts.models import Post
def home(request):
edito = Post.objects.filter(type='edito', active=True).first()
news = Post.objects.filter(type='news', active=True).order_by('-created')[:6]
context = {
'edito': edito,
'news': news,
}
return render(request, "home.html", context)
def custom_404(request, exception):
return render(request, "errors/404.html", status=404)

0
maintenance/__init__.py Normal file
View file

4
maintenance/admin.py Normal file
View file

@ -0,0 +1,4 @@
from django.contrib import admin
from .models import *
admin.site.register(Informations)

6
maintenance/apps.py Normal file
View file

@ -0,0 +1,6 @@
from django.apps import AppConfig
class MaintenanceConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'maintenance'

22
maintenance/middleware.py Normal file
View file

@ -0,0 +1,22 @@
from django.shortcuts import redirect
from django.urls import reverse
from django.contrib.auth import get_user
from .models import Informations
class MaintenanceMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
if not request.user.is_superuser and not request.path.startswith('/admin'):
try:
maintenance_info = Informations.objects.get(pk=1)
except Informations.DoesNotExist:
maintenance_info = None
if maintenance_info and maintenance_info.is_active == True and not request.path.startswith(reverse('maintenance:info')):
return redirect(reverse('maintenance:info'))
response = self.get_response(request)
return response

View file

@ -0,0 +1,23 @@
# Generated by Django 4.2.17 on 2024-12-26 19:59
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Informations',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(default='Maintenance en cours', max_length=255, verbose_name='Titre de la maintenance')),
('content', models.TextField(verbose_name='Contenu de la maintenance')),
('is_active', models.BooleanField(default=False)),
],
),
]

View file

@ -0,0 +1,23 @@
# Generated by Django 4.2.17 on 2025-01-06 17:23
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('maintenance', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='informations',
name='content',
field=models.TextField(default='Votre site rétro favoris fait un chek-up ;)'),
),
migrations.AlterField(
model_name='informations',
name='is_active',
field=models.BooleanField(default=True),
),
]

View file

10
maintenance/models.py Normal file
View file

@ -0,0 +1,10 @@
from django.db import models
class Informations(models.Model):
name = models.CharField("Titre de la maintenance", max_length=255, default="Maintenance en cours")
content = models.TextField(default="Votre site rétro favoris fait un chek-up ;)")
is_active = models.BooleanField(default=True)
def __str__(self):
return "Contenu de la maintenance"

3
maintenance/tests.py Normal file
View file

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

7
maintenance/urls.py Normal file
View file

@ -0,0 +1,7 @@
from django.urls import path
from . import views
app_name = "maintenance"
urlpatterns = [
path('', views.info, name="info"),
]

6
maintenance/views.py Normal file
View file

@ -0,0 +1,6 @@
from django.shortcuts import render, get_object_or_404
from .models import Informations
def info(request):
message = get_object_or_404(Informations, pk=1)
return render(request, 'maintenance/index.html', {'message': message})

22
manage.py Executable file
View file

@ -0,0 +1,22 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "passion_retro.settings")
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == "__main__":
main()

0
messagerie/__init__.py Normal file
View file

3
messagerie/admin.py Normal file
View file

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

6
messagerie/apps.py Normal file
View file

@ -0,0 +1,6 @@
from django.apps import AppConfig
class MessagerieConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'messagerie'

View file

@ -0,0 +1,24 @@
from .models import PrivateMessageSubject, PrivateMessage
from django.db.models import Q
def pending_pm_count(request):
if request.user.is_authenticated:
# Filtrer les sujets de messages où l'utilisateur est soit le receiver soit le sender
pm_subjects = PrivateMessageSubject.objects.filter(
Q(receiver=request.user) | Q(sender=request.user)
)
# Initialiser le compteur de messages non lus
count = 0
# Parcourir chaque sujet de message
for subject in pm_subjects:
# Récupérer le dernier message du sujet
last_message = subject.messages.order_by('-date_sent').first()
# Vérifier si le dernier message n'est pas de l'utilisateur actuel et si le sujet n'est pas lu
if last_message and last_message.author != request.user and not subject.is_read:
count += 1
return {'pending_pm_count': count}
return {'pending_pm_count': 0}

23
messagerie/forms.py Normal file
View file

@ -0,0 +1,23 @@
# Forms pour la messagerie
from django import forms
from .models import PrivateMessageSubject, PrivateMessage
class PrivateMessageSubjectForm(forms.ModelForm):
class Meta:
model = PrivateMessageSubject
fields = ['receiver', 'subject']
labels = {
'receiver': 'Destinataire',
'subject': 'Sujet'
}
class PrivateMessageForm(forms.ModelForm):
class Meta:
model = PrivateMessage
fields = ['message']
labels = {
'message': ''
}
widgets = {
'message': forms.Textarea(attrs={'placeholder': 'Votre message'})
}

View file

@ -0,0 +1,34 @@
# Generated by Django 4.2.17 on 2025-01-06 10:49
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='PrivateMessage',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('subject', models.CharField(max_length=100)),
('message', models.TextField()),
('date_sent', models.DateTimeField(auto_now_add=True)),
('is_read', models.BooleanField(default=False)),
('receiver', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='received_messages', to=settings.AUTH_USER_MODEL)),
('sender', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sent_messages', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Message privé',
'verbose_name_plural': 'Messages privés',
'ordering': ['-date_sent'],
},
),
]

View file

@ -0,0 +1,53 @@
# Generated by Django 4.2.17 on 2025-01-06 11:33
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),
('messagerie', '0001_initial'),
]
operations = [
migrations.RemoveField(
model_name='privatemessage',
name='is_read',
),
migrations.RemoveField(
model_name='privatemessage',
name='receiver',
),
migrations.RemoveField(
model_name='privatemessage',
name='sender',
),
migrations.AddField(
model_name='privatemessage',
name='author',
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='author_messages', to=settings.AUTH_USER_MODEL),
),
migrations.AlterField(
model_name='privatemessage',
name='id',
field=models.AutoField(primary_key=True, serialize=False),
),
migrations.CreateModel(
name='PrivateMessageSubject',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('subject', models.CharField(max_length=100)),
('is_read', models.BooleanField(default=False)),
('receiver', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='received_messages', to=settings.AUTH_USER_MODEL)),
('sender', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sent_messages', to=settings.AUTH_USER_MODEL)),
],
),
migrations.AlterField(
model_name='privatemessage',
name='subject',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='messages', to='messagerie.privatemessagesubject'),
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 4.2.17 on 2025-01-06 13:29
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('messagerie', '0002_remove_privatemessage_is_read_and_more'),
]
operations = [
migrations.AddField(
model_name='privatemessagesubject',
name='is_active',
field=models.BooleanField(default=True),
),
]

View file

28
messagerie/models.py Normal file
View file

@ -0,0 +1,28 @@
from django.db import models
from users.models import User
# Création d'une messagerie privée pour les membres
class PrivateMessageSubject(models.Model):
receiver = models.ForeignKey(User, on_delete=models.CASCADE, related_name='received_messages')
sender = models.ForeignKey(User, on_delete=models.CASCADE, related_name='sent_messages')
subject = models.CharField(max_length=100)
is_read = models.BooleanField(default=False)
is_active = models.BooleanField(default=True)
def __str__(self):
return f'Sujet: {self.subject} (De: {self.sender.username} À: {self.receiver.username})'
class PrivateMessage(models.Model):
id = models.AutoField(primary_key=True)
subject = models.ForeignKey(PrivateMessageSubject, on_delete=models.CASCADE, related_name='messages')
author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='author_messages', default=1)
message = models.TextField()
date_sent = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f'{self.author.username} ({self.date_sent})'
class Meta:
verbose_name = 'Message privé'
verbose_name_plural = 'Messages privés'
ordering = ['-date_sent']

3
messagerie/tests.py Normal file
View file

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

13
messagerie/urls.py Normal file
View file

@ -0,0 +1,13 @@
# urls messagerie
from django.urls import path
from . import views
urlpatterns = [
path('', views.home, name='pm_home'),
path('view/<int:message_id>/', views.view_message, name='pm_view'),
path('new/', views.new_message, name='pm_new'),
path('delete/<int:message_id>/', views.delete_subject, name='pm_delete'),
path('send_all_users/', views.send_all_users, name='pm_send_all_users'),
]

122
messagerie/views.py Normal file
View file

@ -0,0 +1,122 @@
from django.shortcuts import render, redirect
from django.contrib.auth.decorators import login_required
from .models import PrivateMessage, PrivateMessageSubject
from django.db import models
from django.db.models import Max
from .forms import PrivateMessageForm, PrivateMessageSubjectForm
from django.contrib import messages
@login_required
def home(request):
private_messages = PrivateMessageSubject.objects.filter(
(models.Q(receiver=request.user) | models.Q(sender=request.user)) & models.Q(is_active=True)
).annotate(last_message_date=Max('messages__date_sent')).order_by('-last_message_date')
return render(request, 'messagerie/home.html', {'private_messages': private_messages})
@login_required
def view_message(request, message_id):
pm_message = PrivateMessageSubject.objects.get(pk=message_id)
if (pm_message.receiver != request.user and pm_message.sender != request.user) or not pm_message.is_active:
return redirect('pm_home')
private_messages = PrivateMessage.objects.filter(subject=pm_message).order_by('date_sent')
form = PrivateMessageForm(request.POST or None)
if not pm_message.is_read and pm_message.messages.first().author != request.user:
pm_message.is_read = True
pm_message.save()
if form.is_valid():
message = form.save(commit=False)
message.author = request.user
message.subject = pm_message
message.save()
pm_message.is_read = False
pm_message.save()
form = PrivateMessageForm()
return render(request, 'messagerie/view_message.html', {'pm_message': pm_message, 'private_messages': private_messages, 'form': form})
return render(request, 'messagerie/view_message.html', {'pm_message': pm_message, 'private_messages': private_messages, 'form': form})
@login_required
def new_message(request):
form_subject = PrivateMessageSubjectForm(request.POST or None)
form_message = PrivateMessageForm(request.POST or None)
if form_subject.is_valid() and form_message.is_valid():
if form_subject.cleaned_data['receiver'] == request.user:
messages.error(request, 'Vous ne pouvez pas vous envoyer un message à vous-même')
return redirect('pm_home')
subject = form_subject.save(commit=False)
subject.sender = request.user
subject.is_read = False
subject.save()
message = form_message.save(commit=False)
message.author = request.user
message.subject = subject
message.save()
return redirect('pm_view', message_id=message.id)
return render(request, 'messagerie/new_message.html', {'form_subject': form_subject, 'form_message': form_message})
def delete_subject(request, message_id):
pm_message = PrivateMessageSubject.objects.get(pk=message_id)
if pm_message.receiver != request.user and pm_message.sender != request.user:
return redirect('pm_home')
pm_message.is_active = False
pm_message.is_read = True
pm_message.save()
messages.success(request, 'Le sujet a bien été supprimé')
return redirect('pm_home')
@login_required
def send_all_users(request):
from users.models import User
all_users = User.objects.all()
sender = User.objects.get(username='RetroBot')
for user in all_users:
subject = PrivateMessageSubject.objects.create(
receiver=user,
sender=User.objects.get(username='RetroBot'),
subject="Bienvenue sur PassionRetro !"
)
PrivateMessage.objects.create(
subject=subject,
author=User.objects.get(username='RetroBot'),
message=f"""[b]Bienvenue sur Passion Retro ![/b]
Salut [b]{user.username}[/b] !,
Merci de nous avoir rejoints dans cette aventure dédiée aux passionnés de rétro ! Que tu sois fan de consoles vintage, collectionneur d'objets d'époque ou simple curieux, tu es ici chez toi.
[b]Découvre tout ce que Passion Retro a à offrir :[/b]
Plonge dans nos articles pour en apprendre plus sur les trésors du passé, participe aux discussions sur le forum et teste tes connaissances avec nos jeux rétro. Chaque section est pour te permettre de partager ta passion et d'en apprendre davantage.
🚀 [b]Rejoins la communauté :[/b]
Ton avis et tes contributions sont précieux ! Nhésite pas à lancer une discussion sur le forum, à réagir aux articles ou à défier les autres membres sur nos jeux. Plus nous sommes actifs, plus lexpérience sera enrichissante pour tous.
Si tu as des questions ou des suggestions pour améliorer le site, contacte-nous. Nous sommes pour t'accompagner !
Encore une fois, bienvenue parmi nous et prépare-toi à replonger dans lunivers du rétro !
À bientôt,
[b]L'équipe Passion Retro[/b]""")
subject = PrivateMessageSubject.objects.create(receiver=user, sender=sender, subject='Bienvenue sur la messagerie privée')
PrivateMessage.objects.create(
subject=subject,
author=sender,
message=f"Cher [b]{user.username}[/b],\n\nBienvenue sur la messagerie privée de passion-retro. Tu peux désormais échanger des messages privés avec les autres membres du site.\n\nQuant a moi, je suis un nouvel utilisateur du site venu de la planète [b]Retronia[/b] ! J'ai été investis de la mission la plus importante de ma longue vie : [u]être le meilleur assistant de passion-retro[/u] ! J'espère sincèrement y parvenir !\n\nJe suis encore en orbite actuellement, mais guette les infos car je serait bientôt présent !\n\nCordialement,\n{sender.username}"
)
return redirect('pm_home')

0
passion_retro/__init__.py Executable file
View file

16
passion_retro/asgi.py Executable file
View file

@ -0,0 +1,16 @@
"""
ASGI config for passion_retro project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "passion_retro.settings")
application = get_asgi_application()

199
passion_retro/settings.py Executable file
View file

@ -0,0 +1,199 @@
"""
Django settings for passion_retro project.
Generated by 'django-admin startproject' using Django 4.2.16.
For more information on this file, see
https://docs.djangoproject.com/en/4.2/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.2/ref/settings/
"""
from pathlib import Path
import os
from dotenv import load_dotenv
import json
load_dotenv()
if os.getenv('DATABASE_ENGINE') == "django.db.backends.mysql":
print("c'est mariaDB")
import pymysql
pymysql.install_as_MySQLdb()
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.getenv('SECRET_KEY')
GOOGLE_PUBLIC_KEY = os.getenv('GOOGLE_PUBLIC_KEY')
GOOGLE_PRIVATE_KEY = os.getenv('GOOGLE_PRIVATE_KEY')
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = os.getenv('DEBUG') == 'True'
ALLOWED_HOSTS = os.getenv('ALLOWED_HOSTS').split(',')
CSRF_TRUSTED_ORIGINS = os.getenv('CSRF_TRUSTED_ORIGINS').split(',')
USE_X_FORWARDED_HOST = os.getenv('USE_X_FORWARDED_HOST') == 'True'
SECURE_PROXY_SSL_HEADER = tuple(os.getenv('SECURE_PROXY_SSL_HEADER').split(','))
# Application definition
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
# "django.contrib.sites",
"maintenance",
"home",
"posts",
"users",
"gallery",
"forum",
"tchat",
"guestbook",
"games",
"quiz",
"messagerie",
"shop",
]
SITE_ID = 1
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"users.middleware.UserStatsMiddleware",
"forum.middleware.ForumStatsMiddleware",
"guestbook.middleware.GuestbookMiddleware",
"posts.middleware.PostsMiddleware",
'maintenance.middleware.MaintenanceMiddleware',
"users.middleware.UserLevelMiddleware",
"users.middleware.UserLevelUpMiddleware",
]
ROOT_URLCONF = "passion_retro.urls"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": (BASE_DIR, 'templates'),
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
"posts.context_processors.pending_posts_count",
"quiz.context_processors.pending_quizes_count",
"messagerie.context_processors.pending_pm_count",
],
},
},
]
WSGI_APPLICATION = "passion_retro.wsgi.application"
# Database
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': os.getenv('DATABASE_ENGINE'), # Utilise MySQL/MariaDB
'NAME': os.getenv('DATABASE_NAME') if os.getenv('DATABASE_ENGINE') == "django.db.backends.mysql" else BASE_DIR / os.getenv('DATABASE_NAME'),
'USER': os.getenv('DATABASE_USER'),
'PASSWORD': os.getenv('DATABASE_PASSWORD'),
'HOST': os.getenv('DATABASE_HOST'),
'PORT': os.getenv('DATABASE_PORT', '3306'), # Port par défaut de MySQL/MariaDB
'OPTIONS': {
'init_command': "SET sql_mode='STRICT_TRANS_TABLES'",
'charset': 'utf8mb4',
}
}
}
# Password validation
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
},
{
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
},
{
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
},
{
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
},
]
# Internationalization
# https://docs.djangoproject.com/en/4.2/topics/i18n/
LANGUAGE_CODE = "fr-FR"
TIME_ZONE = "Europe/Paris"
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.2/howto/static-files/
# URL pour accéder aux fichiers statiques
STATIC_URL = '/static/'
# Chemin absolu vers le répertoire où collecter les fichiers statiques
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
# Répertoires supplémentaires pour rechercher les fichiers statiques
STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'static'),
]
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/media/'
# Default primary key field type
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
LOGOUT_REDIRECT_URL = os.getenv('LOGOUT_REDIRECT_URL', default='/')
LOGIN_URL = os.getenv('LOGIN_URL', default='/login/')
AUTH_USER_MODEL = os.getenv('AUTH_USER_MODEL', default='users.User')
EMAIL_BACKEND = os.getenv('EMAIL_BACKEND', default='django.core.mail.backends.smtp.EmailBackend')
EMAIL_HOST = os.getenv('EMAIL_HOST', default='smtp.gmail.com')
EMAIL_PORT = os.getenv('EMAIL_PORT', default=587)
EMAIL_USE_TLS = os.getenv('EMAIL_USE_TLS', default=True)
EMAIL_USE_SSL = os.getenv('EMAIL_USE_TLS', default=False)
EMAIL_HOST_USER = os.getenv('EMAIL_HOST_USER', default='webmaster@localhost')
EMAIL_HOST_PASSWORD = os.getenv('EMAIL_HOST_PASSWORD', default='password')
DEFAULT_FROM_EMAIL = os.getenv('DEFAULT_FROM_EMAIL', default='webmaster@localhost')

40
passion_retro/urls.py Executable file
View file

@ -0,0 +1,40 @@
"""
URL configuration for passion_retro project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/4.2/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.contrib.auth import views as auth_views
from django.urls import include, path
from . import views
urlpatterns = [
path("admin/", admin.site.urls),
path("maintenance/", include("maintenance.urls")),
path("", include("home.urls")),
path("posts/", include("posts.urls")),
path("forum/", include("forum.urls")),
path("users/", include("users.urls")),
path("guestbook/", include("guestbook.urls")),
path("gallery/", include("gallery.urls")),
path("games/", include("games.urls")),
path("quiz/", include("quiz.urls")),
path("pm/", include("messagerie.urls")),
path("shop/", include("shop.urls")),
path('logout/', auth_views.LogoutView.as_view(), name='logout'),
]
handler404 = 'home.views.custom_404'

4
passion_retro/views.py Normal file
View file

@ -0,0 +1,4 @@
from django.shortcuts import render
def custom_404(request, exception):
return render(request, "errors/404.html", status=404)

16
passion_retro/wsgi.py Executable file
View file

@ -0,0 +1,16 @@
"""
WSGI config for passion_retro project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "passion_retro.settings")
application = get_wsgi_application()

0
posts/__init__.py Executable file
View file

Some files were not shown because too many files have changed in this diff Show more