From e77ea7e20eff0a6c33af1d283dc7544fcd6b2c77 Mon Sep 17 00:00:00 2001 From: mrtoine Date: Mon, 15 Dec 2025 13:46:02 +0100 Subject: [PATCH 01/26] =?UTF-8?q?Am=C3=A9lioration=20des=20styles=20typogr?= =?UTF-8?q?aphiques=20(H1=E2=80=93H6)=20et=20gestion=20responsive=20de=20l?= =?UTF-8?q?'=C3=A9l=C3=A9ment=20burger=20dans=20les=20media=20queries.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- VERSION.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION.txt b/VERSION.txt index e3b55ea..a46d739 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -1.0.2 (8668690) \ No newline at end of file +1.0.3 (e79ffee) \ No newline at end of file From cab7c07433b01ce52bac5f93ce5b02e1f838388b Mon Sep 17 00:00:00 2001 From: mrtoine Date: Mon, 15 Dec 2025 14:26:26 +0100 Subject: [PATCH 02/26] =?UTF-8?q?Ajout=20d'un=20sitemap=20avec=20gestion?= =?UTF-8?q?=20des=20cours=20et=20des=20pages=20statiques,=20int=C3=A9grati?= =?UTF-8?q?on=20d'une=20vue=20pour=20`robots.txt`,=20et=20mise=20=C3=A0=20?= =?UTF-8?q?jour=20des=20mod=C3=A8les=20avec=20les=20URLs=20absolues=20pour?= =?UTF-8?q?=20am=C3=A9liorer=20le=20SEO.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- courses/models.py | 9 +++++++++ courses/views.py | 2 +- devart/settings.py | 1 + devart/sitemap.py | 29 +++++++++++++++++++++++++++++ devart/urls.py | 22 ++++++++++++++++++++++ 5 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 devart/sitemap.py diff --git a/courses/models.py b/courses/models.py index 1c5a096..3001eb5 100644 --- a/courses/models.py +++ b/courses/models.py @@ -15,6 +15,9 @@ class Course(models.Model): 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() @@ -25,6 +28,9 @@ 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() @@ -43,6 +49,9 @@ 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/views.py b/courses/views.py index 8a33bbb..b51bce0 100644 --- a/courses/views.py +++ b/courses/views.py @@ -9,7 +9,7 @@ def list_courses(request): courses = Course.objects.all() return render(request, 'courses/list.html', {'courses': courses}) -def show(request, course_name, course_id): +def show(request, course_id): # Optimized course fetch with related author and profile (if present) course = get_object_or_404( Course.objects.select_related('author', 'author__profile'), diff --git a/devart/settings.py b/devart/settings.py index 68e1525..fa7d2d5 100644 --- a/devart/settings.py +++ b/devart/settings.py @@ -46,6 +46,7 @@ INSTALLED_APPS = [ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + 'django.contrib.sitemaps', 'core', 'courses', diff --git a/devart/sitemap.py b/devart/sitemap.py new file mode 100644 index 0000000..05f4a77 --- /dev/null +++ b/devart/sitemap.py @@ -0,0 +1,29 @@ +from django.contrib import sitemaps +from django.urls import reverse + +# --- IMPORTS DEPUIS TES DIFFÉRENTES FEATURES --- +from courses.models import Course +from users.models import Profile + +# --- SITEMAP : LES Cours --- +class CourseSitemap(sitemaps.Sitemap): + changefreq = "weekly" + priority = 0.9 + + def items(self): + return Course.objects.filter(enable=True) # Exemple de filtre + + def location(self, item): + # Assure-toi que ton modèle Course a bien une méthode get_absolute_url + 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 "https://partirdezero.com" \ No newline at end of file diff --git a/devart/urls.py b/devart/urls.py index 86830eb..e198bae 100644 --- a/devart/urls.py +++ b/devart/urls.py @@ -18,12 +18,34 @@ 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 + +# La vue pour le robots.txt +def robots_txt(request): + lines = [ + "User-agent: *", + "Disallow: /admin/", + "Disallow: /users/", + "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('admin/', admin.site.urls), path('', include('home.urls')), path('courses/', include('courses.urls')), path('users/', include('users.urls')), + + path('sitemap.xml', sitemap, {'sitemaps': sitemaps_dict}, name='django.contrib.sitemaps.views.sitemap'), + path('robots.txt', robots_txt), ] if settings.DEBUG: From 84c94e28de5e3e35c2918b8b5cc747825d7b86f0 Mon Sep 17 00:00:00 2001 From: mrtoine Date: Mon, 15 Dec 2025 14:27:24 +0100 Subject: [PATCH 03/26] =?UTF-8?q?Mise=20=C3=A0=20jour=20de=20la=20version?= =?UTF-8?q?=20applicative=20dans=20`VERSION.txt`.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- VERSION.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION.txt b/VERSION.txt index a46d739..c71c6cc 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -1.0.3 (e79ffee) \ No newline at end of file +1.0.3 (cab7c07) \ No newline at end of file From 6c7f91c72f1742f0d367d6d2209f5069d1cdc718 Mon Sep 17 00:00:00 2001 From: mrtoine Date: Mon, 15 Dec 2025 14:29:39 +0100 Subject: [PATCH 04/26] =?UTF-8?q?Mise=20=C3=A0=20jour=20de=20la=20version?= =?UTF-8?q?=20applicative=20dans=20`VERSION.txt`.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- VERSION.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION.txt b/VERSION.txt index c71c6cc..2b1a19c 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -1.0.3 (cab7c07) \ No newline at end of file +1.1.0 (84c94e2) \ No newline at end of file From 2bfab05b494c0228a3e1a31521223e27b443a342 Mon Sep 17 00:00:00 2001 From: mrtoine Date: Mon, 15 Dec 2025 14:44:27 +0100 Subject: [PATCH 05/26] =?UTF-8?q?Mise=20=C3=A0=20jour=20de=20la=20version?= =?UTF-8?q?=20applicative=20dans=20`VERSION.txt`.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- devart/sitemap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devart/sitemap.py b/devart/sitemap.py index 05f4a77..83ae446 100644 --- a/devart/sitemap.py +++ b/devart/sitemap.py @@ -26,4 +26,4 @@ class StaticViewSitemap(sitemaps.Sitemap): return ["home"] # Les noms de tes URLs def location(self, item): - return "https://partirdezero.com" \ No newline at end of file + return "" \ No newline at end of file From 45d2cb66f000e4aba345bd9180be19d399251ad4 Mon Sep 17 00:00:00 2001 From: mrtoine Date: Mon, 15 Dec 2025 14:50:33 +0100 Subject: [PATCH 06/26] =?UTF-8?q?Mise=20=C3=A0=20jour=20de=20la=20version?= =?UTF-8?q?=20applicative=20dans=20`VERSION.txt`.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- courses/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/courses/views.py b/courses/views.py index b51bce0..8a33bbb 100644 --- a/courses/views.py +++ b/courses/views.py @@ -9,7 +9,7 @@ def list_courses(request): courses = Course.objects.all() return render(request, 'courses/list.html', {'courses': courses}) -def show(request, course_id): +def show(request, course_name, course_id): # Optimized course fetch with related author and profile (if present) course = get_object_or_404( Course.objects.select_related('author', 'author__profile'), From 3e4401313274a6e5e812deb269e36bcddf15814e Mon Sep 17 00:00:00 2001 From: mrtoine Date: Mon, 15 Dec 2025 16:02:34 +0100 Subject: [PATCH 07/26] =?UTF-8?q?Ajout=20des=20applications=20`blog`=20et?= =?UTF-8?q?=20`progression`=20avec=20mod=C3=A8les,=20vues,=20URLs=20et=20i?= =?UTF-8?q?nt=C3=A9gration=20dans=20le=20sitemap=20et=20les=20configuratio?= =?UTF-8?q?ns=20du=20projet.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- blog/__init__.py | 0 blog/admin.py | 9 +++++ blog/apps.py | 5 +++ blog/migrations/0001_initial.py | 30 ++++++++++++++++ blog/migrations/__init__.py | 0 blog/models.py | 16 +++++++++ blog/tests.py | 3 ++ blog/urls.py | 8 +++++ blog/views.py | 7 ++++ ...ter_course_options_alter_lesson_options.py | 21 ++++++++++++ courses/models.py | 8 +++++ devart/settings.py | 2 ++ devart/sitemap.py | 9 +++++ devart/urls.py | 2 ++ progression/__init__.py | 0 progression/admin.py | 15 ++++++++ progression/apps.py | 5 +++ progression/migrations/0001_initial.py | 34 +++++++++++++++++++ progression/migrations/__init__.py | 0 progression/models.py | 28 +++++++++++++++ progression/tests.py | 3 ++ progression/urls.py | 7 ++++ progression/views.py | 3 ++ 23 files changed, 215 insertions(+) create mode 100644 blog/__init__.py create mode 100644 blog/admin.py create mode 100644 blog/apps.py create mode 100644 blog/migrations/0001_initial.py create mode 100644 blog/migrations/__init__.py create mode 100644 blog/models.py create mode 100644 blog/tests.py create mode 100644 blog/urls.py create mode 100644 blog/views.py create mode 100644 courses/migrations/0004_alter_course_options_alter_lesson_options.py create mode 100644 progression/__init__.py create mode 100644 progression/admin.py create mode 100644 progression/apps.py create mode 100644 progression/migrations/0001_initial.py create mode 100644 progression/migrations/__init__.py create mode 100644 progression/models.py create mode 100644 progression/tests.py create mode 100644 progression/urls.py create mode 100644 progression/views.py diff --git a/blog/__init__.py b/blog/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/blog/admin.py b/blog/admin.py new file mode 100644 index 0000000..879ff7e --- /dev/null +++ b/blog/admin.py @@ -0,0 +1,9 @@ +from django.contrib import admin +from .models import Post + +@admin.register(Post) +class PostAdmin(admin.ModelAdmin): + list_display = ('name', 'tags', 'slug', 'created_at') + list_filter = ('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 new file mode 100644 index 0000000..7930587 --- /dev/null +++ b/blog/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class BlogConfig(AppConfig): + name = 'blog' diff --git a/blog/migrations/0001_initial.py b/blog/migrations/0001_initial.py new file mode 100644 index 0000000..9c7c01b --- /dev/null +++ b/blog/migrations/0001_initial.py @@ -0,0 +1,30 @@ +# 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/__init__.py b/blog/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/blog/models.py b/blog/models.py new file mode 100644 index 0000000..1ca60c0 --- /dev/null +++ b/blog/models.py @@ -0,0 +1,16 @@ +from django.db import models + +class Post(models.Model): + 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) + + class Meta: + verbose_name = "Article" + verbose_name_plural = "Articles" + + def __str__(self): + return self.name \ No newline at end of file diff --git a/blog/tests.py b/blog/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/blog/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/blog/urls.py b/blog/urls.py new file mode 100644 index 0000000..902608b --- /dev/null +++ b/blog/urls.py @@ -0,0 +1,8 @@ +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 new file mode 100644 index 0000000..fd8566d --- /dev/null +++ b/blog/views.py @@ -0,0 +1,7 @@ +from django.shortcuts import render + +def blog_home(request): + return "" + +def blog_view_post(request): + return "" \ 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 new file mode 100644 index 0000000..ff801fb --- /dev/null +++ b/courses/migrations/0004_alter_course_options_alter_lesson_options.py @@ -0,0 +1,21 @@ +# 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 3001eb5..3cc46d1 100644 --- a/courses/models.py +++ b/courses/models.py @@ -12,6 +12,10 @@ 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 @@ -40,6 +44,10 @@ 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: diff --git a/devart/settings.py b/devart/settings.py index fa7d2d5..9bb9173 100644 --- a/devart/settings.py +++ b/devart/settings.py @@ -51,6 +51,8 @@ INSTALLED_APPS = [ 'core', 'courses', 'users', + 'progression', + 'blog', ] MIDDLEWARE = [ diff --git a/devart/sitemap.py b/devart/sitemap.py index 83ae446..a92ac9d 100644 --- a/devart/sitemap.py +++ b/devart/sitemap.py @@ -4,6 +4,7 @@ from django.urls import reverse # --- IMPORTS DEPUIS TES DIFFÉRENTES FEATURES --- from courses.models import Course from users.models import Profile +from blog.models import Post # --- SITEMAP : LES Cours --- class CourseSitemap(sitemaps.Sitemap): @@ -17,6 +18,14 @@ class CourseSitemap(sitemaps.Sitemap): # 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 diff --git a/devart/urls.py b/devart/urls.py index e198bae..b46feda 100644 --- a/devart/urls.py +++ b/devart/urls.py @@ -44,6 +44,8 @@ urlpatterns = [ path('courses/', include('courses.urls')), path('users/', include('users.urls')), + path('blog/', include('blog.urls')), + path('sitemap.xml', sitemap, {'sitemaps': sitemaps_dict}, name='django.contrib.sitemaps.views.sitemap'), path('robots.txt', robots_txt), ] diff --git a/progression/__init__.py b/progression/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/progression/admin.py b/progression/admin.py new file mode 100644 index 0000000..6dd7924 --- /dev/null +++ b/progression/admin.py @@ -0,0 +1,15 @@ +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 new file mode 100644 index 0000000..7bf7a21 --- /dev/null +++ b/progression/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class ProgressionConfig(AppConfig): + name = 'progression' diff --git a/progression/migrations/0001_initial.py b/progression/migrations/0001_initial.py new file mode 100644 index 0000000..f2023ef --- /dev/null +++ b/progression/migrations/0001_initial.py @@ -0,0 +1,34 @@ +# 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 new file mode 100644 index 0000000..e69de29 diff --git a/progression/models.py b/progression/models.py new file mode 100644 index 0000000..d8002a8 --- /dev/null +++ b/progression/models.py @@ -0,0 +1,28 @@ +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 new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/progression/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/progression/urls.py b/progression/urls.py new file mode 100644 index 0000000..a2fd910 --- /dev/null +++ b/progression/urls.py @@ -0,0 +1,7 @@ +from django.urls import path +from . import views + +app_name = 'progression' +urlpatterns = [ + +] \ No newline at end of file diff --git a/progression/views.py b/progression/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/progression/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. From c1749068afc432ec91ef560ad42bf10c9ad0b95f Mon Sep 17 00:00:00 2001 From: mrtoine Date: Mon, 15 Dec 2025 20:58:25 +0100 Subject: [PATCH 08/26] =?UTF-8?q?Ajout=20des=20fonctionnalit=C3=A9s=20de?= =?UTF-8?q?=20blog=20:=20mod=C3=A8les,=20migrations,=20vues,=20templates,?= =?UTF-8?q?=20contexte=20et=20styles.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- blog/admin.py | 1 - blog/context_processor.py | 5 + blog/migrations/0002_post_enable.py | 18 ++++ blog/models.py | 1 + blog/urls.py | 2 +- blog/views.py | 12 ++- core/admin.py | 5 +- ..._sitesettings_blog_description_and_more.py | 23 +++++ ..._sitesettings_blog_description_and_more.py | 23 +++++ core/models.py | 4 + devart/settings.py | 1 + static/css/app.css | 94 +++++++++++++++++++ templates/blog/details.html | 24 +++++ templates/blog/home.html | 17 ++++ templates/blog/partials/_posts_list.html | 20 ++++ templates/partials/_header.html | 4 +- 16 files changed, 245 insertions(+), 9 deletions(-) create mode 100644 blog/context_processor.py create mode 100644 blog/migrations/0002_post_enable.py create mode 100644 core/migrations/0002_sitesettings_blog_description_and_more.py create mode 100644 core/migrations/0003_alter_sitesettings_blog_description_and_more.py create mode 100644 templates/blog/details.html create mode 100644 templates/blog/home.html create mode 100644 templates/blog/partials/_posts_list.html diff --git a/blog/admin.py b/blog/admin.py index 879ff7e..8eb9366 100644 --- a/blog/admin.py +++ b/blog/admin.py @@ -4,6 +4,5 @@ from .models import Post @admin.register(Post) class PostAdmin(admin.ModelAdmin): list_display = ('name', 'tags', 'slug', 'created_at') - list_filter = ('created_at',) search_fields = ('name', 'tags') prepopulated_fields = {"slug": ("name",)} \ No newline at end of file diff --git a/blog/context_processor.py b/blog/context_processor.py new file mode 100644 index 0000000..a1666a4 --- /dev/null +++ b/blog/context_processor.py @@ -0,0 +1,5 @@ +from .models import Post + +def posts_list(request): + posts = Post.objects.all() + return {'posts': posts} \ No newline at end of file diff --git a/blog/migrations/0002_post_enable.py b/blog/migrations/0002_post_enable.py new file mode 100644 index 0000000..bb99a9b --- /dev/null +++ b/blog/migrations/0002_post_enable.py @@ -0,0 +1,18 @@ +# 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/models.py b/blog/models.py index 1ca60c0..a2cfaca 100644 --- a/blog/models.py +++ b/blog/models.py @@ -7,6 +7,7 @@ class Post(models.Model): 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" diff --git a/blog/urls.py b/blog/urls.py index 902608b..ef6ad01 100644 --- a/blog/urls.py +++ b/blog/urls.py @@ -4,5 +4,5 @@ from . import views app_name = 'blog' urlpatterns = [ path('', views.blog_home, name='blog'), - path('/', views.blog_view_post, name='post_detail'), + path('/', views.blog_view_post, name='post_detail'), ] \ No newline at end of file diff --git a/blog/views.py b/blog/views.py index fd8566d..3db99b3 100644 --- a/blog/views.py +++ b/blog/views.py @@ -1,7 +1,11 @@ from django.shortcuts import render -def blog_home(request): - return "" +from blog.models import Post -def blog_view_post(request): - return "" \ No newline at end of file + +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 7f53918..0989d6d 100644 --- a/core/admin.py +++ b/core/admin.py @@ -23,4 +23,7 @@ class SiteSettingsAdmin(admin.ModelAdmin): ('Contact', { 'fields': ('contact_email',) }), - ) \ No newline at end of file + ('Blog', { + 'fields': ('blog_title', 'blog_description') + }), + ) diff --git a/core/migrations/0002_sitesettings_blog_description_and_more.py b/core/migrations/0002_sitesettings_blog_description_and_more.py new file mode 100644 index 0000000..7d669a8 --- /dev/null +++ b/core/migrations/0002_sitesettings_blog_description_and_more.py @@ -0,0 +1,23 @@ +# 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 new file mode 100644 index 0000000..ccfc1e0 --- /dev/null +++ b/core/migrations/0003_alter_sitesettings_blog_description_and_more.py @@ -0,0 +1,23 @@ +# 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/models.py b/core/models.py index de47391..b95ca68 100644 --- a/core/models.py +++ b/core/models.py @@ -13,6 +13,10 @@ 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. diff --git a/devart/settings.py b/devart/settings.py index 9bb9173..2736323 100644 --- a/devart/settings.py +++ b/devart/settings.py @@ -83,6 +83,7 @@ TEMPLATES = [ 'devart.context_processor.app_version', 'core.context_processor.site_settings', 'courses.context_processors.course_list', + 'blog.context_processor.posts_list', ], }, }, diff --git a/static/css/app.css b/static/css/app.css index 508c4be..d95a712 100644 --- a/static/css/app.css +++ b/static/css/app.css @@ -751,6 +751,100 @@ img { 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; } diff --git a/templates/blog/details.html b/templates/blog/details.html new file mode 100644 index 0000000..4ac35bb --- /dev/null +++ b/templates/blog/details.html @@ -0,0 +1,24 @@ +{% extends 'layout.html' %} +{% 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.name }}

+ +
+ +
+ {{ post.content|safe }} +
+
+{% endblock %} \ No newline at end of file diff --git a/templates/blog/home.html b/templates/blog/home.html new file mode 100644 index 0000000..b89265d --- /dev/null +++ b/templates/blog/home.html @@ -0,0 +1,17 @@ +{% 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 %} +
+
+

{{ settings.blog_title|default:'Blog' }}

+

{{ settings.blog_description }}

+
+ +

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 new file mode 100644 index 0000000..08a2d91 --- /dev/null +++ b/templates/blog/partials/_posts_list.html @@ -0,0 +1,20 @@ +
+ {% for post in posts %} +
+

{{ post.name }}

+ +

{{ post.content|striptags|truncatewords:26 }}

+
+ Lire l'article → +
+
+ {% empty %} +

Aucun article pour le moment.

+ {% endfor %} +
diff --git a/templates/partials/_header.html b/templates/partials/_header.html index 7b6107d..20db214 100644 --- a/templates/partials/_header.html +++ b/templates/partials/_header.html @@ -20,8 +20,8 @@ {% endfor %} - + +
  • Blog
  • + +{% endblock %} + +{% block content %} +
    +

    Graphiques statistiques

    + +
    +
    + + + Du {{ start_date }} au {{ end_date }} +
    +
    Mise en cache 15 minutes
    +
    + +
    +
    +

    Visiteurs uniques par jour

    + +
    +
    +

    Conversions (visiteurs devenus utilisateurs) par jour

    + +
    +
    + +
    +
    +

    Top sources (visiteurs uniques)

    + +
    +
    +

    Top pays (visiteurs uniques)

    + +
    +
    + +

    ← Retour au tableau de bord

    +
    + + +{% endblock %} From e1f8a23f3df8c5781a0c220bdc251db746553fb0 Mon Sep 17 00:00:00 2001 From: mrtoine Date: Tue, 16 Dec 2025 10:53:26 +0100 Subject: [PATCH 26/26] =?UTF-8?q?Mise=20=C3=A0=20jour=20de=20la=20version?= =?UTF-8?q?=20applicative=20dans=20`VERSION.txt`.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- VERSION.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION.txt b/VERSION.txt index 3894f62..299b2e2 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -1.3.5 (7869abf) \ No newline at end of file +1.3.5 (1b0ccc5) \ No newline at end of file