Ajout modèle Chant

This commit is contained in:
Claude Paroz 2025-10-19 17:59:46 +02:00
parent 78cb3bae07
commit c7a08b3d3c
14 changed files with 220 additions and 11 deletions

View file

@ -1,7 +1,7 @@
from django.contrib import admin from django.contrib import admin
from django.contrib.auth.admin import UserAdmin from django.contrib.auth.admin import UserAdmin
from beesgospel.models import Agenda, Document, Membre, User from beesgospel.models import Agenda, Chant, Document, Membre, User
@admin.register(Agenda) @admin.register(Agenda)
@ -25,6 +25,14 @@ class MembreAdmin(admin.ModelAdmin):
ordering = ["nom"] ordering = ["nom"]
@admin.register(Chant)
class ChantAdmin(admin.ModelAdmin):
list_display = ["numero", "titre", "particularite", "statut"]
list_filter = ["statut"]
search_fields = ["titre"]
ordering = ["numero"]
@admin.register(User) @admin.register(User)
class UserAdmin(UserAdmin): class UserAdmin(UserAdmin):
list_display = ["email", "is_active", "is_staff", "last_login"] list_display = ["email", "is_active", "is_staff", "last_login"]

View file

@ -4,7 +4,7 @@ from django import forms
from django.contrib.auth import forms as auth_forms from django.contrib.auth import forms as auth_forms
from django.db import transaction from django.db import transaction
from .models import Membre, User from .models import Chant, Membre, User
class BootstrapMixin: class BootstrapMixin:
@ -53,3 +53,9 @@ class MembreEditForm(BootstrapMixin, forms.ModelForm):
self.instance.user.email = self.cleaned_data["courriel"] self.instance.user.email = self.cleaned_data["courriel"]
self.instance.user.save() self.instance.user.save()
return super().save(**kwargs) return super().save(**kwargs)
class ChantEditForm(BootstrapMixin, forms.ModelForm):
class Meta:
model = Chant
fields = ["numero", "titre", "particularite"]

View file

@ -0,0 +1,25 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('beesgospel', '0005_agenda_infos_interne'),
]
operations = [
migrations.CreateModel(
name='Chant',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('numero', models.PositiveSmallIntegerField(verbose_name='Numéro')),
('titre', models.CharField(max_length=100, verbose_name='Titre')),
('particularite', models.CharField(blank=True, max_length=100, verbose_name='Particularité')),
('statut', models.CharField(choices=[('actif', 'Actif'), ('prep', 'En préparation'), ('inactif', 'Inactif')], default='actif', max_length=10, verbose_name='Statut')),
],
),
migrations.AddConstraint(
model_name='chant',
constraint=models.UniqueConstraint(models.F('numero'), name='numero_unique'),
),
]

View file

@ -98,3 +98,23 @@ class Document(models.Model):
return self.url return self.url
elif self.fichier: elif self.fichier:
return self.fichier.url return self.fichier.url
class Chant(models.Model):
class StatutChoices(models.TextChoices):
ACTIF = "actif", "Actif"
PREP = "prep", "En préparation"
INACTIF = "inactif", "Inactif"
numero = models.PositiveSmallIntegerField("Numéro")
titre = models.CharField("Titre", max_length=100)
particularite = models.CharField("Particularité", max_length=100, blank=True)
statut = models.CharField("Statut", max_length=10, choices=StatutChoices, default="actif")
class Meta:
constraints = [
models.UniqueConstraint("numero", name="numero_unique"),
]
def __str__(self):
return f"{self.numero}. {self.titre}"

View file

@ -44,6 +44,15 @@ nav {
@media only screen and (max-width : 500px) { @media only screen and (max-width : 500px) {
img#abeille { width: 75px; } img#abeille { width: 75px; }
} }
.errorlist { list-style: none; padding-left: 0 !important; }
.errorlist li {
padding: .75rem 1.25rem;
margin-bottom: 1rem;
border-radius: .25rem;
color: #721c24;
background-color: #f8d7da;
border-color: #f5c6cb;
}
.alert-danger { background-color: #FFD79C; } .alert-danger { background-color: #FFD79C; }
.main-text { max-width: 100vw; } .main-text { max-width: 100vw; }
.red-bottom { border-bottom: 1px solid red; } .red-bottom { border-bottom: 1px solid red; }
@ -86,8 +95,8 @@ nav {
} }
.portrait { max-width: 13rem; } .portrait { max-width: 13rem; }
tr.editable .edit-button { display: none; } tr.editable .edit-button, tr.editable .delete-button { display: none; }
tr.editable:hover .edit-button { display: inline-block; } tr.editable:hover .edit-button, tr.editable:hover .delete-button { display: inline-block; }
.agenda_line { margin-right: 7em !important; } .agenda_line { margin-right: 7em !important; }
.agenda_container > div:first-of-type { margin-top: 7em !important; } .agenda_container > div:first-of-type { margin-top: 7em !important; }
@ -104,3 +113,17 @@ tr.editable:hover .edit-button { display: inline-block; }
color: red; color: red;
font-style: italic; font-style: italic;
} }
.card-membres {
background-color: #b56a4d;
height: 6em;
}
.card-membres:hover {
background-color: #999;
}
.card-membres a {
color: white;
text-decoration: none;
height: 100%;
}
table.table-chants th.numero { width: 3em; }
table.table-chants th.boutons { width: 3em; }

View file

@ -0,0 +1,14 @@
'use strict';
function attachHandlerSelector(section, selector, event, func) {
section.querySelectorAll(selector).forEach(item => {
item.addEventListener(event, func);
});
}
window.addEventListener('DOMContentLoaded', () => {
attachHandlerSelector(document, 'button.delete-button', 'click', (ev) => {
let resp = confirm("Voulez-vous vraiment supprimer cette ligne ?");
if (!resp) { ev.preventDefault(); ev.stopImmediatePropagation(); }
});
})

View file

@ -1,12 +1,13 @@
from datetime import date, timedelta from datetime import date, timedelta
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.views.generic import ( from django.views.generic import (
CreateView, DeleteView, ListView, TemplateView, UpdateView CreateView, DeleteView, ListView, TemplateView, UpdateView
) )
from .forms import MembreEditForm from .forms import ChantEditForm, MembreEditForm
from .models import Agenda, Document, Membre from .models import Agenda, Chant, Document, Membre
class HomeView(TemplateView): class HomeView(TemplateView):
@ -103,5 +104,37 @@ class MembreDeleteView(PermissionRequiredMixin, DeleteView):
success_url = reverse_lazy("liste-membres") success_url = reverse_lazy("liste-membres")
def form_valid(self, form): def form_valid(self, form):
msg = f"{self.object} a bien été effacé·e de la liste"
self.object.user.delete() self.object.user.delete()
messages.success(self.request, msg)
return super().form_valid(form) return super().form_valid(form)
class ListeChantsView(LoginRequiredMixin, ListView):
model = Chant
template_name = "membres/liste_chants.html"
def get_queryset(self):
return super().get_queryset().order_by("numero")
class ChantAddView(PermissionRequiredMixin, CreateView):
model = Chant
form_class = ChantEditForm
permission_required = "beesgospel.add_chant"
template_name = "membres/chant_edit.html"
success_url = reverse_lazy("liste-chants")
class ChantEditView(PermissionRequiredMixin, UpdateView):
model = Chant
form_class = ChantEditForm
permission_required = "beesgospel.change_chant"
template_name = "membres/chant_edit.html"
success_url = reverse_lazy("liste-chants")
class ChantDeleteView(PermissionRequiredMixin, DeleteView):
model = Chant
permission_required = "beesgospel.delete_chant"
success_url = reverse_lazy("liste-chants")

View file

@ -4,6 +4,7 @@ Django settings for beesgospel project.
from pathlib import Path from pathlib import Path
from django.contrib.messages import constants
from django.urls import reverse_lazy from django.urls import reverse_lazy
# Build paths inside the project like this: BASE_DIR / "subdir". # Build paths inside the project like this: BASE_DIR / "subdir".
@ -113,6 +114,14 @@ STATIC_ROOT = BASE_DIR / "static"
MEDIA_URL = "media/" MEDIA_URL = "media/"
MEDIA_ROOT = BASE_DIR / "media" MEDIA_ROOT = BASE_DIR / "media"
MESSAGE_TAGS = {
constants.DEBUG: "alert-info",
constants.INFO: "alert-info",
constants.SUCCESS: "alert-success",
constants.WARNING: "alert-warning",
constants.ERROR: "alert-danger",
}
LOGOUT_REDIRECT_URL = reverse_lazy("presentation") LOGOUT_REDIRECT_URL = reverse_lazy("presentation")
DEFAULT_FROM_EMAIL = "webmaster@2xlibre.net" DEFAULT_FROM_EMAIL = "webmaster@2xlibre.net"

View file

@ -22,5 +22,9 @@ urlpatterns = [
path("membres/<int:pk>/edit/", views.MembreEditView.as_view(), name="membre-edit"), path("membres/<int:pk>/edit/", views.MembreEditView.as_view(), name="membre-edit"),
path("membres/<int:pk>/delete/", views.MembreDeleteView.as_view(), name="membre-delete"), path("membres/<int:pk>/delete/", views.MembreDeleteView.as_view(), name="membre-delete"),
path("membres/liste/", views.ListeMembresView.as_view(), name="liste-membres"), path("membres/liste/", views.ListeMembresView.as_view(), name="liste-membres"),
path("membres/chants/", views.ListeChantsView.as_view(), name="liste-chants"),
path("membres/chants/nouveau/", views.ChantAddView.as_view(), name="chant-add"),
path("membres/chants/<int:pk>/edit/", views.ChantEditView.as_view(), name="chant-edit"),
path("membres/chants/<int:pk>/delete/", views.ChantDeleteView.as_view(), name="chant-delete"),
path("membres/documents/", views.MediaView.as_view(prive=True), name="docs-membres"), path("membres/documents/", views.MediaView.as_view(prive=True), name="docs-membres"),
] ]

View file

@ -8,6 +8,7 @@
<link href="{% static 'css/main.css' %}" rel="stylesheet"> <link href="{% static 'css/main.css' %}" rel="stylesheet">
<link rel="manifest" href="{% url 'manifest' %}" crossorigin="use-credentials"> <link rel="manifest" href="{% url 'manifest' %}" crossorigin="use-credentials">
<script src="{% static 'vendor/bootstrap.bundle.min.js' %}"></script> <script src="{% static 'vendor/bootstrap.bundle.min.js' %}"></script>
<script src="{% static 'js/main.js' %}"></script>
</head> </head>
<body class="{% block bodyclass %}{% endblock %}"> <body class="{% block bodyclass %}{% endblock %}">
{% block header %} {% block header %}
@ -39,6 +40,17 @@
{% endblock header %} {% endblock header %}
<div class="container mt-4"> <div class="container mt-4">
{% if messages %}
<ul class="list-unstyled messages">
{% for message in messages %}
<li class="alert {{ message.tags }} alert-dismissible" role="alert">
{{ message|linebreaksbr|capfirst }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</li>
{% endfor %}
</ul>
{% endif %}
{% block content %}{% endblock %} {% block content %}{% endblock %}
</div> </div>
</body> </body>

View file

@ -0,0 +1,10 @@
{% extends "base.html" %}
{% block content %}
<h2>Édition/ajout de chant</h2>
<form method="post">{% csrf_token %}
{{ form.as_div }}
<div class="mt-3"><button type="submit" class="btn btn-primary">Enregistrer</button></div>
</form>
{% endblock content %}

View file

@ -5,11 +5,29 @@
<form action="{% url 'logout' %}" method="post">{% csrf_token %}<button class="btn btn-sm btn-light" type="submit">Déconnexion</button></form> <form action="{% url 'logout' %}" method="post">{% csrf_token %}<button class="btn btn-sm btn-light" type="submit">Déconnexion</button></form>
</div> </div>
<h2>Espace membres</h2> <h2>Espace membres</h2>
<div class="row mt-4"> <div class="row align-items-center mt-4 gx-3 gy-3">
<div class="col col-4"><a href="{% url 'liste-membres' %}">Liste des membres</a></div> <div class="col-sm-6 col-md-3">
<div class="col col-4"><a href="{% url 'docs-membres' %}">Documents pour les membres</a></div> <div class="card card-membres">
<a href="{% url 'liste-chants' %}"><div class="card-title text-center p-3 fs-5">Liste des chants</div></a>
</div>
</div>
<div class="col-sm-6 col-md-3">
<div class="card card-membres">
<a href="{% url 'docs-membres' %}"><div class="card-title text-center p-3 fs-5">Documents pour les membres</div></a>
</div>
</div>
<div class="col-sm-6 col-md-3">
<div class="card card-membres">
<a href="{% url 'liste-membres' %}"><div class="card-title text-center p-3 fs-5">Liste des membres</div></a>
</div>
</div>
{% if perms.beesgospel.change_agenda %} {% if perms.beesgospel.change_agenda %}
<div class="col col-4"><a href="{% url 'admin:beesgospel_agenda_changelist' %}">Gestion de lagenda</a></div> <div class="col-sm-6 col-md-3">
<div class="card card-membres">
<a href="{% url 'admin:beesgospel_agenda_changelist' %}"><div class="card-title text-center p-3 fs-5">Gestion de lagenda</div></a>
</div>
</div>
{% endif %} {% endif %}
</div> </div>
{% endblock %} {% endblock %}

View file

@ -18,7 +18,7 @@
<img src="{% static 'admin/img/icon-changelink.svg' %}"> <img src="{% static 'admin/img/icon-changelink.svg' %}">
</a> </a>
<form method="post" class="d-inline" action="{% url 'membre-delete' membre.pk %}">{% csrf_token %} <form method="post" class="d-inline" action="{% url 'membre-delete' membre.pk %}">{% csrf_token %}
<button type="submit" class="btn btn-link edit-button"><img src="{% static 'admin/img/icon-deletelink.svg' %}"></button> <button type="submit" class="btn btn-link delete-button"><img src="{% static 'admin/img/icon-deletelink.svg' %}"></button>
</form> </form>
{% endif %} {% endif %}
</td> </td>

View file

@ -0,0 +1,27 @@
{% extends "base.html" %}
{% block content %}
<h2>Liste des chants</h2>
<table class="table table-striped table-responsive table-chants">
<tr><th scope="col" class="numero"></th><th scope="col">Titre</th><th></th><th scope="col" class="boutons"></th></tr>
{% for chant in object_list %}
<tr class="editable">
<td>{{ chant.numero }}</td>
<td>{{ chant.titre }}</td>
<td>{{ chant.particularite }}</td>
<td class="text-nowrap">{% if perms.beesgospel.change_chant %}
<a href="{% url 'chant-edit' chant.pk %}" class="edit-button">
<img src="{% static 'admin/img/icon-changelink.svg' %}">
</a>
<form method="post" class="d-inline" action="{% url 'chant-delete' chant.pk %}">{% csrf_token %}
<button type="submit" class="btn btn-link delete-button"><img src="{% static 'admin/img/icon-deletelink.svg' %}"></button>
</form>
{% endif %}
</td>
</tr>
{% endfor %}
</table>
{% if perms.beesgospel.add_chant %}
<div class="mt-3"><a class="btn btn-outline-primary" href="{% url 'chant-add' %}">Ajouter un chant</a></div>
{% endif %}
{% endblock %}