diff --git a/VERSION.txt b/VERSION.txt index 0b51dff..b1479a2 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -1.5.0 (2ec4a5c) \ No newline at end of file +1.2.0 (c174906) \ No newline at end of file diff --git a/blog/context_processor.py b/blog/context_processor.py index d09e196..a1666a4 100644 --- a/blog/context_processor.py +++ b/blog/context_processor.py @@ -1,5 +1,5 @@ from .models import Post def posts_list(request): - posts = Post.objects.all().order_by('-created_at') + posts = Post.objects.all() return {'posts': posts} \ No newline at end of file diff --git a/blog/models.py b/blog/models.py index 5d0d68c..387d847 100644 --- a/blog/models.py +++ b/blog/models.py @@ -1,5 +1,4 @@ from django.db import models -from django.urls import reverse class Post(models.Model): name = models.CharField(max_length=200) @@ -16,7 +15,4 @@ class Post(models.Model): verbose_name_plural = "Articles" def __str__(self): - return self.name - - def get_absolute_url(self): - return reverse('blog:post_detail', kwargs={'slug': self.slug}) \ No newline at end of file + return self.name \ No newline at end of file diff --git a/core/admin.py b/core/admin.py index 13f115b..0989d6d 100644 --- a/core/admin.py +++ b/core/admin.py @@ -1,5 +1,5 @@ from django.contrib import admin -from .models import SiteSettings, Visit, Maintenance +from .models import SiteSettings @admin.register(SiteSettings) class SiteSettingsAdmin(admin.ModelAdmin): @@ -14,7 +14,7 @@ class SiteSettingsAdmin(admin.ModelAdmin): # Petite astuce visuelle pour l'admin fieldsets = ( ('Général', { - 'fields': ('site_name', 'site_logo', 'receive_emails_active') + 'fields': ('site_name', 'site_logo') }), ('Réseaux Sociaux', { 'fields': ('facebook_url', 'twitter_url', 'youtube_url'), @@ -27,14 +27,3 @@ class SiteSettingsAdmin(admin.ModelAdmin): 'fields': ('blog_title', 'blog_description') }), ) - -@admin.register(Maintenance) -class MaintenanceAdmin(admin.ModelAdmin): - list_display = ("name","start_date", "end_date") - - -@admin.register(Visit) -class VisitAdmin(admin.ModelAdmin): - list_display = ("date", "visitor_id", "user", "source", "country", "first_seen", "last_seen") - list_filter = ("date", "country", "source") - search_fields = ("visitor_id", "referrer", "utm_source", "utm_medium", "utm_campaign") diff --git a/core/apps.py b/core/apps.py index a2ee61b..26f78a8 100644 --- a/core/apps.py +++ b/core/apps.py @@ -1,7 +1,5 @@ from django.apps import AppConfig + class CoreConfig(AppConfig): name = 'core' - - def ready(self): - import courses.signals \ No newline at end of file diff --git a/core/context_processor.py b/core/context_processor.py index eccaebf..a1410e3 100644 --- a/core/context_processor.py +++ b/core/context_processor.py @@ -1,14 +1,5 @@ -from django.utils.timesince import timesince - -from .models import SiteSettings, Maintenance +from .models import SiteSettings def site_settings(request): # On récupère le premier objet, ou None s'il n'existe pas encore - return {'settings': SiteSettings.objects.first()} - -def site_maintenance(request): - last = Maintenance.objects.last() - start = last.start_date if last else None - end = last.end_date if last else None - delay = timesince(start, end) if start and end else None - return {'maintenance': Maintenance.objects.last(), 'delay': delay} \ No newline at end of file + return {'settings': SiteSettings.objects.first()} \ No newline at end of file diff --git a/core/middleware.py b/core/middleware.py deleted file mode 100644 index 3167553..0000000 --- a/core/middleware.py +++ /dev/null @@ -1,121 +0,0 @@ -import uuid -from urllib.parse import urlparse -from django.utils import timezone -from .models import Visit - - -class VisitTrackingMiddleware: - """Middleware très léger pour enregistrer des statistiques de visites. - - - Assigne un cookie visiteur persistant (vid) si absent - - Enregistre/Met à jour une ligne Visit par visiteur et par jour - - Capture la source (UTM/referrer) et le pays si disponible via headers - """ - - COOKIE_NAME = 'vid' - COOKIE_MAX_AGE = 60 * 60 * 24 * 365 * 2 # 2 ans - - def __init__(self, get_response): - self.get_response = get_response - - def __call__(self, request): - vid = request.COOKIES.get(self.COOKIE_NAME) - if not vid: - vid = uuid.uuid4().hex - - request.visitor_id = vid - - # Enregistrer la visite (agrégée par jour) - try: - self._track(request, vid) - except Exception: - # On ne casse jamais la requête pour des stats - pass - - response = self.get_response(request) - # S'assurer que le cookie est posé - if request.COOKIES.get(self.COOKIE_NAME) != vid: - response.set_cookie( - self.COOKIE_NAME, - vid, - max_age=self.COOKIE_MAX_AGE, - httponly=True, - samesite='Lax', - ) - return response - - def _track(self, request, vid): - # On ignore l'admin et les assets statiques - path = request.path - if path.startswith('/admin') or path.startswith('/static') or path.startswith('/staticfiles'): - return - - now = timezone.now() - date = now.date() - - ref = request.META.get('HTTP_REFERER', '')[:512] - utm_source = request.GET.get('utm_source', '')[:100] - utm_medium = request.GET.get('utm_medium', '')[:100] - utm_campaign = request.GET.get('utm_campaign', '')[:150] - - # Déterminer source - source = '' - if utm_source: - source = utm_source - elif ref: - try: - netloc = urlparse(ref).netloc - source = netloc - except Exception: - source = ref[:150] - - # Déterminer pays via en-têtes si fournis par proxy/CDN - country = ( - request.META.get('HTTP_CF_IPCOUNTRY') - or request.META.get('HTTP_X_APPENGINE_COUNTRY') - or request.META.get('HTTP_X_COUNTRY') - or '' - )[:64] - - became_user_at = now if request.user.is_authenticated else None - visit, created = Visit.objects.get_or_create( - visitor_id=vid, - date=date, - defaults={ - 'path': path[:512], - 'referrer': ref, - 'utm_source': utm_source, - 'utm_medium': utm_medium, - 'utm_campaign': utm_campaign, - 'source': source, - 'country': country, - 'first_seen': now, - 'last_seen': now, - 'user': request.user if request.user.is_authenticated else None, - 'became_user_at': became_user_at, - } - ) - - if not created: - # Mise à jour basique - dirty = False - if not visit.source and source: - visit.source = source - dirty = True - if not visit.country and country: - visit.country = country - dirty = True - if request.user.is_authenticated and visit.user_id is None: - visit.user = request.user - dirty = True - # Marquer la conversion si pas encore définie - if visit.became_user_at is None: - visit.became_user_at = now - # Mettre à jour la page courante et l'horodatage - if visit.path != path: - visit.path = path[:512] - dirty = True - visit.last_seen = now - dirty = True - if dirty: - visit.save(update_fields=['source', 'country', 'user', 'became_user_at', 'path', 'last_seen']) diff --git a/core/migrations/0004_visit.py b/core/migrations/0004_visit.py deleted file mode 100644 index 6301e48..0000000 --- a/core/migrations/0004_visit.py +++ /dev/null @@ -1,41 +0,0 @@ -from django.db import migrations, models -import django.db.models.deletion -from django.conf import settings -import django.utils.timezone - - -class Migration(migrations.Migration): - - dependencies = [ - ('core', '0003_alter_sitesettings_blog_description_and_more'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='Visit', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('visitor_id', models.CharField(db_index=True, max_length=64)), - ('date', models.DateField(db_index=True)), - ('first_seen', models.DateTimeField(default=django.utils.timezone.now)), - ('last_seen', models.DateTimeField(default=django.utils.timezone.now)), - ('path', models.CharField(blank=True, max_length=512)), - ('referrer', models.CharField(blank=True, max_length=512)), - ('utm_source', models.CharField(blank=True, max_length=100)), - ('utm_medium', models.CharField(blank=True, max_length=100)), - ('utm_campaign', models.CharField(blank=True, max_length=150)), - ('source', models.CharField(blank=True, help_text='Domaine de provenance ou utm_source', max_length=150)), - ('country', models.CharField(blank=True, max_length=64)), - ('became_user_at', models.DateTimeField(blank=True, null=True)), - ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='visits', to=settings.AUTH_USER_MODEL)), - ], - options={ - 'ordering': ['-date', '-last_seen'], - }, - ), - migrations.AlterUniqueTogether( - name='visit', - unique_together={('visitor_id', 'date')}, - ), - ] diff --git a/core/migrations/0005_sitesettings_receive_emails_active.py b/core/migrations/0005_sitesettings_receive_emails_active.py deleted file mode 100644 index 81c0441..0000000 --- a/core/migrations/0005_sitesettings_receive_emails_active.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 6.0 on 2025-12-17 09:36 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('core', '0004_visit'), - ] - - operations = [ - migrations.AddField( - model_name='sitesettings', - name='receive_emails_active', - field=models.BooleanField(default=True), - ), - ] diff --git a/core/migrations/0006_maintenance.py b/core/migrations/0006_maintenance.py deleted file mode 100644 index 1cac83f..0000000 --- a/core/migrations/0006_maintenance.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 6.0 on 2025-12-17 10:25 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('core', '0005_sitesettings_receive_emails_active'), - ] - - operations = [ - migrations.CreateModel( - name='Maintenance', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('is_active', models.BooleanField(default=False)), - ('message', models.TextField(blank=True)), - ('start_date', models.DateTimeField(blank=True, null=True)), - ('end_date', models.DateTimeField(blank=True, null=True)), - ], - ), - ] diff --git a/core/migrations/0007_maintenance_name.py b/core/migrations/0007_maintenance_name.py deleted file mode 100644 index eef8f59..0000000 --- a/core/migrations/0007_maintenance_name.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 6.0 on 2025-12-17 10:26 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('core', '0006_maintenance'), - ] - - operations = [ - migrations.AddField( - model_name='maintenance', - name='name', - field=models.CharField(blank=True, max_length=200), - ), - ] diff --git a/core/models.py b/core/models.py index c28ae82..b95ca68 100644 --- a/core/models.py +++ b/core/models.py @@ -1,12 +1,9 @@ from django.db import models -from django.conf import settings -from django.utils import timezone class SiteSettings(models.Model): site_name = models.CharField(max_length=200, default="Mon Super Site") site_logo = models.ImageField(upload_to='settings/', blank=True) contact_email = models.EmailField(blank=True) - receive_emails_active = models.BooleanField(default=True) # Réseaux sociaux facebook_url = models.URLField(blank=True) @@ -33,42 +30,4 @@ class SiteSettings(models.Model): class Meta: verbose_name = "Réglages du site" - verbose_name_plural = "Réglages du site" - -class Maintenance(models.Model): - is_active = models.BooleanField(default=False) - name = models.CharField(max_length=200, blank=True) - message = models.TextField(blank=True) - start_date = models.DateTimeField(blank=True, null=True) - end_date = models.DateTimeField(blank=True, null=True) - -class Visit(models.Model): - """Enregistrement simplifié des visites (agrégées par jour et visiteur). - - Objectif: fournir des stats de base sans dépendances externes. - """ - visitor_id = models.CharField(max_length=64, db_index=True) - user = models.ForeignKey( - settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.SET_NULL, related_name='visits' - ) - date = models.DateField(db_index=True) - first_seen = models.DateTimeField(default=timezone.now) - last_seen = models.DateTimeField(default=timezone.now) - - path = models.CharField(max_length=512, blank=True) - referrer = models.CharField(max_length=512, blank=True) - utm_source = models.CharField(max_length=100, blank=True) - utm_medium = models.CharField(max_length=100, blank=True) - utm_campaign = models.CharField(max_length=150, blank=True) - source = models.CharField(max_length=150, blank=True, help_text="Domaine de provenance ou utm_source") - country = models.CharField(max_length=64, blank=True) - - # Conversion: première fois où un visiteur devient utilisateur authentifié - became_user_at = models.DateTimeField(null=True, blank=True) - - class Meta: - unique_together = ('visitor_id', 'date') - ordering = ['-date', '-last_seen'] - - def __str__(self): - return f"{self.visitor_id} @ {self.date}" \ No newline at end of file + verbose_name_plural = "Réglages du site" \ No newline at end of file diff --git a/core/urls.py b/core/urls.py deleted file mode 100644 index 64f9484..0000000 --- a/core/urls.py +++ /dev/null @@ -1,9 +0,0 @@ -from django.urls import path -from . import views - -urlpatterns = [ - path('update_database', views.update_database, name='update_database'), - path('clear_cache', views.clear_cache, name='clear_cache'), - path('regen_static_files', views.regen_static_files, name='regen_static_files'), - path('reload_server', views.reload_server, name='reload_server') -] \ No newline at end of file diff --git a/core/views.py b/core/views.py index 204f4f0..91ea44a 100644 --- a/core/views.py +++ b/core/views.py @@ -1,23 +1,3 @@ from django.shortcuts import render -from django.core.management import call_command -from django.core.cache import cache -import subprocess -def update_database(request): - call_command('makemigrations') - call_command('migrate') - message = "La base de données à bien été mise à jour !" - return render(request, 'home.html', {'message': message}) - -def clear_cache(request): - cache.clear() - message = "Le cache à bien été effacé !" - return render(request, 'home.html', {'message': message}) - -def regen_static_files(request): - call_command('collectstatic', '--noinput') - message = "Les fichiers statics ont bien été générés !" - return render(request, 'home.html', {'message': message}) - -def reload_server(request): - pass \ No newline at end of file +# Create your views here. diff --git a/courses/apps.py b/courses/apps.py index 9a0a0cd..89f1ba2 100644 --- a/courses/apps.py +++ b/courses/apps.py @@ -1,5 +1,6 @@ from django.apps import AppConfig + class CoursesConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'courses' \ No newline at end of file diff --git a/courses/context_processors.py b/courses/context_processors.py index 4a22831..2a4772e 100644 --- a/courses/context_processors.py +++ b/courses/context_processors.py @@ -1,8 +1,5 @@ -from .models import Course, Comment +from .models import Course def course_list(request): courses = Course.objects.all() - return {'courses': courses} - -def courses_comments(request): - return {'comments_count': Comment.objects.all()} \ No newline at end of file + return {'courses': courses} \ No newline at end of file diff --git a/courses/signals.py b/courses/signals.py deleted file mode 100644 index 0258ca7..0000000 --- a/courses/signals.py +++ /dev/null @@ -1,12 +0,0 @@ -from django.db.models.signals import post_save -from django.dispatch import receiver -from django.core.mail import send_mail -from .models import Comment -from core.models import SiteSettings - -@receiver(post_save, sender=Comment) -def send_email_notification(sender, instance, created, **kwargs): - if created and SiteSettings.objects.first().receive_emails_active: - subject = f"Nouveau commentaire sur la leçon - {instance.lesson.name} du cours {instance.lesson.module.course.name}" - message = f"Le commentaire suivant à été envoyé par {instance.user}:\n{instance.content}" - send_mail(subject, message, "infos@partirdezero.com", ['anthony.violet@outlook.be'], fail_silently=False) diff --git a/courses/views.py b/courses/views.py index ca9fb22..8a33bbb 100644 --- a/courses/views.py +++ b/courses/views.py @@ -3,7 +3,6 @@ from django.urls import reverse from django.views import generic from django.db.models import Prefetch from .models import Course, Lesson, Module, Comment -from progression.models import Progression from .forms import CommentForm def list_courses(request): @@ -24,23 +23,9 @@ def show(request, course_name, course_id): .order_by('order') ) - # Récupération de la progression de l'utilisateur sur le cours - user_progress = None - completed_lesson_ids = [] # On prépare une liste vide par défaut - - if request.user.is_authenticated: - user_progress = Progression.objects.filter(user=request.user, course=course).first() - - # 2. S'il y a une progression, on extrait juste les IDs des leçons finies - if user_progress: - # values_list renvoie une liste simple : [1, 5, 12] au lieu d'objets lourds - completed_lesson_ids = user_progress.completed_lessons.values_list('id', flat=True) - context = { 'course': course, 'lessons': lessons, - 'user_progress': user_progress, - 'completed_lesson_ids': completed_lesson_ids, } return render(request, 'courses/show.html', context) @@ -120,18 +105,6 @@ def lesson_detail(request, course_slug, module_slug, lesson_slug): .order_by('created_at') ) - # Récupération de la progression de l'utilisateur sur le cours - user_progress = None - completed_lesson_ids = [] # On prépare une liste vide par défaut - - if request.user.is_authenticated: - user_progress = Progression.objects.filter(user=request.user, course=course).first() - - # 2. S'il y a une progression, on extrait juste les IDs des leçons finies - if user_progress: - # values_list renvoie une liste simple : [1, 5, 12] au lieu d'objets lourds - completed_lesson_ids = user_progress.completed_lessons.values_list('id', flat=True) - context = { 'course': course, 'module': module, @@ -141,7 +114,5 @@ def lesson_detail(request, course_slug, module_slug, lesson_slug): 'comment_form': form, 'prev_lesson': prev_lesson, 'next_lesson': next_lesson, - 'user_progress': user_progress, - 'completed_lesson_ids': completed_lesson_ids, } return render(request, 'courses/lesson.html', context) \ No newline at end of file diff --git a/devart/settings.py b/devart/settings.py index f2dab3b..2736323 100644 --- a/devart/settings.py +++ b/devart/settings.py @@ -12,8 +12,6 @@ https://docs.djangoproject.com/en/5.0/ref/settings/ from pathlib import Path import os - -import dotenv from dotenv import load_dotenv import devart.context_processor @@ -55,8 +53,6 @@ INSTALLED_APPS = [ 'users', 'progression', 'blog', - - 'discord_integration.apps.DiscordIntegrationConfig', ] MIDDLEWARE = [ @@ -68,7 +64,7 @@ MIDDLEWARE = [ 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'core.middleware.VisitTrackingMiddleware', + ] ROOT_URLCONF = 'devart.urls' @@ -86,7 +82,6 @@ TEMPLATES = [ 'devart.context_processor.app_version', 'core.context_processor.site_settings', - 'core.context_processor.site_maintenance', 'courses.context_processors.course_list', 'blog.context_processor.posts_list', ], @@ -207,12 +202,4 @@ def get_git_version(): else: return "Version inconnue (Fichier manquant)" -GIT_VERSION = get_git_version() - -EMAIL_BACKEND = dotenv.get_key('.env', 'EMAIL_BACKEND') -EMAIL_HOST = dotenv.get_key('.env', 'EMAIL_HOST') -EMAIL_PORT = dotenv.get_key('.env', 'EMAIL_PORT') -EMAIL_USE_TLS = dotenv.get_key('.env', 'EMAIL_USE_TLS') == 'True' -EMAIL_HOST_USER = dotenv.get_key('.env', 'EMAIL_HOST_USER') -EMAIL_HOST_PASSWORD = dotenv.get_key('.env', 'EMAIL_HOST_PASSWORD') -DEFAULT_FROM_EMAIL = EMAIL_HOST_USER \ No newline at end of file +GIT_VERSION = get_git_version() \ No newline at end of file diff --git a/devart/sitemap.py b/devart/sitemap.py index c675d9d..e761d33 100644 --- a/devart/sitemap.py +++ b/devart/sitemap.py @@ -12,7 +12,7 @@ class CourseSitemap(sitemaps.Sitemap): priority = 0.9 def items(self): - return Course.objects.filter(enable=True).order_by('id') + return Course.objects.filter(enable=True) # Exemple de filtre def location(self, item): # Assure-toi que ton modèle Course a bien une méthode get_absolute_url diff --git a/devart/urls.py b/devart/urls.py index 036a891..b46feda 100644 --- a/devart/urls.py +++ b/devart/urls.py @@ -22,13 +22,12 @@ from django.http import HttpResponse from devart.sitemap import CourseSitemap, StaticViewSitemap from django.contrib.sitemaps.views import sitemap +# La vue pour le robots.txt def robots_txt(request): lines = [ "User-agent: *", "Disallow: /admin/", "Disallow: /users/", - "Disallow: /maintenance/", - "Disallow: /core/", "Allow: /", "Sitemap: https://partirdezero.com/sitemap.xml", # On indique déjà où sera le plan ] @@ -40,12 +39,11 @@ sitemaps_dict = { } urlpatterns = [ - path('core/', include('core.urls')), path('admin/', admin.site.urls), path('', include('home.urls')), path('courses/', include('courses.urls')), path('users/', include('users.urls')), - path('progression/', include('progression.urls')), + path('blog/', include('blog.urls')), path('sitemap.xml', sitemap, {'sitemaps': sitemaps_dict}, name='django.contrib.sitemaps.views.sitemap'), diff --git a/discord_integration/__init__.py b/discord_integration/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/discord_integration/admin.py b/discord_integration/admin.py deleted file mode 100644 index 8c38f3f..0000000 --- a/discord_integration/admin.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.contrib import admin - -# Register your models here. diff --git a/discord_integration/apps.py b/discord_integration/apps.py deleted file mode 100644 index 56fe46c..0000000 --- a/discord_integration/apps.py +++ /dev/null @@ -1,8 +0,0 @@ -from django.apps import AppConfig - - -class DiscordIntegrationConfig(AppConfig): - name = 'discord_integration' - - def ready(self): - import discord_integration.signals diff --git a/discord_integration/core/BotClass.py b/discord_integration/core/BotClass.py deleted file mode 100644 index ac9da47..0000000 --- a/discord_integration/core/BotClass.py +++ /dev/null @@ -1,14 +0,0 @@ -import discord -from discord import app_commands - -intents = discord.Intents.default() -intents.members = True -intents.message_content = True - -class Bot(discord.Client): - def __init__(self, *, intents: discord.Intents): - super().__init__(intents=intents) - self.tree = app_commands.CommandTree(self) - - async def setup_hook(self): - await self.tree.sync() \ No newline at end of file diff --git a/discord_integration/core/__init__.py b/discord_integration/core/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/discord_integration/core/announces_logic.py b/discord_integration/core/announces_logic.py deleted file mode 100644 index 6781202..0000000 --- a/discord_integration/core/announces_logic.py +++ /dev/null @@ -1,57 +0,0 @@ -from asgiref.sync import sync_to_async -import discord -from discord.ext import tasks -from discord_integration.models import DiscordNotification - -def get_pending_notifications(): - return list(DiscordNotification.objects.filter(is_announced=False)) - -def mark_as_done(notif): - notif.is_announced = True - notif.save() - -def process_notifications(): - # Cette fonction fait tout le travail SQL "interdit" en mode async - notifs = list(DiscordNotification.objects.filter(is_announced=False)) - results = [] - for n in notifs: - # Ici, on peut toucher à content_object car on est en mode "sync" ! - obj = n.content_object - if obj: - # 1. On cherche la description, sinon le contenu, sinon rien - teaser = getattr(obj, 'description', getattr(obj, 'content', "")) - - # 2. Sécurité : On coupe à 3000 caractères pour éviter les erreurs Discord - if len(teaser) > 3000: - teaser = teaser[:2997] + "..." - - results.append({ - 'notif_model': n, - 'title': getattr(obj, 'title', getattr(obj, 'name', str(obj))), - 'url': obj.get_absolute_url() if hasattr(obj, 'get_absolute_url') else "#", - 'summary': teaser - }) - - return results - -@tasks.loop(seconds=5.0) -async def check_announcements(client, channel_id): - announcements = await sync_to_async(process_notifications)() - - for data in announcements: - title = data['title'] - link = data['url'] - notif = data['notif_model'] - summary = data['summary'] - - embed = discord.Embed( - title = f"📣 Nouveau contenu : {title}", - url = f"https://partirdezero.com{link}", - description = summary, - color=discord.Color.blue() - ) - - channel_id = client.get_channel(channel_id) - if channel_id: - await channel_id.send(embed=embed) - await sync_to_async(mark_as_done)(notif) diff --git a/discord_integration/core/enums.py b/discord_integration/core/enums.py deleted file mode 100644 index 75c7ced..0000000 --- a/discord_integration/core/enums.py +++ /dev/null @@ -1,16 +0,0 @@ -XP = { - "MESSAGE": 5 -} - -RANK = { - 1: "Nouveau membre", - 3: "Membre", - 7: "Habitué du comptoir", - 12: "Expert", - 18: "Chevalier du code", - 25: "Baron C#", - 35: "Lord Script", - 50: "Héros des architectures", - 75: "Vétéran", - 100: "Légende" -} \ No newline at end of file diff --git a/discord_integration/core/level_logic.py b/discord_integration/core/level_logic.py deleted file mode 100644 index fe42f8f..0000000 --- a/discord_integration/core/level_logic.py +++ /dev/null @@ -1,70 +0,0 @@ -from datetime import datetime, timezone - -from asgiref.sync import sync_to_async -import discord -from datetime import datetime -from discord_integration.models import DiscordLevel -from enums import XP, RANK -from discord import app_commands - -def get_user(id_discord): - user, created = DiscordLevel.objects.get_or_create(discord_id=id_discord) - return user, created - -def update_user_xp(user, xp_to_add): - leveled_up = False - - # 1. Mise à jour de l'XP et du temps - user.total_xp += xp_to_add - user.last_message = datetime.now(timezone.utc) - - # 2. Calcul du niveau théorique - calculated_level = int(0.5 + (0.25 + user.total_xp / 50)**0.5) - - # 3. Vérification du Level Up - if calculated_level > user.level: - user.level = calculated_level - leveled_up = True - - new_rank = RANK[1] - - for level_threshold, rank_name in RANK.items(): - if user.level >= level_threshold: - new_rank = rank_name - else: - break - - user.rank = new_rank - - user.save() - return user, leveled_up - -async def check_add_xp(message, client): - id_discord = message.author.id - username = message.author.name - - user_db, created = await sync_to_async(get_user)(id_discord) - - if not created and user_db.last_message: - delta = datetime.now(timezone.utc) - user_db.last_message - if delta.seconds < 6: - return - - user_db, leveled_up = await sync_to_async(update_user_xp)(user_db, XP["MESSAGE"]) - - if leveled_up: - # On crée un petit message sympa - await message.channel.send( - f"🎊 **LEVEL UP** 🎊\nBravo {message.author.mention}, tu passes **niveau {user_db.level}** ! " - f"On applaudit tous bien fort ! Clap Clap !!" - ) - else: - # Juste un petit log console pour toi - print(f"✨ XP ajouté pour {message.author.name} (Total: {user_db.total_xp})") - -# AJOUT DES COMMANDES /xp et /level -@app_commands.command(name="level", description="Permet de connaitre ton xp actuel") -async def get_xp(interaction: discord.Interaction): - user_id = interaction.user.id - user_db, _ = await sync_to_async(get_user)(user_id) - await interaction.response.send_message(f"{interaction.user.mention} : Tu as {user_db.total_xp} XP et tu es niveau {user_db.level} !\nTon rang est **{user_db.rank}** !") \ No newline at end of file diff --git a/discord_integration/core/main_bot.py b/discord_integration/core/main_bot.py deleted file mode 100644 index 6fa7d4c..0000000 --- a/discord_integration/core/main_bot.py +++ /dev/null @@ -1,73 +0,0 @@ -import discord -import django -import os, sys - -import dotenv -from discord.ext import commands - -# On import django pour communiquer avec -BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -if BASE_DIR not in sys.path: - sys.path.insert(0, BASE_DIR) -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'devart.settings') -django.setup() - -# Import des fonctions -from role_logic import check_role_reaction -from announces_logic import check_announcements -from random_phrase import get_random_phrase -from level_logic import check_add_xp, get_xp -import BotClass - -# CONFIGURATION -TOKEN = dotenv.get_key(BASE_DIR + '/.env', 'D_TOKEN') -MESSAGE_ID = 1450928822156263505 # L'ID du message des règles (clic droit > Copier l'identifiant) -ROLE_ID = 1450920002868875435 # L'ID du rôle "Membres" -ANNOUNCEMENT_CHANNEL_ID = 1450912559774306346 -EMOJI_VALIDATION = "✅" - -# LES INTENTS (PERMISSIONS DU BOT) -intents = discord.Intents.default() -intents.members = True # Important pour pouvoir donner des rôles -intents.message_content = True - -client = BotClass.Bot(intents=intents) -client.tree.add_command(get_random_phrase) -client.tree.add_command(get_xp) - -@client.event -async def on_ready(): - print(f'✅ Bot connecté : {client.user}') - - try: - synced = await client.tree.sync() - print(f"🌍 {len(synced)} commandes slash synchronisées !") - except Exception as e: - print(f"❌ Erreur de synchronisation : {e}") - - if not check_announcements.is_running(): - check_announcements.start(client, ANNOUNCEMENT_CHANNEL_ID) - -@client.event -async def on_message(message): - if message.author == client.user: - return - if message.guild is None: - author = message.author - await message.channel.send("Bonjour !\nJe suis un bot destiné à tester les nouvelles fonctionnalités de Discord. Pour le moment, je suis qu'en lecture seule.") - else: - await check_add_xp(message, client) - - -@client.event -async def on_raw_reaction_add(payload): - # On envoie tout le nécessaire à notre fonction dans role_logic.py - await check_role_reaction( - payload, - client, - MESSAGE_ID, - ROLE_ID, - EMOJI_VALIDATION - ) - -client.run(TOKEN) diff --git a/discord_integration/core/random_phrase.py b/discord_integration/core/random_phrase.py deleted file mode 100644 index e8cfea2..0000000 --- a/discord_integration/core/random_phrase.py +++ /dev/null @@ -1,32 +0,0 @@ -import discord -import random - -from discord import app_commands - -phrase = [ - "Yo !", - "Quoi de neuf ?", - "Je suis occupé à compter mes octets.", - "Vive la Belgique ! 🇧🇪", - "C’est pas un bug, c’est une fonctionnalité non documentée ! 🐛", - "Est-ce qu’on peut dire que mon code est une œuvre d’art ? Non ? Dommage.", - "Je ne plante pas, je fais une pause créative.", - "Quelqu’un a vu mon point-virgule ? Il a disparu depuis le dernier commit.", - "Je mangerais bien une mitraillette sauce andalouse, mais mon système digestif est en 404. 🍟", - "42. Voilà. Maintenant, pose-moi une vraie question.", - "On mange quoi ? Ah non, c'est vrai, je suis un robot... Tristesse infinie. 🤖", - "C'est écrit en Python, donc c'est forcément élégant, non ?", - "Un petit café ? Pour moi, une petite dose d'électricité suffira.", - "Je parie que tu n'as pas encore fait ton `git push` aujourd'hui. Je te surveille ! 👀", - "En Belgique, on n'a peut-être pas toujours du soleil, mais on a les meilleures frites ! 🇧🇪🍟", - "Il y a 10 types de personnes : celles qui comprennent le binaire, et les autres.", - "Mon processeur chauffe... soit je réfléchis trop, soit ton code est trop complexe !", - "Tout va bien, tant que personne ne touche au dossier `migrations` de Django...", - "Sais-tu pourquoi les développeurs détestent la nature ? Parce qu'il y a trop de bugs. 🌳", - "On n'est pas là pour trier des lentilles, une fois ! On code ou quoi ? 🇧🇪" -] - -@app_commands.command(name="random_phrase", description="Envoi une phrase aléatoire !") -async def get_random_phrase(interaction: discord.Interaction): - choice = random.choice(phrase) - await interaction.response.send_message(choice) \ No newline at end of file diff --git a/discord_integration/core/role_logic.py b/discord_integration/core/role_logic.py deleted file mode 100644 index 8dd2dbc..0000000 --- a/discord_integration/core/role_logic.py +++ /dev/null @@ -1,39 +0,0 @@ -import discord - - -async def check_role_reaction(payload, client, target_message_id, target_role_id, target_emoji): - # 1. On vérifie si c'est le bon message - if payload.message_id != target_message_id: - return # On ignore si ce n'est pas le bon message - - # 2. On vérifie si c'est le bon emoji - if str(payload.emoji) == target_emoji: - guild = client.get_guild(payload.guild_id) - if guild is None: - print("Erreur: Impossible de trouver le serveur (Guild is None).") - return - - member = guild.get_member(payload.user_id) - if member is None: - print("Erreur: Impossible de trouver le membre (Member is None).") - return - - role = guild.get_role(target_role_id) - if role is None: - print("Erreur : Le role n'existe pas.") - return - - try: - await member.add_roles(role) - print(f"🎉 SUCCÈS : Rôle donné à {member.name} !") - try: - await member.send("Bienvenue ! Tu as accès aux salons.") - except: - print("Note: MP bloqués par l'utilisateur.") - except discord.Forbidden: - print("⛔ ERREUR PERMISSION : Je n'ai pas le droit de donner ce rôle !") - print( - "👉 SOLUTION : Va dans Paramètres Serveur > Rôles. Glisse le rôle 'PartirDeZero Bot' AU-DESSUS du rôle 'Membres'.") - - except Exception as e: - print(f"❌ Erreur inconnue : {e}") \ No newline at end of file diff --git a/discord_integration/migrations/0001_initial.py b/discord_integration/migrations/0001_initial.py deleted file mode 100644 index 6c269a9..0000000 --- a/discord_integration/migrations/0001_initial.py +++ /dev/null @@ -1,26 +0,0 @@ -# Generated by Django 6.0 on 2025-12-18 08:23 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('contenttypes', '0002_remove_content_type_name'), - ] - - operations = [ - migrations.CreateModel( - name='DiscordNotification', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('object_id', models.PositiveIntegerField()), - ('is_announced', models.BooleanField(default=False)), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')), - ], - ), - ] diff --git a/discord_integration/migrations/0002_discordlevel.py b/discord_integration/migrations/0002_discordlevel.py deleted file mode 100644 index b35f4b8..0000000 --- a/discord_integration/migrations/0002_discordlevel.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 6.0 on 2025-12-18 11:57 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('discord_integration', '0001_initial'), - ] - - operations = [ - migrations.CreateModel( - name='DiscordLevel', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('discord_id', models.BigIntegerField()), - ('total_xp', models.PositiveIntegerField(default=0)), - ('level', models.PositiveIntegerField(default=1)), - ('rank', models.TextField(default='Nouveau membre')), - ('last_message', models.DateTimeField(auto_now_add=True)), - ], - ), - ] diff --git a/discord_integration/migrations/__init__.py b/discord_integration/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/discord_integration/models.py b/discord_integration/models.py deleted file mode 100644 index 614a096..0000000 --- a/discord_integration/models.py +++ /dev/null @@ -1,20 +0,0 @@ -from django.contrib.contenttypes.fields import GenericForeignKey -from django.db import models -from django.contrib.contenttypes.models import ContentType - -class DiscordNotification(models.Model): - content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) - object_id = models.PositiveIntegerField() - content_object = GenericForeignKey('content_type', 'object_id') - is_announced = models.BooleanField(default=False) - created_at = models.DateTimeField(auto_now_add=True) - - def __str__(self): - return f"Annonces pour {self.content_object} ({'✅' if self.is_announced else '⏳'})" - -class DiscordLevel(models.Model): - discord_id = models.BigIntegerField() - total_xp = models.PositiveIntegerField(default=0) - level = models.PositiveIntegerField(default=1) - rank = models.TextField(default="Nouveau membre") - last_message = models.DateTimeField(auto_now_add=True) \ No newline at end of file diff --git a/discord_integration/signals.py b/discord_integration/signals.py deleted file mode 100644 index cd5897e..0000000 --- a/discord_integration/signals.py +++ /dev/null @@ -1,18 +0,0 @@ -from django.db.models.signals import post_save -from django.dispatch import receiver -from .models import DiscordNotification - -@receiver(post_save, sender="blog.Post") -def create_discord_notification_blog(sender, instance, created, **kwargs): - if created: - DiscordNotification.objects.create(content_object=instance) - -@receiver(post_save, sender="courses.Course") -def create_discord_notification_course(sender, instance, created, **kwargs): - if created: - DiscordNotification.objects.create(content_object=instance) - -@receiver(post_save, sender="courses.Lesson") -def create_discord_notification_lesson(sender, instance, created, **kwargs): - if created: - DiscordNotification.objects.create(content_object=instance) \ No newline at end of file diff --git a/discord_integration/tests.py b/discord_integration/tests.py deleted file mode 100644 index 7ce503c..0000000 --- a/discord_integration/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/discord_integration/views.py b/discord_integration/views.py deleted file mode 100644 index 91ea44a..0000000 --- a/discord_integration/views.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.shortcuts import render - -# Create your views here. diff --git a/home/urls.py b/home/urls.py index 52e2435..a1ea3c1 100644 --- a/home/urls.py +++ b/home/urls.py @@ -5,8 +5,4 @@ app_name = 'home' urlpatterns = [ path('', views.home, name='home'), path('premium/', views.premium, name='premium'), - # Tableau de bord statistiques (réservé superadministrateurs) - path('dashboard/stats/', views.stats_dashboard, name='stats_dashboard'), - path('dashboard/stats/charts/', views.stats_charts, name='stats_charts'), - path('dashboard/stats/live-activity/', views.live_activity, name='live_activity'), ] \ No newline at end of file diff --git a/home/views.py b/home/views.py index 22ab337..5bd3838 100644 --- a/home/views.py +++ b/home/views.py @@ -1,58 +1,5 @@ from django.shortcuts import render, get_object_or_404 -from django.contrib.auth.decorators import user_passes_test -from django.utils import timezone -from django.db.models import Count -from core.models import Visit -from django.views.decorators.cache import cache_page -from django.contrib.auth.models import User -from courses.models import Course, Lesson -from blog.models import Post -from progression.models import Progression -import json -from django.http import JsonResponse - - -# -------------------- -# Helpers Stats Module -# -------------------- - -def _parse_period(request, default=30, options=None): - """Parse period parameter 'p' from request and compute date range. - - Returns (p, now_dt, start_dt, start_date, end_date) - - now_dt is timezone-aware now - - start_dt is datetime at start of range (inclusive) - - start_date/end_date are date objects for convenient filtering - """ - if options is None: - options = [7, 30, 90, 180] - try: - p = int(request.GET.get('p', default)) - except (TypeError, ValueError): - p = default - if p not in options: - p = default - now_dt = timezone.now() - start_dt = now_dt - timezone.timedelta(days=p - 1) - return p, now_dt, start_dt, start_dt.date(), now_dt.date() - - -def _build_series_for_range(start_date, end_date, qs, date_key='day', count_key='c'): - """Build a continuous daily series from an aggregated queryset. - - qs must yield dicts with date_key (date as string or date) and count_key. - Returns (labels_days, values_counts) - """ - counts = {str(item[date_key]): item[count_key] for item in qs} - days = [] - values = [] - d = start_date - while d <= end_date: - key = str(d) - days.append(key) - values.append(counts.get(key, 0)) - d += timezone.timedelta(days=1) - return days, values +from courses.models import Course def home(request): courses = Course.objects.order_by('-created_at')[:6] @@ -62,295 +9,3 @@ def premium(request, course_id): """Landing page présentant les avantages du Premium.""" course = get_object_or_404(Course, pk=course_id) return render(request, 'premium.html', {'course': course}) - - -# 15 minutes de cache sur la page complète (sauf si décochée plus tard pour des blocs en direct) -@user_passes_test(lambda u: u.is_superuser) -@cache_page(60 * 15) -def stats_dashboard(request): - """ Tableau de bord statistiques réservé aux superadministrateurs. - Périodes supportées: 7, 30, 90, 180 jours (GET param 'p'). """ - - # Période - period_options = [7, 30, 90, 180] - p, now, start_dt, period_start_date, period_end_date = _parse_period( - request, default=30, options=period_options - ) - - # Utilisateurs - total_users = User.objects.count() - new_users_qs = User.objects.filter(date_joined__date__gte=period_start_date, date_joined__date__lte=period_end_date) - # Séries quotidiennes nouveaux utilisateurs - new_users_by_day = ( - new_users_qs - .extra(select={'day': "date(date_joined)"}) - .values('day') - .annotate(c=Count('id')) - ) - - # Activité approximée via Progression mise à jour - active_users_qs = ( - Progression.objects.filter(updated_at__date__gte=period_start_date, updated_at__date__lte=period_end_date) - .values('user').distinct() - ) - active_users_count = active_users_qs.count() - - # Cours - total_courses = Course.objects.count() - total_courses_enabled = Course.objects.filter(enable=True).count() - new_courses_by_day = ( - Course.objects.filter(created_at__date__gte=period_start_date, created_at__date__lte=period_end_date) - .extra(select={'day': "date(created_at)"}) - .values('day').annotate(c=Count('id')) - ) - - # Leçons - total_lessons = Lesson.objects.count() - - # Achèvements de leçons (via table de liaison M2M) - through = Progression.completed_lessons.through - lesson_completions_by_day = ( - through.objects.filter( - progression__updated_at__date__gte=start_dt.date(), - progression__updated_at__date__lte=now.date(), - ) - .extra(select={'day': "date(progression_updated_at)"}) if 'progression_updated_at' in [f.name for f in through._meta.fields] - else through.objects.extra(select={'day': "date(created_at)"}) # fallback si champs créé n'existe pas - ) - # Si la table M2M n'a pas de timestamps, on utilisera updated_at de Progression pour l'activité par jour - # donc on refait une série quotidienne d'activité progression - progress_activity_by_day = ( - Progression.objects.filter(updated_at__date__gte=period_start_date, updated_at__date__lte=period_end_date) - .extra(select={'day': "date(updated_at)"}) - .values('day').annotate(c=Count('id')) - ) - - # Blog - total_posts = Post.objects.count() - new_posts_by_day = ( - Post.objects.filter(created_at__date__gte=period_start_date, created_at__date__lte=period_end_date) - .extra(select={'day': "date(created_at)"}) - .values('day').annotate(c=Count('id')) - ) - - # Revenus/Paiements & Technique (placeholders faute de sources) - revenus_disponibles = False - technique_disponible = False - - # Visites / Trafic - period_visits = Visit.objects.filter(date__gte=period_start_date, date__lte=period_end_date) - - unique_visitors = period_visits.values('visitor_id').distinct().count() - - earlier_visitors_qs = Visit.objects.filter(date__lt=period_start_date).values('visitor_id').distinct() - returning_visitors = period_visits.filter(visitor_id__in=earlier_visitors_qs).values('visitor_id').distinct().count() - - converted_visitors = ( - period_visits - .filter(became_user_at__isnull=False, became_user_at__date__gte=period_start_date, became_user_at__date__lte=period_end_date) - .values('visitor_id').distinct().count() - ) - - top_sources_qs = ( - period_visits - .values('source') - .annotate(c=Count('visitor_id', distinct=True)) - .order_by('-c') - ) - top_countries_qs = ( - period_visits - .exclude(country='') - .values('country') - .annotate(c=Count('visitor_id', distinct=True)) - .order_by('-c') - ) - - top_sources_table = [(row['source'] or 'Direct/Unknown', row['c']) for row in top_sources_qs[:10]] - top_countries_table = [(row['country'], row['c']) for row in top_countries_qs[:10]] - - # Helper pour avoir toutes les dates de la période et remplir les trous - def build_series_dict(qs, date_key='day', count_key='c'): - # Wrapper conservant l'API locale mais utilisant le helper commun - return _build_series_for_range(period_start_date, period_end_date, qs, date_key=date_key, count_key=count_key) - - days_users, values_new_users = build_series_dict(new_users_by_day) - days_courses, values_new_courses = build_series_dict(new_courses_by_day) - days_posts, values_new_posts = build_series_dict(new_posts_by_day) - days_activity, values_activity = build_series_dict(progress_activity_by_day) - - # Tables simples (jour, valeur) - new_users_table = list(zip(days_users, values_new_users)) - new_courses_table = list(zip(days_courses, values_new_courses)) - - context = { - 'period_options': period_options, - 'p': p, - 'start_date': period_start_date, - 'end_date': period_end_date, - - # KPI - 'kpi': { - 'total_users': total_users, - 'new_users_period': sum(values_new_users), - 'active_users_period': active_users_count, - 'unique_visitors': unique_visitors, - 'returning_visitors': returning_visitors, - 'converted_visitors': converted_visitors, - 'total_courses': total_courses, - 'courses_enabled': total_courses_enabled, - 'total_lessons': total_lessons, - 'total_posts': total_posts, - }, - - # Séries pour graphiques - 'series': { - 'days': days_users, # mêmes intervalles pour tous - 'new_users': values_new_users, - 'new_courses': values_new_courses, - 'new_posts': values_new_posts, - 'activity_progress': values_activity, - }, - - # Disponibilité des sections - 'revenus_disponibles': revenus_disponibles, - 'technique_disponible': technique_disponible, - - # Tables - 'new_users_table': new_users_table, - 'new_courses_table': new_courses_table, - 'top_sources_table': top_sources_table, - 'top_countries_table': top_countries_table, - } - - # Sérialisation JSON pour Chart.js - context['series_json'] = json.dumps(context['series']) - context['labels_json'] = json.dumps(context['series']['days']) - - return render(request, 'home/stats_dashboard.html', context) - - -@user_passes_test(lambda u: u.is_superuser) -@cache_page(60 * 15) -def stats_charts(request): - """Page dédiée aux graphiques (réservée superadmins).""" - # Période (utilise les mêmes helpers que le dashboard pour harmonisation) - period_options = [7, 30, 90, 180] - p, _now, _start_dt, period_start_date, period_end_date = _parse_period( - request, default=30, options=period_options - ) - - # Trafic par jour (visiteurs uniques) - visits_qs = ( - Visit.objects - .filter(date__gte=period_start_date, date__lte=period_end_date) - .values('date') - .annotate(c=Count('visitor_id', distinct=True)) - .order_by('date') - ) - - # Conversions par jour (visiteurs devenus utilisateurs) - conversions_qs = ( - Visit.objects - .filter( - became_user_at__isnull=False, - became_user_at__date__gte=period_start_date, - became_user_at__date__lte=period_end_date, - ) - .extra(select={'day': "date(became_user_at)"}) - .values('day') - .annotate(c=Count('visitor_id', distinct=True)) - .order_by('day') - ) - - def build_series_dict(qs, date_key='date', count_key='c'): - counts = {str(item[date_key]): item[count_key] for item in qs} - days = [] - values = [] - d = period_start_date - while d <= period_end_date: - key = str(d) - days.append(key) - values.append(counts.get(key, 0)) - d += timezone.timedelta(days=1) - return days, values - - labels, visitors_series = _build_series_for_range(period_start_date, period_end_date, visits_qs, date_key='date') - _, conversions_series = _build_series_for_range(period_start_date, period_end_date, conversions_qs, date_key='day') - - # Sources & Pays (sur la période) - period_visits = Visit.objects.filter(date__gte=period_start_date, date__lte=period_end_date) - top_sources_qs = ( - period_visits - .values('source') - .annotate(c=Count('visitor_id', distinct=True)) - .order_by('-c')[:10] - ) - top_countries_qs = ( - period_visits - .exclude(country='') - .values('country') - .annotate(c=Count('visitor_id', distinct=True)) - .order_by('-c')[:10] - ) - - sources_labels = [(row['source'] or 'Direct/Unknown') for row in top_sources_qs] - sources_values = [row['c'] for row in top_sources_qs] - countries_labels = [row['country'] for row in top_countries_qs] - countries_values = [row['c'] for row in top_countries_qs] - - context = { - 'period_options': period_options, - 'p': p, - 'start_date': period_start_date, - 'end_date': period_end_date, - 'labels_json': json.dumps(labels), - 'visitors_series_json': json.dumps(visitors_series), - 'conversions_series_json': json.dumps(conversions_series), - 'sources_labels_json': json.dumps(sources_labels), - 'sources_values_json': json.dumps(sources_values), - 'countries_labels_json': json.dumps(countries_labels), - 'countries_values_json': json.dumps(countries_values), - } - - return render(request, 'home/stats_charts.html', context) - - -@user_passes_test(lambda u: u.is_superuser) -def live_activity(request): - """Retourne en JSON l'activité récente (5 dernières minutes): - visiteurs et utilisateurs et leur page actuelle. - """ - now = timezone.now() - since = now - timezone.timedelta(minutes=5) - qs = ( - Visit.objects - .filter(last_seen__gte=since) - .order_by('-last_seen') - ) - - data = [] - for v in qs[:200]: - username = None - is_user = False - if v.user_id: - is_user = True - # safe access if user deleted - try: - username = v.user.username - except Exception: - username = 'Utilisateur' - visitor_label = v.visitor_id[:8] - seconds_ago = int((now - v.last_seen).total_seconds()) - data.append({ - 'visitor': visitor_label, - 'is_user': is_user, - 'username': username, - 'path': v.path, - 'last_seen': v.last_seen.isoformat(), - 'seconds_ago': seconds_ago, - 'date': str(v.date), - 'country': v.country, - 'source': v.source, - }) - - return JsonResponse({'now': now.isoformat(), 'items': data}) diff --git a/progression/urls.py b/progression/urls.py index cef076c..a2fd910 100644 --- a/progression/urls.py +++ b/progression/urls.py @@ -2,9 +2,6 @@ from django.urls import path from . import views app_name = 'progression' - urlpatterns = [ - # On ne garde QUE la route AJAX ici. - # Les routes 'list', 'detail', etc. doivent rester dans courses/urls.py - path('ajax/toggle-lesson/', views.toggle_lesson_completion, name='toggle_lesson'), + ] \ No newline at end of file diff --git a/progression/views.py b/progression/views.py index 1cee11c..91ea44a 100644 --- a/progression/views.py +++ b/progression/views.py @@ -1,41 +1,3 @@ -from django.shortcuts import render, get_object_or_404 -import json -from django.http import JsonResponse -from django.views.decorators.http import require_POST -from django.contrib.auth.decorators import login_required -from progression.models import Progression -from courses.models import Lesson +from django.shortcuts import render -@login_required -@require_POST -def toggle_lesson_completion(request): - data = json.loads(request.body) - lesson_id = data.get('lesson_id') - - lesson = get_object_or_404(Lesson, id=lesson_id) - # On remonte au cours via le module (Lesson -> Module -> Course) - course = lesson.module.course - - # On récupère ou crée la progression - progression, created = Progression.objects.get_or_create( - user=request.user, - course=course - ) - - # La logique du Toggle - if lesson in progression.completed_lessons.all(): - progression.completed_lessons.remove(lesson) - is_completed = False - else: - progression.completed_lessons.add(lesson) - is_completed = True - - # Mise à jour de la dernière leçon vue - progression.last_viewed_lesson = lesson - progression.save() - - return JsonResponse({ - 'status': 'success', - 'is_completed': is_completed, - 'new_percent': progression.percent_completed - }) \ No newline at end of file +# Create your views here. diff --git a/static/css/app.css b/static/css/app.css index 2531ab5..d95a712 100644 --- a/static/css/app.css +++ b/static/css/app.css @@ -247,9 +247,9 @@ html { box-sizing: border-box; } gap: 6px; padding: 2px 8px; margin-left: 8px; - border-radius: 2px; + border-radius: 999px; background: var(--accent); - color: var(--text); + color: var(--warning-contrast); font-size: 11px; font-weight: 800; letter-spacing: .3px; @@ -259,40 +259,6 @@ html { box-sizing: border-box; } white-space: nowrap; } -.current-tag { - display: inline-flex; - align-items: center; - gap: 6px; - padding: 2px 8px; - border-radius: 2px; - background: var(--primary); - color: var(--primary-contrast); - font-size: 11px; - font-weight: 800; - letter-spacing: .3px; - line-height: 1.2; - vertical-align: middle; - text-transform: uppercase; - white-space: nowrap; -} - -.completed-tag { - display: inline-flex; - align-items: center; - gap: 6px; - padding: 2px 8px; - border-radius: 2px; - background: var(--success); - color: var(--success-contrast); - font-size: 11px; - font-weight: 800; - letter-spacing: .3px; - line-height: 1.2; - vertical-align: middle; - text-transform: uppercase; - white-space: nowrap; -} - .courseToc .tocLink.disabled { color: var(--text-muted); background: transparent; @@ -303,70 +269,6 @@ html { box-sizing: border-box; } font-weight: 500; } -/* Progression des cours */ - -.progress-container { - display: flex; - flex-direction: row; -} - -.progress-bar { - display: flex; - border-radius: var(--r-2); - border: 1px solid var(--border); - height: 20px; - background: var(--neutral-900); -} - -.progress-text { - display: flex; - align-items: center; - justify-content: center; - gap: 4px; - color: var(--text); - font-size: 12px; - margin-left: 10px; -} - -.progress-bar-fill { - background: var(--success); - height: 100%; - border-radius: var(--r-2) 0 0 var(--r-2); -} - -.course-completed { - display: flex; - flex-direction: row; - border-radius: var(--r-2); - border: 1px solid var(--border); - margin: var(--space-2) 0; - background: var(--success); -} - -.course-completed .container { - display: flex; - flex-direction: column; - padding: var(--space-2); - height: 100%; - vertical-align: middle; -} - -.course-completed .container .icon { - color: var(--success-contrast); - font-size: 50px; -} - -.course-completed .container .title { - color: var(--success-contrast); - font-size: 28px; - font-weight: 600; -} - -.course-completed .container .content { - color: var(--success-contrast); - font-size: 14px; -} - [data-theme='light'] { /* Palette: plus nuancé, moins "blanc" */ --bg: #eef3f7; /* fond légèrement teinté bleu-gris */ @@ -2330,16 +2232,6 @@ input[type="text"], input[type="email"], input[type="password"], textarea { filter: brightness(1.05); } -.btn-warning, .button-warning { - background-color: var(--warning); - border-color: var(--warning); - color: var(--warning-contrast); -} - -.btn-warning:hover, .button-warning:hover { - filter: brightness(1.05); -} - /* Tailles */ .btn-sm, .button-sm { padding: 6px 10px; font-size: 0.9rem; } .btn-lg, .button-lg { padding: 12px 20px; font-size: 1.05rem; } @@ -2672,22 +2564,4 @@ ul.flash_messages li.error { ul.flash_messages li.success { background-color: var(--success); color: var(--success-contrast); -} - -.message-warning { - color: var(--neutral-900); - background: var(--neutral-200); - border: 1px solid var(--warning); - padding: 10px; - border-radius: 5px; - margin-bottom: 10px; -} - -.message-info { - color: var(--neutral-900); - background: var(--neutral-200); - border: 1px solid var(--primary); - padding: 10px; - border-radius: 5px; - margin-bottom: 10px; } \ No newline at end of file diff --git a/static/css/christmas.css b/static/css/christmas.css deleted file mode 100644 index 19cf591..0000000 --- a/static/css/christmas.css +++ /dev/null @@ -1,99 +0,0 @@ -/* PartirDeZero — Décos de Noël (chargées uniquement en décembre) */ - -/* Barre festive discrète sous la navbar */ -.site-nav { position: relative; } -.site-nav::after { - content: ""; - position: absolute; - left: 0; right: 0; bottom: -1px; - height: 4px; - background: repeating-linear-gradient(45deg, - #d61c4e 0 12px, - #1b8f3a 12px 24px, - #ffffff 24px 36px); - opacity: .55; - pointer-events: none; -} - -/* Emoji sapin à côté du titre */ -.pdz-festive-emoji { - margin-left: .35rem; - filter: drop-shadow(0 1px 0 rgba(0,0,0,.2)); -} - -/* Overlay neige — ultra léger, non bloquant */ -.pdz-snow { - position: fixed; - inset: 0; - pointer-events: none; - z-index: 5; /* au-dessus du fond, sous les modales si existantes */ - background-image: - radial-gradient(2px 2px at 20px 30px, rgba(255,255,255,.9) 50%, rgba(255,255,255,0) 51%), - radial-gradient(3px 3px at 80px 120px, rgba(255,255,255,.8) 50%, rgba(255,255,255,0) 51%), - radial-gradient(2px 2px at 150px 80px, rgba(255,255,255,.85) 50%, rgba(255,255,255,0) 51%), - radial-gradient(3px 3px at 250px 20px, rgba(255,255,255,.8) 50%, rgba(255,255,255,0) 51%); - background-repeat: repeat; - background-size: 300px 200px, 400px 300px, 350px 250px, 500px 400px; - animation: pdzSnowFall 18s linear infinite; -} - -/* Plusieurs couches pour un effet de profondeur via parallax */ -.pdz-snow::before, -.pdz-snow::after { - content: ""; - position: absolute; inset: 0; - background-image: - radial-gradient(2px 2px at 40px 60px, rgba(255,255,255,.8) 50%, rgba(255,255,255,0) 51%), - radial-gradient(3px 3px at 120px 200px, rgba(255,255,255,.75) 50%, rgba(255,255,255,0) 51%), - radial-gradient(2px 2px at 220px 160px, rgba(255,255,255,.8) 50%, rgba(255,255,255,0) 51%); - background-repeat: repeat; -} -.pdz-snow::before { - background-size: 260px 180px, 380px 280px, 320px 220px; - animation: pdzSnowFallSlow 28s linear infinite; -} -.pdz-snow::after { - background-size: 200px 140px, 300px 220px, 260px 200px; - animation: pdzSnowFallFast 12s linear infinite; -} - -@keyframes pdzSnowFall { - from { transform: translateY(-10%); } - to { transform: translateY(100%); } -} -@keyframes pdzSnowFallSlow { - from { transform: translateY(-10%); } - to { transform: translateY(100%); } -} -@keyframes pdzSnowFallFast { - from { transform: translateY(-10%); } - to { transform: translateY(100%); } -} - -/* Respect des préférences d'accessibilité */ -@media (prefers-reduced-motion: reduce) { - .pdz-snow, .pdz-snow::before, .pdz-snow::after { animation: none; } -} - -/* ——— Footer: petite touche festive discrète ——— */ -footer.pdz-xmas { position: relative; } -footer.pdz-xmas::before { - content: ""; - position: absolute; - left: 0; right: 0; top: 0; - height: 4px; - background: repeating-linear-gradient(45deg, - #d61c4e 0 12px, - #1b8f3a 12px 24px, - #ffffff 24px 36px); - opacity: .55; - pointer-events: none; -} - -.footer-legal .pdz-holiday-greeting { - display: inline-flex; - align-items: center; - gap: .35rem; - color: var(--fg); - font-weight: 500; -} diff --git a/static/js/functions.js b/static/js/functions.js index 500562a..db8ff92 100644 --- a/static/js/functions.js +++ b/static/js/functions.js @@ -94,4 +94,28 @@ document.addEventListener('DOMContentLoaded', function() { }); } } catch(e) {} -}); \ No newline at end of file +}); + +// Fonction pour générer des flocons de neige +function createSnowflake() { + const snowflake = document.createElement('div'); + snowflake.classList.add('snowflake'); + snowflake.textContent = '•'; + + snowflake.style.left = `${Math.random() * 100}vw`; + + const size = Math.random() * 1.5 + 0.5; + snowflake.style.fontSize = `${size}em`; + + const duration = Math.random() * 5 + 5; + snowflake.style.animationDuration = `${duration}s`; + + document.body.appendChild(snowflake); + + setTimeout(() => { + snowflake.remove(); + }, duration * 1000); +} + +// On génère les flocons toutes les 300ms +setInterval(createSnowflake, 300); \ No newline at end of file diff --git a/staticfiles/js/functions.js b/staticfiles/js/functions.js index 500562a..d5195f9 100644 --- a/staticfiles/js/functions.js +++ b/staticfiles/js/functions.js @@ -12,86 +12,4 @@ function show(id) { htmlChange.id = `show-${id}`; let buttonChange = document.getElementById(id); buttonChange.onclick = function() { hide(id); }; -} - -// Fonction pour supprimer le message d'alerte après 5 secondes -document.addEventListener('DOMContentLoaded', function() { - let messages = document.querySelector('.flash_messages') - if (messages) { - setTimeout(function() { - messages.style.opacity = 0; - setTimeout(function() { - messages.style.display = 'none'; - }, 1000); - }, 5000); - } - - // Theme toggle setup - var toggle = document.getElementById('themeToggle'); - var themeLink = document.getElementById('theme-css'); - function applyTheme(theme) { - document.documentElement.setAttribute('data-theme', theme); - if (theme === 'light') { - if (themeLink) themeLink.href = themeLink.href.replace('colors_dark.css', 'colors_light.css'); - } else { - if (themeLink) themeLink.href = themeLink.href.replace('colors_light.css', 'colors_dark.css'); - } - var icon = toggle && toggle.querySelector('i'); - if (icon) { - icon.classList.remove('fa-sun','fa-moon'); - icon.classList.add(theme === 'light' ? 'fa-moon' : 'fa-sun'); - } - if (toggle) { - toggle.setAttribute('aria-pressed', theme === 'dark' ? 'true' : 'false'); - toggle.setAttribute('aria-label', theme === 'light' ? 'Activer le thème sombre' : 'Activer le thème clair'); - toggle.title = toggle.getAttribute('aria-label'); - } - } - try { - var current = document.documentElement.getAttribute('data-theme') || 'dark'; - applyTheme(current); - if (toggle) { - toggle.addEventListener('click', function() { - var next = (document.documentElement.getAttribute('data-theme') === 'light') ? 'dark' : 'light'; - localStorage.setItem('pdz-theme', next); - applyTheme(next); - }); - } - } catch(e) {} - - // Mobile nav toggle - try { - var navToggle = document.getElementById('navToggle'); - var primaryNav = document.getElementById('primaryNav'); - if (navToggle && primaryNav) { - function setExpanded(expanded) { - navToggle.setAttribute('aria-expanded', expanded ? 'true' : 'false'); - navToggle.setAttribute('aria-label', expanded ? 'Fermer le menu' : 'Ouvrir le menu'); - var icon = navToggle.querySelector('i'); - if (icon) { - icon.classList.remove('fa-bars','fa-xmark'); - icon.classList.add(expanded ? 'fa-xmark' : 'fa-bars'); - } - primaryNav.classList.toggle('is-open', expanded); - } - - navToggle.addEventListener('click', function() { - var expanded = navToggle.getAttribute('aria-expanded') === 'true'; - setExpanded(!expanded); - }); - - // Close menu when a link is clicked (on small screens) - primaryNav.addEventListener('click', function(e) { - var target = e.target; - if (target.tagName === 'A' || target.closest('a')) { - setExpanded(false); - } - }); - - // Close on Escape - document.addEventListener('keydown', function(e) { - if (e.key === 'Escape') setExpanded(false); - }); - } - } catch(e) {} -}); \ No newline at end of file +} \ No newline at end of file diff --git a/templates/courses/partials/_course_header.html b/templates/courses/partials/_course_header.html index 33fbd36..bff069f 100644 --- a/templates/courses/partials/_course_header.html +++ b/templates/courses/partials/_course_header.html @@ -4,22 +4,3 @@ Un cours proposé par {{ course.author }}

{{ course.content }}

-Progression pour ce cours -{% if user_progress.percent_completed == 100 %} -
-
-
-
-
-
Félicitation
-
Tu as terminé(e) ce cours ! Tu peux réellement être fier(e) de toi !!
-
-
-{% else %} -
-
-
-
-
{{ user_progress.percent_completed }}%
-
-{% endif %} diff --git a/templates/courses/partials/_course_toc.html b/templates/courses/partials/_course_toc.html index 1389e55..9039157 100644 --- a/templates/courses/partials/_course_toc.html +++ b/templates/courses/partials/_course_toc.html @@ -13,8 +13,7 @@
  • {{ item.name }} {% if item.is_premium %}PREMIUM{% endif %} - {% if lesson and lesson.id == item.id %}(cours actuel){% endif %} - {% if item.id in completed_lesson_ids %}Terminé{% endif %} + {% if lesson and lesson.id == item.id %}(cours actuel){% endif %} {% if lesson and lesson.id == item.id %}
    @@ -70,12 +69,6 @@
    Ce que l'on voit durant ce cours :
    {{ lesson.content|comment_markdown }} - - {% if lesson.id in completed_lesson_ids %} - - {% else %} - - {% endif %}

    Commentaires

    @@ -258,64 +251,6 @@ }); })(); - - -
    diff --git a/templates/home/stats_charts.html b/templates/home/stats_charts.html deleted file mode 100644 index 15f4972..0000000 --- a/templates/home/stats_charts.html +++ /dev/null @@ -1,78 +0,0 @@ -{% extends "layout.html" %} -{% load static %} - -{% block title %}- Stats · Graphiques{% endblock %} - -{% block extra_head %} - {% include "partials/_stats_head.html" %} -{% endblock %} - -{% block content %} -
    -

    Graphiques statistiques

    - - {% include "partials/_stats_toolbar.html" %} - -
    -
    -

    Visiteurs uniques par jour

    - -
    -
    -

    Conversions (visiteurs devenus utilisateurs) par jour

    - -
    -
    - -
    -
    -

    Top sources (visiteurs uniques)

    - -
    -
    -

    Top pays (visiteurs uniques)

    - -
    -
    - -

    ← Retour au tableau de bord

    -
    - - -{% endblock %} diff --git a/templates/home/stats_dashboard.html b/templates/home/stats_dashboard.html deleted file mode 100644 index 8cd5325..0000000 --- a/templates/home/stats_dashboard.html +++ /dev/null @@ -1,210 +0,0 @@ -{% extends "layout.html" %} -{% load static %} - -{% block title %}- Tableau de bord{% endblock %} - -{% block extra_head %} - {% include "partials/_stats_head.html" %} -{% endblock %} - -{% block content %} -
    -

    Tableau de bord statistiques

    -

    - → Voir la page de graphiques -

    - - {% include "partials/_stats_toolbar.html" %} - -
    -
    -

    Visiteurs uniques (période)

    -
    {{ kpi.unique_visitors }}
    -
    -
    -

    Visiteurs revenants (période)

    -
    {{ kpi.returning_visitors }}
    -
    -
    -

    Conversions en utilisateurs (période)

    -
    {{ kpi.converted_visitors }}
    -
    -
    -

    Utilisateurs (total)

    -
    {{ kpi.total_users }}
    -
    -
    -

    Nouveaux utilisateurs (période)

    -
    {{ kpi.new_users_period }}
    -
    -
    -

    Utilisateurs actifs (période)

    -
    {{ kpi.active_users_period }}
    -
    -
    -

    Cours (publiés / total)

    -
    {{ kpi.courses_enabled }} / {{ kpi.total_courses }}
    -
    -
    -

    Leçons (total)

    -
    {{ kpi.total_lessons }}
    -
    -
    -

    Articles de blog (total)

    -
    {{ kpi.total_posts }}
    -
    -
    -

    Revenus

    - {% if revenus_disponibles %} -
    - {% else %} -
    N/A
    - {% endif %} -
    -
    -

    Technique

    - {% if technique_disponible %} -
    - {% else %} -
    N/A
    - {% endif %} -
    -
    - -
    -
    -

    Activité en direct (5 dernières minutes)

    - - - - - - - -
    TypeIdentitéPageIl y aSourcePays
    Chargement…
    -
    -
    - -
    -
    -

    Évolution quotidienne

    - -
    -
    - -
    -
    -

    Nouveaux utilisateurs par jour

    - - - - {% for row in new_users_table %} - - {% endfor %} - -
    JourNb
    {{ row.0 }}{{ row.1 }}
    -
    -
    -

    Nouveaux cours par jour

    - - - - {% for row in new_courses_table %} - - {% endfor %} - -
    JourNb
    {{ row.0 }}{{ row.1 }}
    -
    -
    - -
    -
    -

    Top sources (visiteurs uniques)

    - - - - {% for row in top_sources_table %} - - {% empty %} - - {% endfor %} - -
    SourceVisiteurs
    {{ row.0 }}{{ row.1 }}
    Aucune donnée
    -
    -
    -

    Top pays (visiteurs uniques)

    - - - - {% for row in top_countries_table %} - - {% empty %} - - {% endfor %} - -
    PaysVisiteurs
    {{ row.0 }}{{ row.1 }}
    Aucune donnée
    -
    -
    -
    - - -{% endblock %} diff --git a/templates/layout.html b/templates/layout.html index 1d70ca8..2c3189d 100644 --- a/templates/layout.html +++ b/templates/layout.html @@ -31,37 +31,6 @@ - {% now "n" as month %} - {% if month == '12' %} - - - - {% endif %} - {% block extra_head %}{% endblock %} @@ -70,8 +39,8 @@ (function() { try { var stored = localStorage.getItem('pdz-theme'); - var prefersLight = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; - var theme = stored || (prefersLight ? 'dark' : 'light'); + var prefersLight = window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches; + var theme = stored || (prefersLight ? 'light' : 'dark'); document.documentElement.setAttribute('data-theme', theme); if (theme === 'light') { var link = document.getElementById('theme-css'); @@ -84,33 +53,24 @@ - {% if maintenance.is_active == True %} - {% include "maintenance.html" %} - {% else %} - {% now "n" as month %} - {% if month == '12' %} - - + {% block header %} + {% include "partials/_header.html" %} + {% endblock %} + +
    + {% if messages %} +
      + {% for message in messages %} + {{ message }} + {% endfor %} +
    {% endif %} - {% block header %} - {% include "partials/_header.html" %} - {% endblock %} -
    - {% if messages %} -
      - {% for message in messages %} - {{ message }} - {% endfor %} -
    - {% endif %} + {% block content %}{% endblock %} +
    - {% block content %}{% endblock %} -
    - - {% block footer %} - {% include "partials/_footer.html" %} - {% endblock %} - {% endif %} + {% block footer %} + {% include "partials/_footer.html" %} + {% endblock %} \ No newline at end of file diff --git a/templates/maintenance.html b/templates/maintenance.html deleted file mode 100644 index 77a3aa7..0000000 --- a/templates/maintenance.html +++ /dev/null @@ -1,28 +0,0 @@ -{% load comment_format %} -
    -

    Maintenance : {{ maintenance.name }}

    - {{ maintenance.message|comment_markdown }} -
    Durée estimée : {{ delay }}
    -
    Début de la maintenance : {{ maintenance.start_date }}
    -
    Fin de la maintenance estimé : {{ maintenance.end_date }}
    - - {% if message %} -

    {{ message }}

    - {% endif %} - - {% if user.is_superuser %} -
    -
    - -
    -
    Commande de redemarrage serveur : kill -HUP $(cat /var/www/vhosts/partirdezero.com/httpdocs/run/gunicorn.pid)
    - - {% endif %} -
    \ No newline at end of file diff --git a/templates/partials/_footer.html b/templates/partials/_footer.html index 42cc419..29b2d41 100644 --- a/templates/partials/_footer.html +++ b/templates/partials/_footer.html @@ -1,5 +1,4 @@ -{% now "n" as month %} -
    +
    @@ -37,8 +35,5 @@ Partir de Zero ©2024 - {% now "Y" %} v{{ SITE_VERSION }} Site fièrement créer par AV Interactive - {% if month == '12' %} - Joyeuses fêtes - {% endif %}
    \ No newline at end of file diff --git a/templates/partials/_header.html b/templates/partials/_header.html index 4c3e694..20db214 100644 --- a/templates/partials/_header.html +++ b/templates/partials/_header.html @@ -2,12 +2,7 @@
    {% if settings.site_logo %}{% endif %} - Partir de zéro - {% now "n" as month %} - {% if month == '12' %} - - {% endif %} - + Partir de zéro
    /* Anthony Violet */
    @@ -45,10 +40,10 @@ {% endif %}
  • -
  • Discord
  • {% if user.is_authenticated and user.is_staff %} -
  • Admin
  • -
  • Stats
  • +
  • + Admin +
  • {% endif %}
  • {% if user.is_authenticated %} diff --git a/templates/partials/_stats_head.html b/templates/partials/_stats_head.html deleted file mode 100644 index 1eba9b1..0000000 --- a/templates/partials/_stats_head.html +++ /dev/null @@ -1,65 +0,0 @@ -{# Partials: Shared head for stats pages (Chart.js + unified styles) #} - - - - diff --git a/templates/partials/_stats_toolbar.html b/templates/partials/_stats_toolbar.html deleted file mode 100644 index 6241f0a..0000000 --- a/templates/partials/_stats_toolbar.html +++ /dev/null @@ -1,15 +0,0 @@ -{# Partials: Shared period toolbar for stats pages #} -
    -
    - - - Du {{ start_date }} au {{ end_date }} -
    -
    Mise en cache 15 minutes
    - {% block stats_toolbar_extra %}{% endblock %} - {# Keep block for optional extensions on specific pages #} -
    diff --git a/templates/users/my_courses.html b/templates/users/my_courses.html index dab8a7f..f672141 100644 --- a/templates/users/my_courses.html +++ b/templates/users/my_courses.html @@ -6,27 +6,12 @@ {% endblock %}

    Mes cours

    -

    Retrouvez ici la liste de tous les cours que vous suivez et votre progression.

    - - {% if progress_list %} - - {% else %} -

    Vous ne suivez aucun cours pour le moment.

    - {% endif %} +

    Retrouvez ici la liste de tous les cours que vous suivez.

    +
    {% endblock %} \ No newline at end of file diff --git a/templates/users/profile.html b/templates/users/profile.html index f9aae18..57be63e 100644 --- a/templates/users/profile.html +++ b/templates/users/profile.html @@ -22,27 +22,24 @@

    Mes cours

    - {% with progress_list=latest_progress %} - {% if progress_list %} -
      - {% for p in progress_list %} - {% with course=p.course %} -
    • - - {{ course.name }} - {{ course.name }} - -
      Progression: {{ p.percent_completed }}%
      -
    • - {% endwith %} - {% endfor %} -
    - - {% else %} -

    Aucun cours suivi pour le moment.

    - {% endif %} + {% with courses=user.course_set.all %} + {% if courses %} + + + {% else %} +

    Aucun cours suivi pour le moment.

    + {% endif %} {% endwith %}
    diff --git a/users/forms.py b/users/forms.py index 362845e..2335d4c 100644 --- a/users/forms.py +++ b/users/forms.py @@ -27,15 +27,7 @@ class UserRegistrationForm(forms.Form): password2 = cleaned_data.get("password2") if password1 and password2 and password1 != password2: - raise forms.ValidationError("Les mots de passe ne correspondent pas.") - - return cleaned_data - - def clean_username(self): - username = self.cleaned_data.get('username') - if username and User.objects.filter(username__iexact=username).exists(): - raise forms.ValidationError("Ce pseudo est déjà pris. Veuillez en choisir un autre.") - return username + raise forms.ValidationError("Passwords do not match") class UserLoginForm(forms.Form): username = forms.CharField( diff --git a/users/tokens.py b/users/tokens.py deleted file mode 100644 index bc82caf..0000000 --- a/users/tokens.py +++ /dev/null @@ -1,4 +0,0 @@ -from django.contrib.auth.tokens import PasswordResetTokenGenerator - -# Générateur de tokens pour l'activation de compte -activation_token = PasswordResetTokenGenerator() diff --git a/users/urls.py b/users/urls.py index e46826d..cdf7a52 100644 --- a/users/urls.py +++ b/users/urls.py @@ -7,8 +7,6 @@ urlpatterns = [ path('', views.register, name='register'), path('login/', views.login, name='login'), path('logout/', views.logout, name='logout'), - # Activation de compte par lien tokenisé - path('activate///', views.activate, name='activate'), path('profile/view//', views.another_profile, name='another_profile'), path('complete-profile/', views.complete_profile, name='complete_profile'), path('profile/', views.profile, name='profile'), diff --git a/users/views.py b/users/views.py index a6d89cc..7b6ce5e 100644 --- a/users/views.py +++ b/users/views.py @@ -3,14 +3,8 @@ from django.contrib import messages from django.contrib.auth import authenticate, login as auth_login, logout as auth_logout from django.contrib.auth.models import User from courses.models import Course -from progression.models import Progression from .forms import UserRegistrationForm, UserLoginForm, UserUpdateForm, ProfileUpdateForm, CompleteProfileForm from django.contrib.auth.decorators import login_required -from django.core.mail import send_mail -from django.urls import reverse -from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode -from django.utils.encoding import force_bytes -from .tokens import activation_token def register(request): # Si l'utilisateur est deja connecté, on le redirige vers la page de profil @@ -19,36 +13,13 @@ def register(request): if request.method == 'POST': form = UserRegistrationForm(request.POST) if form.is_valid(): - # Crée un utilisateur inactif en attente d'activation par e‑mail user = User.objects.create_user( username=form.cleaned_data['username'], email=form.cleaned_data['email'], password=form.cleaned_data['password1'] ) - user.is_active = False - user.save() - - # Envoi du lien d'activation par e‑mail - uid = urlsafe_base64_encode(force_bytes(user.pk)) - token = activation_token.make_token(user) - activation_link = request.build_absolute_uri( - reverse('activate', kwargs={'uidb64': uid, 'token': token}) - ) - subject = 'Active ton compte sur partirdezero' - message = ( - 'Bienvenue !\n\n' - 'Pour activer ton compte, clique sur le lien suivant (valide pendant une durée limitée) :\n' - f'{activation_link}\n\n' - "Si tu n'es pas à l'origine de cette inscription, ignore ce message." - ) - try: - send_mail(subject, message, None, [user.email], fail_silently=True) - except Exception: - # Même si l'envoi échoue, on n'interrompt pas le flux; un admin vérifiera la config e‑mail - pass - - messages.success(request, "Inscription réussie. Vérifie ta boîte mail pour activer ton compte.") - return redirect('login') + auth_login(request, user) + return redirect('profile') else: form = UserRegistrationForm() return render(request, 'users/register.html', {'form': form}) @@ -61,15 +32,8 @@ def login(request): password = form.cleaned_data['password'] user = authenticate(request, username=username, password=password) if user is not None: - if not user.is_active: - messages.error(request, "Votre compte n'est pas encore activé. Consultez l'e‑mail d'activation envoyé." ) - return render(request, 'users/login.html', {'form': form}) auth_login(request, user) return redirect('profile') - else: - # Identifiants invalides: avertir l'utilisateur - messages.error(request, "Nom d'utilisateur ou mot de passe incorrect.") - return render(request, 'users/login.html', {'form': form}) else: form = UserLoginForm() return render(request, 'users/login.html', {'form': form}) @@ -82,17 +46,7 @@ def logout(request): def profile(request): if not hasattr(request.user, 'profile'): return redirect('complete_profile') - - latest_progress = ( - Progression.objects - .filter(user=request.user) - .select_related('course') - .prefetch_related('completed_lessons') - .order_by('-updated_at')[:5] - ) - - # Affiche les 5 derniers cours regardés avec leur progression - return render(request, 'users/profile.html', {'latest_progress': latest_progress}) + return render(request, 'users/profile.html') @login_required(login_url='login') def complete_profile(request): @@ -141,16 +95,9 @@ def account_update(request): @login_required(login_url='login') def my_courses(request): - # Liste tous les cours suivis par l'utilisateur avec leur progression - progress_list = ( - Progression.objects - .filter(user=request.user) - .select_related('course') - .prefetch_related('completed_lessons') - .order_by('-updated_at') - ) - - return render(request, 'users/my_courses.html', {'progress_list': progress_list}) + user_courses = Course.objects.filter(author=request.user.id) + print(user_courses) + return render(request, 'users/my_courses.html', {'user_courses' : user_courses}) def create_post(request): # Implement post creation logic here @@ -159,21 +106,3 @@ def create_post(request): def another_profile(request, user_id): user = User.objects.get(id=user_id) return render(request, 'users/another_profile.html', {'user': user}) - -# Activation de compte via lien tokenisé -def activate(request, uidb64, token): - try: - uid = urlsafe_base64_decode(uidb64).decode() - user = User.objects.get(pk=uid) - except Exception: - user = None - - if user and activation_token.check_token(user, token): - if not user.is_active: - user.is_active = True - user.save() - messages.success(request, 'Votre compte a été activé. Vous pouvez maintenant vous connecter.') - return redirect('login') - - messages.error(request, "Lien d'activation invalide ou expiré. Demandez un nouveau lien ou inscrivez‑vous à nouveau.") - return redirect('register')