diff --git a/manifeste_velo/package-lock.json b/manifeste_velo/package-lock.json new file mode 100644 index 0000000..0352dab --- /dev/null +++ b/manifeste_velo/package-lock.json @@ -0,0 +1,73 @@ +{ + "name": "manifeste_velo", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "bootstrap": "5.3.*", + "bootstrap-icons": "1.13.*", + "htmx.org": "2.0.*", + "leaflet": "~1.9.4" + } + }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/bootstrap": { + "version": "5.3.8", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.8.tgz", + "integrity": "sha512-HP1SZDqaLDPwsNiqRqi5NcP0SSXciX2s9E+RyqJIIqGo+vJeN5AJVM98CXmW/Wux0nQ5L7jeWUdplCEf0Ee+tg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ], + "license": "MIT", + "peerDependencies": { + "@popperjs/core": "^2.11.8" + } + }, + "node_modules/bootstrap-icons": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.13.1.tgz", + "integrity": "sha512-ijombt4v6bv5CLeXvRWKy7CuM3TRTuPEuGaGKvTV5cz65rQSY8RQ2JcHt6b90cBBAC7s8fsf2EkQDldzCoXUjw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ], + "license": "MIT" + }, + "node_modules/htmx.org": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-2.0.8.tgz", + "integrity": "sha512-fm297iru0iWsNJlBrjvtN7V9zjaxd+69Oqjh4F/Vq9Wwi2kFisLcrLCiv5oBX0KLfOX/zG8AUo9ROMU5XUB44Q==", + "license": "0BSD" + }, + "node_modules/leaflet": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", + "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", + "license": "BSD-2-Clause" + } + } +} diff --git a/manifeste_velo/package.json b/manifeste_velo/package.json new file mode 100644 index 0000000..a82223c --- /dev/null +++ b/manifeste_velo/package.json @@ -0,0 +1,8 @@ +{ + "dependencies": { + "bootstrap": "5.3.*", + "bootstrap-icons": "1.13.*", + "htmx.org": "2.0.*", + "leaflet": "~1.9.4" + } +} \ No newline at end of file diff --git a/manifeste_velo/questionnaire/admin.py b/manifeste_velo/questionnaire/admin.py index 4fb097d..ab2e625 100644 --- a/manifeste_velo/questionnaire/admin.py +++ b/manifeste_velo/questionnaire/admin.py @@ -1,9 +1,24 @@ from django.contrib import admin +from django.contrib.auth.admin import UserAdmin as BaseUserAdmin +from django.contrib.auth.models import User from questionnaire import models # Register your models here. + +class UtilisateurInline(admin.StackedInline): + model = models.Utilisateur + can_delete = False + + +class UserAdmin(BaseUserAdmin): + inlines = [UtilisateurInline] + + +admin.site.unregister(User) +admin.site.register(User, UserAdmin) + admin.site.register(models.Ville) admin.site.register(models.PointManifeste) admin.site.register(models.Liste) diff --git a/manifeste_velo/questionnaire/migrations/0002_utilisateur.py b/manifeste_velo/questionnaire/migrations/0002_utilisateur.py new file mode 100644 index 0000000..89eea63 --- /dev/null +++ b/manifeste_velo/questionnaire/migrations/0002_utilisateur.py @@ -0,0 +1,24 @@ +# Generated by Django 6.0.1 on 2026-01-23 09:35 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('questionnaire', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Utilisateur', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ('ville', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='questionnaire.ville')), + ], + ), + ] diff --git a/manifeste_velo/questionnaire/migrations/0003_candidat_email.py b/manifeste_velo/questionnaire/migrations/0003_candidat_email.py new file mode 100644 index 0000000..6c65187 --- /dev/null +++ b/manifeste_velo/questionnaire/migrations/0003_candidat_email.py @@ -0,0 +1,18 @@ +# Generated by Django 6.0.1 on 2026-01-23 09:37 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('questionnaire', '0002_utilisateur'), + ] + + operations = [ + migrations.AddField( + model_name='candidat', + name='email', + field=models.CharField(blank=True, max_length=255, null=True), + ), + ] diff --git a/manifeste_velo/questionnaire/models.py b/manifeste_velo/questionnaire/models.py index d2b672c..84c1f7f 100644 --- a/manifeste_velo/questionnaire/models.py +++ b/manifeste_velo/questionnaire/models.py @@ -1,3 +1,4 @@ +from django.contrib.auth.models import User from django.db import models from django_extensions.db.fields import AutoSlugField from django_extensions.db.models import TimeStampedModel @@ -14,6 +15,14 @@ class Ville(models.Model): return f"{self.nom} ({self.code_insee})" +class Utilisateur(models.Model): + user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="profile") + ville = models.ForeignKey(Ville, on_delete=models.SET_NULL, blank=True, null=True) + + def __str__(self): + return self.user + + class PointManifeste(models.Model): titre = models.CharField(max_length=255) texte = models.TextField() @@ -32,9 +41,10 @@ class PointManifeste(models.Model): class Candidat(TimeStampedModel, models.Model): nom = models.CharField(max_length=255) prenom = models.CharField(max_length=255, verbose_name="Prénom") + email = models.CharField(max_length=255, blank=True, null=True) def __str__(self): - return f"{self.prenom} {self.nom} - {self.ville.nom}" + return f"{self.prenom} {self.nom}" class Liste(TimeStampedModel, models.Model): @@ -50,10 +60,11 @@ class Liste(TimeStampedModel, models.Model): responsable_mobilites = models.OneToOneField( Candidat, on_delete=models.PROTECT, verbose_name="Responsable mobilités de la liste" ) - email_ouvert = models.DateTimeField(blank=True, null=True, verbose_name="L'e-mail au candidat a été ouvert") + email_envoye = models.DateTimeField(blank=True, null=True, verbose_name="Date et heure de l'envoi de l'e-mail") + email_ouvert = models.DateTimeField(blank=True, null=True, verbose_name="Date et heure d'ouverture de l'e-mail") def __str__(self): - return f"{self.nom} - {self.ville.nom} - {self.tete_liste.prenom} {self.tete_liste.nom}" + return f"{self.nom} - {self.ville.nom} - {self.tete_liste}" class Meta: ordering = ["ville", "nom"] @@ -64,6 +75,10 @@ class ReponseListe(TimeStampedModel, models.Model): expression_libre = models.TextField(verbose_name="Expression libre") email_confirmation = models.BooleanField(verbose_name="E-mail de confirmation envoyé") finalise = models.BooleanField(verbose_name="Réponse finalisée") + date_heure_finalisation = models.DateTimeField(blank=True, null=True) + + def __str__(self): + return f"Réponse pour la liste {self.liste}" class EngagementChoices(models.TextChoices): diff --git a/manifeste_velo/questionnaire/templates/questionnaire/admin.html b/manifeste_velo/questionnaire/templates/questionnaire/admin.html new file mode 100644 index 0000000..b467657 --- /dev/null +++ b/manifeste_velo/questionnaire/templates/questionnaire/admin.html @@ -0,0 +1,37 @@ +{% extends "base.html" %}{% load static %} +{% block body %} +
+ +
+{% block admin_body %} +
+

Bienvenue sur la page d'administration, vous pouvez sélectionner une fonction dans le menu

+
+{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/manifeste_velo/questionnaire/templates/questionnaire/candidats.html b/manifeste_velo/questionnaire/templates/questionnaire/candidats.html new file mode 100644 index 0000000..bb0f9ac --- /dev/null +++ b/manifeste_velo/questionnaire/templates/questionnaire/candidats.html @@ -0,0 +1,4 @@ +{% extends "questionnaire/admin.html" %} +{% block admin_body %} + +{% endblock %} \ No newline at end of file diff --git a/manifeste_velo/questionnaire/templates/questionnaire/login.html b/manifeste_velo/questionnaire/templates/questionnaire/login.html new file mode 100644 index 0000000..216ec33 --- /dev/null +++ b/manifeste_velo/questionnaire/templates/questionnaire/login.html @@ -0,0 +1,60 @@ +{% extends "base.html" %}{% load static %} +{% block body %} +
+
+
+
+
+
+ Place au Vélo Angers +
+
+
+
+

Connexion à la plateforme de gestion du manifeste

+
+
+
+
+
+ {% if form.errors %} +
+
+ +
+
+ {% endif %} +
{% csrf_token %} +
+
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +{% endblock %} \ No newline at end of file diff --git a/manifeste_velo/questionnaire/templates/questionnaire/questions.html b/manifeste_velo/questionnaire/templates/questionnaire/questions.html new file mode 100644 index 0000000..54e857a --- /dev/null +++ b/manifeste_velo/questionnaire/templates/questionnaire/questions.html @@ -0,0 +1,23 @@ +{% extends "questionnaire/admin.html" %} +{% block admin_body %} +
+{% for question in object_list %} +
+
+
+ {% if question.est_commun %} +
{{ question.ordre }}. {{ question.titre }} (Commun)
+

{{ question.texte|truncatewords_html:10 }}

+ {% else %} +
{{ question.ordre }}. {{ question.titre }}{% if request.user.is_staff %} ({{ question.ville.nom }}){% endif %}
+

{{ question.texte|safe }}

+ {% if question.exemple %} +
{{ question.exemple }}
+ {% endif %} + {% endif %} +
+
+
+{% endfor %} +
+{% endblock %} \ No newline at end of file diff --git a/manifeste_velo/questionnaire/templates/questionnaire/reponses.html b/manifeste_velo/questionnaire/templates/questionnaire/reponses.html new file mode 100644 index 0000000..bb0f9ac --- /dev/null +++ b/manifeste_velo/questionnaire/templates/questionnaire/reponses.html @@ -0,0 +1,4 @@ +{% extends "questionnaire/admin.html" %} +{% block admin_body %} + +{% endblock %} \ No newline at end of file diff --git a/manifeste_velo/questionnaire/urls.py b/manifeste_velo/questionnaire/urls.py index 034b476..e19a97e 100644 --- a/manifeste_velo/questionnaire/urls.py +++ b/manifeste_velo/questionnaire/urls.py @@ -1,9 +1,16 @@ from django.contrib.auth.views import LoginView, LogoutView from django.urls import path +from questionnaire import views + app_name = "questionnaire" urlpatterns = [ + path("/logo.png", views.OuvertureEmail.as_view(), name="ouverture-email"), path("login", LoginView.as_view(template_name="questionnaire/login.html"), name="login"), path("logout", LogoutView.as_view(template_name="questionnaire/logout.html"), name="logout"), + path("admin/candidats", views.CandidatsLanding.as_view(), name="admin_candidats"), + path("admin/questions", views.QuestionnairesLanding.as_view(), name="admin_questionnaires"), + path("admin/reponses", views.ReponsesLanding.as_view(), name="admin_reponses"), + path("admin", views.AdminLanding.as_view(), name="admin_landing"), ] diff --git a/manifeste_velo/questionnaire/views.py b/manifeste_velo/questionnaire/views.py index 91ea44a..8fe9fe6 100644 --- a/manifeste_velo/questionnaire/views.py +++ b/manifeste_velo/questionnaire/views.py @@ -1,3 +1,68 @@ -from django.shortcuts import render +from datetime import datetime + +from django.conf import settings +from django.contrib.auth.mixins import LoginRequiredMixin +from django.db.models import Q +from django.urls import reverse_lazy +from django.views.generic.base import RedirectView, TemplateView +from django.views.generic.list import ListView + +from questionnaire.models import Liste, PointManifeste, ReponseListe # Create your views here. + + +class OuvertureEmail(RedirectView): + permanent = True + query_string = False + url = settings.STATIC_URL + "/questionnaire/logo.png" + + def get_redirect_url(self, *args, **kwargs): + liste = Liste.objects.get(uuid=kwargs["liste"]) + if liste.email_ouvert is None and liste.email_envoye is not None: + liste.email_ouvert = datetime.now() + liste.save() + + return super().get_redirect_url(*args, **kwargs) + + +class AdminViewMixin(LoginRequiredMixin): + login_url = reverse_lazy("questionnaire:login") + redirect_field_name = "next" + + +class AdminLanding(AdminViewMixin, TemplateView): + template_name = "questionnaire/admin.html" + + +class ReponsesLanding(AdminViewMixin, ListView): + model = ReponseListe + template_name = "questionnaire/candidats.html" + + def get_queryset(self): + if self.request.user.is_staff: + return super().get_queryset() + return ReponseListe.objects.filter(liste__ville=self.request.user.profile.ville) + + +class CandidatsLanding(AdminViewMixin, ListView): + model = Liste + template_name = "questionnaire/candidats.html" + + def get_queryset(self): + if self.request.user.is_staff: + return super().get_queryset() + return Liste.objects.filter(ville=self.request.user.profile.ville) + + +class QuestionnairesLanding(AdminViewMixin, ListView): + model = PointManifeste + template_name = "questionnaire/questions.html" + + def get_queryset(self): + if self.request.user.is_staff: + queryset = super().get_queryset() + print(queryset.explain()) + print("Return full queryset") + return super().get_queryset() + return PointManifeste.objects.filter(Q(est_commun=True) | Q(ville=self.request.user.profile.ville)) diff --git a/manifeste_velo/templates/admin_base.html b/manifeste_velo/templates/admin_base.html deleted file mode 100644 index c7b2899..0000000 --- a/manifeste_velo/templates/admin_base.html +++ /dev/null @@ -1,88 +0,0 @@ -{% extends 'base.html' %}{% load static %}{% load simple_menu %} -{% block body %}{% generate_menu %} -
- -
- -
- {% block admin_body %} - {% endblock %} -
-
-
-{% endblock %} -{% block javascripts_footer %} -{{ block.super }} - -{% endblock %} \ No newline at end of file