Compare commits
44 commits
ajout_comm
...
master
Author | SHA1 | Date | |
---|---|---|---|
|
98a6f98fda | ||
|
0aae767a3e | ||
|
d88768311e | ||
|
abc6c62a89 | ||
|
3e065f30c0 | ||
|
0ee798a64e | ||
|
1e8b993739 | ||
|
8398805b54 | ||
|
56d7490879 | ||
|
aa430fa4d2 | ||
|
5eb27e2171 | ||
|
31f6dc0961 | ||
|
764ffea61f | ||
|
7122c1c0c4 | ||
|
14e8d963d6 | ||
|
99e3438ead | ||
|
8a36b051a1 | ||
|
07d2e30b32 | ||
|
f88ed9ece6 | ||
|
5062a1e84e | ||
|
bc7430cb5d | ||
|
a7851ff123 | ||
|
50f888a07c | ||
|
343eaeb72e | ||
|
5aebd7bbe1 | ||
|
cb2577c04b | ||
|
bd42fe6fb9 | ||
|
bbff984513 | ||
|
9aed5d1758 | ||
|
f961722fa8 | ||
|
904f9fc74a | ||
|
280e0c625d | ||
|
8e6ca0cc86 | ||
|
0673e64083 | ||
|
82bc6c1e1b | ||
|
26bd2484a2 | ||
|
4d8b97c2cb | ||
|
b62d6f47a4 | ||
|
10159393f1 | ||
|
d217a74da5 | ||
|
079e0d1c3f | ||
|
45fdd4683b | ||
|
95cf80c234 | ||
|
e6f9173c62 |
66 changed files with 1723 additions and 382 deletions
|
@ -35,6 +35,7 @@ ACCOUNT_CREATION_PASS = import_secret("ACCOUNT_CREATION_PASS")
|
|||
BASE_DIR = os.path.join(os.path.dirname(__file__), "..", "..")
|
||||
|
||||
INSTALLED_APPS = [
|
||||
"propositions",
|
||||
"trombonoscope",
|
||||
"actu",
|
||||
"colorful",
|
||||
|
|
23
actu/migrations/0002_actu_rainbow.py
Normal file
23
actu/migrations/0002_actu_rainbow.py
Normal file
|
@ -0,0 +1,23 @@
|
|||
# Generated by Django 2.2.25 on 2022-01-06 12:09
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("actu", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="actu",
|
||||
name="rainbow",
|
||||
field=models.CharField(
|
||||
choices=[("y", "Oui"), ("n", "Non")],
|
||||
default="n",
|
||||
max_length=1,
|
||||
verbose_name="Actu en arc-en-ciel (ne pas mettre d'émoji, il prennent aussi la couleur et c'est moche)",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -7,6 +7,15 @@ class Actu(models.Model):
|
|||
text = models.TextField(_("Info"), null=True, blank=False)
|
||||
text_en = models.TextField(("Info en anglais"), null=True, blank=True)
|
||||
order = models.IntegerField(verbose_name=_("ordre"))
|
||||
rainbow = models.CharField(
|
||||
verbose_name=_(
|
||||
"Actu en arc-en-ciel (ne pas mettre d'émoji, il prennent aussi la couleur et c'est moche)"
|
||||
),
|
||||
max_length=1,
|
||||
choices=(("y", "Oui"), ("n", "Non")),
|
||||
default="n",
|
||||
blank=False,
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.text
|
||||
|
|
|
@ -14,7 +14,7 @@ class ActuList(ChefRequiredMixin, ListView):
|
|||
|
||||
class ActuCreate(ChefRequiredMixin, CreateView):
|
||||
model = Actu
|
||||
fields = ["text", "order", "text_en"]
|
||||
fields = ["text", "order", "text_en", "rainbow"]
|
||||
template_name = "actu/create_actu.html"
|
||||
success_url = reverse_lazy("actu:liste")
|
||||
|
||||
|
@ -26,7 +26,7 @@ class ActuCreate(ChefRequiredMixin, CreateView):
|
|||
|
||||
class ActuUpdate(ChefRequiredMixin, UpdateView):
|
||||
model = Actu
|
||||
fields = ["text", "order", "text_en"]
|
||||
fields = ["text", "order", "text_en", "rainbow"]
|
||||
template_name = "actu/update_actu.html"
|
||||
success_url = reverse_lazy("actu:liste")
|
||||
|
||||
|
|
|
@ -2,6 +2,27 @@ from django.contrib import admin
|
|||
|
||||
from .models import Event, Participants
|
||||
|
||||
|
||||
class ParticipantsAdmin(admin.ModelAdmin):
|
||||
fields = [
|
||||
"event",
|
||||
"participant",
|
||||
"reponse",
|
||||
"instrument",
|
||||
"instrument_autre",
|
||||
"dont_play_main",
|
||||
"details",
|
||||
"creationDate",
|
||||
"updateDate",
|
||||
]
|
||||
readonly_fields = ["creationDate", "updateDate"]
|
||||
list_display = ["participant", "event", "reponse", "creationDate", "updateDate"]
|
||||
def has_add_permission(self, req):
|
||||
return False
|
||||
def has_change_permission(self,obj, change=False):
|
||||
return False
|
||||
|
||||
|
||||
# Add event by admin page return a 502 error
|
||||
admin.site.register(Event)
|
||||
admin.site.register(Participants)
|
||||
admin.site.register(Participants, ParticipantsAdmin)
|
||||
|
|
|
@ -22,11 +22,11 @@ class EventCalendar(HTMLCalendar):
|
|||
for ev in self.events[day]:
|
||||
body.append('<a href="/agenda/' + '%s"' % ev.id)
|
||||
if ev.calendrier == "C":
|
||||
body.append('style="color:#160083">'+esc(ev.nom))
|
||||
body.append('style="color:#160083">' + esc(ev.nom))
|
||||
elif ev.calendrier == "D":
|
||||
body.append('style="color:#770083">'+esc(ev.nom))
|
||||
body.append('style="color:#770083">' + esc(ev.nom))
|
||||
else:
|
||||
body.append('>'+esc(ev.nom))
|
||||
body.append(">" + esc(ev.nom))
|
||||
body.append("</a><br/>")
|
||||
return self.day_cell(
|
||||
cssclass,
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
from django import forms
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from gestion.models import ErnestoUser
|
||||
|
||||
from calendrier.models import Event, Participants
|
||||
from gestion.models import ErnestoUser
|
||||
|
||||
|
||||
class ModifEventForm(forms.ModelForm):
|
||||
|
@ -37,7 +37,13 @@ class EventForm(forms.ModelForm):
|
|||
class ParticipantsForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Participants
|
||||
fields = ("reponse", "details", "dont_play_main", "instrument","instrument_autre")
|
||||
fields = (
|
||||
"reponse",
|
||||
"details",
|
||||
"dont_play_main",
|
||||
"instrument",
|
||||
"instrument_autre",
|
||||
)
|
||||
widgets = {
|
||||
"details": forms.Textarea(attrs={"placeholder": _("50 caractères max")}),
|
||||
}
|
||||
|
|
|
@ -6,23 +6,41 @@ from django.db import migrations, models
|
|||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('calendrier', '0004_auto_20210606_1640'),
|
||||
("calendrier", "0004_auto_20210606_1640"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='event',
|
||||
name='calendrier',
|
||||
field=models.CharField(choices=[('F', 'Visible seulement par les fanfarons'), ('T', 'Afficher dans le calendrier pour tous'), ('H', 'Hall of fame'), ('C', 'Visible seulement par les cheff·e·s'), ('D', "Visible seulement par les cheff·e·s et sur l'agenda public")], default='F', max_length=1),
|
||||
model_name="event",
|
||||
name="calendrier",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
("F", "Visible seulement par les fanfarons"),
|
||||
("T", "Afficher dans le calendrier pour tous"),
|
||||
("H", "Hall of fame"),
|
||||
("C", "Visible seulement par les cheff·e·s"),
|
||||
("D", "Visible seulement par les cheff·e·s et sur l'agenda public"),
|
||||
],
|
||||
default="F",
|
||||
max_length=1,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='event',
|
||||
name='desc_users',
|
||||
field=models.TextField(blank=True, null=True, verbose_name='Infos (visible seulement des fanfaron·ne·s)'),
|
||||
model_name="event",
|
||||
name="desc_users",
|
||||
field=models.TextField(
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name="Infos (visible seulement des fanfaron·ne·s)",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='event',
|
||||
name='desc_users_en',
|
||||
field=models.TextField(blank=True, null=True, verbose_name='Infos en anglais (visible seulement des fanfaron·ne·s'),
|
||||
model_name="event",
|
||||
name="desc_users_en",
|
||||
field=models.TextField(
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name="Infos en anglais (visible seulement des fanfaron·ne·s",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -6,18 +6,36 @@ from django.db import migrations, models
|
|||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('calendrier', '0005_auto_20210726_0949'),
|
||||
("calendrier", "0005_auto_20210726_0949"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='participants',
|
||||
name='instrument_autre',
|
||||
model_name="participants",
|
||||
name="instrument_autre",
|
||||
field=models.CharField(blank=True, max_length=50, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='participants',
|
||||
name='instrument',
|
||||
field=models.CharField(blank=True, choices=[('Clarinette', 'Clarinette'), ('Euphonium', 'Euphonium'), ('Percussion', 'Percussion'), ('Piccolo', 'Piccolo'), ('Saxophone Alto', 'Saxophone Alto'), ('Saxophone Ténor', 'Saxophone Ténor'), ('Saxophone Baryton', 'Saxophone Baryton'), ('Souba', 'Souba'), ('Trombone', 'Trombone'), ('Trompette', 'Trompette'), ('Autre', 'Autre'), ('ne sais pas', 'Je ne sais pas encore')], max_length=50, null=True),
|
||||
model_name="participants",
|
||||
name="instrument",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("Clarinette", "Clarinette"),
|
||||
("Euphonium", "Euphonium"),
|
||||
("Percussion", "Percussion"),
|
||||
("Piccolo", "Piccolo"),
|
||||
("Saxophone Alto", "Saxophone Alto"),
|
||||
("Saxophone Ténor", "Saxophone Ténor"),
|
||||
("Saxophone Baryton", "Saxophone Baryton"),
|
||||
("Souba", "Souba"),
|
||||
("Trombone", "Trombone"),
|
||||
("Trompette", "Trompette"),
|
||||
("Autre", "Autre"),
|
||||
("ne sais pas", "Je ne sais pas encore"),
|
||||
],
|
||||
max_length=50,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
31
calendrier/migrations/0007_auto_20220314_2320.py
Normal file
31
calendrier/migrations/0007_auto_20220314_2320.py
Normal file
|
@ -0,0 +1,31 @@
|
|||
# Generated by Django 2.2.25 on 2022-03-14 23:20
|
||||
|
||||
import django.utils.timezone
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("calendrier", "0006_auto_20210929_1629"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="participants",
|
||||
name="creationDate",
|
||||
field=models.DateTimeField(
|
||||
auto_now_add=True,
|
||||
default=django.utils.timezone.now,
|
||||
verbose_name="Date de création",
|
||||
),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="participants",
|
||||
name="updateDate",
|
||||
field=models.DateTimeField(
|
||||
auto_now=True, verbose_name="Dernière mise à jour"
|
||||
),
|
||||
),
|
||||
]
|
17
calendrier/migrations/0008_auto_20220322_1454.py
Normal file
17
calendrier/migrations/0008_auto_20220322_1454.py
Normal file
|
@ -0,0 +1,17 @@
|
|||
# Generated by Django 2.2.27 on 2022-03-22 13:54
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('calendrier', '0007_auto_20220314_2320'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddConstraint(
|
||||
model_name='participants',
|
||||
constraint=models.UniqueConstraint(fields=('event', 'participant'), name='reponse unique aux event'),
|
||||
),
|
||||
]
|
|
@ -2,9 +2,7 @@ import uuid
|
|||
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from gestion.models import INSTRU_CHOICES
|
||||
from gestion.models import ErnestoUser
|
||||
from gestion.models import INSTRU_CHOICES, ErnestoUser
|
||||
|
||||
ANSWERS = (
|
||||
("oui", _("Oui")),
|
||||
|
@ -62,7 +60,9 @@ class Participants(models.Model):
|
|||
reponse = models.CharField(
|
||||
_("Réponse"), max_length=20, default="non", choices=ANSWERS
|
||||
)
|
||||
instrument = models.CharField(max_length=50, blank=True, null=True, choices=INSTRU_CHOICES)
|
||||
instrument = models.CharField(
|
||||
max_length=50, blank=True, null=True, choices=INSTRU_CHOICES
|
||||
)
|
||||
instrument_autre = models.CharField(max_length=50, blank=True, null=True)
|
||||
dont_play_main = models.CharField(
|
||||
_("Je veux jouer d'un instrument different de mon instrument principal:"),
|
||||
|
@ -72,3 +72,11 @@ class Participants(models.Model):
|
|||
choices=[("Non", _("Non")), ("Oui", _("Oui"))],
|
||||
)
|
||||
details = models.CharField(max_length=50, blank=True)
|
||||
creationDate = models.DateTimeField(
|
||||
auto_now_add=True, verbose_name=_("Date de création")
|
||||
)
|
||||
updateDate = models.DateTimeField(
|
||||
auto_now=True, verbose_name=_("Dernière mise à jour")
|
||||
)
|
||||
class Meta:
|
||||
constraints = [ models.UniqueConstraint(fields=['event', 'participant'], name='reponse unique aux event') ]
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
<h4> <span class="ernestocouleur">{% blocktrans count counter=actu|length %}Actualité des chef·fe·s:{% plural %}Actualités des chef·fe·s:{% endblocktrans %}</span></h4>
|
||||
<ul>
|
||||
{% for a in actu %}
|
||||
<li>{% autotranslate current_language a.text a.text_en %}</li>
|
||||
<li>{% if a.rainbow == 'y' %}<span class="ernestocouleur font-weight-bold">{% endif %}{% autotranslate current_language a.text a.text_en %}{% if a.rainbow %}</span>{% endif %}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
@ -223,6 +223,9 @@
|
|||
</div>
|
||||
</section>
|
||||
</div>
|
||||
{% if event.id == 573 %}
|
||||
<div class="fireworks" style="pointer-events: none; position:fixed; top: 0; left: 0; right: 0; bottom: 0; height: 100%; weight: 100%;"></div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% block script %}
|
||||
<script>
|
||||
|
@ -264,5 +267,13 @@ singleEvent.setOption({ lang: 'fr' });
|
|||
singleEvent.setOption({ lang: 'en' });
|
||||
{% endifequal %}
|
||||
</script>
|
||||
|
||||
{% if event.id == 573 %}
|
||||
<script src="https://unpkg.com/fireworks-js@2.x/dist/index.umd.js"></script>
|
||||
<script>
|
||||
const container = document.querySelector('.fireworks');
|
||||
const fireworks = new Fireworks.default(container);
|
||||
fireworks.updateOptions({ acceleration: 1.01, traceSpeed: 5 });
|
||||
fireworks.start();
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -4,7 +4,6 @@ from django.contrib.auth import get_user_model
|
|||
from django.template.defaultfilters import urlencode
|
||||
from django.test import Client, TestCase
|
||||
from django.utils import timezone
|
||||
|
||||
from gestion.models import ErnestoUser
|
||||
|
||||
from ..models import Event
|
||||
|
|
|
@ -4,20 +4,20 @@ from calendar import monthrange
|
|||
from collections import defaultdict
|
||||
from datetime import date, datetime
|
||||
|
||||
from actu.models import Actu
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.db.models import Q
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse, reverse_lazy
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.views.generic import DeleteView, TemplateView, UpdateView
|
||||
from gestion.mixins import ChefEventRequiredMixin
|
||||
from gestion.models import Photo
|
||||
|
||||
from actu.models import Actu
|
||||
from calendrier.calend import EventCalendar
|
||||
from calendrier.forms import (ChangeDoodleName, EventForm, ModifEventForm,
|
||||
ParticipantsForm)
|
||||
from calendrier.models import Event, Participants
|
||||
from gestion.mixins import ChefRequiredMixin, ChefEventRequiredMixin
|
||||
from gestion.models import Photo
|
||||
|
||||
|
||||
def generer(*args):
|
||||
|
@ -76,9 +76,13 @@ class Calendar(LoginRequiredMixin, TemplateView):
|
|||
lMonth = self.pMonth
|
||||
lCalendarFromMonth = datetime(lYear, lMonth, 1)
|
||||
lCalendarToMonth = datetime(lYear, lMonth, monthrange(lYear, lMonth)[1])
|
||||
lEvents = Event.objects.filter(
|
||||
date__gte=lCalendarFromMonth, date__lte=lCalendarToMonth
|
||||
).exclude(calendrier__iexact="C").exclude(calendrier__iexact="D")
|
||||
lEvents = (
|
||||
Event.objects.filter(
|
||||
date__gte=lCalendarFromMonth, date__lte=lCalendarToMonth
|
||||
)
|
||||
.exclude(calendrier__iexact="C")
|
||||
.exclude(calendrier__iexact="D")
|
||||
)
|
||||
lEvents_chef = Event.objects.filter(
|
||||
date__gte=lCalendarFromMonth, date__lte=lCalendarToMonth
|
||||
)
|
||||
|
@ -170,7 +174,6 @@ class Calendar(LoginRequiredMixin, TemplateView):
|
|||
|
||||
|
||||
class Home(Calendar):
|
||||
|
||||
@property
|
||||
def pYear(self):
|
||||
lToday = datetime.now()
|
||||
|
@ -209,6 +212,7 @@ class ViewEvent(LoginRequiredMixin, TemplateView):
|
|||
else:
|
||||
instru = participant.instrument
|
||||
|
||||
instru = "" if instru is None else instru
|
||||
sure, maybe, namesoui, namespe, namesnon = instrument_count[instru]
|
||||
|
||||
if participant.reponse == "oui":
|
||||
|
@ -234,25 +238,52 @@ class ViewEvent(LoginRequiredMixin, TemplateView):
|
|||
namesnon += [participant.participant.get_doodlename()]
|
||||
instrument_count[instru] = (sure, maybe, namesoui, namespe, namesnon)
|
||||
instrument_count_l = []
|
||||
instru_order = ["Clarinette","Piccolo","Flute","Glockenspiel","Saxophone Alto","Trompette","Trombone","Cor","Saxophone Ténor","Saxophone Baryton","Clarinette Basse","Euphonium","Souba","Percussion"]
|
||||
instru_order = [
|
||||
"Clarinette",
|
||||
"Piccolo",
|
||||
"Flute",
|
||||
"Glockenspiel",
|
||||
"Saxophone Alto",
|
||||
"Trompette",
|
||||
"Trombone",
|
||||
"Cor",
|
||||
"Saxophone Ténor",
|
||||
"Saxophone Baryton",
|
||||
"Clarinette Basse",
|
||||
"Euphonium",
|
||||
"Souba",
|
||||
"Percussion",
|
||||
]
|
||||
for instrument in instru_order:
|
||||
if instrument in instrument_count.keys():
|
||||
(sure,maybe,namesoui,namespe,namesnon) =instrument_count[instrument]
|
||||
instrument_count_l.append(( instrument, sure,
|
||||
maybe,
|
||||
namesoui,
|
||||
namespe,
|
||||
namesnon,
|
||||
))
|
||||
(sure, maybe, namesoui, namespe, namesnon) = instrument_count[
|
||||
instrument
|
||||
]
|
||||
instrument_count_l.append(
|
||||
(
|
||||
instrument,
|
||||
sure,
|
||||
maybe,
|
||||
namesoui,
|
||||
namespe,
|
||||
namesnon,
|
||||
)
|
||||
)
|
||||
for instrument in sorted(instrument_count.keys()):
|
||||
if instrument not in instru_order:
|
||||
(sure,maybe,namesoui,namespe,namesnon) =instrument_count[instrument]
|
||||
instrument_count_l.append(( instrument, sure,
|
||||
maybe,
|
||||
namesoui,
|
||||
namespe,
|
||||
namesnon,
|
||||
))
|
||||
(sure, maybe, namesoui, namespe, namesnon) = instrument_count[
|
||||
instrument
|
||||
]
|
||||
instrument_count_l.append(
|
||||
(
|
||||
instrument,
|
||||
sure,
|
||||
maybe,
|
||||
namesoui,
|
||||
namespe,
|
||||
namesnon,
|
||||
)
|
||||
)
|
||||
|
||||
context["event"] = event
|
||||
context["instrument_count"] = instrument_count_l
|
||||
|
@ -261,7 +292,7 @@ class ViewEvent(LoginRequiredMixin, TemplateView):
|
|||
context["nbpe"] = len(participants.filter(reponse="pe"))
|
||||
context["nbnon"] = len(participants.filter(reponse="non"))
|
||||
context["multi_instrumentistes"] = multi_instrumentistes
|
||||
context["chef_only"] = (event.calendrier == "C")|(event.calendrier == "D")
|
||||
context["chef_only"] = (event.calendrier == "C") | (event.calendrier == "D")
|
||||
return context
|
||||
|
||||
|
||||
|
@ -337,20 +368,22 @@ class ReponseEvent(LoginRequiredMixin, TemplateView):
|
|||
context["form"] = self.form_class()
|
||||
context["ev"] = get_object_or_404(Event, id=self.kwargs["id"])
|
||||
context["id"] = self.kwargs["id"]
|
||||
context["chef_only"] = (context["ev"].calendrier == "C")|(context["ev"].calendrier == "D")
|
||||
context["chef_only"] = (context["ev"].calendrier == "C") | (
|
||||
context["ev"].calendrier == "D"
|
||||
)
|
||||
return context
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
form = self.form_class(request.POST)
|
||||
ev = get_object_or_404(Event, id=self.kwargs["id"])
|
||||
part = request.user.profile
|
||||
try:
|
||||
p = Participants.objects.get(event=ev, participant=part)
|
||||
except Participants.DoesNotExist:
|
||||
p = None
|
||||
form = self.form_class(request.POST, instance=p)
|
||||
if form.is_valid():
|
||||
try:
|
||||
p = Participants.objects.get(event=ev, participant=part)
|
||||
p.delete()
|
||||
except Participants.DoesNotExist:
|
||||
pass
|
||||
obj = form.save(commit=False)
|
||||
# Si la participation existe déjà, ces 2 ligne sont redondantes
|
||||
obj.event = ev
|
||||
obj.participant = part
|
||||
obj.save()
|
||||
|
|
|
@ -1,19 +1,24 @@
|
|||
# Generated by Django 2.2.17 on 2021-06-08 10:29
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
import gestion.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('gestion', '0005_auto_20210427_1834'),
|
||||
("gestion", "0005_auto_20210427_1834"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='photo',
|
||||
name='image',
|
||||
field=models.ImageField(default=None, upload_to='trombonoscope/deco', validators=[gestion.models.Photo.validate_image]),
|
||||
model_name="photo",
|
||||
name="image",
|
||||
field=models.ImageField(
|
||||
default=None,
|
||||
upload_to="trombonoscope/deco",
|
||||
validators=[gestion.models.Photo.validate_image],
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -6,13 +6,15 @@ from django.db import migrations, models
|
|||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('gestion', '0006_auto_20210608_1029'),
|
||||
("gestion", "0006_auto_20210608_1029"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='ernestouser',
|
||||
name='is_chef_event',
|
||||
field=models.BooleanField(default=False, verbose_name='Respo événement Fanfare'),
|
||||
model_name="ernestouser",
|
||||
name="is_chef_event",
|
||||
field=models.BooleanField(
|
||||
default=False, verbose_name="Respo événement Fanfare"
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -6,23 +6,23 @@ from django.db import migrations, models
|
|||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('gestion', '0007_ernestouser_is_chef_event'),
|
||||
("gestion", "0007_ernestouser_is_chef_event"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='ernestouser',
|
||||
name='is_chef_com',
|
||||
field=models.BooleanField(default=False, verbose_name='Respo com'),
|
||||
model_name="ernestouser",
|
||||
name="is_chef_com",
|
||||
field=models.BooleanField(default=False, verbose_name="Respo com"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='ernestouser',
|
||||
name='is_chef_instru',
|
||||
field=models.BooleanField(default=False, verbose_name='Respo instruments'),
|
||||
model_name="ernestouser",
|
||||
name="is_chef_instru",
|
||||
field=models.BooleanField(default=False, verbose_name="Respo instruments"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='ernestouser',
|
||||
name='is_chef_event',
|
||||
field=models.BooleanField(default=False, verbose_name='Respo événements'),
|
||||
model_name="ernestouser",
|
||||
name="is_chef_event",
|
||||
field=models.BooleanField(default=False, verbose_name="Respo événements"),
|
||||
),
|
||||
]
|
||||
|
|
18
gestion/migrations/0009_ernestouser_is_chef_mu.py
Normal file
18
gestion/migrations/0009_ernestouser_is_chef_mu.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 2.2.24 on 2022-01-11 15:12
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("gestion", "0008_auto_20211022_1923"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="ernestouser",
|
||||
name="is_chef_mu",
|
||||
field=models.BooleanField(default=False, verbose_name="Respo musique"),
|
||||
),
|
||||
]
|
|
@ -6,32 +6,75 @@ class ChefRequiredMixin(UserPassesTestMixin):
|
|||
user = self.request.user
|
||||
return (user is not None) and hasattr(user, "profile") and user.profile.is_chef
|
||||
|
||||
|
||||
class ChefEventRequiredMixin(UserPassesTestMixin):
|
||||
def test_func(self):
|
||||
user = self.request.user
|
||||
is_chef = (user is not None) and hasattr(user, "profile") and user.profile.is_chef
|
||||
is_chef_event = (user is not None) and hasattr(user, "profile") and user.profile.is_chef_event
|
||||
is_chef = (
|
||||
(user is not None) and hasattr(user, "profile") and user.profile.is_chef
|
||||
)
|
||||
is_chef_event = (
|
||||
(user is not None)
|
||||
and hasattr(user, "profile")
|
||||
and user.profile.is_chef_event
|
||||
)
|
||||
return is_chef or is_chef_event
|
||||
|
||||
|
||||
class ChefInstruRequiredMixin(UserPassesTestMixin):
|
||||
def test_func(self):
|
||||
user = self.request.user
|
||||
is_chef = (user is not None) and hasattr(user, "profile") and user.profile.is_chef
|
||||
is_chef_instru = (user is not None) and hasattr(user, "profile") and user.profile.is_chef_instru
|
||||
is_chef = (
|
||||
(user is not None) and hasattr(user, "profile") and user.profile.is_chef
|
||||
)
|
||||
is_chef_instru = (
|
||||
(user is not None)
|
||||
and hasattr(user, "profile")
|
||||
and user.profile.is_chef_instru
|
||||
)
|
||||
return is_chef or is_chef_instru
|
||||
|
||||
|
||||
class ChefComRequiredMixin(UserPassesTestMixin):
|
||||
def test_func(self):
|
||||
user = self.request.user
|
||||
is_chef = (user is not None) and hasattr(user, "profile") and user.profile.is_chef
|
||||
is_chef_com = (user is not None) and hasattr(user, "profile") and user.profile.is_chef_com
|
||||
is_chef = (
|
||||
(user is not None) and hasattr(user, "profile") and user.profile.is_chef
|
||||
)
|
||||
is_chef_com = (
|
||||
(user is not None) and hasattr(user, "profile") and user.profile.is_chef_com
|
||||
)
|
||||
return is_chef or is_chef_com
|
||||
|
||||
|
||||
class ChefMuRequiredMixin(UserPassesTestMixin):
|
||||
def test_func(self):
|
||||
user = self.request.user
|
||||
is_chef = (
|
||||
(user is not None) and hasattr(user, "profile") and user.profile.is_chef
|
||||
)
|
||||
is_chef_mu = (
|
||||
(user is not None) and hasattr(user, "profile") and user.profile.is_chef_mu
|
||||
)
|
||||
return is_chef or is_chef_mu
|
||||
|
||||
|
||||
class AllChefRequiredMixin(UserPassesTestMixin):
|
||||
def test_func(self):
|
||||
user = self.request.user
|
||||
is_chef = (user is not None) and hasattr(user, "profile") and user.profile.is_chef
|
||||
is_chef = (
|
||||
(user is not None) and hasattr(user, "profile") and user.profile.is_chef
|
||||
)
|
||||
is_su = (user is not None) and user.is_superuser
|
||||
is_chef_com = (user is not None) and hasattr(user, "profile") and user.profile.is_chef_com
|
||||
is_chef_event = (user is not None) and hasattr(user, "profile") and user.profile.is_chef_event
|
||||
return is_chef or is_chef_com or is_chef_event or is_su
|
||||
is_chef_com = (
|
||||
(user is not None) and hasattr(user, "profile") and user.profile.is_chef_com
|
||||
)
|
||||
is_chef_event = (
|
||||
(user is not None)
|
||||
and hasattr(user, "profile")
|
||||
and user.profile.is_chef_event
|
||||
)
|
||||
is_chef_mu = (
|
||||
(user is not None) and hasattr(user, "profile") and user.profile.is_chef_mu
|
||||
)
|
||||
return is_chef or is_chef_com or is_chef_event or is_su or is_chef_mu
|
||||
|
|
|
@ -1,27 +1,30 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
|
||||
from colorful.fields import RGBColorField
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
import os
|
||||
from django.conf import settings
|
||||
|
||||
INSTRU_CHOICES = [
|
||||
("Clarinette", _("Clarinette")),
|
||||
("Euphonium", _("Euphonium")),
|
||||
("Percussion", _("Percussion")),
|
||||
("Piccolo", _("Piccolo")),
|
||||
("Saxophone Alto", _("Saxophone Alto")),
|
||||
("Saxophone Ténor", _("Saxophone Ténor")),
|
||||
("Saxophone Baryton", _("Saxophone Baryton")),
|
||||
("Souba", _("Souba")),
|
||||
("Trombone", _("Trombone")),
|
||||
("Trompette", _("Trompette")),
|
||||
("Autre", _("Autre")),
|
||||
("ne sais pas", _("Je ne sais pas encore")),
|
||||
]
|
||||
("Clarinette", _("Clarinette")),
|
||||
("Euphonium", _("Euphonium")),
|
||||
("Percussion", _("Percussion")),
|
||||
("Piccolo", _("Piccolo")),
|
||||
("Saxophone Alto", _("Saxophone Alto")),
|
||||
("Saxophone Ténor", _("Saxophone Ténor")),
|
||||
("Saxophone Baryton", _("Saxophone Baryton")),
|
||||
("Souba", _("Souba")),
|
||||
("Trombone", _("Trombone")),
|
||||
("Trompette", _("Trompette")),
|
||||
("Autre", _("Autre")),
|
||||
("ne sais pas", _("Je ne sais pas encore")),
|
||||
]
|
||||
|
||||
|
||||
class Photo(models.Model):
|
||||
PHOTO_PLACEMENT = (
|
||||
("home_join", _("Rejoignez nous")),
|
||||
|
@ -84,6 +87,7 @@ class ErnestoUser(models.Model):
|
|||
is_chef_event = models.BooleanField(_("Respo événements"), default=False)
|
||||
is_chef_com = models.BooleanField(_("Respo com"), default=False)
|
||||
is_chef_instru = models.BooleanField(_("Respo instruments"), default=False)
|
||||
is_chef_mu = models.BooleanField(_("Respo musique"), default=False)
|
||||
phone = models.CharField(
|
||||
_("Téléphone"),
|
||||
max_length=20,
|
||||
|
@ -91,7 +95,6 @@ class ErnestoUser(models.Model):
|
|||
help_text=_("seulement visible par les chef·fe·s"),
|
||||
)
|
||||
|
||||
|
||||
COLORS_CHOICES = [
|
||||
("#e4522f#ffffff", _("Orange et Blanc")),
|
||||
("#ffffff#000000", _("Blanc et Noir")),
|
||||
|
|
|
@ -3704,10 +3704,11 @@ div.spoiler
|
|||
hsl(220, 100%, 50%),
|
||||
hsl(230, 100%, 50%),
|
||||
hsl(240, 100%, 50%),
|
||||
hsl(250, 100%, 50%),
|
||||
hsl(260, 100%, 50%),
|
||||
hsl(270, 100%, 50%),
|
||||
hsl(280, 100%, 50%)
|
||||
hsl(250, 100%, 50%)
|
||||
);
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
select[multiple] {
|
||||
height: 15em;
|
||||
}
|
||||
|
|
BIN
gestion/static/images/cvec.png
Normal file
BIN
gestion/static/images/cvec.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 46 KiB |
|
@ -67,7 +67,7 @@
|
|||
<a class="dropdown-item" href="https://heyzine.com/flip-book/b2cf4809b7.html" target="_blank">{% trans "Year Book 2021" %}</a>
|
||||
</div>
|
||||
</li>
|
||||
{% if user.is_superuser or user.profile.is_chef or user.profile.is_chef_event or user.profile.is_chef_com %}
|
||||
{% if user.is_superuser or user.profile.is_chef or user.profile.is_chef_event or user.profile.is_chef_com or user.profile.is_chef_mu %}
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="{% url 'chef' %}" id="navbardrop" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<b>{% trans 'Le pouvoir des cheff·e·s'%}</b>
|
||||
|
@ -77,18 +77,18 @@
|
|||
<a class="dropdown-item" href="/admin/">{% trans "Administration" %}</a>
|
||||
{% endif %}
|
||||
{% if user.profile.is_chef %}
|
||||
<a class="dropdown-item" href="{% url 'calendrier:create_event' %}">{% trans "Ajouter un événement" %}</a>
|
||||
<a class="dropdown-item" href="{% url 'actu:liste' %}">{% trans "Modifier les actualités" %}</a>
|
||||
<a class="dropdown-item" href="{% url 'liste_photo' %}">{% trans "Modifier les photos" %}</a>
|
||||
<a class="dropdown-item" href="{% url 'liste_video' %}">{% trans "Modifier les vidéos" %}</a>
|
||||
|
||||
{% elif user.profile.is_chef_event %}
|
||||
{% endif %}
|
||||
{% if user.profile.is_chef or user.profile.is_chef_event %}
|
||||
<a class="dropdown-item" href="{% url 'calendrier:create_event' %}">{% trans "Ajouter un événement" %}</a>
|
||||
|
||||
{% elif user.profile.is_chef_com %}
|
||||
{% endif %}
|
||||
{% if user.profile.is_chef or user.profile.is_chef_com %}
|
||||
<a class="dropdown-item" href="{% url 'liste_photo' %}">{% trans "Modifier les photos" %}</a>
|
||||
<a class="dropdown-item" href="{% url 'liste_video' %}">{% trans "Modifier les vidéos" %}</a>
|
||||
{% endif %}
|
||||
{% if user.profile.is_chef or user.profile.is_chef_mu %}
|
||||
<a class="dropdown-item" href="{% url 'partitions:list_setlist' %}">{% trans "Gérer les programmes de répétition" %}</a>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</li>
|
||||
|
@ -145,6 +145,10 @@
|
|||
<!-- Footer -->
|
||||
<footer id="footer" style="background-color:rgb(228, 82, 47);">
|
||||
<div class="copyright">
|
||||
<ul class="icons">
|
||||
<li><a target="_blank" href="https://cvec.etudiant.gouv.fr/"><img alt="Logo de la CVEC" src='{% static "images/cvec.png" %}' width="100px"/></a></li>
|
||||
|
||||
</ul>
|
||||
<ul class="icons">
|
||||
<li><a target="_blank" href="https://www.facebook.com/ernestophone"
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
{% if user.profile.is_chef or user.is_superuser %}
|
||||
<li> <a href="/admin/">{% trans "Administration" %}</a></li>
|
||||
|
||||
|
||||
{% endif %}
|
||||
{% if user.profile.is_chef %}
|
||||
<li><a href="{% url 'actu:liste' %}">{% trans "Modifier les actualités" %}</a></li>
|
||||
|
@ -24,6 +25,9 @@
|
|||
<li><a href="{% url 'liste_photo' %}">{% trans "Modifier les photos" %}</a></li>
|
||||
<li><a href="{% url 'liste_video' %}">{% trans "Modifier les vidéos" %}</a></li>
|
||||
{% endif %}
|
||||
{% if user.profile.is_chef or user.profile.is_chef_mu %}
|
||||
<li> <a href="{% url 'partitions:list_setlist' %}">{% trans "Gérer les programmes de répétition" %}</a> </li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import os
|
||||
import random
|
||||
import string
|
||||
|
||||
|
@ -11,12 +12,12 @@ from django.urls import reverse_lazy
|
|||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic import (CreateView, DeleteView, ListView,
|
||||
TemplateView, UpdateView)
|
||||
import os
|
||||
|
||||
from calendrier.forms import ChangeDoodleName
|
||||
from gestion.forms import (ChangeFormUser, ChangeMembreForm,
|
||||
InscriptionMembreForm, RegistrationFormUser)
|
||||
from gestion.mixins import ChefRequiredMixin, AllChefRequiredMixin, ChefComRequiredMixin
|
||||
from gestion.mixins import (AllChefRequiredMixin, ChefComRequiredMixin,
|
||||
ChefRequiredMixin)
|
||||
from gestion.models import ErnestoUser, Photo, VideoGallery
|
||||
from partitions.models import Category
|
||||
|
||||
|
@ -79,6 +80,7 @@ class Profil(LoginRequiredMixin, TemplateView):
|
|||
class Chef(AllChefRequiredMixin, TemplateView):
|
||||
template_name = "gestion/chef.html"
|
||||
|
||||
|
||||
class YearBook2021(TemplateView):
|
||||
template_name = "gestion/yearbook2021.html"
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ from django.utils.safestring import mark_safe
|
|||
from django.views.generic import (CreateView, DeleteView, TemplateView,
|
||||
UpdateView)
|
||||
|
||||
from gestion.mixins import ChefRequiredMixin, ChefInstruRequiredMixin
|
||||
from gestion.mixins import ChefInstruRequiredMixin, ChefRequiredMixin
|
||||
from gestion.models import Photo
|
||||
from instruments.forms import ChefEditInstrumentForm, ChefReparationForm
|
||||
from instruments.models import Instrument, Reparation
|
||||
|
@ -28,7 +28,17 @@ class ListeInstru(LoginRequiredMixin, TemplateView):
|
|||
|
||||
class CreateInstru(ChefInstruRequiredMixin, CreateView):
|
||||
model = Instrument
|
||||
fields = ["owner","user", "etat", "type", "marque", "model", "serial", "annee", "prix"]
|
||||
fields = [
|
||||
"owner",
|
||||
"user",
|
||||
"etat",
|
||||
"type",
|
||||
"marque",
|
||||
"model",
|
||||
"serial",
|
||||
"annee",
|
||||
"prix",
|
||||
]
|
||||
template_name = "instruments/create_instru.html"
|
||||
success_url = reverse_lazy("instruments:liste")
|
||||
|
||||
|
@ -95,11 +105,12 @@ class FicheInstru(LoginRequiredMixin, TemplateView):
|
|||
def post(self, request, *args, **kwargs):
|
||||
instru = get_object_or_404(self.model, id=self.kwargs["pk"])
|
||||
form = ChefEditInstrumentForm(request.POST, instance=instru)
|
||||
if request.user.profile.is_chef:
|
||||
if request.user.profile.is_chef or request.user.profile.is_chef_instru:
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
context = self.get_context_data()
|
||||
context["form"] = form
|
||||
print(instru.user)
|
||||
return render(request, self.template_name, context)
|
||||
|
||||
|
||||
|
|
Binary file not shown.
File diff suppressed because it is too large
Load diff
|
@ -4,7 +4,7 @@ from django.urls import reverse_lazy
|
|||
from django.utils import timezone
|
||||
from django.views.generic import CreateView, DeleteView, ListView, UpdateView
|
||||
|
||||
from gestion.mixins import ChefRequiredMixin, ChefEventRequiredMixin
|
||||
from gestion.mixins import ChefEventRequiredMixin, ChefRequiredMixin
|
||||
from pads.models import Pad
|
||||
|
||||
|
||||
|
|
|
@ -1,6 +1,13 @@
|
|||
from django.contrib import admin
|
||||
|
||||
from .models import Category, PartitionSet
|
||||
from .models import Category, Partition, PartitionSet, SetList
|
||||
|
||||
|
||||
class PartitionAdmin(admin.ModelAdmin):
|
||||
list_filter = ("morceau",)
|
||||
|
||||
|
||||
admin.site.register(Category)
|
||||
admin.site.register(PartitionSet)
|
||||
admin.site.register(SetList)
|
||||
admin.site.register(Partition, PartitionAdmin)
|
||||
|
|
44
partitions/migrations/0005_setlist.py
Normal file
44
partitions/migrations/0005_setlist.py
Normal file
|
@ -0,0 +1,44 @@
|
|||
# Generated by Django 2.2.25 on 2022-01-09 18:38
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("partitions", "0004_auto_20210331_1350"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="SetList",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("date", models.DateField(verbose_name="Date de la répétition")),
|
||||
(
|
||||
"is_current",
|
||||
models.CharField(
|
||||
choices=[("y", "Oui"), ("n", "Non")],
|
||||
default="y",
|
||||
max_length=1,
|
||||
verbose_name="Afficher le programme de répétition (les répétition vieilles de plus d'une semaine ne sont pas affiché d'office)",
|
||||
),
|
||||
),
|
||||
(
|
||||
"morceaux",
|
||||
models.ManyToManyField(
|
||||
to="partitions.PartitionSet",
|
||||
verbose_name="Morceaux de la répétition (ctrl ou cmd pour en selectionner plusieurs)",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
22
partitions/migrations/0006_auto_20220118_1525.py
Normal file
22
partitions/migrations/0006_auto_20220118_1525.py
Normal file
|
@ -0,0 +1,22 @@
|
|||
# Generated by Django 2.2.24 on 2022-01-18 14:25
|
||||
|
||||
import django.db.models.functions.text
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("partitions", "0005_setlist"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="partition",
|
||||
options={
|
||||
"ordering": (django.db.models.functions.text.Lower("nom"),),
|
||||
"verbose_name": "Partition",
|
||||
"verbose_name_plural": "Partitions",
|
||||
},
|
||||
),
|
||||
]
|
22
partitions/migrations/0007_auto_20220118_1542.py
Normal file
22
partitions/migrations/0007_auto_20220118_1542.py
Normal file
|
@ -0,0 +1,22 @@
|
|||
# Generated by Django 2.2.24 on 2022-01-18 14:42
|
||||
|
||||
import django.db.models.functions.text
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("partitions", "0006_auto_20220118_1525"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="partitionset",
|
||||
options={
|
||||
"ordering": ("category", django.db.models.functions.text.Lower("nom")),
|
||||
"verbose_name": "Morceau",
|
||||
"verbose_name_plural": "Morceaux",
|
||||
},
|
||||
),
|
||||
]
|
|
@ -1,6 +1,7 @@
|
|||
import os
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import admin
|
||||
from django.db import models
|
||||
from django.db.models.functions import Lower
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
@ -33,8 +34,8 @@ class Partition(models.Model):
|
|||
super(Partition, self).delete(*args, **kwargs)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Morceau")
|
||||
verbose_name_plural = _("Morceaux")
|
||||
verbose_name = _("Partition")
|
||||
verbose_name_plural = _("Partitions")
|
||||
ordering = (Lower("nom"),)
|
||||
|
||||
|
||||
|
@ -69,4 +70,43 @@ class PartitionSet(models.Model):
|
|||
class Meta:
|
||||
verbose_name = _("Morceau")
|
||||
verbose_name_plural = _("Morceaux")
|
||||
ordering = (Lower("nom"),)
|
||||
ordering = (
|
||||
"category",
|
||||
Lower("nom"),
|
||||
)
|
||||
|
||||
|
||||
from datetime import date as ddate
|
||||
from datetime import timedelta
|
||||
|
||||
|
||||
class SetList(models.Model):
|
||||
"""
|
||||
Modèle qui stocke les setlists de répétition (date et morceaux)
|
||||
"""
|
||||
|
||||
date = models.DateField(_("Date de la répétition"))
|
||||
is_current = models.CharField(
|
||||
verbose_name=_(
|
||||
"Afficher le programme de répétition (les répétition vieilles de plus d'une semaine ne sont pas affiché d'office)"
|
||||
),
|
||||
max_length=1,
|
||||
choices=(("y", "Oui"), ("n", "Non")),
|
||||
default="y",
|
||||
blank=False,
|
||||
)
|
||||
morceaux = models.ManyToManyField(
|
||||
"PartitionSet",
|
||||
verbose_name=_(
|
||||
"Morceaux de la répétition (ctrl ou cmd pour en selectionner plusieurs)"
|
||||
),
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return "%s - (%s)" % (
|
||||
self.date,
|
||||
", ".join(self.morceaux.all().values_list("nom", flat=True)),
|
||||
)
|
||||
|
||||
def is_visible(self):
|
||||
return self.is_current == "y" and self.date > ddate.today() - timedelta(days=7)
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
<a href="{% url "partitions:listepart" partition.nom partition.auteur %}"
|
||||
class="fichier">{{ partition.nom }} - {{ partition.auteur }}</a>
|
||||
{% endif %}
|
||||
{% if user.profile.is_chef %}
|
||||
{% if user.profile.is_chef or user.profile.is_chef_mu %}
|
||||
<a href="{% url "partitions:conf_delete_morc" partition.nom partition.auteur %}"
|
||||
class="supprimer">Supprimer</a>
|
||||
{% endif %}
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
{% endif %}
|
||||
|
||||
<div class="info_part">
|
||||
{% if user.profile.is_chef %}
|
||||
{% if user.profile.is_chef or user.profile.is_chef_mu %}
|
||||
<form action="{% url "partitions:listepart" nom auteur %}" id="chef-edit-form" method="post">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
|
@ -40,7 +40,7 @@
|
|||
<a href="{% url "partitions:download" nom auteur p.id %}" class="telecharger">Télécharger</a>
|
||||
{% endif %}
|
||||
|
||||
{% if user.profile.is_chef %}
|
||||
{% if user.profile.is_chef or user.profile.is_chef_mu %}
|
||||
<a href="{% url "partitions:conf_delete" nom auteur p.pk %}" class="supprimer">Supprimer</a>
|
||||
{% endif %}
|
||||
|
||||
|
|
|
@ -28,15 +28,17 @@
|
|||
<tbody>
|
||||
{% for p in part %}
|
||||
{% if user.is_authenticated and ".mscz" in p.part.url %}
|
||||
<tr>
|
||||
<td><p class="fichier">{{ p.nom }}</p></td>
|
||||
|
||||
|
||||
{% if user.is_authenticated %}
|
||||
<td> <a href="{% url "partitions:download" nom auteur p.id %}" class="button icon fa-download">{% trans "Télécharger" %}</a></td>
|
||||
{% endif %}
|
||||
{% if user.profile.is_chef %} <td>
|
||||
{% if user.profile.is_chef or user.profile.is_chef_mu %} <td>
|
||||
<a href="{% url "partitions:conf_delete" nom auteur p.pk %}" class="button icon fa-deleate">{% trans "Supprimer" %}</a></td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% for p in part %}
|
||||
|
@ -54,7 +56,7 @@
|
|||
{% if user.is_authenticated %}
|
||||
<td> <a href="{% url "partitions:download" nom auteur p.id %}" class="button icon fa-download">{% trans "Télécharger" %}</a></td>
|
||||
{% endif %}
|
||||
{% if user.profile.is_chef %} <td>
|
||||
{% if user.profile.is_chef or user.profile.is_chef_mu %} <td>
|
||||
<a href="{% url "partitions:conf_delete" nom auteur p.pk %}" class="button icon fa-deleate">{% trans "Supprimer" %}</a></td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
|
@ -65,7 +67,7 @@
|
|||
</tbody>
|
||||
</table>
|
||||
|
||||
{% if user.profile.is_chef %}
|
||||
{% if user.profile.is_chef or user.profile.is_chef_mu %}
|
||||
<p><a href="{% url "partitions:upload" p.nom p.auteur %}" class='button'>{% trans "Ajouter un média" %}</a></p>
|
||||
|
||||
{% if infos or infos_en %}
|
||||
|
@ -86,7 +88,7 @@
|
|||
<p></p>
|
||||
</div>
|
||||
<div class="6u 12u$(small)">
|
||||
{% if user.profile.is_chef %}
|
||||
{% if user.profile.is_chef or user.profile.is_chef_mu %}
|
||||
<form action="{% url "partitions:listepart" nom auteur %}" id="chef-edit-form" method="post">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
|
|
|
@ -7,6 +7,20 @@
|
|||
{% block content %}
|
||||
<div id="main">
|
||||
<section class="wrapper style1">
|
||||
{% if user.is_authenticated and setlists %}
|
||||
<div class="inner">
|
||||
<div class="box" style="background-color:rgba(228,82,47,0.5)">
|
||||
{% for set_list in setlists %}
|
||||
<h4>{% blocktrans with set_list_date=set_list.date %}Programme de répétition de la semaine du {{ set_list_date }}: {% endblocktrans %}</h4>
|
||||
<ul class="pl-5">
|
||||
{% for morceau in set_list.morceaux.all %}
|
||||
<li><a href="{% url "partitions:listepart" morceau.nom morceau.auteur %}">{{ morceau.nom }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="inner">
|
||||
|
||||
<span class="image fit">
|
||||
|
@ -23,7 +37,7 @@
|
|||
<div class="icon fa-copyright" style="color:#000000"> Lucas Gierzack</div></div>
|
||||
{% endif %}
|
||||
</span>
|
||||
{% if user.profile.is_chef %}
|
||||
{% if user.profile.is_chef or user.profile.is_chef_mu %}
|
||||
<a href="{% url "partitions:ajouter_morceau" %}" class="button alt big">{% trans "Ajouter un morceau" %}</a>   <a href="{% url "partitions:download_musecores" %}" class="button alt big">{% trans "Télécharger tous les musecores actifs" %}</a>
|
||||
{% elif user.is_authenticated %}
|
||||
<a href="{% url "partitions:download_musecores" %}" class="button alt big">{% trans "Télécharger tous les musecores actifs" %}</a>
|
||||
|
@ -66,7 +80,7 @@
|
|||
<td> <u><a href="{% url "partitions:listepart" partition.nom partition.auteur %}"
|
||||
class="fichier">{{ partition.nom }}</a> </td>
|
||||
<td> {{ partition.auteur }} </a></u> </td>
|
||||
{% if user.profile.is_chef %}<td>
|
||||
{% if user.profile.is_chef or user.profile.is_chef_mu %}<td>
|
||||
|
||||
<a href="{% url "partitions:conf_delete_morc" partition.nom partition.auteur %}"
|
||||
class="button small icon fa-trash">{% trans "Supprimer" %}</a></td>
|
||||
|
|
21
partitions/templates/partitions/setlist_confirm_delete.html
Normal file
21
partitions/templates/partitions/setlist_confirm_delete.html
Normal file
|
@ -0,0 +1,21 @@
|
|||
{% extends "gestion/base.html" %}
|
||||
{% load i18n %}
|
||||
{% get_current_language as current_language %}
|
||||
{% load autotranslate %}
|
||||
{% block titre %}{% trans "Supprimer un programme de répétition" %}{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<div id="main">
|
||||
<section class="wrapper style1">
|
||||
<div class="inner">
|
||||
<h4>{% blocktrans with set_list_date=setlist.date %} Supprimer le programme de répétition du {{ set_list_date }} :{% endblocktrans %}</h4>
|
||||
<p>{% blocktrans with set_list=setlist %}Êtes-vous sûr.e de vouloir supprimer cette répétition : {{ set_list }}?{% endblocktrans %}</p>
|
||||
<form action="" method="POST">
|
||||
{% csrf_token %}
|
||||
<input class="button alt" type="submit" value="{% trans "Oui" %}">
|
||||
<a class="button alt" href="{% url 'partitions:list_setlist' %}">{% trans "Retour" %}</a>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
{% endblock %}
|
18
partitions/templates/partitions/setlist_form.html
Normal file
18
partitions/templates/partitions/setlist_form.html
Normal file
|
@ -0,0 +1,18 @@
|
|||
{% extends "gestion/base.html" %}
|
||||
{% load i18n %}
|
||||
{%block titre %}{% trans "Ajout/modification d'un programme de répétition" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div id="main">
|
||||
<section class="wrapper style1">
|
||||
<div class="inner">
|
||||
<p><a href="{% url "partitions:list_setlist" %}" class="button alt">{% trans "Retour à la liste" %}</a></p>
|
||||
<form action="" method="post">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<input type="submit" value="{% trans "Enregistrer" %}" />
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
{% endblock %}
|
31
partitions/templates/partitions/setlist_list.html
Normal file
31
partitions/templates/partitions/setlist_list.html
Normal file
|
@ -0,0 +1,31 @@
|
|||
{% extends "gestion/base.html" %}
|
||||
{% load i18n %}
|
||||
{% get_current_language as current_language %}
|
||||
{% load autotranslate %}
|
||||
{% block titre %}{% trans "Liste des programmes de répétition" %}{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<div id="main">
|
||||
<section class="wrapper style1">
|
||||
<div class="inner">
|
||||
<h4>{% trans "Liste des programmes de répétition" %} :</h4>
|
||||
|
||||
|
||||
<p><a href="{% url 'partitions:create_setlist' %}" class="button">{% trans "Ajouter un programme de répétition" %}</a></p>
|
||||
|
||||
<ul class="filelist">
|
||||
{% for a in setlist_list %}
|
||||
<li>
|
||||
<p>{% if a.is_visible %}[VISIBLE] - {% endif %}{{ a }}
|
||||
<a class="button alt" href="{% url 'partitions:update_setlist' a.pk %}">{% trans "Modifier" %}</a>
|
||||
<a class="button alt" href="{% url 'partitions:delete_setlist' a.pk %}">{% trans "Supprimer" %}</a></p>
|
||||
</li>
|
||||
|
||||
{% empty %}
|
||||
<p>{% trans "Pas de programme de répétition pour le moment" %}</p>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -6,6 +6,14 @@ app_name = "partitions"
|
|||
urlpatterns = [
|
||||
path("", views.Repertoire.as_view(), name="liste"),
|
||||
path("download", views.download_musecores, name="download_musecores"),
|
||||
path("setlist/", views.SetListListView.as_view(), name="list_setlist"),
|
||||
path("setlist/create", views.SetListCreate.as_view(), name="create_setlist"),
|
||||
path(
|
||||
"setlist/<int:pk>/update", views.SetListUpdate.as_view(), name="update_setlist"
|
||||
),
|
||||
path(
|
||||
"setlist/<int:pk>/delete", views.SetListDelete.as_view(), name="delete_setlist"
|
||||
),
|
||||
path("<str:nom>/<str:auteur>/upload", views.Upload.as_view(), name="upload"),
|
||||
path("<str:nom>/<str:auteur>", views.Morceau.as_view(), name="listepart"),
|
||||
path("<str:nom>/<str:auteur>/see/<int:partition_id>", views.see, name="see"),
|
||||
|
|
|
@ -7,15 +7,17 @@ from django.core.files import File
|
|||
from django.db.models import Q
|
||||
from django.http import Http404
|
||||
from django.shortcuts import HttpResponse, get_object_or_404, redirect, render
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.text import slugify
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic import TemplateView
|
||||
from django.views.generic import (CreateView, DeleteView, ListView,
|
||||
TemplateView, UpdateView)
|
||||
|
||||
from gestion.mixins import ChefRequiredMixin
|
||||
from gestion.mixins import ChefMuRequiredMixin
|
||||
from gestion.models import Photo
|
||||
from partitions.forms import UploadFileForm, UploadMorceauForm
|
||||
from partitions.models import Category, Partition, PartitionSet
|
||||
from partitions.models import Category, Partition, PartitionSet, SetList
|
||||
|
||||
from .forms import ChefEditMorceauForm
|
||||
|
||||
|
@ -69,11 +71,21 @@ def download_musecores(request):
|
|||
return resp
|
||||
|
||||
|
||||
from datetime import date, timedelta
|
||||
|
||||
|
||||
class Repertoire(TemplateView):
|
||||
template_name = "partitions/repertoire.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["setlists"] = (
|
||||
SetList.objects.filter(
|
||||
is_current="y", date__gt=(date.today() - timedelta(days=7))
|
||||
)
|
||||
.order_by("date")
|
||||
.prefetch_related("morceaux")
|
||||
)
|
||||
context["categories"] = Category.objects.prefetch_related(
|
||||
"partitionset_set"
|
||||
).order_by("order")
|
||||
|
@ -94,7 +106,6 @@ class Morceau(LoginRequiredMixin, TemplateView):
|
|||
form = self.form_class(instance=p)
|
||||
infos = mark_safe(p.infos)
|
||||
infos_en = mark_safe(p.infos_en)
|
||||
|
||||
context["p"] = p
|
||||
context["infos"] = infos
|
||||
context["infos_en"] = infos_en
|
||||
|
@ -117,7 +128,7 @@ class Morceau(LoginRequiredMixin, TemplateView):
|
|||
return render(request, self.template_name, context)
|
||||
|
||||
|
||||
class Upload(ChefRequiredMixin, TemplateView):
|
||||
class Upload(ChefMuRequiredMixin, TemplateView):
|
||||
form_class = UploadFileForm
|
||||
template_name = "partitions/upload.html"
|
||||
|
||||
|
@ -197,7 +208,7 @@ def see(request, nom, auteur, partition_id):
|
|||
return redirect("login")
|
||||
|
||||
|
||||
class DeletePart(ChefRequiredMixin, TemplateView):
|
||||
class DeletePart(ChefMuRequiredMixin, TemplateView):
|
||||
model = PartitionSet
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
|
@ -214,7 +225,7 @@ class DeletePart(ChefRequiredMixin, TemplateView):
|
|||
)
|
||||
|
||||
|
||||
class CreateMorc(ChefRequiredMixin, TemplateView):
|
||||
class CreateMorc(ChefMuRequiredMixin, TemplateView):
|
||||
form_class = UploadMorceauForm
|
||||
template_name = "partitions/new.html"
|
||||
|
||||
|
@ -259,7 +270,7 @@ class CreateMorc(ChefRequiredMixin, TemplateView):
|
|||
return render(request, self.template_name, context)
|
||||
|
||||
|
||||
class ConfDelete(ChefRequiredMixin, TemplateView):
|
||||
class ConfDelete(ChefMuRequiredMixin, TemplateView):
|
||||
template_name = "partitions/conf_delete.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
|
@ -270,7 +281,7 @@ class ConfDelete(ChefRequiredMixin, TemplateView):
|
|||
return context
|
||||
|
||||
|
||||
class DeleteMorc(ChefRequiredMixin, TemplateView):
|
||||
class DeleteMorc(ChefMuRequiredMixin, TemplateView):
|
||||
model = PartitionSet
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
|
@ -284,7 +295,7 @@ class DeleteMorc(ChefRequiredMixin, TemplateView):
|
|||
return redirect("partitions:liste")
|
||||
|
||||
|
||||
class ConfDeleteMorc(ChefRequiredMixin, TemplateView):
|
||||
class ConfDeleteMorc(ChefMuRequiredMixin, TemplateView):
|
||||
template_name = "partitions/conf_delete_morc.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
|
@ -313,3 +324,27 @@ def download(request, nom, auteur, partition_id):
|
|||
return response
|
||||
else:
|
||||
return redirect("login")
|
||||
|
||||
|
||||
class SetListListView(ChefMuRequiredMixin, ListView):
|
||||
model = SetList
|
||||
|
||||
def get_queryset(self):
|
||||
return SetList.objects.all().order_by("-date")
|
||||
|
||||
|
||||
class SetListCreate(ChefMuRequiredMixin, CreateView):
|
||||
model = SetList
|
||||
fields = ["date", "morceaux", "is_current"]
|
||||
success_url = reverse_lazy("partitions:list_setlist")
|
||||
|
||||
|
||||
class SetListUpdate(ChefMuRequiredMixin, UpdateView):
|
||||
model = SetList
|
||||
fields = ["date", "morceaux", "is_current"]
|
||||
success_url = reverse_lazy("partitions:list_setlist")
|
||||
|
||||
|
||||
class SetListDelete(ChefMuRequiredMixin, DeleteView):
|
||||
model = SetList
|
||||
success_url = reverse_lazy("partitions:list_setlist")
|
||||
|
|
0
propositions/__init__.py
Normal file
0
propositions/__init__.py
Normal file
0
propositions/admin.py
Normal file
0
propositions/admin.py
Normal file
38
propositions/migrations/0001_initial.py
Normal file
38
propositions/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,38 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('gestion', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Prop',
|
||||
fields=[
|
||||
('id', models.AutoField(primary_key=True, verbose_name='ID', auto_created=True, serialize=False)),
|
||||
('nom', models.CharField(max_length=100)),
|
||||
('artiste', models.CharField(max_length=100, blank=True)),
|
||||
('lien', models.URLField(blank=True)),
|
||||
('nboui', models.IntegerField(verbose_name='oui', default=0)),
|
||||
('nbnon', models.IntegerField(verbose_name='non', default=0)),
|
||||
('user', models.ForeignKey(verbose_name='Proposé par', to='gestion.ErnestoUser', on_delete=models.CASCADE)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Proposition',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Reponses',
|
||||
fields=[
|
||||
('id', models.AutoField(primary_key=True, verbose_name='ID', auto_created=True, serialize=False)),
|
||||
('reponse', models.CharField(verbose_name='Réponse', choices=[('oui', 'Oui'), ('non', 'Non')], max_length=20, blank=True)),
|
||||
('part', models.ForeignKey(to='gestion.ErnestoUser', on_delete=models.CASCADE)),
|
||||
('prop', models.ForeignKey(to='propositions.Prop', on_delete=models.CASCADE)),
|
||||
],
|
||||
),
|
||||
]
|
18
propositions/migrations/0002_nom_verbose_name.py
Normal file
18
propositions/migrations/0002_nom_verbose_name.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 2.2.9 on 2020-01-04 23:01
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('propositions', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='prop',
|
||||
name='nom',
|
||||
field=models.CharField(max_length=100, verbose_name='nom du morceau'),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,75 @@
|
|||
# Generated by Django 2.2.9 on 2020-01-05 13:32
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def move_profile_to_user(apps, schema_editor):
|
||||
Reponses = apps.get_model("propositions", "reponses")
|
||||
for answer in Reponses.objects.all():
|
||||
answer.user = answer.part.user
|
||||
answer.save()
|
||||
|
||||
|
||||
def move_user_to_profile(apps, schema_editor):
|
||||
# One should do something similar to ``move_profile_to_user`` AND make the
|
||||
# ``part`` field temporarily nullable in the operations below.
|
||||
# => Grosse flemme
|
||||
raise NotImplementedError("Who uses migrations backwards anyway?")
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("gestion", "0001_initial"),
|
||||
("propositions", "0002_nom_verbose_name"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="reponses",
|
||||
options={
|
||||
"verbose_name": "Réponse à une proposition",
|
||||
"verbose_name_plural": "Réponses à une proposition",
|
||||
},
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name="reponses", old_name="prop", new_name="proposition",
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="reponses",
|
||||
name="user",
|
||||
field=models.ForeignKey(
|
||||
on_delete=models.deletion.CASCADE,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
migrations.RunPython(move_profile_to_user, move_user_to_profile),
|
||||
migrations.AlterField(
|
||||
model_name="reponses",
|
||||
name="user",
|
||||
field=models.ForeignKey(
|
||||
on_delete=models.deletion.CASCADE,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
null=False,
|
||||
),
|
||||
),
|
||||
migrations.RemoveField(model_name="reponses", name="part"),
|
||||
migrations.AddField(
|
||||
model_name="reponses",
|
||||
name="answer",
|
||||
field=models.CharField(
|
||||
choices=[("oui", "Oui"), ("non", "Non")],
|
||||
default="non",
|
||||
max_length=3,
|
||||
verbose_name="Réponse",
|
||||
),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name="reponses", unique_together={("proposition", "user")},
|
||||
),
|
||||
migrations.RemoveField(model_name="reponses", name="reponse",),
|
||||
migrations.RenameModel(old_name="reponses", new_name="answer"),
|
||||
]
|
43
propositions/migrations/0004_prop_renaming_and_cleaning.py
Normal file
43
propositions/migrations/0004_prop_renaming_and_cleaning.py
Normal file
|
@ -0,0 +1,43 @@
|
|||
# Generated by Django 2.2.9 on 2020-01-05 14:41
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("propositions", "0003_reponse_renaming_and_cleaning"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="prop",
|
||||
options={
|
||||
"verbose_name": "Proposition de morceau",
|
||||
"verbose_name_plural": "Propositions de morceaux",
|
||||
},
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name="prop", old_name="artiste", new_name="artist",
|
||||
),
|
||||
migrations.RenameField(model_name="prop", old_name="lien", new_name="link"),
|
||||
migrations.RenameField(model_name="prop", old_name="nom", new_name="name"),
|
||||
migrations.RenameField(model_name="prop", old_name="nbnon", new_name="nb_no"),
|
||||
migrations.RenameField(model_name="prop", old_name="nboui", new_name="nb_yes"),
|
||||
migrations.AlterField(
|
||||
model_name="prop",
|
||||
name="nb_no",
|
||||
field=models.IntegerField(default=0, verbose_name="nombre de réponses non"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="prop",
|
||||
name="nb_yes",
|
||||
field=models.IntegerField(default=0, verbose_name="nombre de réponses oui"),
|
||||
),
|
||||
migrations.RenameModel(old_name="prop", new_name="proposition"),
|
||||
migrations.AlterField(
|
||||
model_name='answer',
|
||||
name='proposition',
|
||||
field=models.ForeignKey(on_delete=models.deletion.CASCADE, to='propositions.Proposition'),
|
||||
),
|
||||
]
|
24
propositions/migrations/0005_remove_nb_yes_no_fields.py
Normal file
24
propositions/migrations/0005_remove_nb_yes_no_fields.py
Normal file
|
@ -0,0 +1,24 @@
|
|||
# Generated by Django 2.2.9 on 2020-01-05 15:26
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("propositions", "0004_prop_renaming_and_cleaning"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(model_name="proposition", name="nb_no",),
|
||||
migrations.RemoveField(model_name="proposition", name="nb_yes",),
|
||||
migrations.AlterField(
|
||||
model_name="answer",
|
||||
name="proposition",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="propositions.Proposition",
|
||||
),
|
||||
),
|
||||
]
|
53
propositions/migrations/0006_proposition_profile_to_user.py
Normal file
53
propositions/migrations/0006_proposition_profile_to_user.py
Normal file
|
@ -0,0 +1,53 @@
|
|||
# Generated by Django 2.2.9 on 2020-01-05 16:28
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
def move_profile_to_user(apps, schema_editor):
|
||||
Proposition = apps.get_model("propositions", "Proposition")
|
||||
for proposition in Proposition.objects.all():
|
||||
proposition.user = proposition.profile.user
|
||||
proposition.save()
|
||||
|
||||
|
||||
def move_user_to_profile(apps, schema_editor):
|
||||
# One should do something similar to ``move_profile_to_user`` AND make the
|
||||
# ``profile`` field temporarily nullable in the operations below.
|
||||
# => Grosse flemme
|
||||
raise NotImplementedError("Who uses migrations backwards anyway?")
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("propositions", "0005_remove_nb_yes_no_fields"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name="proposition", old_name="user", new_name="profile"
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="proposition",
|
||||
name="user",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
verbose_name="Proposé par",
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
migrations.RunPython(move_profile_to_user, move_user_to_profile),
|
||||
migrations.RemoveField(model_name="proposition", name="profile"),
|
||||
migrations.AlterField(
|
||||
model_name="proposition",
|
||||
name="user",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
verbose_name="Proposé par",
|
||||
),
|
||||
),
|
||||
]
|
19
propositions/migrations/0007_auto_20220322_1455.py
Normal file
19
propositions/migrations/0007_auto_20220322_1455.py
Normal file
|
@ -0,0 +1,19 @@
|
|||
# Generated by Django 2.2.27 on 2022-03-22 13:55
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('propositions', '0006_proposition_profile_to_user'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='answer',
|
||||
name='proposition',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='propositions.Proposition'),
|
||||
),
|
||||
]
|
19
propositions/migrations/0008_auto_20220914_1244.py
Normal file
19
propositions/migrations/0008_auto_20220914_1244.py
Normal file
|
@ -0,0 +1,19 @@
|
|||
# Generated by Django 2.2.27 on 2022-09-14 10:44
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('propositions', '0007_auto_20220322_1455'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='answer',
|
||||
name='proposition',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='propositions.Proposition'),
|
||||
),
|
||||
]
|
19
propositions/migrations/0009_auto_20240615_1303.py
Normal file
19
propositions/migrations/0009_auto_20240615_1303.py
Normal file
|
@ -0,0 +1,19 @@
|
|||
# Generated by Django 2.2.28 on 2024-06-15 13:03
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('propositions', '0008_auto_20220914_1244'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='answer',
|
||||
name='proposition',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='propositions.Proposition'),
|
||||
),
|
||||
]
|
0
propositions/migrations/__init__.py
Normal file
0
propositions/migrations/__init__.py
Normal file
34
propositions/models.py
Normal file
34
propositions/models.py
Normal file
|
@ -0,0 +1,34 @@
|
|||
from django.contrib.auth import get_user_model
|
||||
from django.db import models
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class Proposition(models.Model):
|
||||
name = models.CharField(max_length=100, verbose_name="nom du morceau")
|
||||
artist = models.CharField(blank=True, max_length=100)
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="Proposé par")
|
||||
link = models.URLField(blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Proposition de morceau"
|
||||
verbose_name_plural = "Propositions de morceaux"
|
||||
|
||||
|
||||
class Answer(models.Model):
|
||||
YES = "oui"
|
||||
NO = "non"
|
||||
|
||||
REP_CHOICES = [(YES, "Oui"), (NO, "Non")]
|
||||
|
||||
proposition = models.ForeignKey(Proposition, on_delete=models.CASCADE)
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
answer = models.CharField("Réponse", max_length=3, choices=REP_CHOICES)
|
||||
|
||||
class Meta:
|
||||
unique_together = ("proposition", "user")
|
||||
verbose_name = "Réponse à une proposition"
|
||||
verbose_name_plural = "Réponses à une proposition"
|
14
propositions/templates/propositions/create.html
Normal file
14
propositions/templates/propositions/create.html
Normal file
|
@ -0,0 +1,14 @@
|
|||
{% extends "gestion/base.html" %}
|
||||
|
||||
{% block titre %}Proposition de morceau{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<p><a href="{% url "propositions:list" %}">Retour aux propositions</a></p>
|
||||
<form action="{% url "propositions:create" %}" method="post">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<input type="submit" value="Enregistrer" />
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
10
propositions/templates/propositions/delete.html
Normal file
10
propositions/templates/propositions/delete.html
Normal file
|
@ -0,0 +1,10 @@
|
|||
{% extends "gestion/base.html" %}
|
||||
|
||||
{% block titre %}Suppression d'une proposition{% endblock %}
|
||||
{% block content %}<form action="" method="post">
|
||||
{% csrf_token %}
|
||||
<p><a href="{% url "propositions:list" %}">Retour aux propositions</a></p>
|
||||
<p>Voulez vous vraiment supprimer la proposition {{ object }}?</p>
|
||||
<input type="submit" value="Oui" />
|
||||
</form>
|
||||
{% endblock %}
|
44
propositions/templates/propositions/liste.html
Normal file
44
propositions/templates/propositions/liste.html
Normal file
|
@ -0,0 +1,44 @@
|
|||
{% extends "gestion/base.html" %}
|
||||
|
||||
{% block titre %}Propositions de morceau{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Liste des propositions</h1>
|
||||
|
||||
<p><a href="{% url "propositions:create" %}">Proposer un morceau</a></p>
|
||||
|
||||
{% if propositions.exists %}
|
||||
<table>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Oui</th>
|
||||
<th>Non</th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
{% for p in propositions %}
|
||||
<tr class="prop">
|
||||
<td>
|
||||
{% if p.link %}<a href={{ p.link }}>{% endif %}
|
||||
<b>{{ p.name }}</b>{% if p.artist %} - {{ p.artist }}{% endif %}
|
||||
{% if p.link %}</a>{% endif %}
|
||||
</td>
|
||||
<td>{{ p.nb_yes }}</td>
|
||||
<td>{{ p.nb_no }}</td>
|
||||
<td><a href="{% url "propositions:oui" p.id %}">Oui</a></td>
|
||||
<td><a href="{% url "propositions:non" p.id %}">Non</a></td>
|
||||
<td>{% if p.user_answer %}Vous avez voté {{ p.user_answer }}{% endif %}</td>
|
||||
<td>
|
||||
{% if p.user == request.user or request.user.profile.is_chef %}
|
||||
<a class="supprimer" href="{% url "propositions:delete" p.id %}">Supprimer</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% else %}
|
||||
Pas de proposition pour le moment
|
||||
{% endif %}
|
||||
{% endblock %}
|
165
propositions/tests.py
Normal file
165
propositions/tests.py
Normal file
|
@ -0,0 +1,165 @@
|
|||
from django.contrib.auth import get_user_model
|
||||
from django.test import Client, TestCase
|
||||
from django.urls import reverse_lazy, reverse
|
||||
|
||||
from gestion.models import ErnestoUser
|
||||
from propositions.models import Answer, Proposition
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
def new_user(username):
|
||||
u = User.objects.create_user(username=username)
|
||||
ErnestoUser.objects.create(user=u, slug=username, is_ernesto=True)
|
||||
return u
|
||||
|
||||
|
||||
class PropositionCreateTest(TestCase):
|
||||
url = reverse_lazy("propositions:create")
|
||||
|
||||
def test_anonymous_cannot_get(self):
|
||||
response = Client().get(self.url)
|
||||
self.assertRedirects(response, "/login?next={}".format(self.url))
|
||||
|
||||
def test_ernesto_user_can_get(self):
|
||||
user = new_user("toto")
|
||||
client = Client()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.get(self.url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_ernesto_user_can_post(self):
|
||||
user = new_user("toto")
|
||||
client = Client()
|
||||
client.force_login(user)
|
||||
|
||||
data = {"name": "foo", "artist": "bar", "link": "example.com"}
|
||||
client.post(self.url, data)
|
||||
proposition = Proposition.objects.all()
|
||||
self.assertEqual(1, proposition.count())
|
||||
|
||||
proposition = proposition.get()
|
||||
self.assertEqual(proposition.name, "foo")
|
||||
self.assertEqual(proposition.artist, "bar")
|
||||
self.assertEqual(proposition.link, "http://example.com")
|
||||
self.assertEqual(proposition.user, user)
|
||||
|
||||
|
||||
class PropositionDeleteTest(TestCase):
|
||||
def setUp(self):
|
||||
self.owner = new_user("owner")
|
||||
self.random_user = new_user("toto")
|
||||
self.chef = new_user("chef")
|
||||
self.chef.profile.is_chef = True
|
||||
self.chef.profile.save()
|
||||
|
||||
proposition = Proposition.objects.create(name="prop", user=self.owner)
|
||||
self.url = reverse("propositions:delete", args=(proposition.id,))
|
||||
|
||||
def test_anonymous_cannot_get(self):
|
||||
response = Client().get(self.url)
|
||||
self.assertRedirects(response, "/login?next={}".format(self.url))
|
||||
|
||||
def test_anonymous_cannot_post(self):
|
||||
response = Client().post(self.url, {})
|
||||
self.assertRedirects(response, "/login?next={}".format(self.url))
|
||||
self.assertTrue(Proposition.objects.exists())
|
||||
|
||||
def test_random_user_cannot_get(self):
|
||||
client = Client()
|
||||
client.force_login(self.random_user)
|
||||
response = client.get(self.url)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_not_owner_cannot_post(self):
|
||||
client = Client()
|
||||
client.force_login(self.random_user)
|
||||
response = client.post(self.url, {})
|
||||
self.assertEqual(response.status_code, 403)
|
||||
self.assertTrue(Proposition.objects.exists())
|
||||
|
||||
def test_chef_can_get(self):
|
||||
client = Client()
|
||||
client.force_login(self.chef)
|
||||
response = client.get(self.url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_chef_can_post(self):
|
||||
client = Client()
|
||||
client.force_login(self.chef)
|
||||
client.post(self.url, {})
|
||||
self.assertFalse(Proposition.objects.exists())
|
||||
|
||||
def test_owner_can_get(self):
|
||||
client = Client()
|
||||
client.force_login(self.owner)
|
||||
response = client.get(self.url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_owner_can_post(self):
|
||||
client = Client()
|
||||
client.force_login(self.owner)
|
||||
client.post(self.url, {})
|
||||
self.assertFalse(Proposition.objects.exists())
|
||||
|
||||
|
||||
class PropositionListTest(TestCase):
|
||||
url = reverse_lazy("propositions:list")
|
||||
|
||||
def setUp(self):
|
||||
self.user = new_user("toto")
|
||||
for name in ["foo", "bar", "baz"]:
|
||||
p = Proposition.objects.create(name=name, user=self.user)
|
||||
Answer.objects.create(proposition=p, user=self.user, answer=Answer.YES)
|
||||
for name in ["oof", "rab", "zab"]:
|
||||
p = Proposition.objects.create(name=name, user=self.user)
|
||||
Answer.objects.create(proposition=p, user=self.user, answer=Answer.NO)
|
||||
|
||||
def test_anonymous_cannot_get(self):
|
||||
response = Client().get(self.url)
|
||||
self.assertRedirects(response, "/login?next={}".format(self.url))
|
||||
|
||||
def test_ernesto_user_can_get(self):
|
||||
client = Client()
|
||||
client.force_login(self.user)
|
||||
|
||||
response = client.get(self.url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class ReponseTest(TestCase):
|
||||
def setUp(self):
|
||||
self.user = new_user("toto")
|
||||
self.prop = Proposition.objects.create(name="foo", user=self.user)
|
||||
|
||||
def _url(self, rep):
|
||||
assert rep in Answer.REP_CHOICES
|
||||
return reverse("propositions:{}".format(rep), args=(self.prop.id,))
|
||||
|
||||
def test_anonymous_cannot_get(self):
|
||||
client = Client()
|
||||
|
||||
url = reverse("propositions:oui", args=(self.prop.id,))
|
||||
response = client.get(url)
|
||||
self.assertRedirects(response, "/login?next={}".format(url))
|
||||
|
||||
url = reverse("propositions:non", args=(self.prop.id,))
|
||||
response = client.get(url)
|
||||
self.assertRedirects(response, "/login?next={}".format(url))
|
||||
|
||||
def test_ernesto_user_can_get(self):
|
||||
client = Client()
|
||||
client.force_login(self.user)
|
||||
|
||||
client.get(reverse("propositions:oui", args=(self.prop.id,)))
|
||||
self.prop.refresh_from_db()
|
||||
self.assertEqual(
|
||||
list(self.prop.answer_set.values_list("answer", flat=True)), [Answer.YES],
|
||||
)
|
||||
|
||||
client.get(reverse("propositions:non", args=(self.prop.id,)))
|
||||
self.prop.refresh_from_db()
|
||||
self.assertEqual(
|
||||
list(self.prop.answer_set.values_list("answer", flat=True)), [Answer.NO]
|
||||
)
|
13
propositions/urls.py
Normal file
13
propositions/urls.py
Normal file
|
@ -0,0 +1,13 @@
|
|||
from django.urls import path
|
||||
|
||||
from propositions import views
|
||||
from propositions.models import Answer
|
||||
|
||||
app_name = "propositions"
|
||||
urlpatterns = [
|
||||
path("", views.PropositionList.as_view(), name="list"),
|
||||
path("new", views.PropositionCreate.as_view(), name="create"),
|
||||
path("<int:id>/oui", views.answer, {"ans": "oui"}, name=Answer.YES),
|
||||
path("<int:id>/non", views.answer, {"ans": "non"}, name=Answer.NO),
|
||||
path("<int:pk>/supprimer", views.PropositionDelete.as_view(), name="delete"),
|
||||
]
|
8
propositions/utils.py
Normal file
8
propositions/utils.py
Normal file
|
@ -0,0 +1,8 @@
|
|||
import string
|
||||
import random
|
||||
|
||||
|
||||
def generer(*args):
|
||||
caracteres = string.ascii_letters + string.digits
|
||||
aleatoire = [random.choice(caracteres) for _ in range(6)]
|
||||
return ''.join(aleatoire)
|
63
propositions/views.py
Normal file
63
propositions/views.py
Normal file
|
@ -0,0 +1,63 @@
|
|||
from django.shortcuts import redirect, get_object_or_404
|
||||
from django.urls import reverse_lazy
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
|
||||
from django.db.models import Count, OuterRef, Q, Subquery
|
||||
from django.views.generic import CreateView, DeleteView, ListView
|
||||
from django.http import HttpResponseRedirect
|
||||
|
||||
from propositions.models import Answer, Proposition
|
||||
|
||||
|
||||
class PropositionCreate(LoginRequiredMixin, CreateView):
|
||||
template_name = "propositions/create.html"
|
||||
success_url = reverse_lazy("propositions:list")
|
||||
model = Proposition
|
||||
fields = ["name", "artist", "link"]
|
||||
|
||||
def form_valid(self, form):
|
||||
proposition = form.save(commit=False)
|
||||
proposition.user = self.request.user
|
||||
proposition.save()
|
||||
return HttpResponseRedirect(self.success_url)
|
||||
|
||||
|
||||
class PropositionList(LoginRequiredMixin, ListView):
|
||||
template_name = "propositions/liste.html"
|
||||
context_object_name = "propositions"
|
||||
model = Proposition
|
||||
|
||||
def get_queryset(self):
|
||||
user = self.request.user
|
||||
user_answers = (
|
||||
Answer.objects
|
||||
.filter(proposition=OuterRef("id"), user=user)
|
||||
.values_list("answer", flat=True)
|
||||
)
|
||||
return (
|
||||
Proposition.objects
|
||||
.annotate(nb_yes=Count("answer", filter=Q(answer__answer=Answer.YES)))
|
||||
.annotate(nb_no=Count("answer", filter=Q(answer__answer=Answer.NO)))
|
||||
.annotate(user_answer=Subquery(user_answers[:1]))
|
||||
.order_by("-nb_yes", "nb_no", "name")
|
||||
)
|
||||
|
||||
|
||||
class PropositionDelete(LoginRequiredMixin, UserPassesTestMixin, DeleteView):
|
||||
model = Proposition
|
||||
template_name = "propositions/delete.html"
|
||||
success_url = reverse_lazy("propositions:list")
|
||||
|
||||
def test_func(self):
|
||||
proposition = self.get_object()
|
||||
user = self.request.user
|
||||
return (proposition.user == user or user.profile.is_chef)
|
||||
|
||||
|
||||
@login_required
|
||||
def answer(request, id, ans):
|
||||
proposition = get_object_or_404(Proposition, id=id)
|
||||
user = request.user
|
||||
Answer.objects.filter(proposition=proposition, user=user).delete()
|
||||
Answer.objects.create(proposition=proposition, user=user, answer=ans)
|
||||
return redirect("propositions:list")
|
|
@ -1,8 +1,8 @@
|
|||
Django==2.2.*
|
||||
|
||||
# Pour la prod
|
||||
#psycopg2
|
||||
gunicorn
|
||||
django-colorful
|
||||
Pillow
|
||||
django-avatar==5.0.*
|
||||
Django==2.2.28
|
||||
django-appconf==1.0.4
|
||||
django-avatar==5.0.0
|
||||
django-colorful==1.3
|
||||
gunicorn==20.1.0
|
||||
Pillow==9.2.0
|
||||
#Pour le prod
|
||||
#psycopg2==2.8.6
|
||||
|
|
Loading…
Reference in a new issue