From b7f792a18295077a6c62a80d3a66c9d0dbca6757 Mon Sep 17 00:00:00 2001 From: mrtoine Date: Mon, 15 Dec 2025 13:16:40 +0100 Subject: [PATCH 01/31] =?UTF-8?q?Am=C3=A9lioration=20de=20la=20r=C3=A9acti?= =?UTF-8?q?vit=C3=A9=20et=20des=20styles=20:=20ajustements=20CSS,=20templa?= =?UTF-8?q?tes=20dynamiques,=20et=20ajout=20de=20la=20gestion=20de=20versi?= =?UTF-8?q?on=20applicative.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- devart/settings.py | 20 ++++- static/css/app.css | 125 +++++++++++++++++--------------- templates/home.html | 2 +- templates/layout.html | 2 +- templates/partials/_footer.html | 2 +- 5 files changed, 87 insertions(+), 64 deletions(-) diff --git a/devart/settings.py b/devart/settings.py index 1936a8a..044d68c 100644 --- a/devart/settings.py +++ b/devart/settings.py @@ -14,6 +14,8 @@ from pathlib import Path import os from dotenv import load_dotenv +import devart.context_processor + # Charger les variables d'environnement depuis le fichier .env load_dotenv() @@ -74,7 +76,8 @@ 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', 'courses.context_processors.course_list', ], @@ -152,3 +155,18 @@ 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(): + try: + import subprocess + hash_version = subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD']).strip().decode('utf-8') + app_version = subprocess.check_output( + ["git", "describe", "--tags", "--abbrev=0"], + stderr=subprocess.STDOUT + ).strip().decode('utf-8') + return app_version + " (" + hash_version + ")" + except Exception: + return 'Dev / Pas de git' + +GIT_VERSION = get_git_version() \ No newline at end of file diff --git a/static/css/app.css b/static/css/app.css index 8d25550..9aa06af 100644 --- a/static/css/app.css +++ b/static/css/app.css @@ -79,8 +79,9 @@ --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; /* Layout */ - --container-w: 1100px; - --gutter: 20px; + /* Fluid container and gutters for responsive spacing */ + --container-w: clamp(320px, 92vw, 1100px); + --gutter: clamp(14px, 3vw, 24px); --focus-ring: 0 0 0 3px rgba(78,158,214,0.45); /* Utility base colors */ @@ -108,6 +109,23 @@ --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 */ /* ========================== @@ -469,8 +487,7 @@ body { height: 100vh; /* Ensure total box never exceeds the viewport width (padding + border included) */ box-sizing: border-box; - width: min(85vw, 380px); - max-width: 100vw; + width: min(92vw, 380px); 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); @@ -510,8 +527,8 @@ body { z-index: 1100; } - .navbar ul { flex-direction: column; width: 100%; } - .navbar li { margin: 0; } + .navbar ul { flex-direction: column; width: 100%; align-items: flex-start; } + .navbar li { margin: 0; width: 100%; } .navbar a { display: block; padding: 12px 14px; border-radius: var(--r-1); } /* Dropdowns behave as inline lists on mobile */ @@ -524,18 +541,22 @@ body { padding-left: 10px; } - .navbar ul ul li { border: 0; margin: 0; padding: 6px 8px; } + .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; } .navend { width: 100%; margin-top: auto; /* push to bottom */ display: flex; flex-direction: column; - align-items: stretch; + align-items: flex-start; /* start-align items on mobile */ + justify-content: flex-start; gap: 6px; } - .navend ul { width: 100%; } + .navend ul { width: 100%; align-items: flex-start; } + .navend a { text-align: left; } } /* Respect reduced motion preferences */ @@ -650,17 +671,18 @@ section { display: flex; flex-direction: column; padding: var(--space-6) var(--gutter); - width: min(100%, 60%); - margin: 20px auto; + width: 100%; + max-width: var(--container-w); + margin: clamp(12px, 3vw, 24px) auto; } .courseNav { display: block; float: left; - position: fixed; + position: sticky; left: 0; - top: 150px; - max-width: 15%; + top: clamp(64px, 12vh, 150px); + max-width: clamp(220px, 22vw, 300px); border: 1px solid; } @@ -691,9 +713,7 @@ section { h1 { color: var(--primary); font-size: clamp(1.8rem, 4vw, 2.5rem); - border-left: 5px solid var(--accent); - border-bottom: 1px solid var(--accent); - padding-left: 10px; + padding-left: var(--space-3); } h2 { @@ -703,8 +723,15 @@ h2 { img { max-width: 100%; + height: 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 } @@ -714,21 +741,20 @@ img { } .container-inline { - display: flex; - flex-direction: row; - flex-wrap: wrap; - gap: 20px; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: var(--space-5); } .card { display: flex; flex-direction: column; - max-width: 250px; + width: 100%; border-left: 1px solid var(--border); border-top: 1px solid var(--border); border-radius: var(--r-2); overflow: hidden; - margin: 20px; + margin: 0; background: var(--card); box-shadow: var(--shadow-1); transition: transform var(--transition-1), box-shadow var(--transition-1); @@ -767,7 +793,7 @@ img { .def-author { display: flex; - padding: 10px; + padding: var(--space-3); flex-direction: row; align-items: center; font-size: 0.8rem; @@ -780,18 +806,18 @@ img { } pre { - border-radius: 5px; + border-radius: var(--r-2); /* Ne dépasse pas du parent en largeur et défile si nécessaire */ - max-width: 800px; + max-width: clamp(320px, 100%, 800px); overflow: auto; /* scroll horizontal et vertical si besoin */ - max-height: 800px; + max-height: clamp(360px, 70vh, 800px); } /* ====== Homepage enhancements ====== */ .hero { display: block; width: 100%; - padding: 80px 20px 56px; + padding: clamp(48px, 10vw, 96px) var(--gutter) clamp(32px, 6vw, 64px); text-align: center; background: radial-gradient(1200px 600px at 50% -10%, rgba(78,158,214,0.20), transparent 60%), @@ -800,10 +826,7 @@ pre { position: relative; } -.hero-inner { - max-width: 960px; - margin: 0 auto; -} +.hero-inner { max-width: var(--container-w); margin: 0 auto; padding: 0 var(--gutter); } .hero-decor { position: absolute; @@ -816,31 +839,12 @@ pre { opacity: .6; } -.hero .hero-sub { - font-size: 1.125rem; - font-weight: 500; - margin: 10px auto 0; - max-width: 800px; -} +.hero .hero-sub { font-size: clamp(1rem, 2.4vw, 1.125rem); font-weight: 500; margin: var(--space-3) auto 0; max-width: 65ch; } -.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); -} +.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); } -.hero-cta .button { - padding: 12px 18px; - font-size: 1rem; - border-radius: 6px; -} +.hero-cta .button { padding: 12px 18px; font-size: 1rem; border-radius: var(--r-2); } .cta-primary { /* Make primary CTA more visible */ @@ -927,11 +931,12 @@ pre { } .pricing-teaser { - width: 60%; - margin: 10px auto 40px; + width: 100%; + max-width: 820px; + margin: var(--space-3) auto var(--space-6); border: 1px solid var(--border); border-radius: var(--r-3); - padding: 20px; + padding: var(--space-5); background: var(--card); } @@ -943,7 +948,7 @@ pre { .carousel-track { display: grid; grid-auto-flow: column; - grid-auto-columns: 80%; + grid-auto-columns: minmax(260px, 80%); gap: var(--space-4); overflow-x: auto; scroll-snap-type: x mandatory; @@ -967,7 +972,7 @@ pre { display: grid; gap: var(--space-4); } -.testimonials-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--space-4); } +.testimonials-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: var(--space-4); } .testimonial { background: var(--card); border: 1px solid var(--border); diff --git a/templates/home.html b/templates/home.html index fd4b276..76ebca2 100644 --- a/templates/home.html +++ b/templates/home.html @@ -6,7 +6,7 @@
-

Apprenez à coder de A à Z

+

Apprendre le développement de zéro : Cours complets pour débutant (ou pas !)

Des cours gratuits, structurés et concrets, pour progresser rapidement en programmation.

\ No newline at end of file From a5c1aa06a5ca1103e6ed5e88cced4e917895e7a6 Mon Sep 17 00:00:00 2001 From: mrtoine Date: Mon, 15 Dec 2025 13:19:03 +0100 Subject: [PATCH 02/31] Ajout d'un context processor pour fournir la version applicative via `SITE_VERSION`. --- devart/context_processor.py | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 devart/context_processor.py diff --git a/devart/context_processor.py b/devart/context_processor.py new file mode 100644 index 0000000..a5d2be3 --- /dev/null +++ b/devart/context_processor.py @@ -0,0 +1,4 @@ +from django.conf import settings + +def app_version(request): + return {'SITE_VERSION': settings.GIT_VERSION} \ No newline at end of file From b3f201dd8dbe7b9982cb6c8a45e6774f75a7f5f3 Mon Sep 17 00:00:00 2001 From: mrtoine Date: Mon, 15 Dec 2025 13:37:56 +0100 Subject: [PATCH 03/31] =?UTF-8?q?Ajout=20d'une=20gestion=20conditionnelle?= =?UTF-8?q?=20de=20la=20version=20applicative=20avec=20mise=20=C3=A0=20jou?= =?UTF-8?q?r=20ou=20lecture=20depuis=20`VERSION.txt`=20selon=20le=20mode?= =?UTF-8?q?=20(DEV/PROD).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- devart/settings.py | 49 ++++++++++++++++++++++++++++++++++++---------- 1 file changed, 39 insertions(+), 10 deletions(-) diff --git a/devart/settings.py b/devart/settings.py index 044d68c..68e1525 100644 --- a/devart/settings.py +++ b/devart/settings.py @@ -158,15 +158,44 @@ 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(): - try: - import subprocess - hash_version = subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD']).strip().decode('utf-8') - app_version = subprocess.check_output( - ["git", "describe", "--tags", "--abbrev=0"], - stderr=subprocess.STDOUT - ).strip().decode('utf-8') - return app_version + " (" + hash_version + ")" - except Exception: - return 'Dev / Pas de git' + 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() \ No newline at end of file From 8668690d110afa80953c5f7d61208d2196ac57fd Mon Sep 17 00:00:00 2001 From: mrtoine Date: Mon, 15 Dec 2025 13:39:49 +0100 Subject: [PATCH 04/31] =?UTF-8?q?Ajout=20d'une=20gestion=20conditionnelle?= =?UTF-8?q?=20de=20la=20version=20applicative=20avec=20mise=20=C3=A0=20jou?= =?UTF-8?q?r=20ou=20lecture=20depuis=20`VERSION.txt`=20selon=20le=20mode?= =?UTF-8?q?=20(DEV/PROD).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- VERSION.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 VERSION.txt diff --git a/VERSION.txt b/VERSION.txt new file mode 100644 index 0000000..9cc3383 --- /dev/null +++ b/VERSION.txt @@ -0,0 +1 @@ +1.0.2 (b3f201d) \ No newline at end of file From e79ffeeffaa85cc3490ccf209b37e875be74d084 Mon Sep 17 00:00:00 2001 From: mrtoine Date: Mon, 15 Dec 2025 13:45:25 +0100 Subject: [PATCH 05/31] =?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 +- static/css/app.css | 29 +++++++++++++++++++++++++++-- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/VERSION.txt b/VERSION.txt index 9cc3383..e3b55ea 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -1.0.2 (b3f201d) \ No newline at end of file +1.0.2 (8668690) \ No newline at end of file diff --git a/static/css/app.css b/static/css/app.css index 9aa06af..508c4be 100644 --- a/static/css/app.css +++ b/static/css/app.css @@ -466,6 +466,11 @@ 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; @@ -710,17 +715,37 @@ 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); - font-size: clamp(1.8rem, 4vw, 2.5rem); + border-left: 4px solid var(--accent); padding-left: var(--space-3); } h2 { + font-size: clamp(1.5rem, 3.5vw, 2rem); color: var(--accent); - font-size: clamp(1.3rem, 3vw, 1.8rem); + border-bottom: 1px solid var(--border); + padding-bottom: 6px; + margin-top: var(--space-6); } +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; From e77ea7e20eff0a6c33af1d283dc7544fcd6b2c77 Mon Sep 17 00:00:00 2001 From: mrtoine Date: Mon, 15 Dec 2025 13:46:02 +0100 Subject: [PATCH 06/31] =?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 07/31] =?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 08/31] =?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 09/31] =?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 10/31] =?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 11/31] =?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 12/31] =?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 13/31] =?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 31/31] =?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