Ajout modèle ChantDoc
| 
						 | 
					@ -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 Chant, Membre, User
 | 
					from .models import Chant, ChantDoc, Membre, User
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class BootstrapMixin:
 | 
					class BootstrapMixin:
 | 
				
			||||||
| 
						 | 
					@ -59,3 +59,20 @@ class ChantEditForm(BootstrapMixin, forms.ModelForm):
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        model = Chant
 | 
					        model = Chant
 | 
				
			||||||
        fields = ["numero", "titre", "particularite"]
 | 
					        fields = ["numero", "titre", "particularite"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, **kwargs):
 | 
				
			||||||
 | 
					        super().__init__(**kwargs)
 | 
				
			||||||
 | 
					        self.formset = None
 | 
				
			||||||
 | 
					        if self.instance.pk:
 | 
				
			||||||
 | 
					            DocFormSet = forms.inlineformset_factory(Chant, ChantDoc, fields=["fichier", "titre"], extra=1)
 | 
				
			||||||
 | 
					            self.formset = DocFormSet(data=kwargs.get("data"), files=kwargs.get("files"), instance=self.instance)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def is_valid(self):
 | 
				
			||||||
 | 
					        return all([super().is_valid()] + [self.formset.is_valid()] if self.formset else [])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @transaction.atomic()
 | 
				
			||||||
 | 
					    def save(self, **kwargs):
 | 
				
			||||||
 | 
					        obj = super().save(**kwargs)
 | 
				
			||||||
 | 
					        if self.formset:
 | 
				
			||||||
 | 
					            self.formset.save()
 | 
				
			||||||
 | 
					        return obj
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										21
									
								
								beesgospel/migrations/0007_chantdoc.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1,21 @@
 | 
				
			||||||
 | 
					import django.db.models.deletion
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('beesgospel', '0006_chant'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.CreateModel(
 | 
				
			||||||
 | 
					            name='ChantDoc',
 | 
				
			||||||
 | 
					            fields=[
 | 
				
			||||||
 | 
					                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
				
			||||||
 | 
					                ('fichier', models.FileField(blank=True, upload_to='chants', verbose_name='Fichier')),
 | 
				
			||||||
 | 
					                ('titre', models.CharField(max_length=200, verbose_name='Titre')),
 | 
				
			||||||
 | 
					                ('chant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='beesgospel.chant')),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
| 
						 | 
					@ -118,3 +118,12 @@ class Chant(models.Model):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __str__(self):
 | 
					    def __str__(self):
 | 
				
			||||||
        return f"{self.numero}. {self.titre}"
 | 
					        return f"{self.numero}. {self.titre}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ChantDoc(models.Model):
 | 
				
			||||||
 | 
					    chant = models.ForeignKey(Chant, on_delete=models.CASCADE)
 | 
				
			||||||
 | 
					    fichier = models.FileField("Fichier", upload_to="chants", blank=True)
 | 
				
			||||||
 | 
					    titre = models.CharField("Titre", max_length=200)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __str__(self):
 | 
				
			||||||
 | 
					        return f"Document {self.titre} pour le chant {self.chant}"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										2
									
								
								beesgospel/static/ficons/README
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1,2 @@
 | 
				
			||||||
 | 
					Many thanks to Daniel M. Hendricks, http://daniel.hn
 | 
				
			||||||
 | 
					https://github.com/dmhendricks/file-icon-vectors/
 | 
				
			||||||
							
								
								
									
										1
									
								
								beesgospel/static/ficons/docx.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1 @@
 | 
				
			||||||
 | 
					<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><style>.st1{fill:#2372ba}</style><path fill="#fff" d="M0 0h100v100H0z"/><path class="st1" d="M100 100H0V0h100v100zM9.7 90h80.7V10H9.7"/><path class="st1" d="M27.6 27l7.9 29.7L45.2 27h9.5l9.8 29.7L72.4 27H85L71.2 73H59l-9-26.7L41 73H28.8L15 27h12.6z"/></svg>
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 319 B  | 
							
								
								
									
										1
									
								
								beesgospel/static/ficons/image.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1 @@
 | 
				
			||||||
 | 
					<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><style>.st1{fill:#5b2d8d}</style><path fill="#fff" d="M0 0h100v100H0z"/><path class="st1" d="M100 100H0V0h100v100zM9.7 90h80.7V10H9.7"/><circle class="st1" cx="32.4" cy="35" r="8"/><path class="st1" d="M78.9 47.3l-9.7-9.6L50 57l-9.6-9.7-19.3 19.3V73h57.8z"/></svg>
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 326 B  | 
							
								
								
									
										1
									
								
								beesgospel/static/ficons/master.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1 @@
 | 
				
			||||||
 | 
					<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><path fill="#fff" d="M0 0h100v100H0z"/><path d="M100 100H0V0h100v100zM9.7 90h80.7V10H9.7" fill="#bababa"/><path d="M71 36.3L57.8 23.1c-.4-.4-.9-.6-1.4-.6h-26c-1.1 0-2 .9-2 2v51.1c0 1.1.9 2 2 2h39.3c1.1 0 2-.9 2-2V37.7c-.1-.5-.3-1-.7-1.4zm-3.9 2.3H55.5V27l11.6 11.6zm.1 34.5H32.8V26.9h18.5v13.3c0 1.4 1.2 2.6 2.6 2.6h13.3v30.3z" fill="#bababa" stroke="#bababa" stroke-miterlimit="10"/></svg>
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 452 B  | 
							
								
								
									
										1
									
								
								beesgospel/static/ficons/mp3.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1 @@
 | 
				
			||||||
 | 
					<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><style>.st2{fill:#039}</style><path fill="#fff" d="M0 0h100v100H0z"/><path d="M100 100H0V0h100v100zM9.7 90h80.7V10H9.7" fill="#0e3693"/><path class="st2" d="M32.5 37.5h-9v25h9L53.6 77V23L32.5 37.5M71.9 50c0 6.8-3.7 12.7-9.1 15.8l2.8 4.9c7.1-4.1 11.9-11.8 11.9-20.7 0-8.8-4.8-16.6-11.9-20.7l-2.8 4.9c5.4 3.1 9.1 9 9.1 15.8z"/><path class="st2" d="M62.1 50c0 3.2-1.7 5.9-4.3 7.4l2.7 4.7c4.2-2.4 7-6.9 7-12.1 0-5.2-2.8-9.7-7-12.1l-2.7 4.7c2.6 1.5 4.3 4.2 4.3 7.4z"/></svg>
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 531 B  | 
							
								
								
									
										1
									
								
								beesgospel/static/ficons/odt.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1 @@
 | 
				
			||||||
 | 
					<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><style>.st1,.st2{fill:#1a75ce}.st2{stroke:#1a75ce;stroke-width:.5;stroke-miterlimit:10}</style><path fill="#fff" d="M0 0h100v100H0z"/><path class="st1" d="M100 100H0V0h100v100zM9.7 90h80.7V10H9.7"/><path class="st2" d="M59 42.8h19.4v3.1H59v-3.1zm-37.9 8.4h57.4v3.4H21.1v-3.4zm0 8.4h57.4V63H21.1v-3.4z"/><path class="st1" d="M53.3 28.7h2.1c3.2.2 5.7 1 8.1 2 4.7-2.1 11.3-.5 15.5 1.1-5.3-.5-11.6 0-15.2 2.1C60 31 53.1 30.1 46.6 31c1.9-1.3 4.1-2 6.7-2.3zm-1.5 8c-3.8.2-6.9 1.5-9 3.2-6.3-2.8-17.1-1.6-21.1 2.2-.4.2-.7.5-.6.9 3.6-1.1 7.9-2 12.3-1.6 4.4.4 7.7 1.8 10.2 3.8 4.5-4.1 11.3-6.4 19.8-6.3-3-1.3-7.3-2.4-11.6-2.2z"/><path class="st2" d="M21.1 67.8h57.4v3.4H21.1v-3.4z"/></svg>
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 741 B  | 
							
								
								
									
										1
									
								
								beesgospel/static/ficons/pdf.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1 @@
 | 
				
			||||||
 | 
					<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><path fill="#fff" d="M0 0h100v100H0z"/><path d="M100 100H0V0h100v100zM9.7 90h80.7V10H9.7" fill="#c11e07"/><path d="M46.2 21.8c-3.5 0-6.3 2.9-6.3 6.3 0 4.3 2.4 9.6 4.9 14.7-2 6.1-4.1 12.7-7 18.2-5.8 2.3-11 4-14 6.6l-.2.2c-1.1 1.2-1.8 2.7-1.8 4.4 0 3.5 2.9 6.3 6.3 6.3 1.7 0 3.4-.6 4.4-1.8 0 0 .2 0 .2-.2 2.3-2.7 5-7.8 7.5-12.2 5.5-2.1 11.5-4.4 16.9-5.8 4.1 3.4 10.1 5.5 15 5.5 3.5 0 6.3-2.9 6.3-6.3 0-3.5-2.9-6.3-6.3-6.3-4 0-9.6 1.4-13.9 2.9-3.5-3.4-6.7-7.5-9.2-11.9C50.6 37 52.6 32 52.6 28c-.2-3.5-2.9-6.2-6.4-6.2zm0 3.6c1.4 0 2.4 1.1 2.4 2.4 0 1.8-1.1 5.3-2.1 9-1.5-3.7-2.9-7.2-2.9-9 .1-1.2 1.2-2.4 2.6-2.4zm1.1 21.5c1.8 3.1 4.1 5.8 6.6 8.2-3.7 1.1-7.3 2.3-11 3.7 1.8-3.8 3.1-7.9 4.4-11.9zM72 55c1.4 0 2.4 1.1 2.4 2.4 0 1.4-1.1 2.4-2.4 2.4-2.9 0-6.9-1.2-10.1-3.1C65.6 56 69.7 55 72 55zM34.6 66.2c-1.8 3.2-3.5 6.1-4.7 7.6-.5.5-.9.6-1.7.6-1.4 0-2.4-1.1-2.4-2.4 0-.6.3-1.4.6-1.7 1.3-1.2 4.5-2.6 8.2-4.1z" fill="#c11e07" stroke="#c11e07" stroke-width="1.25" stroke-miterlimit="10"/></svg>
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 1 KiB  | 
							
								
								
									
										1
									
								
								beesgospel/static/ficons/tsv.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1 @@
 | 
				
			||||||
 | 
					<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><style>.st1{fill:#1f7244}</style><path fill="#fff" d="M0 0h100v100H0z"/><path class="st1" d="M100 100H0V0h100v100zM9.7 90h80.7V10H9.7"/><path class="st1" d="M62.1 30.9h14.1v9.4H62.1zm0 14.5h14.1v9.4H62.1zm0 14.3h14.1v9.4H62.1zm-19.2 0H57v9.4H42.9zm-19 0H38v9.4H23.9zm19.2-14.2h14.1v9.4H43.1zm-19.2 0H38v9.4H23.9zm19.2-14.6h14.1v9.4H43.1zm-19.2 0H38v9.4H23.9z"/></svg>
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 429 B  | 
| 
						 | 
					@ -11,4 +11,7 @@ window.addEventListener('DOMContentLoaded', () => {
 | 
				
			||||||
        let resp = confirm("Voulez-vous vraiment supprimer cette ligne ?");
 | 
					        let resp = confirm("Voulez-vous vraiment supprimer cette ligne ?");
 | 
				
			||||||
        if (!resp) { ev.preventDefault(); ev.stopImmediatePropagation(); }
 | 
					        if (!resp) { ev.preventDefault(); ev.stopImmediatePropagation(); }
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]')
 | 
				
			||||||
 | 
					    const tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl))
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										27
									
								
								beesgospel/templatetags/bees_utils.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1,27 @@
 | 
				
			||||||
 | 
					from pathlib import Path
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.template import Library
 | 
				
			||||||
 | 
					from django.templatetags.static import static
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					IMAGE_EXTS = [".jpg", ".jpeg", ".png", ".tif", ".tiff", ".gif"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					register = Library()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@register.filter
 | 
				
			||||||
 | 
					def icon_url(file_name):
 | 
				
			||||||
 | 
					    ext = Path(file_name).suffix.lower()
 | 
				
			||||||
 | 
					    icon = "master"  # default
 | 
				
			||||||
 | 
					    if ext in IMAGE_EXTS:
 | 
				
			||||||
 | 
					        icon = "image"
 | 
				
			||||||
 | 
					    elif ext == ".pdf":
 | 
				
			||||||
 | 
					        icon = "pdf"
 | 
				
			||||||
 | 
					    elif ext in [".mp3", ".wav"]:
 | 
				
			||||||
 | 
					        icon = "mp3"
 | 
				
			||||||
 | 
					    elif ext in (".xls", ".xlsx", ".ods", ".csv"):
 | 
				
			||||||
 | 
					        icon = "tsv"
 | 
				
			||||||
 | 
					    elif ext == ".odt":
 | 
				
			||||||
 | 
					        icon = "odt"
 | 
				
			||||||
 | 
					    elif ext in (".doc", ".docx"):
 | 
				
			||||||
 | 
					        icon = "docx"
 | 
				
			||||||
 | 
					    return static(f"ficons/{icon}.svg")
 | 
				
			||||||
| 
						 | 
					@ -115,7 +115,7 @@ class ListeChantsView(LoginRequiredMixin, ListView):
 | 
				
			||||||
    template_name = "membres/liste_chants.html"
 | 
					    template_name = "membres/liste_chants.html"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_queryset(self):
 | 
					    def get_queryset(self):
 | 
				
			||||||
        return super().get_queryset().order_by("numero")
 | 
					        return super().get_queryset().prefetch_related("chantdoc_set").order_by("numero")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ChantAddView(PermissionRequiredMixin, CreateView):
 | 
					class ChantAddView(PermissionRequiredMixin, CreateView):
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,8 +3,19 @@
 | 
				
			||||||
{% block content %}
 | 
					{% block content %}
 | 
				
			||||||
    <h2>Édition/ajout de chant</h2>
 | 
					    <h2>Édition/ajout de chant</h2>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <form method="post">{% csrf_token %}
 | 
					    <form method="post" enctype="multipart/form-data">{% csrf_token %}
 | 
				
			||||||
        {{ form.as_div }}
 | 
					        {{ form.as_div }}
 | 
				
			||||||
 | 
					        {% if form.formset %}
 | 
				
			||||||
 | 
					            <h4 class="mt-3">Fichiers joints à ce chant</h4>
 | 
				
			||||||
 | 
					            {{ form.formset.management_form }}
 | 
				
			||||||
 | 
					            {% for subform in form.formset %}
 | 
				
			||||||
 | 
					            <div class="card text-bg-secondary mt-2">
 | 
				
			||||||
 | 
					              <div class="card-body">
 | 
				
			||||||
 | 
					                {{ subform }}
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            {% endfor %}
 | 
				
			||||||
 | 
					        {% endif %}
 | 
				
			||||||
        <div class="mt-3"><button type="submit" class="btn btn-primary">Enregistrer</button></div>
 | 
					        <div class="mt-3"><button type="submit" class="btn btn-primary">Enregistrer</button></div>
 | 
				
			||||||
    </form>
 | 
					    </form>
 | 
				
			||||||
{% endblock content %}
 | 
					{% endblock content %}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,23 +1,31 @@
 | 
				
			||||||
{% extends "base.html" %}
 | 
					{% extends "base.html" %}
 | 
				
			||||||
 | 
					{% load bees_utils %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block content %}
 | 
					{% block content %}
 | 
				
			||||||
    <h2>Liste des chants</h2>
 | 
					    <h2>Liste des chants</h2>
 | 
				
			||||||
    <table class="table table-striped table-responsive table-chants">
 | 
					    <table class="table table-striped table-bordered table-responsive table-chants">
 | 
				
			||||||
        <tr><th scope="col" class="numero">N°</th><th scope="col">Titre</th><th></th><th scope="col" class="boutons"></th></tr>
 | 
					        <tr><th scope="col" class="numero">N°</th><th scope="col">Titre</th><th></th><th></th>
 | 
				
			||||||
 | 
					            {% if perms.beesgospel.change_chant %}<th scope="col" class="boutons"></th>{% endif %}</tr>
 | 
				
			||||||
    {% for chant in object_list %}
 | 
					    {% for chant in object_list %}
 | 
				
			||||||
        <tr class="editable">
 | 
					        <tr class="editable">
 | 
				
			||||||
            <td>{{ chant.numero }}</td>
 | 
					            <td>{{ chant.numero }}</td>
 | 
				
			||||||
            <td>{{ chant.titre }}</td>
 | 
					            <td>{{ chant.titre }}</td>
 | 
				
			||||||
 | 
					            <td>{% for doc in chant.chantdoc_set.all %}
 | 
				
			||||||
 | 
					                <a href="{{ doc.fichier.url }}">
 | 
				
			||||||
 | 
					                    <img src="{{ doc.fichier.path|icon_url }}" title="{{ doc.titre }}" data-bs-toggle="tooltip">
 | 
				
			||||||
 | 
					                </a>{% endfor %}
 | 
				
			||||||
 | 
					            </td>
 | 
				
			||||||
            <td>{{ chant.particularite }}</td>
 | 
					            <td>{{ chant.particularite }}</td>
 | 
				
			||||||
            <td class="text-nowrap">{% if perms.beesgospel.change_chant %}
 | 
					            {% if perms.beesgospel.change_chant %}
 | 
				
			||||||
 | 
					            <td class="text-nowrap">
 | 
				
			||||||
                <a href="{% url 'chant-edit' chant.pk %}" class="edit-button">
 | 
					                <a href="{% url 'chant-edit' chant.pk %}" class="edit-button">
 | 
				
			||||||
                    <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 'chant-delete' chant.pk %}">{% csrf_token %}
 | 
					                <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>
 | 
					                    <button type="submit" class="btn btn-link delete-button"><img src="{% static 'admin/img/icon-deletelink.svg' %}"></button>
 | 
				
			||||||
                </form>
 | 
					                </form>
 | 
				
			||||||
                {% endif %}
 | 
					 | 
				
			||||||
            </td>
 | 
					            </td>
 | 
				
			||||||
 | 
					            {% endif %}
 | 
				
			||||||
        </tr>
 | 
					        </tr>
 | 
				
			||||||
    {% endfor %}
 | 
					    {% endfor %}
 | 
				
			||||||
    </table>
 | 
					    </table>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||