From 3d1b8a9bee33d954dbacbb5669fa006ded50d81f Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Sat, 26 Jul 2025 18:00:03 +0200 Subject: [PATCH 1/3] =?UTF-8?q?Ajout=20mod=C3=A8les=20membres/agenda/docum?= =?UTF-8?q?ents?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- beesgospel/admin.py | 56 ++++++ beesgospel/migrations/0001_initial.py | 166 ++++++++++++++++++ beesgospel/migrations/0002_agenda_document.py | 73 ++++++++ beesgospel/migrations/__init__.py | 0 beesgospel/models.py | 71 ++++++++ common/settings.py | 3 +- 6 files changed, 368 insertions(+), 1 deletion(-) create mode 100644 beesgospel/admin.py create mode 100644 beesgospel/migrations/0001_initial.py create mode 100644 beesgospel/migrations/0002_agenda_document.py create mode 100644 beesgospel/migrations/__init__.py create mode 100644 beesgospel/models.py diff --git a/beesgospel/admin.py b/beesgospel/admin.py new file mode 100644 index 0000000..916000f --- /dev/null +++ b/beesgospel/admin.py @@ -0,0 +1,56 @@ +from django.contrib import admin +from django.contrib.auth.admin import UserAdmin + +from beesgospel.models import Agenda, Document, Membre, User + + +@admin.register(Agenda) +class AgendaAdmin(admin.ModelAdmin): + list_display = ["titre", "lieu", "date_heure", "prive"] + ordering = ["-date_heure"] + search_fields = ["titre", "lieu"] + date_hierarchy = "date_heure" + + +@admin.register(Document) +class DocumentAdmin(admin.ModelAdmin): + list_display = ["titre", "quand", "prive"] + ordering = ["-quand"] + + +@admin.register(Membre) +class MembreAdmin(admin.ModelAdmin): + list_display = ["nom", "prenom", "localite", "user__email"] + ordering = ["nom"] + + +@admin.register(User) +class UserAdmin(UserAdmin): + list_display = ["email", "is_active", "is_staff", "last_login"] + search_fields = ["email"] + ordering = ["email"] + fieldsets = ( + (None, {"fields": ("email", "password",)}), + ( + "Permissions", + { + "fields": ( + "is_active", + "is_staff", + "is_superuser", + "groups", + "user_permissions", + ), + }, + ), + ("Important dates", {"fields": ("last_login", "date_joined")}), + ) + add_fieldsets = ( + ( + None, + { + "classes": ("wide",), + "fields": ("email", "usable_password", "password1", "password2"), + }, + ), + ) diff --git a/beesgospel/migrations/0001_initial.py b/beesgospel/migrations/0001_initial.py new file mode 100644 index 0000000..cffd9fb --- /dev/null +++ b/beesgospel/migrations/0001_initial.py @@ -0,0 +1,166 @@ +import beesgospel.models +import django.db.models.deletion +import django.utils.timezone +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [ + ("auth", "0012_alter_user_first_name_max_length"), + ] + + operations = [ + migrations.CreateModel( + name="User", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("password", models.CharField(max_length=128, verbose_name="password")), + ( + "last_login", + models.DateTimeField( + blank=True, null=True, verbose_name="last login" + ), + ), + ( + "is_superuser", + models.BooleanField( + default=False, + help_text="Designates that this user has all permissions without explicitly assigning them.", + verbose_name="superuser status", + ), + ), + ( + "first_name", + models.CharField( + blank=True, max_length=150, verbose_name="first name" + ), + ), + ( + "last_name", + models.CharField( + blank=True, max_length=150, verbose_name="last name" + ), + ), + ( + "email", + models.EmailField( + blank=True, max_length=254, verbose_name="email address" + ), + ), + ( + "is_staff", + models.BooleanField( + default=False, + help_text="Designates whether the user can log into this admin site.", + verbose_name="staff status", + ), + ), + ( + "is_active", + models.BooleanField( + default=True, + help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.", + verbose_name="active", + ), + ), + ( + "date_joined", + models.DateTimeField( + default=django.utils.timezone.now, verbose_name="date joined" + ), + ), + ( + "groups", + models.ManyToManyField( + blank=True, + help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", + related_name="user_set", + related_query_name="user", + to="auth.group", + verbose_name="groups", + ), + ), + ( + "user_permissions", + models.ManyToManyField( + blank=True, + help_text="Specific permissions for this user.", + related_name="user_set", + related_query_name="user", + to="auth.permission", + verbose_name="user permissions", + ), + ), + ], + managers=[ + ("objects", beesgospel.models.CustomUserManager()), + ], + ), + migrations.CreateModel( + name="Membre", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("nom", models.CharField(max_length=40, verbose_name="Nom")), + ("prenom", models.CharField(max_length=40, verbose_name="Prénom")), + ( + "avatar", + models.ImageField( + blank=True, upload_to="avatars", verbose_name="Avatar" + ), + ), + ( + "rue", + models.CharField(blank=True, max_length=80, verbose_name="Rue"), + ), + ("npa", models.CharField(blank=True, max_length=5, verbose_name="NPA")), + ( + "localite", + models.CharField( + blank=True, max_length=40, verbose_name="Localité" + ), + ), + ( + "tel1", + models.CharField(blank=True, max_length=20, verbose_name="Tél. 1"), + ), + ( + "tel2", + models.CharField(blank=True, max_length=20, verbose_name="Tél. 2"), + ), + ( + "user", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + migrations.AddConstraint( + model_name="user", + constraint=models.UniqueConstraint( + fields=("email",), name="unique_user_email" + ), + ), + ] diff --git a/beesgospel/migrations/0002_agenda_document.py b/beesgospel/migrations/0002_agenda_document.py new file mode 100644 index 0000000..7093470 --- /dev/null +++ b/beesgospel/migrations/0002_agenda_document.py @@ -0,0 +1,73 @@ +# Generated by Django 5.2.5.dev20250725113223 on 2025-07-26 15:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("beesgospel", "0001_initial"), + ] + + operations = [ + migrations.CreateModel( + name="Agenda", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("titre", models.CharField(max_length=150, verbose_name="Titre")), + ( + "lieu", + models.CharField(blank=True, max_length=80, verbose_name="Lieu"), + ), + ("date_heure", models.DateTimeField(verbose_name="Date/heure")), + ("infos", models.TextField(verbose_name="Informations")), + ( + "prive", + models.BooleanField( + default=False, + help_text="Un évènement privé ne peut être consulté que par les membres de l'association, tandis qu'un évènement public est visible de tous.", + verbose_name="Privé", + ), + ), + ], + ), + migrations.CreateModel( + name="Document", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "fichier", + models.FileField( + blank=True, upload_to="documents", verbose_name="Fichier" + ), + ), + ("url", models.URLField(blank=True, verbose_name="URL")), + ("quand", models.DateField(verbose_name="Date")), + ("titre", models.CharField(max_length=150, verbose_name="Titre")), + ("infos", models.TextField(blank=True, verbose_name="Infos")), + ( + "prive", + models.BooleanField( + default=False, + help_text="Un document privé ne peut être consulté que par les membres de l'association, tandis qu'un document public est visible de tous.", + verbose_name="Privé", + ), + ), + ], + ), + ] diff --git a/beesgospel/migrations/__init__.py b/beesgospel/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/beesgospel/models.py b/beesgospel/models.py new file mode 100644 index 0000000..51695ea --- /dev/null +++ b/beesgospel/models.py @@ -0,0 +1,71 @@ +from django.contrib.auth.models import AbstractUser, UserManager +from django.db import models + + +class CustomUserManager(UserManager): + def create_superuser(self, **kwargs): + return super().create_superuser("foo", **kwargs) + + +class User(AbstractUser): + username = None + USERNAME_FIELD = "email" + REQUIRED_FIELDS = [] + + objects = CustomUserManager() + + class Meta: + constraints = [ + models.UniqueConstraint(name="unique_user_email", fields=["email"]), + ] + + def __init__(self, *args, username=None, **kwargs): + super().__init__(*args, **kwargs) + + +class Membre(models.Model): + nom = models.CharField("Nom", max_length=40) + prenom = models.CharField("Prénom", max_length=40) + user = models.ForeignKey(User, on_delete=models.CASCADE, null=True, blank=True) + avatar = models.ImageField("Avatar", upload_to="avatars", blank=True) + rue = models.CharField("Rue", max_length=80, blank=True) + npa = models.CharField("NPA", max_length=5, blank=True) + localite = models.CharField("Localité", max_length=40, blank=True) + tel1 = models.CharField("Tél. 1", max_length=20, blank=True) + tel2 =models.CharField("Tél. 2", max_length=20, blank=True) + + def __str__(self): + return f"{self.nom} {self.prenom}" + + +class Agenda(models.Model): + titre = models.CharField("Titre", max_length=150) + lieu = models.CharField("Lieu", max_length=80, blank=True) + date_heure = models.DateTimeField("Date/heure") + infos = models.TextField("Informations") + prive = models.BooleanField( + "Privé", default=False, help_text=( + "Un évènement privé ne peut être consulté que par les membres de " + "l'association, tandis qu'un évènement public est visible de tous." + ) + ) + + def __str__(self): + return f"{self.titre} {self.date_heure}" + + +class Document(models.Model): + fichier = models.FileField("Fichier", upload_to="documents", blank=True) + url = models.URLField("URL", blank=True) + quand = models.DateField("Date") + titre = models.CharField("Titre", max_length=150) + infos = models.TextField("Infos", blank=True) + prive = models.BooleanField( + "Privé", default=False, help_text=( + "Un document privé ne peut être consulté que par les membres de " + "l'association, tandis qu'un document public est visible de tous." + ) + ) + + def __str__(self): + return f"{self.titre} {self.date}" diff --git a/common/settings.py b/common/settings.py index 6f00a18..b5d3dc2 100644 --- a/common/settings.py +++ b/common/settings.py @@ -69,7 +69,7 @@ DATABASES = { "NAME": BASE_DIR / "db.sqlite3", } } - +DEFAULT_AUTO_FIELD = "django.db.models.AutoField" # Password validation # https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators @@ -88,6 +88,7 @@ AUTH_PASSWORD_VALIDATORS = [ "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", }, ] +AUTH_USER_MODEL = "beesgospel.User" # Internationalization From 6c3a1e6ddc8b8fc2571fdad171713ddd0c8fe564 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Sat, 16 Aug 2025 15:55:07 +0200 Subject: [PATCH 2/3] =?UTF-8?q?Ajout=20connexion/d=C3=A9connexion=20=C3=A0?= =?UTF-8?q?=20l'espace=20membres?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- beesgospel/forms.py | 31 +++++++++++++++++++++++++++++++ beesgospel/views.py | 13 +++++++++++++ common/settings.py | 4 ++++ common/urls.py | 7 ++++++- requirements.txt | 2 ++ templates/base.html | 2 +- templates/membres/index.html | 11 +++++++++++ templates/membres/liste.html | 14 ++++++++++++++ templates/registration/login.html | 26 ++++++++++++++++++++++++++ 9 files changed, 108 insertions(+), 2 deletions(-) create mode 100644 beesgospel/forms.py create mode 100644 beesgospel/views.py create mode 100644 requirements.txt create mode 100644 templates/membres/index.html create mode 100644 templates/membres/liste.html create mode 100644 templates/registration/login.html diff --git a/beesgospel/forms.py b/beesgospel/forms.py new file mode 100644 index 0000000..51a22e5 --- /dev/null +++ b/beesgospel/forms.py @@ -0,0 +1,31 @@ +from dajngo import forms +from django.contrib.auth import forms as auth_forms + + +class BootstrapMixin: + required_css_class = "required" + + widget_classes = { + "checkbox": "form-check-input", + "select": "form-select", + } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + for field in self.fields.values(): + if getattr(field.widget, "_bs_enabled", False): + continue + widgets = getattr(field.widget, "widgets", [field.widget]) + for widget in widgets: + input_type = getattr(widget, "input_type", "") + class_name = self.widget_classes.get(input_type, "form-control") + if "class" in widget.attrs: + widget.attrs["class"] += " " + class_name + else: + widget.attrs.update({"class": class_name}) + + +class LoginForm(BootstrapMixin, auth_forms.AuthenticationForm): + username = forms.EmailField( + widget=forms.EmailInput(attrs={"autofocus": True}), + ) diff --git a/beesgospel/views.py b/beesgospel/views.py new file mode 100644 index 0000000..a96a2e1 --- /dev/null +++ b/beesgospel/views.py @@ -0,0 +1,13 @@ +from django.contrib.auth.mixins import LoginRequiredMixin +from django.views.generic import ListView, TemplateView + +from .models import Membre + + +class EspaceMembresView(LoginRequiredMixin, TemplateView): + template_name = "membres/index.html" + + +class ListeMembresView(LoginRequiredMixin, ListView): + model = Membre + template_name = "membres/liste.html" diff --git a/common/settings.py b/common/settings.py index b5d3dc2..dd44793 100644 --- a/common/settings.py +++ b/common/settings.py @@ -4,6 +4,8 @@ Django settings for beesgospel project. from pathlib import Path +from django.urls import reverse_lazy + # Build paths inside the project like this: BASE_DIR / "subdir". BASE_DIR = Path(__file__).resolve().parent.parent @@ -109,6 +111,8 @@ USE_TZ = True STATIC_URL = "static/" STATIC_ROOT = BASE_DIR / "static" +LOGOUT_REDIRECT_URL = reverse_lazy("presentation") + # Default primary key field type # https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field diff --git a/common/urls.py b/common/urls.py index 4ec1d5f..2056d55 100644 --- a/common/urls.py +++ b/common/urls.py @@ -1,11 +1,16 @@ from django.contrib import admin -from django.urls import path +from django.urls import include, path from django.views.generic import TemplateView +from beesgospel import views + urlpatterns = [ path("admin/", admin.site.urls), + path("accounts/", include("django.contrib.auth.urls")), path("", TemplateView.as_view(template_name="index.html"), name="home"), path("v2", TemplateView.as_view(template_name="index2.html"), name="home"), path("presentation/", TemplateView.as_view(template_name="presentation.html"), name="presentation"), path("contact/", TemplateView.as_view(template_name="contact.html"), name="contact"), + path("membres/", views.EspaceMembresView.as_view(), name="membres"), + path("membres/liste/", views.ListeMembresView.as_view(), name="liste-membres"), ] diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..860172b --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +django==5.2.* +pillow==11.3.* diff --git a/templates/base.html b/templates/base.html index 3692f54..64d885f 100644 --- a/templates/base.html +++ b/templates/base.html @@ -30,7 +30,7 @@ - + diff --git a/templates/membres/index.html b/templates/membres/index.html new file mode 100644 index 0000000..5bfeca4 --- /dev/null +++ b/templates/membres/index.html @@ -0,0 +1,11 @@ +{% extends "base.html" %} + +{% block content %} +
+
{% csrf_token %}
+
+

Espace membres

+ +{% endblock %} diff --git a/templates/membres/liste.html b/templates/membres/liste.html new file mode 100644 index 0000000..2e33609 --- /dev/null +++ b/templates/membres/liste.html @@ -0,0 +1,14 @@ +{% extends "base.html" %} + +{% block content %} +

Liste des membres

+ + {% for membre in object_list %} + + + + + + {% endfor %} +
{{ membre.nom }} {{ membre.prenom }}{{ membre.rue }}
{{ membre.npa }} {{ membre.localite }}
{{ membre.tel1 }}
{{ membre.tel2 }}
{{ membre.email }}
+{% endblock %} diff --git a/templates/registration/login.html b/templates/registration/login.html new file mode 100644 index 0000000..f77368a --- /dev/null +++ b/templates/registration/login.html @@ -0,0 +1,26 @@ +{% extends "base.html" %} + +{% block content %} +
+
+
+ {% csrf_token %} +

Connexion

+ + {{ form.non_field_errors }} +
+ + {{ form.username.errors }} {{ form.username }} +
+
+ + {{ form.password.errors }} {{ form.password }} +
+ + +
+
+
+{% endblock content %} From b58d85198a5a76799ff78cfaacb1501c7d550498 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Sat, 16 Aug 2025 18:44:58 +0200 Subject: [PATCH 3/3] Page d'agenda --- beesgospel/migrations/0002_agenda_document.py | 1 + beesgospel/models.py | 4 ++++ beesgospel/static/css/main.css | 5 +++++ beesgospel/views.py | 16 ++++++++++++++- common/settings.py | 6 +++--- common/urls.py | 1 + templates/admin/base_site.html | 5 +++++ templates/agenda.html | 20 +++++++++++++++++++ templates/base.html | 4 ++-- templates/membres/index.html | 10 ++++++++-- 10 files changed, 64 insertions(+), 8 deletions(-) create mode 100644 templates/admin/base_site.html create mode 100644 templates/agenda.html diff --git a/beesgospel/migrations/0002_agenda_document.py b/beesgospel/migrations/0002_agenda_document.py index 7093470..ba6ce1a 100644 --- a/beesgospel/migrations/0002_agenda_document.py +++ b/beesgospel/migrations/0002_agenda_document.py @@ -37,6 +37,7 @@ class Migration(migrations.Migration): ), ), ], + options={'verbose_name': 'Agenda', 'verbose_name_plural': 'Agenda'}, ), migrations.CreateModel( name="Document", diff --git a/beesgospel/models.py b/beesgospel/models.py index 51695ea..f9af292 100644 --- a/beesgospel/models.py +++ b/beesgospel/models.py @@ -50,6 +50,10 @@ class Agenda(models.Model): ) ) + class Meta: + verbose_name = "Agenda" + verbose_name_plural = "Agenda" + def __str__(self): return f"{self.titre} {self.date_heure}" diff --git a/beesgospel/static/css/main.css b/beesgospel/static/css/main.css index cb920a3..66307cf 100644 --- a/beesgospel/static/css/main.css +++ b/beesgospel/static/css/main.css @@ -73,3 +73,8 @@ nav { .left-red { border-left: 2px solid red; } + +.prive { + background-image: linear-gradient(45deg, #333333 41.67%, #6b0c0c 41.67%, #6b0c0c 50%, #333333 50%, #333333 91.67%, #6b0c0c 91.67%, #6b0c0c 100%); + background-size: 33.94px 33.94px; +} diff --git a/beesgospel/views.py b/beesgospel/views.py index a96a2e1..e97bf0c 100644 --- a/beesgospel/views.py +++ b/beesgospel/views.py @@ -1,7 +1,21 @@ +from datetime import date, timedelta from django.contrib.auth.mixins import LoginRequiredMixin from django.views.generic import ListView, TemplateView -from .models import Membre +from .models import Agenda, Membre + + +class AgendaView(ListView): + model = Agenda + template_name = "agenda.html" + + def get_queryset(self): + qs = Agenda.objects.filter( + date_heure__gt=date.today() - timedelta(days=3), + ).order_by("date_heure") + if not self.request.user.is_authenticated: + qs = qs.filter(prive=False) + return qs class EspaceMembresView(LoginRequiredMixin, TemplateView): diff --git a/common/settings.py b/common/settings.py index dd44793..3876c66 100644 --- a/common/settings.py +++ b/common/settings.py @@ -96,11 +96,11 @@ AUTH_USER_MODEL = "beesgospel.User" # Internationalization # https://docs.djangoproject.com/en/5.2/topics/i18n/ -LANGUAGE_CODE = "fr" +LANGUAGE_CODE = "fr-ch" -TIME_ZONE = "UTC" +TIME_ZONE = "Europe/Zurich" -USE_I18N = False +USE_I18N = True USE_TZ = True diff --git a/common/urls.py b/common/urls.py index 2056d55..6896b7e 100644 --- a/common/urls.py +++ b/common/urls.py @@ -13,4 +13,5 @@ urlpatterns = [ path("contact/", TemplateView.as_view(template_name="contact.html"), name="contact"), path("membres/", views.EspaceMembresView.as_view(), name="membres"), path("membres/liste/", views.ListeMembresView.as_view(), name="liste-membres"), + path("agenda/", views.AgendaView.as_view(), name="agenda"), ] diff --git a/templates/admin/base_site.html b/templates/admin/base_site.html new file mode 100644 index 0000000..25f1cdb --- /dev/null +++ b/templates/admin/base_site.html @@ -0,0 +1,5 @@ +{% extends 'admin/base.html' %} + +{% block branding %} + +{% endblock %} diff --git a/templates/agenda.html b/templates/agenda.html new file mode 100644 index 0000000..2d4dd35 --- /dev/null +++ b/templates/agenda.html @@ -0,0 +1,20 @@ +{% extends "base.html" %} + +{% block page_title %} - Agenda{% endblock %} + +{% block content %} +

Agenda des prochaines prestations de la chorale

+ +{% for item in object_list %} +
+
+
+ {{ item.date_heure|date:'D d F à H:i' }} +
+
{{ item.titre }}
+
+
{{ item.lieu }}
+
{{ item.infos }}
+
+{% endfor %} +{% endblock %} diff --git a/templates/base.html b/templates/base.html index 64d885f..8d423cc 100644 --- a/templates/base.html +++ b/templates/base.html @@ -3,7 +3,7 @@ - Le Gospel de l’Abeille - Bee's Gospel + Le Gospel de l’Abeille - Bee's Gospel{% block page_title %}{% endblock %} @@ -26,7 +26,7 @@ - + diff --git a/templates/membres/index.html b/templates/membres/index.html index 5bfeca4..65e3c25 100644 --- a/templates/membres/index.html +++ b/templates/membres/index.html @@ -5,7 +5,13 @@
{% csrf_token %}

Espace membres

-
- Liste des membres +
+ + {% if perms.beesgospel.change_agenda %} + + {% endif %} + {% if perms.beesgospel.change_document %} + + {% endif %}
{% endblock %}