diff --git a/VERSION.txt b/VERSION.txt
deleted file mode 100644
index 0b51dff..0000000
--- a/VERSION.txt
+++ /dev/null
@@ -1 +0,0 @@
-1.5.0 (2ec4a5c)
\ No newline at end of file
diff --git a/blog/__init__.py b/blog/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/blog/admin.py b/blog/admin.py
deleted file mode 100644
index 8eb9366..0000000
--- a/blog/admin.py
+++ /dev/null
@@ -1,8 +0,0 @@
-from django.contrib import admin
-from .models import Post
-
-@admin.register(Post)
-class PostAdmin(admin.ModelAdmin):
- list_display = ('name', 'tags', 'slug', 'created_at')
- search_fields = ('name', 'tags')
- prepopulated_fields = {"slug": ("name",)}
\ No newline at end of file
diff --git a/blog/apps.py b/blog/apps.py
deleted file mode 100644
index 7930587..0000000
--- a/blog/apps.py
+++ /dev/null
@@ -1,5 +0,0 @@
-from django.apps import AppConfig
-
-
-class BlogConfig(AppConfig):
- name = 'blog'
diff --git a/blog/context_processor.py b/blog/context_processor.py
deleted file mode 100644
index d09e196..0000000
--- a/blog/context_processor.py
+++ /dev/null
@@ -1,5 +0,0 @@
-from .models import Post
-
-def posts_list(request):
- posts = Post.objects.all().order_by('-created_at')
- return {'posts': posts}
\ No newline at end of file
diff --git a/blog/migrations/0001_initial.py b/blog/migrations/0001_initial.py
deleted file mode 100644
index 9c7c01b..0000000
--- a/blog/migrations/0001_initial.py
+++ /dev/null
@@ -1,30 +0,0 @@
-# Generated by Django 6.0 on 2025-12-15 14:58
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- initial = True
-
- dependencies = [
- ]
-
- operations = [
- migrations.CreateModel(
- name='Post',
- fields=[
- ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('name', models.CharField(max_length=200)),
- ('tags', models.CharField(max_length=200)),
- ('slug', models.SlugField()),
- ('content', models.TextField()),
- ('created_at', models.DateTimeField(auto_now_add=True)),
- ('updated_at', models.DateTimeField(auto_now=True)),
- ],
- options={
- 'verbose_name': 'Article',
- 'verbose_name_plural': 'Articles',
- },
- ),
- ]
diff --git a/blog/migrations/0002_post_enable.py b/blog/migrations/0002_post_enable.py
deleted file mode 100644
index bb99a9b..0000000
--- a/blog/migrations/0002_post_enable.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Generated by Django 6.0 on 2025-12-15 19:40
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('blog', '0001_initial'),
- ]
-
- operations = [
- migrations.AddField(
- model_name='post',
- name='enable',
- field=models.BooleanField(default=True),
- ),
- ]
diff --git a/blog/migrations/0003_post_description.py b/blog/migrations/0003_post_description.py
deleted file mode 100644
index 1d12e7e..0000000
--- a/blog/migrations/0003_post_description.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Generated by Django 6.0 on 2025-12-15 20:13
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('blog', '0002_post_enable'),
- ]
-
- operations = [
- migrations.AddField(
- model_name='post',
- name='description',
- field=models.TextField(default='Courte description'),
- ),
- ]
diff --git a/blog/migrations/__init__.py b/blog/migrations/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/blog/models.py b/blog/models.py
deleted file mode 100644
index 5d0d68c..0000000
--- a/blog/models.py
+++ /dev/null
@@ -1,22 +0,0 @@
-from django.db import models
-from django.urls import reverse
-
-class Post(models.Model):
- name = models.CharField(max_length=200)
- tags = models.CharField(max_length=200)
- slug = models.SlugField()
- description = models.TextField(default="Courte description")
- content = models.TextField()
- created_at = models.DateTimeField(auto_now_add=True)
- updated_at = models.DateTimeField(auto_now=True)
- enable = models.BooleanField(default=True)
-
- class Meta:
- verbose_name = "Article"
- 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
diff --git a/blog/tests.py b/blog/tests.py
deleted file mode 100644
index 7ce503c..0000000
--- a/blog/tests.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from django.test import TestCase
-
-# Create your tests here.
diff --git a/blog/urls.py b/blog/urls.py
deleted file mode 100644
index ef6ad01..0000000
--- a/blog/urls.py
+++ /dev/null
@@ -1,8 +0,0 @@
-from django.urls import path
-from . import views
-
-app_name = 'blog'
-urlpatterns = [
- path('', views.blog_home, name='blog'),
- path('/', views.blog_view_post, name='post_detail'),
-]
\ No newline at end of file
diff --git a/blog/views.py b/blog/views.py
deleted file mode 100644
index 3db99b3..0000000
--- a/blog/views.py
+++ /dev/null
@@ -1,11 +0,0 @@
-from django.shortcuts import render
-
-from blog.models import Post
-
-
-def blog_home(request):
- return render(request, 'blog/home.html')
-
-def blog_view_post(request, slug):
- post = Post.objects.get(slug=slug)
- return render(request, 'blog/details.html', {'post': post})
\ No newline at end of file
diff --git a/core/admin.py b/core/admin.py
index 13f115b..7f53918 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'),
@@ -23,18 +23,4 @@ class SiteSettingsAdmin(admin.ModelAdmin):
('Contact', {
'fields': ('contact_email',)
}),
- ('Blog', {
- '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")
+ )
\ No newline at end of file
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/0002_sitesettings_blog_description_and_more.py b/core/migrations/0002_sitesettings_blog_description_and_more.py
deleted file mode 100644
index 7d669a8..0000000
--- a/core/migrations/0002_sitesettings_blog_description_and_more.py
+++ /dev/null
@@ -1,23 +0,0 @@
-# Generated by Django 6.0 on 2025-12-15 19:35
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('core', '0001_initial'),
- ]
-
- operations = [
- migrations.AddField(
- model_name='sitesettings',
- name='blog_description',
- field=models.TextField(blank=True),
- ),
- migrations.AddField(
- model_name='sitesettings',
- name='blog_title',
- field=models.CharField(default='Mon Blog', max_length=200),
- ),
- ]
diff --git a/core/migrations/0003_alter_sitesettings_blog_description_and_more.py b/core/migrations/0003_alter_sitesettings_blog_description_and_more.py
deleted file mode 100644
index ccfc1e0..0000000
--- a/core/migrations/0003_alter_sitesettings_blog_description_and_more.py
+++ /dev/null
@@ -1,23 +0,0 @@
-# Generated by Django 6.0 on 2025-12-15 19:40
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('core', '0002_sitesettings_blog_description_and_more'),
- ]
-
- operations = [
- migrations.AlterField(
- model_name='sitesettings',
- name='blog_description',
- field=models.TextField(blank=True, default='Je documente la construction de PartirDeZero.com : mes choix techniques, mes bugs résolus et mes conseils pour lancer tes propres projets web. Apprends en regardant faire.'),
- ),
- migrations.AlterField(
- model_name='sitesettings',
- name='blog_title',
- field=models.CharField(default='Blog du développeur', max_length=200),
- ),
- ]
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..de47391 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)
@@ -16,10 +13,6 @@ class SiteSettings(models.Model):
linkedin_url = models.URLField(blank=True)
github_url = models.URLField(blank=True)
- # Blog
- blog_title = models.CharField(max_length=200, default="Blog du développeur")
- blog_description = models.TextField(blank=True, default="Je documente la construction de PartirDeZero.com : mes choix techniques, mes bugs résolus et mes conseils pour lancer tes propres projets web. Apprends en regardant faire.")
-
# L'astuce pour qu'il n'y ait qu'un seul réglage
def save(self, *args, **kwargs):
self.pk = 1 # On force l'ID à 1. Si tu sauvegardes, ça écrase l'existant.
@@ -33,42 +26,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/migrations/0004_alter_course_options_alter_lesson_options.py b/courses/migrations/0004_alter_course_options_alter_lesson_options.py
deleted file mode 100644
index ff801fb..0000000
--- a/courses/migrations/0004_alter_course_options_alter_lesson_options.py
+++ /dev/null
@@ -1,21 +0,0 @@
-# Generated by Django 6.0 on 2025-12-15 14:58
-
-from django.db import migrations
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('courses', '0003_comment_parent'),
- ]
-
- operations = [
- migrations.AlterModelOptions(
- name='course',
- options={'verbose_name': 'Cours', 'verbose_name_plural': 'Cours'},
- ),
- migrations.AlterModelOptions(
- name='lesson',
- options={'verbose_name': 'Leçon', 'verbose_name_plural': 'Leçons'},
- ),
- ]
diff --git a/courses/models.py b/courses/models.py
index 3cc46d1..1c5a096 100644
--- a/courses/models.py
+++ b/courses/models.py
@@ -12,16 +12,9 @@ class Course(models.Model):
updated_at = models.DateTimeField(auto_now=True)
enable = models.BooleanField(default=True)
- class Meta:
- verbose_name = "Cours"
- verbose_name_plural = "Cours"
-
def __str__(self):
return self.name
- def get_absolute_url(self):
- return f"/courses/{self.slug}-{self.id}/"
-
class Module(models.Model):
name = models.CharField(max_length=200)
slug = models.SlugField()
@@ -32,9 +25,6 @@ class Module(models.Model):
enable = models.BooleanField(default=True)
order = models.PositiveIntegerField()
- def __str__(self):
- return self.name
-
class Lesson(models.Model):
name = models.CharField(max_length=200)
slug = models.SlugField()
@@ -44,10 +34,6 @@ class Lesson(models.Model):
is_premium = models.BooleanField(default=False)
order = models.PositiveIntegerField()
- class Meta:
- verbose_name = "Leçon"
- verbose_name_plural = "Leçons"
-
def clean(self):
# Remplacer les chevrons par leurs équivalents HTML
if self.content:
@@ -57,9 +43,6 @@ class Lesson(models.Model):
def __str__(self):
return self.name
- def get_absolute_url(self):
- return f"/courses/{self.module.course.slug}/{self.module.slug}/{self.slug}/"
-
class Comment(models.Model):
lesson = models.ForeignKey(Lesson, on_delete=models.CASCADE, related_name='comments')
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/templatetags/comment_format.py b/courses/templatetags/comment_format.py
index 64ea248..2fbfd7e 100644
--- a/courses/templatetags/comment_format.py
+++ b/courses/templatetags/comment_format.py
@@ -27,19 +27,6 @@ def _format_inline(text: str) -> str:
lambda m: f"",
text,
)
- # H1
- text = re.sub(r"^# (.+)$", r"\1
", text, flags=re.MULTILINE)
- # H2
- text = re.sub(r"^## (.+)$", r"\1
", text, flags=re.MULTILINE)
- # H3
- text = re.sub(r"^### (.+)$", r"\1
", text, flags=re.MULTILINE)
- # H4
- text = re.sub(r"^#### (.+)$", r"\1
", text, flags=re.MULTILINE)
- # H5
- text = re.sub(r"^##### (.+)$", r"\1
", text, flags=re.MULTILINE)
- # H6
- text = re.sub(r"^###### (.+)$", r"\1
", text, flags=re.MULTILINE)
-
# bold **text**
text = re.sub(
r"\*\*([^*]+)\*\*",
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/context_processor.py b/devart/context_processor.py
deleted file mode 100644
index a5d2be3..0000000
--- a/devart/context_processor.py
+++ /dev/null
@@ -1,4 +0,0 @@
-from django.conf import settings
-
-def app_version(request):
- return {'SITE_VERSION': settings.GIT_VERSION}
\ No newline at end of file
diff --git a/devart/settings.py b/devart/settings.py
index f2dab3b..1936a8a 100644
--- a/devart/settings.py
+++ b/devart/settings.py
@@ -12,12 +12,8 @@ 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
-
# Charger les variables d'environnement depuis le fichier .env
load_dotenv()
@@ -48,15 +44,10 @@ INSTALLED_APPS = [
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
- 'django.contrib.sitemaps',
'core',
'courses',
'users',
- 'progression',
- 'blog',
-
- 'discord_integration.apps.DiscordIntegrationConfig',
]
MIDDLEWARE = [
@@ -68,7 +59,7 @@ MIDDLEWARE = [
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
- 'core.middleware.VisitTrackingMiddleware',
+
]
ROOT_URLCONF = 'devart.urls'
@@ -83,12 +74,9 @@ TEMPLATES = [
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
-
- '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',
],
},
},
@@ -164,55 +152,3 @@ STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')]
# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
-
-# On défini une fonction qui va s'occuper de récupérer la version actuelle de l'application
-def get_git_version():
- import subprocess
- """
- DEV : Calcule la version Git et met à jour le fichier VERSION.txt
- PROD : Lit simplement le fichier VERSION.txt
- """
- version_file = os.path.join(BASE_DIR, 'VERSION.txt')
-
- # --- CAS 1 : MODE DÉVELOPPEMENT (Mise à jour du fichier) ---
- if DEBUG:
- try:
- # 1. On récupère les infos Git
- hash_part = subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD'],
- stderr=subprocess.DEVNULL).strip().decode('utf-8')
-
- try:
- tag_part = subprocess.check_output(["git", "describe", "--tags", "--abbrev=0"],
- stderr=subprocess.DEVNULL).strip().decode('utf-8')
- except:
- tag_part = "v0.0.x" # Fallback si pas de tag
-
- full_version = f"{tag_part} ({hash_part})"
-
- # 2. On ÉCRIT/MET À JOUR le fichier texte
- with open(version_file, 'w') as f:
- f.write(full_version)
-
- return full_version
-
- except Exception as e:
- # Si Git échoue en local (rare, mais possible)
- return f"Dev Mode (Err: {e})"
-
- # --- CAS 2 : MODE PRODUCTION (Lecture seule) ---
- else:
- if os.path.exists(version_file):
- with open(version_file, 'r') as f:
- return f.read().strip()
- 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
diff --git a/devart/sitemap.py b/devart/sitemap.py
deleted file mode 100644
index c675d9d..0000000
--- a/devart/sitemap.py
+++ /dev/null
@@ -1,38 +0,0 @@
-from django.contrib import sitemaps
-from django.urls import reverse
-
-# --- FEATURES ---
-from courses.models import Course
-from users.models import Profile
-from blog.models import Post
-
-# --- SITEMAP : LES Cours ---
-class CourseSitemap(sitemaps.Sitemap):
- changefreq = "weekly"
- priority = 0.9
-
- def items(self):
- return Course.objects.filter(enable=True).order_by('id')
-
- def location(self, item):
- # Assure-toi que ton modèle Course a bien une méthode get_absolute_url
- return item.get_absolute_url()
-
-# --- SITEMAP : BLOG ---
-class BlogSitemap(sitemaps.Sitemap):
- changefreq = "weekly"
- priority = 0.8
-
- def location(self, item):
- return item.get_absolute_url()
-
-# --- SITEMAP : PAGES STATIQUES ---
-class StaticViewSitemap(sitemaps.Sitemap):
- priority = 0.5
- changefreq = "monthly"
-
- def items(self):
- return ["home"] # Les noms de tes URLs
-
- def location(self, item):
- return ""
\ No newline at end of file
diff --git a/devart/urls.py b/devart/urls.py
index 036a891..86830eb 100644
--- a/devart/urls.py
+++ b/devart/urls.py
@@ -18,38 +18,12 @@ from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import path, include
-from django.http import HttpResponse
-from devart.sitemap import CourseSitemap, StaticViewSitemap
-from django.contrib.sitemaps.views import sitemap
-
-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
- ]
- return HttpResponse("\n".join(lines), content_type="text/plain")
-
-sitemaps_dict = {
- 'cours': CourseSitemap,
- 'static': StaticViewSitemap,
-}
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'),
- path('robots.txt', robots_txt),
]
if settings.DEBUG:
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/__init__.py b/progression/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/progression/admin.py b/progression/admin.py
deleted file mode 100644
index 6dd7924..0000000
--- a/progression/admin.py
+++ /dev/null
@@ -1,15 +0,0 @@
-from django.contrib import admin
-from .models import Progression
-from courses.models import Course, Lesson
-
-@admin.register(Progression)
-class ProgressionAdmin(admin.ModelAdmin):
- list_display = ('user', 'course', 'get_percent', 'updated_at')
- list_filter = ('course', 'updated_at')
- search_fields = ('user__username', 'course__name')
-
- autocomplete_fields = ['course', 'completed_lessons']
-
- def get_percent(self, obj):
- return f"{obj.percent_completed}"
- get_percent.short_description = 'Progression'
\ No newline at end of file
diff --git a/progression/apps.py b/progression/apps.py
deleted file mode 100644
index 7bf7a21..0000000
--- a/progression/apps.py
+++ /dev/null
@@ -1,5 +0,0 @@
-from django.apps import AppConfig
-
-
-class ProgressionConfig(AppConfig):
- name = 'progression'
diff --git a/progression/migrations/0001_initial.py b/progression/migrations/0001_initial.py
deleted file mode 100644
index f2023ef..0000000
--- a/progression/migrations/0001_initial.py
+++ /dev/null
@@ -1,34 +0,0 @@
-# Generated by Django 6.0 on 2025-12-15 14:25
-
-import django.db.models.deletion
-from django.conf import settings
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- initial = True
-
- dependencies = [
- ('courses', '0003_comment_parent'),
- migrations.swappable_dependency(settings.AUTH_USER_MODEL),
- ]
-
- operations = [
- migrations.CreateModel(
- name='Progression',
- fields=[
- ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('created_at', models.DateTimeField(auto_now_add=True)),
- ('updated_at', models.DateTimeField(auto_now=True)),
- ('completed_lessons', models.ManyToManyField(blank=True, related_name='completed_by', to='courses.lesson')),
- ('course', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_progress', to='courses.course')),
- ('last_viewed_lesson', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='last_viewed_by', to='courses.lesson')),
- ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='progress', to=settings.AUTH_USER_MODEL)),
- ],
- options={
- 'verbose_name': 'Progression du cours',
- 'unique_together': {('user', 'course')},
- },
- ),
- ]
diff --git a/progression/migrations/__init__.py b/progression/migrations/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/progression/models.py b/progression/models.py
deleted file mode 100644
index d8002a8..0000000
--- a/progression/models.py
+++ /dev/null
@@ -1,28 +0,0 @@
-from django.db import models
-from django.conf import settings
-
-class Progression(models.Model):
- user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='progress')
- course = models.ForeignKey('courses.Course', on_delete=models.CASCADE, related_name='user_progress')
- completed_lessons = models.ManyToManyField('courses.Lesson', blank=True, related_name='completed_by')
- last_viewed_lesson = models.ForeignKey('courses.Lesson', on_delete=models.SET_NULL, null=True, blank=True, related_name='last_viewed_by')
- created_at = models.DateTimeField(auto_now_add=True)
- updated_at = models.DateTimeField(auto_now=True)
-
- class Meta:
- unique_together = ('user', 'course')
- verbose_name = "Progression du cours"
-
- def __str__(self):
- return f"{self.user} - {self.course.name}"
-
- @property
- def percent_completed(self):
- from courses.models import Lesson
- total_lessons = Lesson.objects.filter(module__course=self.course).count()
- if total_lessons == 0:
- return 0
-
- completed_lessons = self.completed_lessons.count()
-
- return int((completed_lessons / total_lessons) * 100)
\ No newline at end of file
diff --git a/progression/tests.py b/progression/tests.py
deleted file mode 100644
index 7ce503c..0000000
--- a/progression/tests.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from django.test import TestCase
-
-# Create your tests here.
diff --git a/progression/urls.py b/progression/urls.py
deleted file mode 100644
index cef076c..0000000
--- a/progression/urls.py
+++ /dev/null
@@ -1,10 +0,0 @@
-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
deleted file mode 100644
index 1cee11c..0000000
--- a/progression/views.py
+++ /dev/null
@@ -1,41 +0,0 @@
-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
-
-@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
diff --git a/static/css/app.css b/static/css/app.css
index 2531ab5..8d25550 100644
--- a/static/css/app.css
+++ b/static/css/app.css
@@ -79,9 +79,8 @@
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
/* Layout */
- /* Fluid container and gutters for responsive spacing */
- --container-w: clamp(320px, 92vw, 1100px);
- --gutter: clamp(14px, 3vw, 24px);
+ --container-w: 1100px;
+ --gutter: 20px;
--focus-ring: 0 0 0 3px rgba(78,158,214,0.45);
/* Utility base colors */
@@ -109,23 +108,6 @@
--border-glass-light: rgba(255,255,255,0.05);
}
-/*
- Mobile width guardrail
- - Cap container width on touch devices to improve readability and avoid edge overflows
-*/
-@media (hover: none) and (pointer: coarse) {
- :root {
- /* Keep fluid gutters but prevent overly wide lines on large/mobile landscape */
- --container-w: min(92vw, 480px);
- }
- html, body { overflow-x: hidden; }
-}
-
-/* Global box-sizing reset to prevent width overflow on mobile
- Ensures padding/border are included in the element's total width/height */
-html { box-sizing: border-box; }
-*, *::before, *::after { box-sizing: inherit; }
-
/* Light theme values — applied via colors_light.css inclusion */
/* ==========================
@@ -247,9 +229,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 +241,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 +251,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 */
@@ -564,11 +448,6 @@ body {
border-radius: var(--r-2);
}
-/* Hide burger on large screens explicitly */
-@media (min-width: 1025px) {
- .nav-toggle { display: none !important; }
-}
-
@media (max-width: 1024px) {
.site-nav {
position: sticky;
@@ -590,7 +469,8 @@ body {
height: 100vh;
/* Ensure total box never exceeds the viewport width (padding + border included) */
box-sizing: border-box;
- width: min(92vw, 380px);
+ width: min(85vw, 380px);
+ max-width: 100vw;
margin: 0;
/* Safe-area aware padding on the right (iOS notch) */
padding: var(--space-5) max(var(--gutter), env(safe-area-inset-right)) var(--space-5) var(--gutter);
@@ -630,8 +510,8 @@ body {
z-index: 1100;
}
- .navbar ul { flex-direction: column; width: 100%; align-items: flex-start; }
- .navbar li { margin: 0; width: 100%; }
+ .navbar ul { flex-direction: column; width: 100%; }
+ .navbar li { margin: 0; }
.navbar a { display: block; padding: 12px 14px; border-radius: var(--r-1); }
/* Dropdowns behave as inline lists on mobile */
@@ -644,22 +524,18 @@ body {
padding-left: 10px;
}
- .navbar ul ul { align-items: flex-start; }
- .navbar ul ul li { border: 0; margin: 0; padding: 6px 8px; width: 100%; }
- .navbar ul ul a { text-align: left; }
+ .navbar ul ul li { border: 0; margin: 0; padding: 6px 8px; }
.navend {
width: 100%;
margin-top: auto; /* push to bottom */
display: flex;
flex-direction: column;
- align-items: flex-start; /* start-align items on mobile */
- justify-content: flex-start;
+ align-items: stretch;
gap: 6px;
}
- .navend ul { width: 100%; align-items: flex-start; }
- .navend a { text-align: left; }
+ .navend ul { width: 100%; }
}
/* Respect reduced motion preferences */
@@ -774,18 +650,17 @@ section {
display: flex;
flex-direction: column;
padding: var(--space-6) var(--gutter);
- width: 100%;
- max-width: var(--container-w);
- margin: clamp(12px, 3vw, 24px) auto;
+ width: min(100%, 60%);
+ margin: 20px auto;
}
.courseNav {
display: block;
float: left;
- position: sticky;
+ position: fixed;
left: 0;
- top: clamp(64px, 12vh, 150px);
- max-width: clamp(220px, 22vw, 300px);
+ top: 150px;
+ max-width: 15%;
border: 1px solid;
}
@@ -813,142 +688,23 @@ section {
font-size: 1.5rem;
}
-/* --- Headings system (H1–H6) --- */
-/* Base: consistent rhythm, weight, and accessibility across themes */
-:where(h1,h2,h3,h4,h5,h6) {
- font-family: var(--font-sans);
- font-weight: 700;
- line-height: 1.2;
- margin: 0 0 var(--space-4);
- color: var(--fg);
- letter-spacing: -0.01em;
-}
-
h1 {
- font-size: clamp(2rem, 4.5vw, 2.75rem);
color: var(--primary);
- border-left: 4px solid var(--accent);
- padding-left: var(--space-3);
+ font-size: clamp(1.8rem, 4vw, 2.5rem);
+ border-left: 5px solid var(--accent);
+ border-bottom: 1px solid var(--accent);
+ padding-left: 10px;
}
h2 {
- font-size: clamp(1.5rem, 3.5vw, 2rem);
color: var(--accent);
- border-bottom: 1px solid var(--border);
- padding-bottom: 6px;
- margin-top: var(--space-6);
+ font-size: clamp(1.3rem, 3vw, 1.8rem);
}
-h3 { font-size: clamp(1.25rem, 3vw, 1.5rem); color: var(--fg); }
-h4 { font-size: clamp(1.1rem, 2.2vw, 1.25rem); color: var(--fg); }
-h5 { font-size: clamp(1rem, 2vw, 1.1rem); color: var(--muted); text-transform: uppercase; letter-spacing: .04em; }
-h6 { font-size: 0.95rem; color: var(--muted); font-weight: 600; text-transform: uppercase; letter-spacing: .06em; }
-
img {
max-width: 100%;
- height: auto;
}
-/* ======================================
- Blog components
- ====================================== */
-
-/* Accessibilité: élément visuellement masqué mais accessible aux lecteurs d'écran */
-.sr-only {
- position: absolute !important;
- width: 1px !important;
- height: 1px !important;
- padding: 0 !important;
- margin: -1px !important;
- overflow: hidden !important;
- clip: rect(0,0,0,0) !important;
- white-space: nowrap !important;
- border: 0 !important;
-}
-
-/* Blog layout wrappers */
-.blog.blog-home .blog-header {
- display: grid;
- gap: var(--space-3);
- margin-bottom: var(--space-6);
-}
-.blog-title {
- margin: 0;
-}
-.blog-description {
- color: var(--text-muted);
- font-size: 1.05rem;
-}
-
-/* Post list grid */
-.post-list {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
- gap: var(--space-5);
-}
-
-.post-card {
- background: var(--surface);
- border: 1px solid var(--border-subtle);
- border-radius: var(--r-3);
- box-shadow: var(--shadow-1);
- padding: var(--space-5);
- display: grid;
- gap: var(--space-3);
- transition: transform var(--transition-1), box-shadow var(--transition-1), border-color var(--transition-1);
-}
-.post-card:hover {
- transform: translateY(-2px);
- box-shadow: var(--shadow-2);
- border-color: var(--border-strong);
-}
-.post-card-title {
- margin: 0;
-}
-.post-card-title a { color: var(--fg); text-decoration: none; }
-.post-card-title a:hover { color: var(--link-hover); text-decoration: underline; }
-.post-excerpt { color: var(--text); opacity: 0.95; }
-.post-actions { margin-top: 2px; }
-
-/* Post meta (date, tags) */
-.post-meta { color: var(--text-muted); font-size: 0.95rem; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
-.post-meta i { color: var(--muted); margin-right: 6px; }
-.post-meta .sep { color: var(--muted); }
-
-/* Post detail */
-.post-detail .post-header { margin-bottom: var(--space-5); }
-.post-detail .post-title { margin-bottom: var(--space-2); }
-
-/* Prose content: typographic rhythm inside articles */
-.prose {
- line-height: 1.75;
- color: var(--text);
-}
-.prose :where(p, ul, ol, blockquote, pre, table, img) { margin: 0 0 var(--space-4); }
-.prose a { color: var(--link); }
-.prose a:hover { color: var(--link-hover); text-decoration: underline; }
-.prose blockquote {
- margin-left: 0;
- padding-left: var(--space-4);
- border-left: 3px solid var(--border-strong);
- color: var(--text-muted);
- font-style: italic;
-}
-.prose code { font-family: var(--font-mono); background: var(--neutral-300); padding: 0 4px; border-radius: var(--r-1); }
-.prose pre {
- background: var(--code-bg);
- color: var(--code-text);
- padding: var(--space-4);
- border-radius: var(--r-2);
- overflow: auto;
-}
-
-/* Make embedded media responsive */
-iframe, video { max-width: 100%; height: auto; }
-
-/* Responsive tables: wrap in a container if needed */
-.table-responsive { width: 100%; overflow-x: auto; -webkit-overflow-scrolling: touch; }
-
.card-header img.thumbnails {
transition: transform 1s ease
}
@@ -958,20 +714,21 @@ iframe, video { max-width: 100%; height: auto; }
}
.container-inline {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
- gap: var(--space-5);
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ gap: 20px;
}
.card {
display: flex;
flex-direction: column;
- width: 100%;
+ max-width: 250px;
border-left: 1px solid var(--border);
border-top: 1px solid var(--border);
border-radius: var(--r-2);
overflow: hidden;
- margin: 0;
+ margin: 20px;
background: var(--card);
box-shadow: var(--shadow-1);
transition: transform var(--transition-1), box-shadow var(--transition-1);
@@ -1010,7 +767,7 @@ iframe, video { max-width: 100%; height: auto; }
.def-author {
display: flex;
- padding: var(--space-3);
+ padding: 10px;
flex-direction: row;
align-items: center;
font-size: 0.8rem;
@@ -1023,18 +780,18 @@ iframe, video { max-width: 100%; height: auto; }
}
pre {
- border-radius: var(--r-2);
+ border-radius: 5px;
/* Ne dépasse pas du parent en largeur et défile si nécessaire */
- max-width: clamp(320px, 100%, 800px);
+ max-width: 800px;
overflow: auto; /* scroll horizontal et vertical si besoin */
- max-height: clamp(360px, 70vh, 800px);
+ max-height: 800px;
}
/* ====== Homepage enhancements ====== */
.hero {
display: block;
width: 100%;
- padding: clamp(48px, 10vw, 96px) var(--gutter) clamp(32px, 6vw, 64px);
+ padding: 80px 20px 56px;
text-align: center;
background:
radial-gradient(1200px 600px at 50% -10%, rgba(78,158,214,0.20), transparent 60%),
@@ -1043,7 +800,10 @@ pre {
position: relative;
}
-.hero-inner { max-width: var(--container-w); margin: 0 auto; padding: 0 var(--gutter); }
+.hero-inner {
+ max-width: 960px;
+ margin: 0 auto;
+}
.hero-decor {
position: absolute;
@@ -1056,12 +816,31 @@ pre {
opacity: .6;
}
-.hero .hero-sub { font-size: clamp(1rem, 2.4vw, 1.125rem); font-weight: 500; margin: var(--space-3) auto 0; max-width: 65ch; }
+.hero .hero-sub {
+ font-size: 1.125rem;
+ font-weight: 500;
+ margin: 10px auto 0;
+ max-width: 800px;
+}
-.badge-row { display: flex; gap: var(--space-3); justify-content: center; margin: var(--space-4) 0 var(--space-2); flex-wrap: wrap; }
-.badge { display: inline-flex; align-items: center; gap: var(--space-2); padding: 6px 10px; border: 1px solid var(--border-subtle); border-radius: 999px; background: color-mix(in oklab, var(--card) 80%, transparent); font-size: .9rem; color: var(--text-muted); }
+.badge-row { display: flex; gap: 10px; justify-content: center; margin: 18px 0 6px; flex-wrap: wrap; }
+.badge {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ padding: 6px 10px;
+ border: 1px solid var(--border-subtle);
+ border-radius: 999px;
+ background: color-mix(in oklab, var(--card) 80%, transparent);
+ font-size: .9rem;
+ color: var(--text-muted);
+}
-.hero-cta .button { padding: 12px 18px; font-size: 1rem; border-radius: var(--r-2); }
+.hero-cta .button {
+ padding: 12px 18px;
+ font-size: 1rem;
+ border-radius: 6px;
+}
.cta-primary {
/* Make primary CTA more visible */
@@ -1148,12 +927,11 @@ pre {
}
.pricing-teaser {
- width: 100%;
- max-width: 820px;
- margin: var(--space-3) auto var(--space-6);
+ width: 60%;
+ margin: 10px auto 40px;
border: 1px solid var(--border);
border-radius: var(--r-3);
- padding: var(--space-5);
+ padding: 20px;
background: var(--card);
}
@@ -1165,7 +943,7 @@ pre {
.carousel-track {
display: grid;
grid-auto-flow: column;
- grid-auto-columns: minmax(260px, 80%);
+ grid-auto-columns: 80%;
gap: var(--space-4);
overflow-x: auto;
scroll-snap-type: x mandatory;
@@ -1189,7 +967,7 @@ pre {
display: grid;
gap: var(--space-4);
}
-.testimonials-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: var(--space-4); }
+.testimonials-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--space-4); }
.testimonial {
background: var(--card);
border: 1px solid var(--border);
@@ -2330,16 +2108,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 +2440,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/blog/details.html b/templates/blog/details.html
deleted file mode 100644
index 0cf8ef9..0000000
--- a/templates/blog/details.html
+++ /dev/null
@@ -1,25 +0,0 @@
-{% extends 'layout.html' %}
-{% load comment_format %}
-{% block title %} | Blog : {{ post.name }}{% endblock %}
-{% block og_title %}Blog de Partir De Zéro : {{ post.name }}{% endblock %}
-{% block description %}{{ post.content|striptags|truncatewords:20 }}{% endblock %}
-{% block og_description %}{{ post.content|striptags|truncatewords:20 }}{% endblock %}
-
-{% block content %}
-
-
-
-
- {{ post.content|comment_markdown }}
-
-
-{% endblock %}
\ No newline at end of file
diff --git a/templates/blog/home.html b/templates/blog/home.html
deleted file mode 100644
index b89265d..0000000
--- a/templates/blog/home.html
+++ /dev/null
@@ -1,17 +0,0 @@
-{% extends 'layout.html' %}
-{% block title %} | Blog{% endblock %}
-{% block og_title %}Blog de Partir De Zéro{% endblock %}
-{% block description %}{{ settings.blog_description }}{% endblock %}
-{% block og_description %}{{ settings.blog_description }}{% endblock %}
-
-{% block content %}
-
-
-
- Liste des articles
- {% include 'blog/partials/_posts_list.html' %}
-
-{% endblock %}
\ No newline at end of file
diff --git a/templates/blog/partials/_posts_list.html b/templates/blog/partials/_posts_list.html
deleted file mode 100644
index c1b05e8..0000000
--- a/templates/blog/partials/_posts_list.html
+++ /dev/null
@@ -1,21 +0,0 @@
-{% load comment_format %}
-
- {% for post in posts %}
-
-
-
- {{ post.created_at|date:"d F Y" }}
- {% if post.tags %}
- •
- {{ post.tags }}
- {% endif %}
-
- {{ post.description|comment_markdown|truncatewords:26 }}
-
-
- {% empty %}
-
Aucun article pour le moment.
- {% endfor %}
-
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 %}
diff --git a/templates/home.html b/templates/home.html
index 76ebca2..fd4b276 100644
--- a/templates/home.html
+++ b/templates/home.html
@@ -6,7 +6,7 @@
-
Apprendre le développement de zéro : Cours complets pour débutant (ou pas !)
+
Apprenez à coder de A à Z
Des cours gratuits, structurés et concrets, pour progresser rapidement en programmation.
Logiciel, Web, Jeux Vidéos
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)
-
-
- | Type | Identité | Page | Il y a | Source | Pays |
-
-
- | Chargement… |
-
-
-
-
-
-
-
-
Évolution quotidienne
-
-
-
-
-
-
-
Nouveaux utilisateurs par jour
-
- | Jour | Nb |
-
- {% for row in new_users_table %}
- | {{ row.0 }} | {{ row.1 }} |
- {% endfor %}
-
-
-
-
-
Nouveaux cours par jour
-
- | Jour | Nb |
-
- {% for row in new_courses_table %}
- | {{ row.0 }} | {{ row.1 }} |
- {% endfor %}
-
-
-
-
-
-
-
-
Top sources (visiteurs uniques)
-
- | Source | Visiteurs |
-
- {% for row in top_sources_table %}
- | {{ row.0 }} | {{ row.1 }} |
- {% empty %}
- | Aucune donnée |
- {% endfor %}
-
-
-
-
-
Top pays (visiteurs uniques)
-
- | Pays | Visiteurs |
-
- {% for row in top_countries_table %}
- | {{ row.0 }} | {{ row.1 }} |
- {% empty %}
- | Aucune donnée |
- {% endfor %}
-
-
-
-
-
-
-
-{% endblock %}
diff --git a/templates/layout.html b/templates/layout.html
index 1d70ca8..a81b6d8 100644
--- a/templates/layout.html
+++ b/templates/layout.html
@@ -6,7 +6,7 @@
{% load static %}
-
PartirDeZero {% block title %}{% endblock %}
+
PartirDeZero - {% block title %}{% endblock %}
@@ -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 %}