diff --git a/core/admin.py b/core/admin.py index 90b60d0..13f115b 100644 --- a/core/admin.py +++ b/core/admin.py @@ -1,5 +1,5 @@ from django.contrib import admin -from .models import SiteSettings, Visit +from .models import SiteSettings, Visit, Maintenance @admin.register(SiteSettings) class SiteSettingsAdmin(admin.ModelAdmin): @@ -28,6 +28,10 @@ class SiteSettingsAdmin(admin.ModelAdmin): }), ) +@admin.register(Maintenance) +class MaintenanceAdmin(admin.ModelAdmin): + list_display = ("name","start_date", "end_date") + @admin.register(Visit) class VisitAdmin(admin.ModelAdmin): diff --git a/core/context_processor.py b/core/context_processor.py index a1410e3..eccaebf 100644 --- a/core/context_processor.py +++ b/core/context_processor.py @@ -1,5 +1,14 @@ -from .models import SiteSettings +from django.utils.timesince import timesince + +from .models import SiteSettings, Maintenance def site_settings(request): # On récupère le premier objet, ou None s'il n'existe pas encore - return {'settings': SiteSettings.objects.first()} \ No newline at end of file + 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 diff --git a/core/migrations/0006_maintenance.py b/core/migrations/0006_maintenance.py new file mode 100644 index 0000000..1cac83f --- /dev/null +++ b/core/migrations/0006_maintenance.py @@ -0,0 +1,23 @@ +# 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 new file mode 100644 index 0000000..eef8f59 --- /dev/null +++ b/core/migrations/0007_maintenance_name.py @@ -0,0 +1,18 @@ +# 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 297916d..c28ae82 100644 --- a/core/models.py +++ b/core/models.py @@ -35,6 +35,12 @@ class SiteSettings(models.Model): 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). diff --git a/core/urls.py b/core/urls.py new file mode 100644 index 0000000..abf8ffc --- /dev/null +++ b/core/urls.py @@ -0,0 +1,8 @@ +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') +] \ No newline at end of file diff --git a/core/views.py b/core/views.py index 91ea44a..3d8b995 100644 --- a/core/views.py +++ b/core/views.py @@ -1,3 +1,20 @@ from django.shortcuts import render +from django.core.management import call_command +from django.core.cache import cache +import subprocess -# Create your views here. +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}) \ No newline at end of file diff --git a/devart/settings.py b/devart/settings.py index f846daa..db43f75 100644 --- a/devart/settings.py +++ b/devart/settings.py @@ -84,6 +84,7 @@ TEMPLATES = [ 'devart.context_processor.app_version', 'core.context_processor.site_settings', + 'core.context_processor.site_maintenance', 'courses.context_processors.course_list', 'blog.context_processor.posts_list', ], diff --git a/devart/urls.py b/devart/urls.py index 17cb4e7..036a891 100644 --- a/devart/urls.py +++ b/devart/urls.py @@ -22,12 +22,13 @@ from django.http import HttpResponse from devart.sitemap import CourseSitemap, StaticViewSitemap from django.contrib.sitemaps.views import sitemap -# La vue pour le robots.txt def robots_txt(request): lines = [ "User-agent: *", "Disallow: /admin/", "Disallow: /users/", + "Disallow: /maintenance/", + "Disallow: /core/", "Allow: /", "Sitemap: https://partirdezero.com/sitemap.xml", # On indique déjà où sera le plan ] @@ -39,6 +40,7 @@ sitemaps_dict = { } urlpatterns = [ + path('core/', include('core.urls')), path('admin/', admin.site.urls), path('', include('home.urls')), path('courses/', include('courses.urls')), diff --git a/staticfiles/js/functions.js b/staticfiles/js/functions.js index d5195f9..500562a 100644 --- a/staticfiles/js/functions.js +++ b/staticfiles/js/functions.js @@ -12,4 +12,86 @@ function show(id) { htmlChange.id = `show-${id}`; let buttonChange = document.getElementById(id); buttonChange.onclick = function() { hide(id); }; -} \ No newline at end of file +} + +// 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 diff --git a/templates/layout.html b/templates/layout.html index 2a39086..1d70ca8 100644 --- a/templates/layout.html +++ b/templates/layout.html @@ -84,29 +84,33 @@ - {% now "n" as month %} - {% if month == '12' %} - - - {% endif %} - {% block header %} - {% include "partials/_header.html" %} - {% endblock %} - -
- {% if messages %} - + {% if maintenance.is_active == True %} + {% include "maintenance.html" %} + {% else %} + {% now "n" as month %} + {% if month == '12' %} + + {% endif %} + {% block header %} + {% include "partials/_header.html" %} + {% endblock %} - {% block content %}{% endblock %} -
+
+ {% if messages %} + + {% endif %} - {% block footer %} - {% include "partials/_footer.html" %} - {% endblock %} + {% block content %}{% endblock %} +
+ + {% block footer %} + {% include "partials/_footer.html" %} + {% endblock %} + {% endif %} \ No newline at end of file diff --git a/templates/maintenance.html b/templates/maintenance.html new file mode 100644 index 0000000..52f40a9 --- /dev/null +++ b/templates/maintenance.html @@ -0,0 +1,26 @@ +{% load comment_format %} +
+

Maintenance : {{ maintenance.name }}

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

{{ message }}

+ {% endif %} + + {% if user.is_superuser %} +
+
+ +
+ {% endif %} +
\ No newline at end of file